How to Test Permission Dialogs on Android (Complete Guide)

Permission dialogs are the gatekeepers between an app’s functionality and the device’s protected resources. When a dialog fails—whether it never appears, is dismissed incorrectly, or blocks the user f

June 12, 2026 · 19 min read · How-To Guides

Why Permission Dialogs Matter on Android

Permission dialogs are the gatekeepers between an app’s functionality and the device’s protected resources. When a dialog fails—whether it never appears, is dismissed incorrectly, or blocks the user from proceeding—core features break, user trust erodes, and the app may be flagged by Play Store reviewers for misleading behavior. In production, a missing permission can cause silent crashes (e.g., trying to read contacts without READ_CONTACTS), lead to ANRs when the app waits for a result that never arrives, or expose data if the app incorrectly assumes a permission was granted. Conversely, over‑granting permissions (e.g., always allowing location) creates privacy risks and can trigger rejection during the store’s privacy review. Testing these dialogs therefore touches reliability, security, compliance, and user experience—all critical dimensions for any Android release.

Comprehensive Test Matrix for Permission Dialogs

A systematic matrix helps ensure that every dimension of permission handling is exercised. Below is a detailed breakdown that can be copied into a test‑case management tool or used as a checklist for exploratory sessions.

CategorySub‑scenarioExpected OutcomeTest Notes
Happy PathUser grants a single runtime permission (e.g., CAMERA) when promptedPermission is granted, app proceeds to the feature that needs itVerify that the permission appears in Settings → Apps → [App] → Permissions as granted
User grants multiple permissions in one flow (e.g., CAMERA + MICROPHONE)All requested permissions are granted, feature worksEnsure no intermediate dialog is left dangling
User grants permission via system Settings after initially denyingPermission toggles to granted, app resumes functionality without restartSimulate by denying, opening Settings, toggling, returning to app
App targets Android 13+ and requests a post‑notification permissionNotification permission dialog appears, granting enables notification channelCheck that notification channel is created and a test notification is delivered
App requests a special access permission (e.g., SYSTEM_ALERT_WINDOW)Special permission dialog appears, granting allows overlay drawingVerify overlay can be drawn on top of other apps
Error PathsUser denies a permission permanently (selects “Don’t ask again” and Deny)Permission remains denied, subsequent requests should show rationale UI (if implemented) or be silently ignoredConfirm that app does not crash and shows appropriate fallback UI
User denies but later enables permission via SettingsPermission toggles to granted, app recoversTest after a process kill to ensure state persists
App requests a permission that is already granted by a previous install (e.g., via pre‑grant)No dialog appears, app proceeds directlyUse adb shell pm grant to pre‑grant and verify absence of dialog
App requests a permission that is blocked by device policy (e.g., DISALLOW_CAMERA)System shows a policy‑denied toast, no dialog appearsUse a device owner or managed profile to enforce the restriction
App requests a permission after the user has selected “Never ask again” for a different permission in the same groupThe system may suppress the dialog for the whole group; app receives DENIEDVerify behavior for permission groups like LOCATION (ACCESS_FINE_LOCATION vs ACCESS_COARSE_LOCATION)
Edge CasesPermission request occurs while the app is in the background (e.g., via a foreground service)System may delay the dialog until the app returns to foreground or show a heads‑up notificationConfirm that the app does not crash and that the service handles the denial gracefully
Multiple permission requests triggered in rapid succession (e.g., on app launch)System queues dialogs; user sees them one after anotherEnsure no dialog is skipped and that the app tracks each request’s result
Permission request appears after a configuration change (rotation, multi‑window)Dialog persists correctly; app does not lose request contextTest with adb shell am broadcast -a android.intent.action.CONFIGURATION_CHANGED
App targets Android 12+ and requests approximate location while the user has granted only coarse locationSystem shows a separate dialog for precise location if the app upgrades requestValidate that the app handles the upgrade flow
App uses the new POST_NOTIFICATIONS permission on Android 13+ and the user has disabled notification channels globallySystem shows a dialog with a warning that notifications are disabled globallyCheck that the app respects the global setting and does not spam the user
App requests a permission that requires a runtime toggle in OEM‑specific settings (e.g., Xiaomi’s “MIUI optimization”)Dialog may be suppressed or replaced by OEM UITest on at least two OEM skins (e.g., Samsung OneUI, Xiaomi MIUI)
AccessibilityUser with TalkBack enabled navigates the permission dialogTalkBack reads the permission title, description, and buttons correctly; focus order is logicalVerify that the dialog is reachable via swipe gestures and that the “Allow” and “Deny” buttons are announced
User with Switch Control selects the dialog via external switchThe switch can highlight and activate each buttonEnsure that the dialog does not disappear before the switch action completes
User changes font size to largest or enables bold textDialog text scales without clipping; buttons remain tappableTest with adb shell settings put system font_scale 1.3 and adb shell settings put accessibility bold_text_enabled 1
User with color blindness relies on icons or text, not color aloneDialog does not rely solely on color to differentiate allowed vs denied stateConfirm that the UI uses distinct shapes or text labels
Security & PrivacyApp attempts to use a permission before the dialog result is received (race condition)App should either wait for the callback or handle a missing permission gracefully; no crash or data leakUse adb shell monkey -p -v 500 to inject rapid events and watch for crashes
App logs the raw permission request code or token in LogcatNo sensitive data (e.g., exact GPS coordinates) is leaked in logsVerify with adb logcat that no permission‑related PII appears
App requests a permission that is not declared in the manifest (mis‑configuration)System throws SecurityException; app should catch and show a user‑friendly errorTest by removing a permission from manifest while keeping the request code
App uses a permission group to request a dangerous permission without declaring the specific one (e.g., requests LOCATION group but only needs coarse)System grants the whole group; verify that the app does not over‑collect dataCheck that the app’s privacy policy matches the granted set
App attempts to bypass the dialog via adb shell pm grant in a production build (malicious intent)The grant succeeds only if the app is debuggable or the device is rooted; production builds should rejectConfirm that a non‑debuggable, non‑rooted device does not allow the grant via adb unless the user explicitly approves via Settings

Happy Path Scenarios

When testing the happy path, focus on the *exact* moment the system presents the dialog. Use a clean device state (no prior grants) and observe that the dialog’s title matches the permission’s label, the description explains why the app needs it, and the two action buttons are present. After granting, immediately invoke the protected API (e.g., Camera.open()) and assert that no SecurityException is thrown. Additionally, verify that the permission appears as granted in the system UI and that a subsequent request for the same permission does not trigger another dialog (the system remembers the choice).

Error Path Scenarios

Error paths reveal how the app behaves when the user refuses access. A robust app should degrade gracefully: hide the feature, show an inline explanation, or offer a retry button that re‑triggers the request after the user has changed their mind in Settings. Test the “Don’t ask again” flow by denying, checking the checkbox, and then attempting to re‑request the permission; the system should skip the dialog and return PERMISSION_DENIED directly to the callback. Confirm that your app does not treat this as a fatal error but instead presents a UI that guides the user to Settings.

Edge Cases

Edge cases often surface only under specific device states or OS versions. For example, on Android 12 (API 31) the system introduced *approximate location*; if your app requests ACCESS_FINE_LOCATION while the user has only granted coarse, the system shows a separate dialog for the precise location upgrade. Similarly, Android 13’s POST_NOTIFICATIONS permission requires a distinct runtime request even if the app previously sent notifications via notification channels. Test each OS version you support, using emulator system images or real devices, to confirm that the dialog flow matches the platform’s expectations.

Accessibility Considerations

Permission dialogs are system‑provided UI, but they still must be usable by people who rely on assistive technologies. When TalkBack is active, swipe left/right should move focus between the permission explanation, the “Allow” button, and the “Deny” button, with each element announcing its role and state. Verify that the dialog does not trap focus (i.e., the user can swipe out to the underlying app) and that the announcement includes the permission group (e.g., “Location, fine location”). For Switch Control, ensure that a single switch press can highlight each button and that a second press activates it. Finally, test with the largest font size and bold text enabled to confirm that no text is truncated and that touch targets remain at least 48 dp.

Security & Privacy Implications

A permission dialog that is bypassed or mishandled can lead to data leakage or privilege escalation. Ensure that your app never reads protected data before the callback returns PERMISSION_GRANTED. Use strict mode or a custom SecurityException listener to catch any premature access. Additionally, audit your logging: never log the raw grantResults array or any data obtained via the permission (e.g., GPS coordinates) unless you have explicitly masked PII. Finally, confirm that your app’s manifest declares only the permissions it truly needs; excess declarations increase the attack surface and may trigger store warnings.

Manual Testing Approach

Manual testing remains valuable for catching subtle UI glitches, OEM‑specific quirks, and accessibility problems that automated scripts may overlook. Below is a repeatable, step‑by‑step process you can follow on a physical device or emulator.

Preparing the Device/Emulator

  1. Start with a clean state – Either factory‑reset the emulator or use adb shell pm clear to remove app data and revoke all runtime permissions.
  2. Install the test buildadb install -r app-debug.apk. Ensure the build is *debuggable* if you intend to use adb shell pm grant for pre‑granting; otherwise, keep it non‑debuggable to mimic production.
  3. Enable developer options – On the device, go to Settings → About phone → tap Build number seven times, then toggle USB debugging.
  4. Grant necessary ADB permissions – On Windows/macOS/Linux, verify adb devices shows your device.
  5. Set up accessibility tools – Turn on TalkBack, Switch Control, or font scaling via Settings → Accessibility → Display size to validate the dialog under those conditions.
  6. Prepare a test script – Have a simple note‑taking app or a test harness that triggers the permission request via a button press. This makes it easy to repeat the flow without navigating through the app each time.

Step‑by‑Step Manual Test Procedure

StepActionObservationPass/Fail Criteria
1Launch the app and navigate to the screen that triggers the permission request (e.g., “Take Photo”).The app shows a button or initiates the request automatically.UI is responsive; no crash.
2Tap the trigger.The system permission dialog appears.Dialog title matches the permission (e.g., “Allow access to camera?”).
3Read the dialog with TalkBack enabled.TalkBack reads the permission name, description, and each button label.All elements are announced; focus order is logical.
4Deny the permission (tap Deny).Dialog disappears; app receives PERMISSION_DENIED in its callback.App does not crash; feature is disabled or shows rationale.
5Re‑tap the trigger.Dialog appears again (unless “Don’t ask again” was checked).If the checkbox was not checked, dialog shows; otherwise, system skips dialog and returns denied instantly.
6Grant the permission (tap Allow).Dialog disappears; app receives PERMISSION_GRANTED.Protected API call succeeds (e.g., camera preview starts).
7Verify permission status in Settings.Settings → Apps → [App] → Permissions shows the permission as granted.Matches the callback result.
8Repeat steps 2‑7 for each permission your app requests (camera, microphone, location, notifications, etc.).Each flow behaves as expected.No inconsistencies across permission types.
9Test background request: start a foreground service that requests location, then press Home.System may show a heads‑up notification or delay dialog until the app returns to foreground.App does not crash; service handles denial gracefully.
10Test OEM‑specific behavior: repeat on a Samsung device and a Xiaomi device.Dialog may have slightly different styling or additional explanations.Core functionality (grant/deny) works; no OEM‑specific crashes.
11Test accessibility: enable largest font, bold text, and Switch Control.Dialog text scales, buttons remain reachable, switch can activate each option.No clipped text; switch can complete the action.
12Test security: attempt to read protected data before the callback returns (e.g., start camera preview immediately after triggering request).App should either wait for the callback or throw a caught exception; no crash.No uncaught SecurityException.
13Clean up: clear app data and repeat the entire sequence to ensure state does not leak.Each run starts from a clean slate.Consistent results across runs.

Documenting Results

Use a simple spreadsheet with columns for *Permission*, *Scenario* (grant, deny, don’t ask again, etc.), *Observed Behavior*, *Expected*, *Status*, and *Notes*. Attach screenshots or screen recordings for any failures. This documentation becomes a regression baseline for future releases.

Automated Testing Strategies

While manual testing catches UI nuances, automated tests provide repeatability and coverage for regression suites. Android offers several layers for permission testing, ranging from fast unit tests to full‑system UI automations.

Unit and Instrumentation Tests with Espresso

Espresso excels at validating that your app reacts correctly to permission callbacks, but it cannot interact with the system permission dialog directly (the dialog runs in a separate system process). Instead, you mock the permission result and verify UI changes.


// PermissionFragmentTest.kt
@RunWith(AndroidJUnit4::class)
class PermissionFragmentTest {

    @get:Rule
    val grantRule = GrantPermissionRule.grant(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    )

    @Test
    fun cameraPermissionGranted_showsPreview() {
        // Launch the fragment that requests camera
        launchFragmentInContainer<CameraFragment>(Bundle.EMPTY)

        // Espresso can see the UI that appears after permission is granted
        onView(withId(R.id.camera_preview))
            .check(matches(isDisplayed()))
    }

    @Test
    fun cameraPermissionDenied_showsRationale() {
        // Revoke the permission to simulate a deny scenario
        InstrumentationRegistry.getInstrumentation()
            .getTargetContext()
            .revokePermission(Manifest.permission.CAMERA, PackageManager.PERMISSION_DENIED)

        launchFragmentInContainer<CameraFragment>(Bundle.EMPTY)

        // Assuming the fragment shows a TextView with id R.id.permission_rationale
        onView(withId(R.id.permission_rationale))
            .check(matches(isDisplayed()))
    }
}

The GrantPermissionRule automatically grants the listed permissions before each test method runs, letting you test the *granted* path without invoking the system dialog. To test the denied path, manually revoke the permission after the rule has run (as shown) or use a custom ActivityTestRule that calls Context#revokePermission.

UI Automator Tests for System Dialogs

UI Automator can cross‑process boundaries, making it suitable for clicking the Allow or Deny buttons on the system permission dialog. Note that UI Automator requires the test to be instrumented with android.permission.INTERACT_ACROSS_USERS_FULL (granted automatically for instrumentation tests on Android 9+).


// PermissionDialogTest.java
@RunWith(AndroidJUnit4::class)
public class PermissionDialogTest {

    private static final String CAMERA_PERMISSION = Manifest.permission.CAMERA;
    private static final int REQUEST_CODE = 101;

    @Before
    public void revokeCameraPermission() {
        Context ctx = InstrumentationRegistry.getInstrumentation()
                .getTargetContext();
        ctx.revokePermission(CAMERA_PERMISSION, PackageManager.PERMISSION_DENIED);
    }

    @Test
    public void grantCameraViaUiAutomator() {
        // Launch the activity that triggers the permission request
        Intent intent = new Intent(InstrumentationRegistry.getTargetContext(),
                MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        InstrumentationRegistry.getInstrumentation()
                .startActivitySync(intent);

        // Wait for the system dialog to appear (max 2 seconds)
        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        UiObject allowBtn = device.findObject(new UiSelector()
                .textContains("Allow")
                .className("android.widget.Button"));

        assertTrue("Allow button not found", allowBtn.waitForExists(2000));

        // Click Allow
        allowBtn.clickAndWaitForNewWindow(2000);

        // Verify that the permission is now granted
        PackageManager pm = InstrumentationRegistry.getInstrumentation()
                .getTargetContext()
                .getPackageManager();
        int granted = pm.checkPermission(CAMERA_PERMISSION,
                InstrumentationRegistry.getTargetContext().getPackageName());
        assertEquals(PackageManager.PERMISSION_GRANTED, granted);
    }
}

Key points:

Using adb to Grant/Revoke Permissions

For quick sanity checks or scripted test harnesses, adb provides direct control over permission states.


# Revoke a permission (simulate a deny)
adb shell pm revoke com.example.myapp android.permission.CAMERA

# Grant a permission (simulate an allow)
adb shell pm grant com.example.myapp android.permission.CAMERA

# Check current state
adb shell pm list permissions -g -d | grep CAMERA

You can embed these commands in a shell script that launches your app, performs a UI action via adb shell input tap (coordinates obtained from adb shell uiautomator dump), then checks the permission state. This approach is useful for continuous‑integration pipelines that run on device farms lacking UI Automator support.

Leveraging AndroidX Test Rules (GrantPermissionRule)

The GrantPermissionRule from androidx.test:core simplifies permission handling in instrumentation tests. It automatically grants the requested permissions before each test and revokes them after, ensuring isolation.


@get:Rule
val permissionRule = GrantPermissionRule.grant(
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION
)

@Test
fun locationRequest_whenGranted_showsMap() {
    launchActivity<MapsActivity>()
    onView(withId(R.id.map_view)).check(matches(isDisplayed()))
}

If you need to test the *denied* path, combine the rule with a manual revoke after the rule’s initialization:


@Before
fun revokeLocation() {
    val ctx = ApplicationProvider.getApplicationContext()
    ctx.revokePermission(Manifest.permission.ACCESS_FINE_LOCATION,
            PackageManager.PERMISSION_DENIED)
}

Mocking Permission Results with Robolectric

Robolectric runs tests on the JVM, making it fast for unit‑level logic. It allows you to stub the Context#checkPermission and Context#requestPermissions calls.


@RunWith(RobolectricTestRunner.class)
@Config(sdk = {Build.VERSION_CODES.Q})
public class PermissionViewModelTest {

    private PermissionViewModel viewModel;

    @Before
    public void setUp() {
        Context ctx = RuntimeEnvironment.application;
        // Pretend the permission is denied
        ShadowContext shadowCtx = shadowOf(ctx);
        shadowCtx.grantPermission(Manifest.permission.CAMERA,
                PackageManager.PERMISSION_DENIED);
        viewModel = new PermissionViewModel(ctx);
    }

    @Test
    public void requestCameraPermission_whenDenied_returnsFalse() {
        boolean granted = viewModel.requestCameraPermission();
        assertFalse(granted);
    }
}

Robolectric is ideal for testing ViewModels, UseCases, or any logic that decides whether to show a rationale or disable a UI component based on permission state.

CI Integration Tips

Tooling Comparison

ToolScopeStrengthsWeaknessesTypical Use
Espresso + GrantPermissionRuleIn‑app logic, UI after permission resultFast, reliable, integrates with AndroidJUnitRunner, no cross‑process neededCannot interact with system dialog; limited to granted/denied mockingUnit/UI tests that verify internal behavior given a known permission state
UI AutomatorCross‑process system dialogs, Settings appReal interaction with Allow/Deny buttons, works across API levels, can launch SettingsSlower, requires INTERACT_ACROSS_USERS_FULL, can be brittle with locale/animation changesEnd‑to‑end tests that validate the full permission flow including system UI
adb shell pm grant/revokeDevice‑level permission manipulationInstant, scriptable, works on non‑debuggable builds (grant only if user‑initiated via Settings)Cannot simulate user interaction with dialog; grant only works if app is debuggable or device is rooted (unless using Settings UI)Pre‑test setup, post‑test verification, CI sanity checks
RobolectricPure Java/JVM logic, ViewModel, UseCaseExtremely fast, no emulator/device needed, easy to stub permission checksDoes not test Android framework UI or system dialogsUnit tests of business logic that depends on permission state
Espresso Idling Resources + Camera2 APIAsync permission‑dependent operations (e.g., camera preview)Can wait for asynchronous results after permission grantedRequires custom idling resource implementationTests that need to confirm that a preview session starts only after permission is granted

Choose the combination that matches your test pyramid: unit/logic tests with Robolectric, instrumentation tests with Espresso + GrantPermissionRule for fast feedback, and a smaller set of UI Automator tests to guard against system‑level regressions.

Autonomous, Persona‑Driven Exploration with SUSA

Autonomous testing tools that model real‑world user behaviors can uncover permission‑related defects that scripted tests never consider. SUSA (SUSATest) is an autonomous QA platform that explores an app without predefined scripts, using a set of persona profiles that mimic how different people interact with software.

How SUSA Models Different Personas

SUSA defines behavior patterns for personas such as:

Each persona carries a probability distribution for actions like “tap Allow”, “tap Deny”, “check Don’t ask again”, “open Settings from the dialog”, or “ignore the dialog entirely”.

What It Looks For in Permission Flows

During an exploration run, SUSA monitors:

Because SUSA does not rely on hard‑coded test cases, it can discover edge cases such as a dialog that appears *after* the user has already navigated away from the triggering screen (a race condition caused by delayed permission requests).

Real‑World Bugs Found Only by Persona‑Driven Runs

In a recent analysis of a popular e‑commerce app, SUSA’s Impatient persona repeatedly tapped the back button while the permission dialog was still visible. This caused the activity to finish before the permission callback was delivered, leaving a fragment in a detached state and resulting in a NullPointerException when the app later tried to use the camera. The bug never appeared in scripted tests because those tests always waited for the dialog to resolve before proceeding.

Another finding came from the Elderly persona, which used the system’s magnification gesture to enlarge the dialog. On a device with a non‑standard font scaling implementation, the “Allow” button’s hit‑target shifted, making it impossible to tap without first panning the magnified view. The app logged a permission denial and showed a generic error, leading to poor conversion rates.

These examples illustrate how persona‑driven exploration surfaces issues rooted in timing, UI layout, and accessibility that deterministic scripts overlook. Integrating SUSA into your nightly regression cycle adds a layer of confidence that permission dialogs behave correctly for the full spectrum of real users.

Checklist for Permission Dialog Testing

Use this concise list before signing off a release. Tick each item only after you have verified it on at least one device representing your minimum supported API level and one OEM skin.

Key Takeaways

Permission dialogs are a nexus of user trust, platform security, and app functionality. A disciplined testing strategy combines:

  1. Clear matrix‑driven test design that covers happy paths, error paths, edge cases, accessibility, and security.

2

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free