androjack-mcp 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +34 -0
- package/.github/pull_request_template.md +16 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +592 -0
- package/SECURITY.md +26 -0
- package/assets/AndroJack banner.png +0 -0
- package/assets/killer_argument.png +0 -0
- package/build/constants.js +412 -0
- package/build/http-server.js +163 -0
- package/build/http.js +151 -0
- package/build/index.js +553 -0
- package/build/install.js +379 -0
- package/build/logger.js +57 -0
- package/build/tools/api-level.js +170 -0
- package/build/tools/api36-compliance.js +282 -0
- package/build/tools/architecture.js +75 -0
- package/build/tools/build-publish.js +362 -0
- package/build/tools/component.js +90 -0
- package/build/tools/debugger.js +82 -0
- package/build/tools/gradle.js +234 -0
- package/build/tools/kmp.js +348 -0
- package/build/tools/kotlin-patterns.js +500 -0
- package/build/tools/large-screen.js +366 -0
- package/build/tools/m3-expressive.js +447 -0
- package/build/tools/navigation3.js +331 -0
- package/build/tools/ondevice-ai.js +283 -0
- package/build/tools/permissions.js +404 -0
- package/build/tools/play-policy.js +221 -0
- package/build/tools/scalability.js +621 -0
- package/build/tools/search.js +89 -0
- package/build/tools/testing.js +439 -0
- package/build/tools/wear.js +337 -0
- package/build/tools/xr.js +274 -0
- package/config/antigravity_mcp.json +32 -0
- package/config/claude_desktop_config.json +17 -0
- package/config/cursor_mcp.json +21 -0
- package/config/jetbrains_mcp.json +28 -0
- package/config/kiro_mcp.json +40 -0
- package/config/vscode_mcp.json +24 -0
- package/config/windsurf_mcp.json +18 -0
- package/package.json +51 -0
- package/src/constants.ts +436 -0
- package/src/http-server.ts +186 -0
- package/src/http.ts +190 -0
- package/src/index.ts +702 -0
- package/src/install.ts +441 -0
- package/src/logger.ts +67 -0
- package/src/tools/api-level.ts +198 -0
- package/src/tools/api36-compliance.ts +289 -0
- package/src/tools/architecture.ts +94 -0
- package/src/tools/build-publish.ts +379 -0
- package/src/tools/component.ts +106 -0
- package/src/tools/debugger.ts +111 -0
- package/src/tools/gradle.ts +288 -0
- package/src/tools/kmp.ts +352 -0
- package/src/tools/kotlin-patterns.ts +534 -0
- package/src/tools/large-screen.ts +391 -0
- package/src/tools/m3-expressive.ts +473 -0
- package/src/tools/navigation3.ts +338 -0
- package/src/tools/ondevice-ai.ts +287 -0
- package/src/tools/permissions.ts +445 -0
- package/src/tools/play-policy.ts +229 -0
- package/src/tools/scalability.ts +646 -0
- package/src/tools/search.ts +112 -0
- package/src/tools/testing.ts +460 -0
- package/src/tools/wear.ts +343 -0
- package/src/tools/xr.ts +278 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool 9 – android_permission_advisor
|
|
3
|
+
*
|
|
4
|
+
* Complete Android permissions reference grounded in official docs:
|
|
5
|
+
* developer.android.com/guide/topics/permissions/overview
|
|
6
|
+
*
|
|
7
|
+
* Covers: normal vs dangerous vs signature permissions, runtime request
|
|
8
|
+
* patterns (API 23+), Play Store restrictions, the correct Compose-based
|
|
9
|
+
* ActivityResultContracts API, and per-permission notes.
|
|
10
|
+
*/
|
|
11
|
+
import { secureFetch, extractPageText } from "../http.js";
|
|
12
|
+
// ── Permission Registry ───────────────────────────────────────────────────────
|
|
13
|
+
const PERMISSIONS = {
|
|
14
|
+
// ── Location ───────────────────────────────────────────────────────────────
|
|
15
|
+
ACCESS_FINE_LOCATION: {
|
|
16
|
+
fullName: "android.permission.ACCESS_FINE_LOCATION",
|
|
17
|
+
type: "dangerous", runtimeRequest: true, group: "Location",
|
|
18
|
+
docUrl: "https://developer.android.com/training/location/permissions",
|
|
19
|
+
notes: "On API 31+ the system lets users downgrade to COARSE. Always request COARSE first, then FINE if needed. Never request background + foreground together.",
|
|
20
|
+
},
|
|
21
|
+
ACCESS_COARSE_LOCATION: {
|
|
22
|
+
fullName: "android.permission.ACCESS_COARSE_LOCATION",
|
|
23
|
+
type: "dangerous", runtimeRequest: true, group: "Location",
|
|
24
|
+
docUrl: "https://developer.android.com/training/location/permissions",
|
|
25
|
+
notes: "Prefer over FINE unless GPS precision is genuinely required.",
|
|
26
|
+
},
|
|
27
|
+
ACCESS_BACKGROUND_LOCATION: {
|
|
28
|
+
fullName: "android.permission.ACCESS_BACKGROUND_LOCATION",
|
|
29
|
+
type: "dangerous", runtimeRequest: true, addedApi: 29, group: "Location",
|
|
30
|
+
playRestriction: "RESTRICTED — requires Play Store policy approval. Must show in-app disclosure before requesting. Only navigation, rideshare, family-safety categories normally qualify.",
|
|
31
|
+
docUrl: "https://developer.android.com/training/location/permissions#background",
|
|
32
|
+
notes: "Must be requested separately AFTER foreground location is granted. System takes user to settings on API 30+, not a dialog.",
|
|
33
|
+
},
|
|
34
|
+
// ── Camera / Media ─────────────────────────────────────────────────────────
|
|
35
|
+
CAMERA: {
|
|
36
|
+
fullName: "android.permission.CAMERA",
|
|
37
|
+
type: "dangerous", runtimeRequest: true, group: "Camera",
|
|
38
|
+
docUrl: "https://developer.android.com/training/camera2",
|
|
39
|
+
notes: "Consider Photo Picker (no permission needed) for image selection use cases.",
|
|
40
|
+
},
|
|
41
|
+
READ_MEDIA_IMAGES: {
|
|
42
|
+
fullName: "android.permission.READ_MEDIA_IMAGES",
|
|
43
|
+
type: "dangerous", runtimeRequest: true, addedApi: 33, group: "Storage",
|
|
44
|
+
docUrl: "https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions",
|
|
45
|
+
notes: "Use on API 33+. Below API 33, use READ_EXTERNAL_STORAGE instead.",
|
|
46
|
+
},
|
|
47
|
+
READ_MEDIA_VIDEO: {
|
|
48
|
+
fullName: "android.permission.READ_MEDIA_VIDEO",
|
|
49
|
+
type: "dangerous", runtimeRequest: true, addedApi: 33, group: "Storage",
|
|
50
|
+
docUrl: "https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions",
|
|
51
|
+
},
|
|
52
|
+
READ_MEDIA_AUDIO: {
|
|
53
|
+
fullName: "android.permission.READ_MEDIA_AUDIO",
|
|
54
|
+
type: "dangerous", runtimeRequest: true, addedApi: 33, group: "Storage",
|
|
55
|
+
docUrl: "https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions",
|
|
56
|
+
},
|
|
57
|
+
READ_EXTERNAL_STORAGE: {
|
|
58
|
+
fullName: "android.permission.READ_EXTERNAL_STORAGE",
|
|
59
|
+
type: "dangerous", runtimeRequest: true, group: "Storage",
|
|
60
|
+
deprecatedOrRemovedApi: 33,
|
|
61
|
+
replacement: "READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO on API 33+",
|
|
62
|
+
docUrl: "https://developer.android.com/training/data-storage",
|
|
63
|
+
},
|
|
64
|
+
WRITE_EXTERNAL_STORAGE: {
|
|
65
|
+
fullName: "android.permission.WRITE_EXTERNAL_STORAGE",
|
|
66
|
+
type: "removed", runtimeRequest: false,
|
|
67
|
+
deprecatedOrRemovedApi: 29,
|
|
68
|
+
replacement: "Scoped Storage via MediaStore API or Storage Access Framework (SAF)",
|
|
69
|
+
docUrl: "https://developer.android.com/training/data-storage/shared/media",
|
|
70
|
+
notes: "Ignored on API 29+. Do NOT request this in new apps.",
|
|
71
|
+
},
|
|
72
|
+
MANAGE_EXTERNAL_STORAGE: {
|
|
73
|
+
fullName: "android.permission.MANAGE_EXTERNAL_STORAGE",
|
|
74
|
+
type: "special", runtimeRequest: true, addedApi: 30,
|
|
75
|
+
playRestriction: "HEAVILY RESTRICTED — Play Store only allows file manager, antivirus, backup apps. Requires declaration form and policy approval.",
|
|
76
|
+
docUrl: "https://developer.android.com/training/data-storage/manage-all-files",
|
|
77
|
+
notes: "Sends user to Special App Access settings. Use Scoped Storage instead.",
|
|
78
|
+
},
|
|
79
|
+
// ── Notifications ──────────────────────────────────────────────────────────
|
|
80
|
+
POST_NOTIFICATIONS: {
|
|
81
|
+
fullName: "android.permission.POST_NOTIFICATIONS",
|
|
82
|
+
type: "dangerous", runtimeRequest: true, addedApi: 33, group: "Notifications",
|
|
83
|
+
docUrl: "https://developer.android.com/develop/ui/views/notifications/notification-permission",
|
|
84
|
+
notes: "Auto-granted for apps targeting API <33. Request contextually — not on launch. Create a NotificationChannel first (required API 26+).",
|
|
85
|
+
},
|
|
86
|
+
// ── Microphone ─────────────────────────────────────────────────────────────
|
|
87
|
+
RECORD_AUDIO: {
|
|
88
|
+
fullName: "android.permission.RECORD_AUDIO",
|
|
89
|
+
type: "dangerous", runtimeRequest: true, group: "Microphone",
|
|
90
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#RECORD_AUDIO",
|
|
91
|
+
notes: "Shows microphone indicator in status bar while active (API 30+).",
|
|
92
|
+
},
|
|
93
|
+
// ── Bluetooth ──────────────────────────────────────────────────────────────
|
|
94
|
+
BLUETOOTH_SCAN: {
|
|
95
|
+
fullName: "android.permission.BLUETOOTH_SCAN",
|
|
96
|
+
type: "dangerous", runtimeRequest: true, addedApi: 31, group: "Bluetooth",
|
|
97
|
+
docUrl: "https://developer.android.com/guide/topics/connectivity/bluetooth/permissions",
|
|
98
|
+
notes: "If scanning for devices NOT using location, add android:usesPermissionFlags='neverForLocation' to avoid triggering location rationale.",
|
|
99
|
+
},
|
|
100
|
+
BLUETOOTH_CONNECT: {
|
|
101
|
+
fullName: "android.permission.BLUETOOTH_CONNECT",
|
|
102
|
+
type: "dangerous", runtimeRequest: true, addedApi: 31, group: "Bluetooth",
|
|
103
|
+
docUrl: "https://developer.android.com/guide/topics/connectivity/bluetooth/permissions",
|
|
104
|
+
},
|
|
105
|
+
BLUETOOTH_ADVERTISE: {
|
|
106
|
+
fullName: "android.permission.BLUETOOTH_ADVERTISE",
|
|
107
|
+
type: "dangerous", runtimeRequest: true, addedApi: 31, group: "Bluetooth",
|
|
108
|
+
docUrl: "https://developer.android.com/guide/topics/connectivity/bluetooth/permissions",
|
|
109
|
+
},
|
|
110
|
+
// ── Internet / Network ─────────────────────────────────────────────────────
|
|
111
|
+
INTERNET: {
|
|
112
|
+
fullName: "android.permission.INTERNET",
|
|
113
|
+
type: "normal", runtimeRequest: false,
|
|
114
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#INTERNET",
|
|
115
|
+
notes: "Normal permission — auto-granted at install. No runtime request needed.",
|
|
116
|
+
},
|
|
117
|
+
ACCESS_NETWORK_STATE: {
|
|
118
|
+
fullName: "android.permission.ACCESS_NETWORK_STATE",
|
|
119
|
+
type: "normal", runtimeRequest: false,
|
|
120
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#ACCESS_NETWORK_STATE",
|
|
121
|
+
},
|
|
122
|
+
ACCESS_WIFI_STATE: {
|
|
123
|
+
fullName: "android.permission.ACCESS_WIFI_STATE",
|
|
124
|
+
type: "normal", runtimeRequest: false,
|
|
125
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#ACCESS_WIFI_STATE",
|
|
126
|
+
},
|
|
127
|
+
// ── Contacts ───────────────────────────────────────────────────────────────
|
|
128
|
+
READ_CONTACTS: {
|
|
129
|
+
fullName: "android.permission.READ_CONTACTS",
|
|
130
|
+
type: "dangerous", runtimeRequest: true, group: "Contacts",
|
|
131
|
+
docUrl: "https://developer.android.com/training/contacts-provider",
|
|
132
|
+
},
|
|
133
|
+
WRITE_CONTACTS: {
|
|
134
|
+
fullName: "android.permission.WRITE_CONTACTS",
|
|
135
|
+
type: "dangerous", runtimeRequest: true, group: "Contacts",
|
|
136
|
+
docUrl: "https://developer.android.com/training/contacts-provider",
|
|
137
|
+
},
|
|
138
|
+
// ── Phone / SMS ────────────────────────────────────────────────────────────
|
|
139
|
+
READ_PHONE_STATE: {
|
|
140
|
+
fullName: "android.permission.READ_PHONE_STATE",
|
|
141
|
+
type: "dangerous", runtimeRequest: true, group: "Phone",
|
|
142
|
+
playRestriction: "Restricted. Avoid unless app is a dialer/phone app. Justify use in Play Console.",
|
|
143
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE",
|
|
144
|
+
},
|
|
145
|
+
CALL_PHONE: {
|
|
146
|
+
fullName: "android.permission.CALL_PHONE",
|
|
147
|
+
type: "dangerous", runtimeRequest: true, group: "Phone",
|
|
148
|
+
playRestriction: "Core phone or VoIP apps only.",
|
|
149
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#CALL_PHONE",
|
|
150
|
+
},
|
|
151
|
+
SEND_SMS: {
|
|
152
|
+
fullName: "android.permission.SEND_SMS",
|
|
153
|
+
type: "dangerous", runtimeRequest: true, group: "SMS",
|
|
154
|
+
playRestriction: "Default SMS apps only. Play Store requires declaration.",
|
|
155
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#SEND_SMS",
|
|
156
|
+
},
|
|
157
|
+
READ_SMS: {
|
|
158
|
+
fullName: "android.permission.READ_SMS",
|
|
159
|
+
type: "dangerous", runtimeRequest: true, group: "SMS",
|
|
160
|
+
playRestriction: "Default SMS apps only. Heavily restricted.",
|
|
161
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#READ_SMS",
|
|
162
|
+
},
|
|
163
|
+
// ── Background / Battery ───────────────────────────────────────────────────
|
|
164
|
+
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: {
|
|
165
|
+
fullName: "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
|
|
166
|
+
type: "special", runtimeRequest: true,
|
|
167
|
+
playRestriction: "RESTRICTED — allowed only for VoIP, health monitoring, device management. Must submit declaration to Play Store.",
|
|
168
|
+
docUrl: "https://developer.android.com/training/monitoring-device-state/doze-standby#support_for_other_use_cases",
|
|
169
|
+
notes: "Prefer WorkManager with appropriate constraints instead.",
|
|
170
|
+
},
|
|
171
|
+
SCHEDULE_EXACT_ALARM: {
|
|
172
|
+
fullName: "android.permission.SCHEDULE_EXACT_ALARM",
|
|
173
|
+
type: "special", runtimeRequest: true, addedApi: 31,
|
|
174
|
+
playRestriction: "Calendar and alarm apps only. Justify to Play Store.",
|
|
175
|
+
docUrl: "https://developer.android.com/training/scheduling/alarms",
|
|
176
|
+
notes: "Use inexact alarms or WorkManager for non-user-visible timing.",
|
|
177
|
+
},
|
|
178
|
+
USE_EXACT_ALARM: {
|
|
179
|
+
fullName: "android.permission.USE_EXACT_ALARM",
|
|
180
|
+
type: "normal", runtimeRequest: false, addedApi: 33,
|
|
181
|
+
docUrl: "https://developer.android.com/training/scheduling/alarms",
|
|
182
|
+
notes: "Auto-granted for alarm/clock/calendar apps only. Others use SCHEDULE_EXACT_ALARM.",
|
|
183
|
+
},
|
|
184
|
+
// ── Biometrics ────────────────────────────────────────────────────────────
|
|
185
|
+
USE_BIOMETRIC: {
|
|
186
|
+
fullName: "android.permission.USE_BIOMETRIC",
|
|
187
|
+
type: "normal", runtimeRequest: false, addedApi: 28,
|
|
188
|
+
docUrl: "https://developer.android.com/training/sign-in/biometric-auth",
|
|
189
|
+
notes: "Normal permission — auto-granted. Use BiometricPrompt API, not FingerprintManager (deprecated).",
|
|
190
|
+
},
|
|
191
|
+
USE_FINGERPRINT: {
|
|
192
|
+
fullName: "android.permission.USE_FINGERPRINT",
|
|
193
|
+
type: "normal", runtimeRequest: false,
|
|
194
|
+
deprecatedOrRemovedApi: 28,
|
|
195
|
+
replacement: "USE_BIOMETRIC + BiometricPrompt",
|
|
196
|
+
docUrl: "https://developer.android.com/training/sign-in/biometric-auth",
|
|
197
|
+
},
|
|
198
|
+
// ── Overlay ───────────────────────────────────────────────────────────────
|
|
199
|
+
SYSTEM_ALERT_WINDOW: {
|
|
200
|
+
fullName: "android.permission.SYSTEM_ALERT_WINDOW",
|
|
201
|
+
type: "special", runtimeRequest: true,
|
|
202
|
+
playRestriction: "Restricted. Sends user to Special App Access. Justify in Play Console. Only for accessibility, call apps, floating widgets.",
|
|
203
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW",
|
|
204
|
+
notes: "Check Settings.canDrawOverlays(context) before attempting to use.",
|
|
205
|
+
},
|
|
206
|
+
// ── Health ────────────────────────────────────────────────────────────────
|
|
207
|
+
ACTIVITY_RECOGNITION: {
|
|
208
|
+
fullName: "android.permission.ACTIVITY_RECOGNITION",
|
|
209
|
+
type: "dangerous", runtimeRequest: true, addedApi: 29, group: "Activity",
|
|
210
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#ACTIVITY_RECOGNITION",
|
|
211
|
+
},
|
|
212
|
+
// ── NFC ───────────────────────────────────────────────────────────────────
|
|
213
|
+
NFC: {
|
|
214
|
+
fullName: "android.permission.NFC",
|
|
215
|
+
type: "normal", runtimeRequest: false,
|
|
216
|
+
docUrl: "https://developer.android.com/guide/topics/connectivity/nfc/nfc",
|
|
217
|
+
},
|
|
218
|
+
// ── Vibrate / Wake ─────────────────────────────────────────────────────────
|
|
219
|
+
VIBRATE: {
|
|
220
|
+
fullName: "android.permission.VIBRATE",
|
|
221
|
+
type: "normal", runtimeRequest: false,
|
|
222
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#VIBRATE",
|
|
223
|
+
},
|
|
224
|
+
WAKE_LOCK: {
|
|
225
|
+
fullName: "android.permission.WAKE_LOCK",
|
|
226
|
+
type: "normal", runtimeRequest: false,
|
|
227
|
+
docUrl: "https://developer.android.com/training/scheduling/wakelock",
|
|
228
|
+
notes: "Prefer WorkManager over manual wake locks for background work.",
|
|
229
|
+
},
|
|
230
|
+
FOREGROUND_SERVICE: {
|
|
231
|
+
fullName: "android.permission.FOREGROUND_SERVICE",
|
|
232
|
+
type: "normal", runtimeRequest: false,
|
|
233
|
+
docUrl: "https://developer.android.com/develop/background-work/services/foreground-services",
|
|
234
|
+
notes: "On API 34+ you must also declare FOREGROUND_SERVICE_<type> (e.g. FOREGROUND_SERVICE_LOCATION).",
|
|
235
|
+
},
|
|
236
|
+
FOREGROUND_SERVICE_LOCATION: {
|
|
237
|
+
fullName: "android.permission.FOREGROUND_SERVICE_LOCATION",
|
|
238
|
+
type: "normal", runtimeRequest: false, addedApi: 34,
|
|
239
|
+
docUrl: "https://developer.android.com/develop/background-work/services/fg-service-types",
|
|
240
|
+
},
|
|
241
|
+
RECEIVE_BOOT_COMPLETED: {
|
|
242
|
+
fullName: "android.permission.RECEIVE_BOOT_COMPLETED",
|
|
243
|
+
type: "normal", runtimeRequest: false,
|
|
244
|
+
docUrl: "https://developer.android.com/reference/android/Manifest.permission#RECEIVE_BOOT_COMPLETED",
|
|
245
|
+
notes: "Declare in manifest. Use WorkManager with PERSIST_ACROSS_REBOOTS for deferrable tasks.",
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
// ── Patterns ─────────────────────────────────────────────────────────────────
|
|
249
|
+
const RUNTIME_REQUEST_PATTERN = `
|
|
250
|
+
## Runtime Permission Request Pattern (Modern — ActivityResultContracts)
|
|
251
|
+
|
|
252
|
+
\`\`\`kotlin
|
|
253
|
+
// Source: developer.android.com/training/permissions/requesting
|
|
254
|
+
|
|
255
|
+
// Single permission
|
|
256
|
+
val requestPermissionLauncher = registerForActivityResult(
|
|
257
|
+
ActivityResultContracts.RequestPermission()
|
|
258
|
+
) { isGranted ->
|
|
259
|
+
if (isGranted) {
|
|
260
|
+
// Permission granted — proceed
|
|
261
|
+
} else {
|
|
262
|
+
// Show rationale or gracefully degrade
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Multiple permissions
|
|
267
|
+
val requestMultipleLauncher = registerForActivityResult(
|
|
268
|
+
ActivityResultContracts.RequestMultiplePermissions()
|
|
269
|
+
) { permissions ->
|
|
270
|
+
val cameraGranted = permissions[Manifest.permission.CAMERA] == true
|
|
271
|
+
val micGranted = permissions[Manifest.permission.RECORD_AUDIO] == true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Triggering the request
|
|
275
|
+
fun requestCameraPermission() {
|
|
276
|
+
when {
|
|
277
|
+
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
|
278
|
+
== PackageManager.PERMISSION_GRANTED -> {
|
|
279
|
+
// Already granted
|
|
280
|
+
}
|
|
281
|
+
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
|
|
282
|
+
// Show rationale UI, then request
|
|
283
|
+
showRationale { requestPermissionLauncher.launch(Manifest.permission.CAMERA) }
|
|
284
|
+
}
|
|
285
|
+
else -> requestPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
### In Jetpack Compose — use Accompanist or rememberLauncherForActivityResult
|
|
291
|
+
\`\`\`kotlin
|
|
292
|
+
@Composable
|
|
293
|
+
fun CameraFeature() {
|
|
294
|
+
val launcher = rememberLauncherForActivityResult(
|
|
295
|
+
ActivityResultContracts.RequestPermission()
|
|
296
|
+
) { granted ->
|
|
297
|
+
if (!granted) { /* show rationale */ }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
val context = LocalContext.current
|
|
301
|
+
val hasPermission = ContextCompat.checkSelfPermission(
|
|
302
|
+
context, Manifest.permission.CAMERA
|
|
303
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
304
|
+
|
|
305
|
+
if (hasPermission) {
|
|
306
|
+
CameraPreview()
|
|
307
|
+
} else {
|
|
308
|
+
Button(onClick = { launcher.launch(Manifest.permission.CAMERA) }) {
|
|
309
|
+
Text("Enable Camera")
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
### Anti-patterns
|
|
316
|
+
| ❌ Don't | ✅ Do Instead |
|
|
317
|
+
|---------|-------------|
|
|
318
|
+
| Request all permissions at startup | Request only when the feature is invoked |
|
|
319
|
+
| Use \`onRequestPermissionsResult\` override | Use \`ActivityResultContracts\` |
|
|
320
|
+
| Ignore \`shouldShowRequestPermissionRationale\` | Always check and show rationale |
|
|
321
|
+
| Request foreground + background together | Request foreground first, background later |
|
|
322
|
+
| Crash when permission denied | Gracefully degrade UI |
|
|
323
|
+
`;
|
|
324
|
+
// ── Lookup helpers ────────────────────────────────────────────────────────────
|
|
325
|
+
function findPermission(query) {
|
|
326
|
+
const q = query.toUpperCase().replace(/^android\.permission\./i, "").replace(/\s+/g, "_");
|
|
327
|
+
for (const [key, entry] of Object.entries(PERMISSIONS)) {
|
|
328
|
+
if (key.toUpperCase() === q)
|
|
329
|
+
return [key, entry];
|
|
330
|
+
}
|
|
331
|
+
for (const [key, entry] of Object.entries(PERMISSIONS)) {
|
|
332
|
+
if (key.toUpperCase().includes(q) || q.includes(key.toUpperCase()))
|
|
333
|
+
return [key, entry];
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
function formatEntry(key, entry) {
|
|
338
|
+
const emoji = {
|
|
339
|
+
normal: "🟢", dangerous: "🔴", signature: "🔒", special: "🟠", removed: "❌"
|
|
340
|
+
};
|
|
341
|
+
return [
|
|
342
|
+
`## Permission: \`${entry.fullName}\``,
|
|
343
|
+
`**Type:** ${emoji[entry.type]} ${entry.type.toUpperCase()}`,
|
|
344
|
+
`**Runtime Request Required:** ${entry.runtimeRequest ? "✅ Yes (API 23+)" : "❌ No — install-time grant"}`,
|
|
345
|
+
entry.group ? `**Permission Group:** ${entry.group}` : "",
|
|
346
|
+
entry.addedApi ? `**Added:** API ${entry.addedApi}` : "",
|
|
347
|
+
entry.deprecatedOrRemovedApi ? `**⚠️ Deprecated/Removed:** API ${entry.deprecatedOrRemovedApi}` : "",
|
|
348
|
+
entry.replacement ? `**Use Instead:** ${entry.replacement}` : "",
|
|
349
|
+
entry.playRestriction ? `\n**🏪 Play Store:** ⚠️ ${entry.playRestriction}` : "",
|
|
350
|
+
`**Official Docs:** ${entry.docUrl}`,
|
|
351
|
+
entry.notes ? `**Notes:** ${entry.notes}` : "",
|
|
352
|
+
"",
|
|
353
|
+
entry.type === "removed"
|
|
354
|
+
? `> ❌ GROUNDING GATE: Do NOT use this permission. Use: ${entry.replacement ?? "see docs"}.`
|
|
355
|
+
: entry.type === "special"
|
|
356
|
+
? `> 🟠 GROUNDING GATE: Special permission — sends user to Settings. Ensure genuine use case before requesting.`
|
|
357
|
+
: entry.runtimeRequest
|
|
358
|
+
? `> 🔴 GROUNDING GATE: Runtime permission — use ActivityResultContracts.RequestPermission().`
|
|
359
|
+
: `> 🟢 GROUNDING GATE: Normal permission — declare in AndroidManifest.xml, no runtime request needed.`,
|
|
360
|
+
].filter(Boolean).join("\n");
|
|
361
|
+
}
|
|
362
|
+
const INDEX = `
|
|
363
|
+
## Android Permission Advisor
|
|
364
|
+
|
|
365
|
+
**Query a specific permission** — pass the name (e.g. "CAMERA", "ACCESS_FINE_LOCATION", "POST_NOTIFICATIONS")
|
|
366
|
+
**Query "runtime pattern"** — see the official ActivityResultContracts request pattern
|
|
367
|
+
**Query "list"** — see all permissions in registry
|
|
368
|
+
|
|
369
|
+
### Permission Types Quick Reference
|
|
370
|
+
| Type | Risk | Granted By |
|
|
371
|
+
|------|------|-----------|
|
|
372
|
+
| 🟢 Normal | Low | Auto at install |
|
|
373
|
+
| 🔴 Dangerous | High | Runtime dialog (API 23+) |
|
|
374
|
+
| 🔒 Signature | App-to-app | Auto if same certificate |
|
|
375
|
+
| 🟠 Special | System-level | User navigates to Settings |
|
|
376
|
+
| ❌ Removed | N/A | Do not use |
|
|
377
|
+
|
|
378
|
+
**Official reference:** https://developer.android.com/guide/topics/permissions/overview
|
|
379
|
+
`;
|
|
380
|
+
// ── Main handler ──────────────────────────────────────────────────────────────
|
|
381
|
+
export async function androidPermissionAdvisor(query) {
|
|
382
|
+
const trimmed = query.trim();
|
|
383
|
+
if (!trimmed || trimmed.toLowerCase() === "list") {
|
|
384
|
+
const list = Object.entries(PERMISSIONS)
|
|
385
|
+
.map(([k, v]) => `- \`${k}\` — ${v.type}${v.runtimeRequest ? " (runtime)" : ""}`)
|
|
386
|
+
.join("\n");
|
|
387
|
+
return INDEX + `\n### Registry (${Object.keys(PERMISSIONS).length} permissions)\n` + list;
|
|
388
|
+
}
|
|
389
|
+
if (trimmed.toLowerCase().includes("runtime pattern") || trimmed.toLowerCase().includes("how to request")) {
|
|
390
|
+
return RUNTIME_REQUEST_PATTERN + `\n\n**Official guide:** https://developer.android.com/training/permissions/requesting`;
|
|
391
|
+
}
|
|
392
|
+
const found = findPermission(trimmed);
|
|
393
|
+
if (found)
|
|
394
|
+
return formatEntry(found[0], found[1]);
|
|
395
|
+
// Live fallback
|
|
396
|
+
const url = `https://developer.android.com/s/results?q=${encodeURIComponent("android permission " + trimmed)}`;
|
|
397
|
+
try {
|
|
398
|
+
const html = await secureFetch(url);
|
|
399
|
+
return `## Permission: "${trimmed}"\n\nNot in registry. Live search from developer.android.com:\n\n${extractPageText(html, 1500)}\n\n**Search URL:** ${url}`;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return `## Permission: "${trimmed}"\n\nNot in registry. Check manually:\nhttps://developer.android.com/reference/android/Manifest.permission\nhttps://developer.android.com/guide/topics/permissions/overview`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Tool 18: Play Store Policy Advisor
|
|
2
|
+
// October 2025 policy changes affect app code, architecture, and manifest declarations.
|
|
3
|
+
// AI tools have zero awareness of these changes. Review failures cost weeks of re-submission.
|
|
4
|
+
export async function androidPlayPolicyAdvisor(topic) {
|
|
5
|
+
const t = topic.toLowerCase().trim();
|
|
6
|
+
const overview = `
|
|
7
|
+
# Google Play Store Policy Reference (2025–2026)
|
|
8
|
+
Source: https://support.google.com/googleplay/android-developer/answer/9904549
|
|
9
|
+
|
|
10
|
+
## Most Recent Policy Changes (October 2025 Enforcement Start)
|
|
11
|
+
|
|
12
|
+
| Policy | Effective | Who's Affected |
|
|
13
|
+
|--------|-----------|---------------|
|
|
14
|
+
| Restrict Minor Access API | Oct 2025 | Dating, gambling, real-money gaming apps |
|
|
15
|
+
| Medical Device labeling (EU) | Oct 2025 | Health/medical apps in European markets |
|
|
16
|
+
| Digital Lending compliance (India) | Oct 2025 | Personal loan apps in India |
|
|
17
|
+
| Subscription charge transparency | Oct 2025 | All apps with subscriptions |
|
|
18
|
+
| API 36 targeting mandate | Aug 2026 | ALL apps — large-screen compliance |
|
|
19
|
+
| Large-screen quality badge | Active | All apps — affects search placement |
|
|
20
|
+
|
|
21
|
+
## API 36 Targeting Mandate (August 2026)
|
|
22
|
+
|
|
23
|
+
Every app published on Google Play must target API 36 by August 2026. This means:
|
|
24
|
+
- minSdk can remain at 21+ (no change to minimum)
|
|
25
|
+
- targetSdk must be 36
|
|
26
|
+
- Apps MUST handle all screen sizes, aspect ratios, and multi-window on ≥600dp devices
|
|
27
|
+
- Apps MUST support Predictive Back Gesture
|
|
28
|
+
|
|
29
|
+
See the android_api36_compliance tool for the full technical implementation guide.
|
|
30
|
+
|
|
31
|
+
Source: https://support.google.com/googleplay/android-developer/answer/9904549
|
|
32
|
+
`;
|
|
33
|
+
const restrictMinorAccess = `
|
|
34
|
+
# Play Store Policy — Restrict Minor Access (October 2025)
|
|
35
|
+
Source: https://support.google.com/googleplay/android-developer/answer/14191470
|
|
36
|
+
|
|
37
|
+
## Who This Affects
|
|
38
|
+
Apps in these categories on Google Play:
|
|
39
|
+
- Dating apps
|
|
40
|
+
- Gambling and casino apps
|
|
41
|
+
- Real-money games
|
|
42
|
+
- Apps containing mature content
|
|
43
|
+
|
|
44
|
+
## What's Required — Code Implementation
|
|
45
|
+
|
|
46
|
+
Apps must implement the "Restrict Minor Access" feature. This is NOT just a content rating.
|
|
47
|
+
It requires a specific API integration with Google Play's Family Policy system.
|
|
48
|
+
|
|
49
|
+
\`\`\`kotlin
|
|
50
|
+
// Implementation uses Play Core Family APIs
|
|
51
|
+
// Check the current official docs for latest SDK version
|
|
52
|
+
// https://developer.android.com/google/play/billing/families
|
|
53
|
+
|
|
54
|
+
// Manifest — declare the feature
|
|
55
|
+
<meta-data
|
|
56
|
+
android:name="com.google.android.gms.version"
|
|
57
|
+
android:value="@integer/google_play_services_version" />
|
|
58
|
+
|
|
59
|
+
// In your app, check parental controls status before showing restricted content
|
|
60
|
+
class AgeVerificationRepository @Inject constructor(
|
|
61
|
+
private val context: Context
|
|
62
|
+
) {
|
|
63
|
+
fun isRestrictedContentAllowed(): Flow<Boolean> = callbackFlow {
|
|
64
|
+
// Integrate with Google Play Family APIs
|
|
65
|
+
// Implementation details depend on your app category
|
|
66
|
+
// Reference: https://developer.android.com/google/play/billing/families
|
|
67
|
+
trySend(true) // Default — replace with actual implementation
|
|
68
|
+
awaitClose()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
## What Happens If You Don't Comply
|
|
74
|
+
Apps that don't implement this in affected categories will be removed from Play Store
|
|
75
|
+
for the applicable markets where enforcement is active.
|
|
76
|
+
|
|
77
|
+
Source: https://support.google.com/googleplay/android-developer/answer/14191470
|
|
78
|
+
`;
|
|
79
|
+
const subscriptions = `
|
|
80
|
+
# Play Store Policy — Subscription Transparency (2025)
|
|
81
|
+
Source: https://support.google.com/googleplay/android-developer/answer/140504
|
|
82
|
+
|
|
83
|
+
## What's Required
|
|
84
|
+
|
|
85
|
+
All subscription purchase screens must:
|
|
86
|
+
1. Clearly display the **total charge** before the free trial ends (if applicable)
|
|
87
|
+
2. Show the **recurring billing amount and frequency** prominently
|
|
88
|
+
3. Include a **cancellation mechanism** accessible within the app (not just email/web)
|
|
89
|
+
4. Not obscure prices with dark patterns
|
|
90
|
+
|
|
91
|
+
## Implementation Checklist
|
|
92
|
+
|
|
93
|
+
\`\`\`kotlin
|
|
94
|
+
// ✅ Use Play Billing Library 7+ for subscription management
|
|
95
|
+
// libs.versions.toml
|
|
96
|
+
[versions]
|
|
97
|
+
billing = "7.1.1"
|
|
98
|
+
|
|
99
|
+
[libraries]
|
|
100
|
+
billing = { group = "com.android.billingclient", name = "billing-ktx", version.ref = "billing" }
|
|
101
|
+
|
|
102
|
+
// ✅ Display full price prominently using ProductDetails
|
|
103
|
+
fun displaySubscriptionDetails(productDetails: ProductDetails) {
|
|
104
|
+
val subscriptionOffer = productDetails.subscriptionOfferDetails?.firstOrNull()
|
|
105
|
+
val pricingPhase = subscriptionOffer?.pricingPhases?.pricingPhaseList?.lastOrNull()
|
|
106
|
+
|
|
107
|
+
// Show: formattedPrice + "/" + billingPeriod
|
|
108
|
+
// e.g. "$9.99/month" — NOT just "$9.99"
|
|
109
|
+
val billingPeriod = pricingPhase?.billingPeriod // ISO 8601: P1M = 1 month, P1Y = 1 year
|
|
110
|
+
val price = pricingPhase?.formattedPrice
|
|
111
|
+
|
|
112
|
+
// ✅ Required text: "You will be charged $price per [period] until you cancel"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ✅ In-app subscription management (required — not just deep link to Play)
|
|
116
|
+
fun openSubscriptionManagement(context: Context) {
|
|
117
|
+
// Must be accessible in-app, not just via Play Store URL
|
|
118
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
119
|
+
data = Uri.parse("https://play.google.com/store/account/subscriptions")
|
|
120
|
+
}
|
|
121
|
+
context.startActivity(intent)
|
|
122
|
+
// OR implement a native in-app subscription management screen
|
|
123
|
+
}
|
|
124
|
+
\`\`\`
|
|
125
|
+
|
|
126
|
+
Source: https://support.google.com/googleplay/android-developer/answer/140504
|
|
127
|
+
`;
|
|
128
|
+
const permissions = `
|
|
129
|
+
# Play Store Policy — Permissions & Data Safety (2025)
|
|
130
|
+
Source: https://support.google.com/googleplay/android-developer/answer/10787469
|
|
131
|
+
|
|
132
|
+
## Data Safety Section — Required Declarations
|
|
133
|
+
|
|
134
|
+
Every app on Play Store must accurately declare in the Data Safety section:
|
|
135
|
+
- What data is collected
|
|
136
|
+
- Whether data is shared with third parties
|
|
137
|
+
- Whether data is encrypted in transit and at rest
|
|
138
|
+
- Whether users can request data deletion
|
|
139
|
+
|
|
140
|
+
**Non-disclosure = policy violation.** Review the Data Safety form annually as your
|
|
141
|
+
app's data practices evolve.
|
|
142
|
+
|
|
143
|
+
## Restricted Permissions — Still Requiring Justification in 2026
|
|
144
|
+
|
|
145
|
+
These permissions require a declaration of purpose and may be rejected at review:
|
|
146
|
+
|
|
147
|
+
| Permission | Restriction | Alternative |
|
|
148
|
+
|-----------|-------------|-------------|
|
|
149
|
+
| READ_CALL_LOG | High restriction | Use CallScreeningService for screening only |
|
|
150
|
+
| PROCESS_OUTGOING_CALLS | High restriction | Deprecated — use CallRedirectionService |
|
|
151
|
+
| MANAGE_EXTERNAL_STORAGE | High restriction | Use MediaStore or SAF (Storage Access Framework) |
|
|
152
|
+
| ACCESS_BACKGROUND_LOCATION | High restriction | Must justify in console — prefer foreground only |
|
|
153
|
+
| READ_CONTACTS (bulk export) | Medium restriction | Request individual contacts via ContactsContract |
|
|
154
|
+
| SYSTEM_ALERT_WINDOW | Medium restriction | Use overlay only for documented use cases |
|
|
155
|
+
|
|
156
|
+
## SMS & Call Log Policies
|
|
157
|
+
|
|
158
|
+
Apps that are NOT the default SMS app, default dialer, or device/profile owner CANNOT use:
|
|
159
|
+
- READ_SMS, RECEIVE_SMS, READ_CALL_LOG, WRITE_CALL_LOG, PROCESS_OUTGOING_CALLS
|
|
160
|
+
|
|
161
|
+
If your app requests these and is not the default handler, it will be rejected.
|
|
162
|
+
|
|
163
|
+
Source: https://support.google.com/googleplay/android-developer/answer/10787469
|
|
164
|
+
`;
|
|
165
|
+
const largeScreenQuality = `
|
|
166
|
+
# Play Store — Large-Screen Quality Program (Direct Revenue Impact)
|
|
167
|
+
Source: https://developer.android.com/docs/quality-guidelines/large-screen-app-quality
|
|
168
|
+
|
|
169
|
+
## Why This Matters for Business
|
|
170
|
+
|
|
171
|
+
Google Play actively promotes adaptive apps and demotes non-compliant ones:
|
|
172
|
+
- Apps that FAIL large-screen checks receive a visible warning badge on their Play listing
|
|
173
|
+
- Warning badges are a direct conversion-rate deterrent — users see it before downloading
|
|
174
|
+
- Apps that PASS quality checks are eligible for Editorial promotion and search boosts
|
|
175
|
+
- Tablet + phone users spend 9× more on apps. Foldable users spend 14× more.
|
|
176
|
+
|
|
177
|
+
## The Three Tiers — What Each Unlocks
|
|
178
|
+
|
|
179
|
+
### Tier 3: Large Screen Ready (Required to avoid warning badge)
|
|
180
|
+
Shows badge: "Designed for tablets" — minimum bar
|
|
181
|
+
- No orientation/resizability locks
|
|
182
|
+
- Handles configuration changes without crashes
|
|
183
|
+
- Basic keyboard/mouse support
|
|
184
|
+
|
|
185
|
+
### Tier 2: Large Screen Optimized
|
|
186
|
+
Eligible for "Optimized for large screens" badge
|
|
187
|
+
- Adaptive layouts with WindowSizeClass
|
|
188
|
+
- No content clipped in any window size or orientation
|
|
189
|
+
|
|
190
|
+
### Tier 1: Large Screen Differentiated
|
|
191
|
+
Eligible for Editors' Choice and app features
|
|
192
|
+
- Multi-pane layouts
|
|
193
|
+
- Drag-and-drop
|
|
194
|
+
- Foldable hinge awareness
|
|
195
|
+
|
|
196
|
+
## Checking Your App's Quality Tier
|
|
197
|
+
|
|
198
|
+
In Google Play Console:
|
|
199
|
+
1. Go to **Android vitals** → **App compatibility** → **Large screens**
|
|
200
|
+
2. Review the automated checks and their pass/fail status
|
|
201
|
+
3. Fix failing checks using the android_large_screen_guide and android_api36_compliance tools
|
|
202
|
+
|
|
203
|
+
Source: https://developer.android.com/docs/quality-guidelines/large-screen-app-quality
|
|
204
|
+
`;
|
|
205
|
+
if (t.includes("minor") || t.includes("restrict") || t.includes("age") || t.includes("dating") || t.includes("gambling")) {
|
|
206
|
+
return restrictMinorAccess;
|
|
207
|
+
}
|
|
208
|
+
if (t.includes("subscri") || t.includes("billing") || t.includes("payment")) {
|
|
209
|
+
return subscriptions;
|
|
210
|
+
}
|
|
211
|
+
if (t.includes("permission") || t.includes("data safety") || t.includes("sms") || t.includes("call log")) {
|
|
212
|
+
return permissions;
|
|
213
|
+
}
|
|
214
|
+
if (t.includes("large screen") || t.includes("tablet") || t.includes("quality") || t.includes("badge")) {
|
|
215
|
+
return largeScreenQuality;
|
|
216
|
+
}
|
|
217
|
+
return overview + "\n\n---\n\n" +
|
|
218
|
+
"**Query topics:** 'restrict minor access' (dating/gambling apps), 'subscriptions' (billing transparency), " +
|
|
219
|
+
"'permissions' (restricted permissions + data safety), 'large screen quality' (Play Store badge + revenue impact)\n\n" +
|
|
220
|
+
"Source: https://support.google.com/googleplay/android-developer/";
|
|
221
|
+
}
|