Handling runtime permissions on Android Marshmallow and newer
When Google introduced Android 6.0 Marshmallow, the way applications asked for access to sensitive device features changed forever. Instead of granting every privilege the moment a user tapped Install, the operating system pushed those decisions into the moment of actual use, asking for consent only when an app reached for the camera, microphone, location services, contacts, or storage.
For developers building apps distributed through Google Play to Australian users, the shift carried extra weight. The Australian Privacy Principles, which sit under the Privacy Act 1988 and are enforced by the Office of the Australian Information Commissioner, expect organisations to collect personal information only when reasonably necessary. The runtime permission framework on Android mirrors that philosophy surprisingly well, and learning it properly sets the foundation for compliant, trustworthy software.
This walkthrough covers the practical mechanics of asking for, checking, and responding to runtime permissions in Java and Kotlin. Whether you are shipping a fitness tracker from a co-working space in Brisbane or a field-service tool used by tradies in Perth, the patterns below will keep your app respectful of user choice.
You will move from declaring entries in AndroidManifest.xml, through checking the current grant state, to handling the user's response, including the awkward edge cases such as "Don't ask again" and permanent denial. A short comparison table near the middle of the article summarises the three permission categories, so you always know which path to take.
Understanding the permission tiers
Android groups every declared authority into one of three protection levels, and recognising them saves hours of confusion. Normal permissions cover features that pose minimal risk to user privacy, such as setting the network state or vibrating the device. The system grants these automatically at install time, and your code never has to think about them again.
Dangerous permissions, by contrast, gate access to sensitive data and hardware, including the camera, microphone, fine and coarse location, calendar, call log, SMS, and external storage. These are the entitlements that trigger the runtime prompt your users will see, and they are also the ones most relevant when aligning with the expectations of the Office of the Australian Information Commissioner.
Signature permissions are reserved for cases where two apps share the same signing certificate, and the platform grants them only when the requesting package was signed with the same key as the declaring package. Most consumer apps will never touch this tier, but if you build a suite of tools for an internal team, you may rely on it to share data safely between modules.
Declaring entries in the manifest
Even with the runtime model, the manifest remains the source of truth. Every dangerous permission your app might request must appear inside the manifest's top-level uses-permission element, otherwise the system silently refuses the request. Place the file under src/main and edit it before you write a single line of request code.
In Java projects, the entry looks like a simple child tag, while Kotlin modules accept exactly the same XML. A camera-heavy application, for example, would declare CAMERA, and a navigation tool would list ACCESS_FINE_LOCATION alongside ACCESS_COARSE_LOCATION, because the platform differentiates between the two and a request for fine location automatically implies a need for the coarse variant.
Group your entries logically and keep the list minimal. Australian users are increasingly savvy, and an app that requests READ_CONTACTS when it has no obvious need for an address book will get uninstalled quickly, regardless of how polished the onboarding flow looks.
Checking the current grant state
Before triggering any prompt, check whether the user has already granted the privilege. The ContextCompat.checkSelfPermission method, paired with the Manifest.permission constant, returns PackageManager.PERMISSION_GRANTED or PackageManager.PERMISSION_DENIED. Reading the value first prevents the system from displaying redundant dialogs and respects the choices users have already made.
A common pattern places the check inside the activity's onResume or onCreate, or in a ViewModel if you follow the Jetpack architecture guidelines. The key is to ask only when the feature is genuinely required, not when the screen first appears. A maps screen that pulls the user's current coordinates should request location the first time the user taps "Where am I?", not the moment the activity inflates.
When developing against a targetSdkVersion of 23 or higher, even older permissions like WRITE_EXTERNAL_STORAGE flip into the dangerous category. Plan an audit of your manifest before bumping the target SDK, especially if you maintain codebases that predate the Marshmallow release. The table below summarises the three protection levels and clarifies which of them require a runtime check at all.
| Protection Level | Granted When | Runtime Prompt? | Typical Examples |
|---|---|---|---|
| Normal | At install | No | INTERNET, ACCESS_NETWORK_STATE, VIBRATE |
| Dangerous | At use | Yes | CAMERA, ACCESS_FINE_LOCATION, READ_CONTACTS |
| Signature | When signing keys match | No | Custom provider access, OEM system features |
Requesting the privilege
When the check returns DENIED, call ActivityCompat.requestPermissions, passing the activity, a String array of the permissions you need, and an integer request code that uniquely identifies this particular ask. The system then queues the dialog, and your code continues without blocking. Request several related permissions in a single call to reduce prompt fatigue; users in Melbourne commuting on the train will appreciate a single, clear question rather than a sequence of interruptions.
Avoid bundling unrelated entitlements in one request. Asking for camera and contacts in the same prompt confuses users and may trigger higher uninstall rates, because the rationale becomes harder to communicate. Group by feature, not by convenience.
The request code is purely an internal identifier, so pick something stable per feature. A common convention is to define constants like REQUEST_LOCATION or REQUEST_CAMERA at the top of the activity. The value is returned in the callback so you can route the response to the right branch of code.
Handling the user's response
The system delivers the result through the onRequestPermissionsResult callback, which Android routes to the activity for backward compatibility. The grantResults array holds one entry per requested permission, and a simple loop is enough to discover which ones the user accepted and which were refused. A non-zero PERMISSION_GRANTED value confirms the right to proceed, while PERMISSION_DENIED signals the need for further handling.
When the user grants the requested authority, continue with the original flow. If they decline, decide whether the feature is essential. A barcode scanner without camera access is useless, so you might redirect the user to a screen explaining the dependency, while a contact picker in a sharing feature could simply fall back to manual entry.
If the user selected "Don't ask again", the system will skip future prompts for that permission, and the only remaining option is to direct the user to the system settings screen. Build a helper that constructs an Intent with ACTION_APPLICATION_DETAILS_SETTINGS, so the journey is one tap away rather than buried in menus.
Showing a rationale before the request
Calling shouldShowRequestPermissionRationale returns true when the user previously denied the request without the permanent block. That window is the right moment to display a custom dialog explaining why the app needs the privilege, framed in language relevant to the user. For an Australian audience, that might mean mentioning offline map caching for a weekend in the Blue Mountains, or quick access to a tradie's job site for proof of delivery in a regional town.
A well-timed rationale turns a hesitant tap into a confident grant. Keep the message short, avoid jargon, and show the consequence of declining in plain English. Many developers include a small illustration on the rationale screen, which tends to lift acceptance rates noticeably across both consumer and enterprise builds.
If the method returns false and you are not on the first request, the user has either selected "Don't ask again" or the device runs a policy that suppresses prompts. In both cases, your code path should pivot to the settings deep link described earlier rather than calling requestPermissions, which would silently do nothing.
Persisting the decision and testing edge cases
Store a lightweight flag in SharedPreferences for choices that affect the user experience beyond a single session, such as a dismissed onboarding hint. A boolean such as hasSeenLocationRationale helps you decide whether to show an introductory explanation the very first time the request fires, without nagging returning users.
Test thoroughly across the supported API range. The Android emulator image for API 23 and above exposes a panel under App Actions where you can manually grant or revoke each permission, mimicking the long-press app info screen on a real device. Run through the install, grant, deny, and permanent-deny paths on at least one phone running the latest stable release of Android, since the dialog copy and visual treatment vary between versions.
Aggregate the data on which permissions users refuse most often, and feed those insights back into product planning. Teams shipping to a privacy-conscious market like Australia often find that features designed around non-sensitive data, such as manual entry or on-device caching, deliver a smoother overall experience than chasing every convenient entitlement.