On this page
Testing Android runtime permissions on physical mobile devices represents one of the most persistent and fragile hurdles in mobile software quality assurance. Ever since Android 6.0 Marshmallow introduced dynamic runtime permissions, mobile applications have been required to request sensitive access—such as camera, precise location, audio recording, read media, and notification privileges—at the exact moment of execution rather than during initial package installation. For end users, this granular permission model provides essential security and data privacy. For mobile engineering teams and continuous integration pipelines, however, it introduces an agonizing testing paradox. The system permission prompt that appears on screen does not belong to the application under test; it is an external modal managed exclusively by the operating system's internal com.google.android.permissioncontroller package. Traditional in-process test runners like Espresso are structurally sandboxed to the target application's memory space and process boundary, rendering them completely blind to the system dialog. Consequently, developers either bypass permission testing entirely using artificial pre-grant rules like GrantPermissionRule, or construct brittle UIAutomator scripts that break whenever an OEM modifies button resource IDs. By pairing terminal-based AI coding agents like Claude Code with the Model Context Protocol (MCP) through ScreenSync MCP, developers can finally bridge the gap between application business logic and system-level security dialogs, orchestrating comprehensive automated permission flows across real physical Android hardware.
Key Takeaways
- System-level modal traversal: ScreenSync MCP extracts the complete system accessibility tree via
get_ui_hierarchy, allowing Claude Code to inspect and interact with Android'scom.google.android.permissioncontrollerdialogs without encountering in-process runner barriers. - Deterministic ADB permission manipulation: Claude Code programmatically grants, revokes, and resets runtime permissions via Android Debug Bridge shell commands (
pm grant,pm revoke,pm reset-permissions) to validate edge-case state machines. - Continuous local MediaProjection streaming: High-fidelity display frames stream from the physical Android device to the desktop hub via native MediaProjection at approximately 94 milliseconds median latency without consuming ADB bandwidth or requiring root privileges.
- Complete denial and rationale validation: Automate multi-step permission lifecycles, including first-time user denial, contextual in-app educational rationale dialogs, permanent refusal ("Don't ask again"), and automated redirects to system app settings.
- Modern Android 13 and 14 permission matrix: Full support for evaluating granular media access (
READ_MEDIA_VISUAL_USER_SELECTED), runtime notification prompts (POST_NOTIFICATIONS), and precise versus approximate location selections. - Unified 226-tool automation ecosystem: ScreenSync MCP combines 29 mobile and desktop tools (12
control_*, 3os_*, and 14 capture tools) with 197 browser tools to verify cross-surface permission lifecycles and administrative cloud synchronization.
Why In-Process Test Suites Fail at Android Permission Dialogs
In-process test suites like Espresso fail at Android permission dialogs because permission prompts execute inside the system's external com.google.android.permissioncontroller process rather than the application's process sandbox. Isolated test runners cannot interact with system windows without crashing, while pre-granting rules completely bypass educational rationale and denial logic.
To understand why automated mobile permission testing has historically been so brittle, one must examine the execution sandbox of traditional Android instrumentation frameworks. When a developer runs an Espresso test suite via Android JUnit Runner (AndroidJUnitRunner), the testing framework compiles a secondary test APK that executes within the target application's memory space via InstrumentationRegistry. Espresso communicates directly with the application's View hierarchy and the Android main looper (Looper.getMainLooper()), idling automatically until user interface animations and asynchronous message queues settle.
However, when an application invokes ActivityResultContracts.RequestPermission() to request access to dangerous hardware features like the camera or microphone, the Android framework delegates the prompt presentation to an entirely separate system service. The system suspends user interaction with the application activity and inflates an interactive system dialog managed by com.google.android.permissioncontroller (or com.android.permissioncontroller on AOSP builds).
Because Espresso's view matchers (onView(withId(R.id.permission_allow_button))) search strictly within the decor view of the application under test, Espresso immediately throws a fatal NoMatchingViewException. The test runner cannot reach across process boundaries into the system window manager:
androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: with id: permission_allow_button
If the target view is not part of the view hierarchy, you may need to use UIAutomator instead.
To circumvent this barrier, mobile developers frequently resort to one of two suboptimal compromises:
- Synthetic Pre-Granting (
GrantPermissionRule): Frameworks like Espresso provideGrantPermissionRule.grant(Manifest.permission.CAMERA)to inject granted permission flags before the activity launches. While this permits testing downstream features that depend on an active camera, it completely ignores the permission request interaction itself. The app's permission rationale banner, user denial handling, graceful feature degradation, and retry states remain entirely untested in continuous integration. - Brittle UIAutomator Scripts: Developers combine Espresso with UIAutomator (
UiDevice.getInstance().findObject()). However, UIAutomator scripts rely heavily on hardcoded string selectors (textMatches("(?i)(Allow|While using the app)")) or undocumented OEM resource IDs. When OEM vendors like Samsung (One UI), Xiaomi (HyperOS), and Google (Pixel) alter button wording, dialog geometry, or layout hierarchies, test suites fail spuriously withUiObjectNotFoundException.
ScreenSync MCP resolves this dilemma by decoupling test execution from the application process memory space. Operating over a clean Model Context Protocol interface, Claude Code interacts with the Android device from the host workstation. ScreenSync MCP queries the Android Window Manager through system-level accessibility interfaces, providing Claude Code with the authoritative node hierarchy of the active foreground window—regardless of whether that window belongs to your application, the OS Permission Controller, or the Android Settings app.
| Permission Testing Dimension | In-Process Espresso + GrantPermissionRule | Standalone UIAutomator Scripts | Cloud Device Farms (SauceLabs / BrowserStack) | ScreenSync MCP + Claude Code (Physical Device) |
|---|---|---|---|---|
| System Dialog Interaction | Impossible; cannot cross process boundary into system UI | Possible, but requires fragile selector regex and hardcoded timeouts | Inconsistent; OEM permission variations frequently cause test timeouts | Native; inspects exact system dialog hierarchy via get_ui_hierarchy |
| Testing User Denial ("Don't allow") | Impossible; pre-granting forces all permissions to GRANTED | Brittle; requires complex conditional branch scripting | Difficult; resetting permissions between runs adds heavy latency | Effortless; agent triggers denial via control_tap and asserts fallback UI |
| Educational Rationale Validation | Completely bypassed; rationale is never rendered | Extremely complex to coordinate with async UI state | High flakiness due to network latency and cloud frame lag | Full verification; asserts rationale cards before triggering system prompt |
| Mid-Test Permission Revocation | Impossible without terminating the test instrumentation runner | Requires awkward executeShellCommand sub-invocations |
Drops remote video session or resets whole virtual machine container | Instantaneous; agent executes adb shell pm revoke and observes app triage |
| Android 14 Partial Media Testing | Unsupported; cannot select specific media items in picker | Brittle; cannot reliably navigate native Photo Picker grid | High latency when streaming remote photo selection grids | Complete; agent navigates system Photo Picker and verifies selected IDs |
| Visual Assertion Fidelity | Pixel tests require separate in-memory screenshot libraries | Low frame rate screen capture with heavy ADB latency | Compressed cloud WebRTC stream prone to compression artifacts | Native MediaProjection mirror (~94 ms latency) via get_latest_screenshot |
Android Modern Runtime Permission Architecture: Sandboxing, Grants, and Revocations
Android's runtime permission model isolates sensitive hardware and data behind dangerous protection levels requiring explicit user consent during execution. Modern releases introduce granular media selections, one-time permission grants, and automatic background revocation, demanding exhaustive testing across prompt displays, user denials, and permanent refusal states.
Navigating modern Android permission requirements demands a rigorous understanding of the operating system's security architecture. Android categorizes permissions into distinct protection levels:
- Normal Permissions: Low-risk privileges that do not compromise user privacy or expose sensitive hardware (such as
ACCESS_NETWORK_STATE,INTERNET, orWAKE_LOCK). The operating system grants normal permissions automatically at install time without user prompts. - Dangerous (Runtime) Permissions: High-risk privileges that access private user data or control device hardware (such as
CAMERA,RECORD_AUDIO,ACCESS_FINE_LOCATION,READ_CONTACTS, andBODY_SENSORS). Applications must request dangerous permissions dynamically at runtime, and users maintain absolute authority to grant, deny, or revoke them at any time. - Special App Access Permissions: Elevated system capabilities (such as
SYSTEM_ALERT_WINDOW,REQUEST_INSTALL_PACKAGES, andMANAGE_EXTERNAL_STORAGE). These permissions cannot be granted through standard dialogs; applications must direct users to dedicated sub-pages inside system Settings.
Recent Android platform versions have progressively refined dangerous permissions to maximize privacy:
- Android 11 (API 30) One-Time Permissions & Auto-Reset: Introduced "Only this time" permission grants. If a user backgrounds an application after selecting this option, the OS revokes the permission when the activity process terminates. Furthermore, Android automatically resets runtime permissions for applications left unused for several months.
- Android 12 (API 31) Approximate vs. Precise Location: Splinters
ACCESS_FINE_LOCATIONinto separate UI selections. Users can grant approximate location (ACCESS_COARSE_LOCATION) while explicitly withholding precise GPS coordinates. Similarly, Android 12 decoupled Bluetooth scanning from location into separate permissions, as demonstrated in our guide on testing Android Bluetooth and BLE with Claude Code and MCP. - Android 13 (API 33) Runtime Notifications: Introduced
POST_NOTIFICATIONSas a dangerous runtime permission. Applications targeting API 33+ can no longer post notification messages without explicit user approval. - Android 14 (API 34) Selected Photos Access: Replaced broad storage access with
READ_MEDIA_VISUAL_USER_SELECTED. Users can grant an application temporary access to specific photos or videos without exposing their entire media library. - Android 14/15 Foreground Service Type Permissions: In API 34 and API 35, declaring
FOREGROUND_SERVICErequires specialized type permissions and user-initiated execution; explore our complete guide on testing Android foreground services with Claude Code and MCP. - Android 14 Export Security on Receivers: Android 14 also introduced mandatory export scoping for dynamic listeners; learn how to automate intent validation in our guide on testing Android broadcast receivers with Claude Code and MCP.
Modern Kotlin Implementation with Activity Result Contracts
In modern Android engineering, legacy permission dispatchers (ActivityCompat.requestPermissions and onRequestPermissionsResult) have been entirely superseded by the Activity Result API (ActivityResultContracts). Here is a production-grade Kotlin implementation demonstrating a defensive permission architecture with educational rationale handling and system settings redirection:
package com.example.screensync.permissions
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
class CameraPermissionActivity : ComponentActivity() {
private var permissionState by mutableStateOf("NOT_REQUESTED")
private var showRationaleDialog by mutableStateOf(false)
private var showSettingsRedirectDialog by mutableStateOf(false)
// Modern Activity Result Contract for Single Permission Request
private val requestCameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
permissionState = "GRANTED"
} else {
// Determine if the user clicked "Don't allow" or checked "Don't ask again"
val shouldShowRationale = shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)
if (shouldShowRationale) {
// First or second denial: show contextual educational rationale
permissionState = "DENIED"
showRationaleDialog = true
} else {
// Permanent denial ("Don't ask again" or OS policy): direct to settings
permissionState = "PERMANENTLY_DENIED"
showSettingsRedirectDialog = true
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
updateInitialState()
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
PermissionScreen(
permissionState = permissionState,
showRationaleDialog = showRationaleDialog,
showSettingsRedirectDialog = showSettingsRedirectDialog,
onRequestPermission = { launchPermissionFlow() },
onDismissRationale = { showRationaleDialog = false },
onOpenSettings = { openAppSettings() }
)
}
}
}
}
override fun onResume() {
super.onResume()
// Refresh permission state in case the user toggled it in System Settings
updateInitialState()
}
private fun updateInitialState() {
val current = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
permissionState = if (current == PackageManager.PERMISSION_GRANTED) "GRANTED" else "NOT_REQUESTED"
}
private fun launchPermissionFlow() {
when {
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
permissionState = "GRANTED"
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showRationaleDialog = true
}
else -> {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
private fun openAppSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
}
When building apps with modern reactive UI toolchains, developers can apply testing Jetpack Compose apps with Claude Code and MCP to verify that composable state holders respond instantaneously to permission state updates without visual tearing or UI freezes.
| Android Runtime Permission | Manifest Permission Constant | Protection Level & Target API | System Permission Dialog Action / Label | ADB Manipulation Shell Command |
|---|---|---|---|---|
| Camera Access | android.permission.CAMERA |
Dangerous (API 23+) | "While using the app" / "Only this time" / "Don't allow" | adb shell pm grant <pkg> android.permission.CAMERA |
| Precise GPS Location | android.permission.ACCESS_FINE_LOCATION |
Dangerous (API 23+) | "Precise" toggle + "While using the app" | adb shell pm grant <pkg> android.permission.ACCESS_FINE_LOCATION |
| Approximate Location | android.permission.ACCESS_COARSE_LOCATION |
Dangerous (API 23+) | "Approximate" toggle + "While using the app" | adb shell pm grant <pkg> android.permission.ACCESS_COARSE_LOCATION |
| Microphone / Audio | android.permission.RECORD_AUDIO |
Dangerous (API 23+) | "While using the app" / "Only this time" / "Don't allow" | adb shell pm grant <pkg> android.permission.RECORD_AUDIO |
| Notification Posting | android.permission.POST_NOTIFICATIONS |
Dangerous (API 33+ Android 13) | "Allow" / "Don't allow" modal | adb shell pm grant <pkg> android.permission.POST_NOTIFICATIONS |
| Selected Photo Picker | android.permission.READ_MEDIA_VISUAL_USER_SELECTED |
Dangerous (API 34+ Android 14) | "Select photos and videos" / "Allow all" / "Don't allow" | adb shell pm grant <pkg> android.permission.READ_MEDIA_VISUAL_USER_SELECTED |
| Read Audio Files | android.permission.READ_MEDIA_AUDIO |
Dangerous (API 33+ Android 13) | "Allow" / "Don't allow" modal | adb shell pm grant <pkg> android.permission.READ_MEDIA_AUDIO |
| All Files Management | android.permission.MANAGE_EXTERNAL_STORAGE |
Special Access (API 30+) | System Settings Toggle Switch | adb shell appops set <pkg> MANAGE_EXTERNAL_STORAGE allow |
How ScreenSync MCP Bridges Claude Code to the System Permission Controller
ScreenSync MCP bridges Claude Code to Android's system permission controller through decoupled MediaProjection visual streams and ADB accessibility node extraction. Claude Code inspects the active system dialog hierarchy via get_ui_hierarchy and dispatches native button taps through control_tap without requiring fragile coordinate hacks or device rooting.
The breakthrough of using ScreenSync MCP for permission testing lies in its tripartite architecture, which completely eliminates the limitations of in-process test runners:
| Architectural Plane | Underlying Channel | Latency & Transport | Function During Permission Testing |
|---|---|---|---|
| Cognitive Agent Plane | Local stdio Process Pipe | Sub-millisecond JSON-RPC | Claude Code parses technical prompts, coordinates test sequences, and interprets UI hierarchy nodes |
| Visual Verification Plane | Local Wi-Fi / LAN / USB Reverse Sockets | Native MediaProjection (~94 ms median) | Streams live video frames (Android 7.0+, zero root, zero ADB overhead) to verify dialog renders and camera blackout screens |
| Hardware & OS Control Plane | Physical USB or Wireless Debugging | ADB Daemon Socket | Dispatches adb shell pm state commands, extracts accessibility hierarchies, and triggers touch gestures |
1. Traversing External System Dialogs via get_ui_hierarchy
When an application triggers a permission request, the Android Window Manager places the com.google.android.permissioncontroller window at the top of the window hierarchy. When Claude Code calls ScreenSync MCP's get_ui_hierarchy tool, the hub executes a high-speed accessibility dump across the active foreground window. Unlike Espresso, which is restricted to the activity view tree, get_ui_hierarchy exposes the complete XML node hierarchy of the system permission dialog:
<node index="0" package="com.google.android.permissioncontroller" class="android.widget.FrameLayout">
<node index="0" class="android.widget.LinearLayout" resource-id="com.android.permissioncontroller:id/grant_dialog">
<node index="0" class="android.widget.TextView" resource-id="com.android.permissioncontroller:id/permission_message"
text="Allow Camera Permission Demo to take pictures and record video?" />
<node index="1" class="android.widget.Button" resource-id="com.android.permissioncontroller:id/permission_allow_foreground_only_button"
text="While using the app" clickable="true" />
<node index="2" class="android.widget.Button" resource-id="com.android.permissioncontroller:id/permission_allow_one_time_button"
text="Only this time" clickable="true" />
<node index="3" class="android.widget.Button" resource-id="com.android.permissioncontroller:id/permission_deny_button"
text="Don’t allow" clickable="true" />
</node>
</node>
Claude Code can inspect node attributes (text, resource-id, class, bounds, clickable) directly. Because the agent understands semantic intent, it does not rely on fragile coordinate offsets. It locates the target button's center point and invokes control_tap directly on the node.
2. High-Speed Visual Capture via Native MediaProjection
While structural node trees provide the assertions for automated logic, visual verification confirms actual UI layout integrity. ScreenSync MCP captures real-time video frames using Android's native MediaProjection API (Android 7.0 or higher). Display capture requires zero device rooting and consumes zero ADB bandwidth, maintaining a median latency of approximately 94 milliseconds over a local area network (LAN). When testing whether a camera viewfinder correctly displays an educational privacy placeholder rather than an unrendered black box upon permission denial, Claude Code invokes get_latest_screenshot to evaluate visual fidelity. For end-to-end verification of viewfinder rendering, tap-to-focus, and photo capture on physical devices, explore our guide on testing Android Camera and CameraX with Claude Code and MCP.
3. The 226-Tool Ecosystem Advantage
ScreenSync MCP exposes 226 MCP tools to developer agents:
- 29 Phone & Desktop Tools: Comprising 12 device control tools (
control_tap,control_swipe,control_type,control_key,control_open_url,control_launch_app), 3 operating system automation tools (os_mouse_click,os_type,os_hotkey), and 14 capture and telemetry tools (get_latest_screenshot,get_ui_hierarchy,get_logcat,record_screen,get_device_status). - 197 Browser Tools: The
web_*suite operates through an authenticated Chrome/Edge browser extension. In enterprise multi-factor scenarios where mobile app permissions sync with desktop cloud IAM portals, Claude Code can seamlessly jump between native mobile screen triage and web browser configuration within a single automated run. For an exhaustive breakdown of every available capability, review our complete Android MCP tool reference deep dive.
Step-by-Step Setup: Workstation Hub, Physical Device, and Claude Code Registration
Configuring ScreenSync MCP for permission testing requires launching the desktop hub on Node.js 18, registering the stdio MCP server in Claude Code, and connecting an Android 7.0+ device via USB or wireless debugging. Establishing reverse port forwarding guarantees continuous visual verification during automated permission state sweeps.
Engineering teams can deploy this automated testing harness on Windows, macOS, or Linux through five sequential steps:
Step 1: Workstation and Hardware Prerequisites
Ensure your testing workstation meets the following operational baseline:
- Workstation Environment: Windows 10/11, macOS, or Linux running Node.js 18 or later with
adb(Android Platform Tools) installed and accessible in your systemPATH. - Physical Smartphone: An authentic Android device running Android 7.0 (API level 24) or higher connected via USB cable or local Wi-Fi.
- Application Build: Debug build of your application installed with runtime permission requests configured.
Step 2: Launch the Desktop Hub
Download the ScreenSync MCP desktop hub distribution from screensyncmcp.tech and extract the archive to a permanent workstation directory. Execute the platform-specific launcher:
# On Windows (PowerShell or Command Prompt)
cd C:\tools\screensync-hub
.\start-hub.bat
# On macOS or Linux (Terminal)
cd ~/tools/screensync-hub
chmod +x start-hub.sh
./start-hub.sh
The desktop hub initializes an HTTP server on port 3000 to ingest local MediaProjection video frames and prepares a JSON-RPC Model Context Protocol server over stdio. The console output displays your unique authentication token (SCREEN_SYNC_TOKEN). For comprehensive documentation, visit the official ScreenSync MCP setup guide.
Step 3: Register ScreenSync MCP with Claude Code
Register the desktop hub with the Claude Code CLI in your terminal:
claude mcp add screensync node "/path/to/screensync-hub/screensync-hub.js"
If you are developing inside Claude Desktop, edit your claude_desktop_config.json file (Settings > Developer > Edit Config):
{
"mcpServers": {
"screensync": {
"command": "node",
"args": ["/path/to/screensync-hub/screensync-hub.js"],
"env": {
"SCREEN_SYNC_TOKEN": "your_authenticated_token_here"
}
}
}
}
(Note: ScreenSync MCP integrates with local stdio MCP clients including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. ChatGPT's web application requires remote HTTPS endpoints and does not support local stdio processes).
Step 4: Install the Android Application and Authorize MediaProjection
- Download the free ScreenSync MCP application (package
com.screensync.mcp) from Google Play. - Open the application. Allow local network mDNS discovery to automatically detect your desktop hub on the local Wi-Fi subnet, or scan the pairing QR code displayed in the hub console.
- Tap Start Projection and confirm Android's native MediaProjection screen capture consent prompt.
- Unlock Developer Options on your smartphone (Settings > About Phone > tap Build Number 7 times) and enable USB Debugging (or Wireless Debugging). If you prefer wireless operation, review our guide on pairing Android wireless debugging to an MCP server without USB.
- Verify ADB connectivity by running
adb devicesin your workstation terminal.
Step 5: Establish USB Reverse Port Forwarding
To ensure uninterrupted visual streaming even when toggling airplane mode or isolating network radios during security testing, configure USB reverse port forwarding:
adb reverse tcp:3000 tcp:3000
With this reverse socket configured, the ScreenSync MCP mobile application delivers MediaProjection frames directly across the USB physical link to the hub, even when the phone has airplane mode enabled and Wi-Fi disabled.
Practical QA Workflows: Grants, Denials, Educational Rationales, and App Settings
Practical permission QA workflows validate four critical execution pathways: granting immediate access, handling initial user denial, rendering contextual educational rationales, and navigating permanent denial redirects to system settings. Claude Code automates these multi-step journeys by combining UI hierarchy assertions with real-time logcat telemetry.
Workflow 1: Validating the "Allow" Flow and Hardware Initialization
The fundamental happy path requires asserting that tapping "While using the app" on a fresh install grants the dangerous permission, clears the prompt, and initializes the target hardware service.
Developer Prompt:
"Claude, launch our app com.example.screensync.permissions. First, reset all permissions via ADB so we start with a clean slate. Tap 'Request Camera', assert that the system permission dialog appears, tap 'While using the app', and verify that the UI updates to 'Camera Active' with an active preview viewfinder."
Step 1: Clean State Reset via ADB
Claude Code issues the package reset command through the terminal:
adb shell pm reset-permissions com.example.screensync.permissions
Step 2: Launch App and Trigger Request
Claude Code calls control_launch_app with package com.example.screensync.permissions. Once launched, it calls get_ui_hierarchy to locate the request button and triggers it via control_tap:
<node text="Request Camera Permission" resource-id="com.example.screensync.permissions:id/btn_request_camera" />
Step 3: Assert System Permission Controller Dialog
Claude Code calls get_ui_hierarchy and asserts the presence of the system dialog:
<node package="com.google.android.permissioncontroller" class="android.widget.LinearLayout">
<node text="Allow Camera Permission Demo to take pictures and record video?" resource-id="com.android.permissioncontroller:id/permission_message" />
<node text="While using the app" resource-id="com.android.permissioncontroller:id/permission_allow_foreground_only_button" />
</node>
Step 4: Tap Allow and Verify State
Claude Code invokes control_tap on the permission_allow_foreground_only_button node. It then invokes get_ui_hierarchy to confirm that the app screen displays Camera Active, followed by get_latest_screenshot to confirm the camera preview is rendering valid frames.
Workflow 2: Testing Initial Denial and In-App Educational Rationale
Android design guidelines require that if a user denies a permission request once, the application must present a contextual educational rationale before prompting again.
Developer Prompt:
"Claude, test the permission denial flow. Reset permissions, launch the app, trigger the camera prompt, and tap 'Don't allow'. Assert that our custom educational rationale card appears explaining why camera access is required. Then tap 'Try Again' on the rationale card, assert that the system prompt reappears, and tap 'While using the app'."
Step 1: Trigger and Deny
Claude Code resets permissions via adb shell pm reset-permissions, launches the app, triggers the permission dialog, and calls control_tap on com.android.permissioncontroller:id/permission_deny_button.
Step 2: Assert Custom Rationale UI
Claude Code immediately inspects get_ui_hierarchy. It asserts that the application did not crash and that the custom Jetpack Compose rationale card rendered:
<node text="Camera Access Required" resource-id="com.example.screensync.permissions:id/text_rationale_title" />
<node text="This app needs your camera to scan QR codes and barcodes. We never store photos without your consent." resource-id="com.example.screensync.permissions:id/text_rationale_explanation" />
<node text="Try Again" resource-id="com.example.screensync.permissions:id/btn_rationale_retry" />
Step 3: Recover and Grant
Claude Code calls control_tap on btn_rationale_retry. It verifies via get_ui_hierarchy that the system dialog re-inflates, then taps "While using the app", verifying that the application recovers cleanly.
Workflow 3: Permanent Denial ("Don't ask again") and System Settings Redirection
When a user denies a permission twice (or checks "Don't ask again" on legacy Android releases), the operating system permanently blocks the application from showing the system prompt. The app must guide the user to system App Info Settings.
Developer Prompt:
"Claude, test permanent permission denial. Deny the camera permission twice so shouldShowRequestPermissionRationale returns false. Assert that our app displays the 'Permission Permanently Denied' modal with an 'Open Settings' button. Tap 'Open Settings', assert that the Android System App Info page opens, grant the camera permission in Settings, navigate back, and verify the app reflects the granted state."
Step 1: Simulate Double Denial
Claude Code triggers the request twice and taps permission_deny_button on both occurrences.
Step 2: Assert Settings Redirection Dialog
Claude Code calls get_ui_hierarchy and asserts the presence of the fallback dialog:
<node text="Permission Permanently Denied" resource-id="com.example.screensync.permissions:id/text_permanent_denial_title" />
<node text="Camera permission has been permanently denied. Please enable it in system settings to continue." resource-id="com.example.screensync.permissions:id/text_permanent_denial_body" />
<node text="Open Settings" resource-id="com.example.screensync.permissions:id/btn_open_settings" />
Step 3: Navigate to System Settings
Claude Code calls control_tap on btn_open_settings. It calls get_ui_hierarchy to assert that the foreground window has transitioned to Android System Settings:
<node package="com.android.settings" class="android.widget.TextView" text="App info" />
<node package="com.android.settings" class="android.widget.TextView" text="Permissions" resource-id="android:id/title" />
Step 4: Toggle Permission and Return
Claude Code navigates the settings menu, taps "Permissions", selects "Camera", and toggles "Allow only while using the app". It then calls control_key with key code 4 (KEYCODE_BACK) twice to return to the application. The activity's onResume() lifecycle executes, refreshing the permission state and updating the UI to "Camera Active". For an end-to-end framework perspective, explore our autonomous mobile app testing Claude Code guide.
| Permission QA Scenario | ScreenSync MCP Tools Used | Primary Assertion Mechanism | Critical Defect Caught |
|---|---|---|---|
| First-Time Grant | get_ui_hierarchy, control_tap |
System dialog permission_allow_foreground_only_button |
Application fails to register ActivityResultContract callback |
| User Denial Fallback | control_tap, get_ui_hierarchy |
Rationale card resource-id and explanation text |
Application crashes with NullPointerException on denied state |
| Permanent Denial Redirect | control_tap, get_ui_hierarchy |
Transition to com.android.settings package |
App loops in infinite request cycles instead of directing to settings |
| Android 13 Notifications | get_ui_hierarchy, get_logcat |
Node POST_NOTIFICATIONS modal prompt |
App crashes on launch targeting API 33 without runtime check |
| Android 14 Partial Media | get_ui_hierarchy, control_tap |
System Photo Picker grid selection | App assumes full library access; crashes on partial URI grant |
| Background Location Warning | get_ui_hierarchy |
Multi-step system settings location disclosure | App prompts for background location concurrently with foreground |
Mid-Session Permission Revocation and Activity Recreation Triage
Mid-session permission revocation tests evaluate how an Android application responds when operating system permissions are stripped while the process is active or backgrounded. Claude Code executes ADB revocation commands, triages instantaneous process termination, and asserts that cold relaunch recovers application state without throwing unhandled security exceptions. When coordinating background services and persistent jobs across permission shifts, developers can automate constraint verification following our guide on testing Android WorkManager with Claude Code and MCP.
A notorious edge case in Android software engineering occurs when a user grants a runtime permission, uses the application, switches to Android System Settings, and manually revokes the permission. In Android's security model, revoking a runtime permission immediately terminates the application process (killProcessGroup). The operating system kills the process to prevent memory inconsistencies and eliminate compromised handles to hardware resources.
When the user switches back to the application, Android attempts to restore the top activity from its saved instance state bundle (onRestoreInstanceState). If the activity assumes that permissions granted during previous execution remain valid without re-checking ContextCompat.checkSelfPermission(), the application crashes instantly with a fatal SecurityException:
E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: java.lang.SecurityException: Permission Denial: starting camera requires android.permission.CAMERA
E AndroidRuntime: at android.os.Parcel.createExceptionOrNull(Parcel.java:3011)
E AndroidRuntime: at com.example.screensync.permissions.CameraManager.startCamera(CameraManager.kt:84)
Claude Code automates this rigorous edge-case triage with absolute precision:
Developer Prompt:
"Claude, launch our app and grant camera permissions. Verify the camera is actively streaming. Now, execute 'adb shell pm revoke com.example.screensync.permissions android.permission.CAMERA' via the terminal while the app is running. Assert that the OS terminates the process. Relaunch the app, verify that it does not crash on cold restoration, and assert that the UI falls back to the permission prompt."
Step 1: Assert Initial Granted State
Claude Code launches the application, confirms that ContextCompat.checkSelfPermission returns PERMISSION_GRANTED, and calls get_ui_hierarchy to assert that the camera viewfinder is active.
Step 2: Strip Permission via ADB
Claude Code executes the revocation shell command:
adb shell pm revoke com.example.screensync.permissions android.permission.CAMERA
Step 3: Triage Process Kill via Logcat
Claude Code calls get_logcat with filter ActivityManager:
I ActivityManager: Killing 28412:com.example.screensync.permissions/u0a214 (adj 100): permissions revoked
I WindowManager: WIN DEATH: Window{7b812a0 u0 com.example.screensync.permissions/com.example.screensync.permissions.CameraPermissionActivity}
Step 4: Relaunch and Verify Defensive Restoration
Claude Code invokes control_launch_app to restore the application. It asserts via get_ui_hierarchy that the application caught the missing permission in onResume(), rendered the initial request interface, and did not throw a fatal runtime crash. When evaluating notification permission lifecycles, developers can also apply testing Android push notifications with Claude Code and MCP to verify notification channels and system shade behavior.
Common Permission Implementation Pitfalls and Logcat Diagnostics
Common permission implementation pitfalls include throwing unhandled SecurityException crashes on the main thread, bypassing shouldShowRequestPermissionRationale checks, and misinterpreting partial media access in Android 14. Claude Code identifies these defects by streaming logcat telemetry and auditing package permission tables directly via ADB.
1. The Main-Thread SecurityException Crash
The most prevalent permission defect is attempting to invoke hardware APIs (such as LocationManager.requestLocationUpdates or CameraDevice.createCaptureSession) without wrapping the call site in defensive checks:
// Defective Implementation: Assumes permission is always granted once initialized
fun captureFrame() {
cameraDevice.createCaptureSession(...) // CRASH: Throws SecurityException if revoked
}
// Resilient Implementation: Always verify permission before hardware access
fun captureFrame() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
cameraDevice.createCaptureSession(...)
} else {
showPermissionRequiredSnackbar()
}
}
Claude Code parses get_logcat for SecurityException occurrences, locates the unguarded API invocation in the source code, and refactors the method to verify permission status before dispatch.
2. Requesting Coarse and Fine Location Simultaneously Without Rationale
Under Android 12+, requesting ACCESS_FINE_LOCATION without ACCESS_COARSE_LOCATION causes the system to reject the request automatically on certain OEM configurations. Furthermore, if the user selects "Approximate", attempting to force another immediate prompt for "Precise" within the same session violates Google Play policies. Claude Code audits the permission request array using ADB dumpsys: For an end-to-end simulation harness covering dynamic GPS routes, see our comprehensive guide on testing Android location and geofencing with Claude Code and MCP.
adb shell dumpsys package com.example.screensync.permissions | grep -A 10 "runtime permissions:"
Output:
runtime permissions:
android.permission.ACCESS_COARSE_LOCATION: granted=true, flags=[ USER_SET ]
android.permission.ACCESS_FINE_LOCATION: granted=false, flags=[ USER_SET ]
Claude Code verifies that the application functions properly with approximate coordinates and does not lock the user in a modal loop demanding precise GPS.
3. Misinterpreting Android 14 Partial Media Permissions
In Android 14, querying ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) returns PERMISSION_DENIED if the user granted selected photo access. Applications that only check READ_MEDIA_IMAGES will falsely believe they have zero access, failing to load the images the user explicitly selected:
// Resilient Android 14 Selected Media Check
val hasFullAccess = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED
val hasPartialAccess = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED
if (hasFullAccess || hasPartialAccess) {
// Proceed to load granted URIs from MediaStore
loadGrantedMedia()
} else {
// Prompt for media access
requestMediaPermissionLauncher.launch(arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED))
}
Claude Code detects whether an application correctly implements the READ_MEDIA_VISUAL_USER_SELECTED contract by verifying that the MediaStore query populates successfully after the user selects a subset of items in the native photo picker. For an end-to-end implementation and testing guide across scoped sandboxes and media collections, read testing Android Scoped Storage with Claude Code and MCP.
Implementation Checklist for Automated Android Permission Testing
- [ ] Ensure host development workstation runs Node.js 18+ with Android Platform Tools
adbconfigured in systemPATH. - [ ] Unpack the ScreenSync MCP desktop hub and execute
start-hub.bat(Windows) orstart-hub.sh(macOS/Linux). - [ ] Register ScreenSync MCP with the Claude Code CLI via
claude mcp add screensync node "/path/to/screensync-hub.js". - [ ] Install the ScreenSync MCP client (version 2.5.7+) on physical Android 7.0+ hardware from Google Play.
- [ ] Connect smartphone and workstation to the same local Wi-Fi subnet; complete mDNS pairing or QR code authentication.
- [ ] Authorize native MediaProjection display recording on the Android device for low-latency (~94 ms) local screen mirroring.
- [ ] Execute
adb reverse tcp:3000 tcp:3000over USB to guarantee uninterrupted video streaming during airplane mode or radio tests. - [ ] Verify that all dangerous permissions are requested dynamically via
ActivityResultContracts.RequestPermissionorRequestMultiplePermissions. - [ ] Assert that initial user denial triggers a contextual educational rationale explaining why access is necessary.
- [ ] Validate that permanent denial ("Don't ask again") directs users to system settings via
Settings.ACTION_APPLICATION_DETAILS_SETTINGS. - [ ] Test mid-session permission revocation via
adb shell pm revoketo confirm that cold restoration recovers cleanly without crashes. - [ ] Verify compliance with Android 13
POST_NOTIFICATIONSand Android 14READ_MEDIA_VISUAL_USER_SELECTEDgranular contracts.
| Target Permission Lifecycle | Verification Command / Tool | Assertion Mechanism | Success Criteria |
|---|---|---|---|
| Initial Permission Reset | adb shell pm reset-permissions |
Package Manager state reset | All runtime permission flags return to default unrequested |
| System Dialog Presentation | get_ui_hierarchy |
Package: com.google.android.permissioncontroller |
System modal rendered with active "Allow" and "Deny" buttons |
| Educational Rationale UI | control_tap on Deny + get_ui_hierarchy |
In-app composable resource-id |
Custom rationale card visible explaining feature necessity |
| Permanent Denial Handling | Double denial + get_ui_hierarchy |
Fallback modal assertion | App renders "Open Settings" button; stops triggering OS prompt |
| System Settings Navigation | control_tap on Settings + get_ui_hierarchy |
Package: com.android.settings |
Foreground window transitions to App Info Settings page |
| Revocation Survival | adb shell pm revoke + control_launch_app |
get_logcat & UI hierarchy |
Process kill handled; app restarts without SecurityException |
| Android 14 Media Access | get_ui_hierarchy |
Selected photo picker node tree | App displays selected images without demanding full library access |
| Visual State Fidelity | get_latest_screenshot |
Native MediaProjection capture (~94 ms) | UI elements render with correct contrast and zero visual artifacts |
Conclusion
Automating Android runtime permission testing with Claude Code and ScreenSync MCP bridges one of the most stubborn architectural divides in modern mobile quality assurance. By decoupling test orchestration from application memory sandboxes, mobile engineering teams can inspect system com.google.android.permissioncontroller modals, validate in-app educational rationale flows, and assert mid-session revocation survival directly on physical smartphones. For sensitive applications safeguarding cryptographic keys or biometric authentication dialogs, see our companion guide on testing Android biometrics with Claude Code and MCP. To automate next-generation passwordless authentication and Credential Manager flows, read our architectural guide on testing Android passkeys with Claude Code and MCP.
ScreenSync MCP provides the ScreenSync MCP Android app, ScreenSync MCP hub, and ScreenSync MCP browser extension for development teams worldwide. Explore our comprehensive technical documentation at screensyncmcp.tech, contact our engineering group directly at [email protected], and Get it on Google Play to start automating real physical Android hardware today.
Questions, answered
Why can Espresso not test Android runtime permission dialogs directly?
Espresso cannot test permission dialogs directly because Espresso executes strictly inside the target application's process sandbox. System permission prompts belong to an external operating system process named com.google.android.permissioncontroller. When Espresso attempts to locate system dialog buttons, it throws a fatal NoMatchingViewException because the view hierarchy belongs to the OS.
How does ScreenSync MCP interact with the Android permission controller?
ScreenSync MCP interacts with the permission controller through system-level accessibility interfaces and Android Debug Bridge. By executing get_ui_hierarchy from the desktop hub, ScreenSync MCP captures the complete window hierarchy of the active system dialog, allowing Claude Code to locate and tap permission buttons via semantic control_tap actions.
Does ScreenSync MCP require root access to grant or revoke Android permissions?
ScreenSync MCP requires no root privileges to manipulate permissions or capture screen frames. Display capture operates via Android's native MediaProjection API after user consent, while permission grants, revocations, and resets execute through standard Android Debug Bridge commands over physical USB or wireless debugging connections.
How does Claude Code simulate Android permanent permission denial during testing?
Claude Code simulates permanent permission denial by triggering the permission prompt and clicking Don't allow consecutively until shouldShowRequestPermissionRationale returns false. Claude Code then asserts that the application displays a custom fallback modal and successfully directs the user to system App Info Settings via ACTION_APPLICATION_DETAILS_SETTINGS.
Why does Android terminate an application process when a permission is revoked?
Android immediately terminates an application process upon permission revocation to maintain system security and prevent corrupted state. If an application held active hardware handles or cached security credentials in memory, revoking the permission without process termination could lead to illegal resource access or fatal native runtime crashes.
How do developers test Android 14 partial photo access with Claude Code?
Developers test partial photo access by requesting READ_MEDIA_VISUAL_USER_SELECTED alongside standard media permissions. When the system Photo Picker appears, Claude Code uses get_ui_hierarchy and control_tap to select specific images, verifying that the application successfully queries MediaStore for the granted items without demanding full storage access.
What is the difference between ScreenSync MCP capture and control channels?
ScreenSync MCP separates visual capture from device control. Screen capture streams high-fidelity video frames using Android's native MediaProjection API (Android 7.0+, zero root, zero ADB overhead) at approximately 94 milliseconds median latency. Device control tools (tap, swipe, type, launch app) execute through ADB from the hub over USB or wireless debugging.
Which MCP clients support automated Android permission testing with ScreenSync MCP?
ScreenSync MCP integrates seamlessly with any MCP client supporting local stdio process execution, including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. The ChatGPT web application is unsupported because ChatGPT requires remote HTTPS web endpoints rather than local stdio processes executing on your developer workstation.




