Blog / How to Test App Permissions on iOS and Android (Camera, Location, Notifications)
How-To

How to Test App Permissions on iOS and Android (Camera, Location, Notifications)

An iOS permission prompt appears exactly once, and that single rule is why most teams never automate permission testing. Here is how to make it reliable in CI.

An iOS permission prompt appears exactly once. The user taps Allow or Don’t Allow, the status locks, and every subsequent call to request that permission returns the stored answer without showing a dialog again. That rule, documented in Apple’s authorization guidance, is the single reason most teams never automate permission testing. You get one shot per install, and a test that consumes it leaves the next test running against a state it never chose.

We hit that wall building Pie’s mobile agent. We reset permission state a half-dozen wrong ways before one held cleanly across a parallel CI run. This guide is what we learned: how iOS and Android actually model permissions, how to put a device into the exact state you want before each test, and how to drive the native dialog that lives outside your app entirely.

What you’ll learn

  • Why permission prompts break test determinism
  • How iOS and Android permission models differ
  • The exact simctl and adb commands to reset state
  • How to drive the native dialog across frameworks
  • The permission scenarios most teams skip

Why Testing App Permissions Is So Hard to Automate

Permission testing is really three problems stacked, and every one has to be solved before a single assertion is trustworthy.

  1. State is sticky and global. Permission state is controlled by the operating system, not your app, and it outlives the test. Grant camera access in one test and you have changed the starting condition for every test that follows, so a suite that passes in one order fails in another. That is what breaks determinism.
  2. The OS refuses to re-ask. On iOS, the system alert for a data type shows once and never again for that install, so you cannot re-run the prompt mid-suite. On Android, denying the same runtime permission twice flips the app into a permanently-denied state where shouldShowRequestPermissionRationale returns false and no dialog reappears until the user re-grants from Settings.
  3. The prompt is not yours to click. The system renders it in a separate process, so a framework querying your app’s view hierarchy finds nothing to tap.

Both platforms treat a denial as a decision to respect, which is good for users and brutal for naive automation. Solve all three, in that order, before you trust a result.

How iOS and Android Handle Permissions Differently

Both platforms gate sensitive data behind user consent, but the timing and granularity differ enough that one test strategy will not cover both. iOS asks at first use and locks the answer for the install. Android grants at runtime too, but layers in one-time grants, foreground-versus-background location, and automatic reset for dormant apps.

On iOS, every protected resource needs a usage-description string in Info.plist or the app crashes the moment it asks, and the prompt fires the first time you call the request API. Tracking is its own special case. Since iOS 14.5, reaching the advertising identifier requires the separate App Tracking Transparency prompt, and worldwide opt-in has stayed low, reaching only about 24% across all apps by late 2021 in Flurry’s tracking. A denied ATT path is the common path, not the exception, which is exactly why it needs a test.

On Android, runtime permissions have applied to dangerous groups since Android 6.0 (API 23). Android 11 added one-time permissions (“Only this time”) and the auto-reset of permissions for unused apps. Your matrix has to include states iOS does not even have.

The states that actually differ

iOS location alone moves through five separate states, from not-determined all the way to always-on, while Android layers on one-time grants, permanent denial, and a silent auto-reset for dormant apps. A suite that only checks grant-versus-deny quietly skips half of what Android can actually do.

How to Reset Permission State Between Test Runs

Reset permission state with the platform command-line tools before each test, never by tapping through Settings in the UI. Deterministic permission tests depend on a known starting state, and the only reliable way to guarantee it is to set the state programmatically while the app is not running. Two commands do most of the work.

On the iOS Simulator, simctl exposes a privacy subcommand that grants, revokes, or resets any service for a bundle ID:

# Reset every privacy permission for the app (back to "not determined")
xcrun simctl privacy booted reset all com.yourcompany.app

# Or target one service
xcrun simctl privacy booted revoke camera com.yourcompany.app
xcrun simctl privacy booted grant location com.yourcompany.app

On Android, adb and pm cover the same ground, with appops for the finer-grained operations:

# Reset runtime permissions for all apps to their defaults
adb shell pm reset-permissions

# Or target one permission for one package
adb shell pm revoke com.yourcompany.app android.permission.ACCESS_FINE_LOCATION
adb shell pm grant  com.yourcompany.app android.permission.CAMERA

The pattern is the same on both platforms. Reset to a clean baseline in test setup, set only the permission the test cares about, then launch. Do this and a denied-location test produces the same result whether it runs first or hundredth. Skip it and you are debugging order-dependent flakiness, a well-known category of non-deterministic tests.

How to Handle the Native Permission Dialog

Handle the native permission dialog with a tool that can reach outside your app’s process, because the alert is drawn by the OS, not by you. Selector-based frameworks each solve this differently, and the right choice depends on whether the prompt is the thing under test or just an obstacle on the way to it.

The fastest path, when permissions are not the point of the test, is to suppress the prompt entirely. Appium exposes autoGrantPermissions on the UiAutomator2 driver and autoAcceptAlerts / autoDismissAlerts on the XCUITest driver. Espresso has GrantPermissionRule to pre-grant before the activity launches. The catch is that auto-granting means you never exercise your own priming screen or denial handling, so it cannot be your only strategy.

ApproachPlatformHow it taps the dialogTrade-off
Appium auto-grantAndroidPre-grants before launch; no dialog shownSkips priming and denial paths entirely
Appium auto-acceptiOSAuto-taps the system alert buttonAccepts all alerts, including ones you wanted to deny
UiAutomator selectorAndroidFinds the Allow button by resource-id/textButton id and wording shift across OS versions
XCUITest monitoriOSInterruption monitor catches the alertTiming-sensitive; needs a follow-up interaction to fire
Vision-based (Pie)iOS + AndroidSees and taps the dialog like a userOne flow covers both platforms; no selectors to maintain

When you genuinely need to test the dialog, the brittle part is the locator. On Android the Allow button’s resource id and text vary across OS versions and OEM skins; on iOS the alert text changes with locale and release. Selector-based handling works until a version bump renames a button, the same selector-brittleness that plagues mobile regression testing, just relocated to the system layer.

How to Test Denied and Revoked Permissions

Test denied and revoked permissions by forcing the device into that state before launch and asserting the app degrades correctly, rather than hoping a user taps Don’t Allow during the run. The denied path is where real bugs hide, because the happy path gets exercised constantly and the fallback rarely does. Set the state, then check the consequences.

Start each scenario from a known baseline with the reset commands above, then verify three things the happy path never touches:

  1. The fallback UI renders. With camera revoked, the scanner screen should show its empty state or a clear call to action, not a frozen viewfinder or a crash.
  2. The Settings redirect works. iOS cannot re-prompt after a denial, so the only recovery is deep-linking to Settings. Assert the button exists and that returning with the permission granted unblocks the feature.
  3. The permanent-denial path on Android. After two denials, shouldShowRequestPermissionRationale returns false. Drive that state and confirm you show the Settings path instead of a prompt that will never appear.

Mid-session revocation deserves its own test. Android kills your process when a permission is revoked from system Settings while the app runs, so the user returns to a fresh launch, and iOS delivers the change on next access. A robust app handles that return without losing the user’s place, and the only way to know it does is to revoke and reattach.

One test drives both platforms.

Drive both native permission dialogs from one test. No selectors, no forks.

Book a Demo

What Permission Scenarios You Should Test

The scenarios worth testing go well beyond grant and deny. A complete matrix covers the request moment, every response state, recovery after denial, and the OS-driven changes that happen without the user touching your app. Most suites cover the first two and skip the rest, which is exactly where production incidents come from.

At minimum, cover these per sensitive permission (camera, location, microphone, notifications, photos, contacts):

  • First-run grant. The prompt appears, the user allows, the feature works.
  • First-run denial. The user declines; the fallback UI and Settings path appear.
  • Pre-denied cold start. The app launches already denied (a returning user or auto-reset) and never assumes access.
  • Location granularity. Foreground-only versus always, and on Android the approximate-versus-precise toggle introduced in Android 12.
  • One-time grant (Android). “Only this time” is granted, then revoked on next launch.
  • Permanent denial (Android). Two declines route to Settings, not a dead prompt.
  • App Tracking Transparency (iOS). The ATT prompt and the denied branch, since most users land there.

A useful rule from building this: the number of permission tests you need is roughly the number of sensitive permissions times the number of response states, not times two. Notifications alone span provisional authorization, full grant, and denial. Treat the matrix as real coverage, the same way you would treat test coverage anywhere else, and the gaps stop being surprises.

How Pie Tests Permissions Across iOS and Android

Pie tests permissions by looking at the screen the way a user does, so the native dialog stops being a special case. Because the agent is vision-based rather than selector-based, it sees the system Allow and Don’t Allow buttons and taps them, whether iOS SpringBoard or the Android permission controller drew them. Four things fall away:

  • No selectors to babysit. Pie is not matching a button by a resource id that the next OS release renames, and there is no interruption monitor whose timing you have to tune.
  • One test, both platforms. You describe the scenario once (“deny location, then verify the map shows the enable-location prompt”) and Pie runs it on iOS and Android.
  • Survives OS bumps. When the dialog’s wording or layout shifts, a human reading the screen still finds the button, and so does Pie.
  • Deterministic by default. Pie drives each scenario from a known baseline, so the denied-camera test means the same thing on run one and run one thousand.

The work shifts from maintaining permission plumbing to deciding which states matter, which is where it belonged all along.

Solve the Three Problems in Order

Permission testing breaks automation for three reasons: the state is sticky and global, iOS gives you one prompt per install, and the dialog lives outside your app. Handle them in that order.

  • Reset state with simctl and adb before every run.
  • Build a matrix that covers the denied and auto-reset paths, not just grant-versus-deny.
  • Drive the native dialog in a way that survives an OS version bump.

Get this right and denial stops being an edge case. It is a state a large share of your users already live in, and the permission flow turns into one more deterministic test instead of the flaky one everyone reruns. Pie handles the dialog and the cross-platform duplication for you, so the only thing left to decide is which states matter.

Test the states your suite skips.

Cover every denial and cold-start path on iOS and Android.

Book a Demo

Frequently Asked Questions

iOS shows the system authorization alert for a given data type once per install. After the user responds, the status is locked to granted or denied, and calling the request API again returns the stored result without showing a dialog. The one built-in exception is location, where an app can upgrade from when-in-use to always with a second system prompt. Otherwise, the only way back to the prompt is uninstalling the app or resetting privacy state on a simulator with simctl. To re-ask a real user, you deep-link them to the Settings app.
On the iOS Simulator, use xcrun simctl privacy booted reset all to clear every permission, or reset a single service by name. On Android, use adb shell pm reset-permissions to clear runtime grants for every app, or adb shell pm revoke for one. Resetting state before each run is what makes permission tests deterministic instead of order-dependent.
Yes. The Appium UiAutomator2 driver accepts autoGrantPermissions, and the XCUITest driver accepts autoAcceptAlerts and autoDismissAlerts. These are useful for getting past prompts when permissions are not what you are testing. The trade-off is that they skip the prompt entirely, so they cannot verify that your in-context priming screen, denial handling, or Settings redirect actually work.
Revoke the permission with simctl or adb before the run so the app starts in the denied state, then assert that your fallback UI appears: the empty state, the disabled feature, or the prompt to open Settings. On Android, denying twice triggers the permanent-denial path where shouldShowRequestPermissionRationale returns false, which is a separate state worth its own test.
No. The system permission alert is rendered by the OS in a separate process, outside your app's view tree. That is why selector-based frameworks need special handling such as UiAutomator on Android or SpringBoard alert handling on iOS to tap Allow or Don't Allow. A vision-based agent sees the dialog the way a user does and taps it directly.
Android automatically resets the runtime permissions of apps the user has not opened for a few months, returning them to the unauthorized state. The feature shipped in Android 11 and was backported through Google Play services to Android 6.0 and up. Your app has to re-request when the user returns, so the cold-start permission flow is a real path that deserves a test, not an edge case.
Both. Emulators and simulators are fast and scriptable, which makes them ideal for the bulk of permission-state combinations in CI. Real devices catch the differences that matter for permissions specifically: OEM permission managers, location accuracy hardware, and OS-version prompt wording. Run the matrix on virtual devices and smoke the critical paths on a few physical ones.
Adithya Aggarwal
Adithya Aggarwal
CTO & Co-founder at Pie

Eight years building search and delivery systems at Amazon. The kind of scale where flaky tests block billion-dollar releases. Now CTO at Pie, building AI agents that adapt when your UI changes. LinkedIn →