Runtime Permissions
Starting with Android 6.0 (API 23), declaring a sensitive permission in the manifest is only half the story. Dangerous permissions — camera, location, and similar — must also be requested while the app is running, with the user seeing an explicit system prompt and choosing to allow or deny it. TrailLog needs this for exactly the feature the last lesson set up: GPS-tagging a hike.
Step One: Declaring the Permission in the Manifest
Every dangerous permission still starts with a manifest declaration — without it, requesting the permission at runtime has nothing to ask for:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
This declaration alone doesn't grant anything — it just tells the Play Store and the OS that the app may ask for this capability. The actual grant only happens through the runtime flow below.
Checking Whether a Permission Is Already Granted
Before doing anything location-related, always check the current status with
ContextCompat.checkSelfPermission() — a permission granted once (in a previous
session) stays granted, so there's no need to re-prompt every time:
fun hasLocationPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
Requesting a Permission the Modern Way
Activities & Intents already introduced
ActivityResultContracts as the modern replacement for
startActivityForResult(). The exact same API family replaces the older, deprecated
requestPermissions() call — ActivityResultContracts.RequestPermission()
registers a permission request the same structured way:
Requesting location permission for GPS-tagging a hike
private val locationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
startHikeTracking(this) // see kotlin-services.php
} else {
showLocationDeniedMessage()
}
}
private fun requestLocationPermission() {
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
The callback registered in registerForActivityResult fires once with the user's
decision — no loosely-typed request codes to track by hand, and no separate
onRequestPermissionsResult() override to implement.
Explaining Before Re-Asking: shouldShowRequestPermissionRationale()
If the user denies a permission once, immediately asking again with the exact same system dialog
tends to just train them to tap "Deny" faster. shouldShowRequestPermissionRationale()
tells you whether the system thinks an explanation would help — it returns true after
a first denial (before the user has permanently blocked the permission), which is your cue to show
your own explanation first:
private fun askForLocationPermission() {
when {
hasLocationPermission(this) -> startHikeTracking(this)
shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) -> {
// show a dialog explaining *why* TrailLog needs this, then request again
showRationaleDialog {
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
else -> locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
Explaining why a permission matters, in your own words, right before re-prompting is a genuinely good UX practice — it turns an unexplained system dialog into an informed choice, and noticeably improves how often users grant the permission on the second try.
Requesting Multiple Permissions at Once
Some features need more than one permission together — a "log a hike with a photo and GPS point"
flow might reasonably ask for location and camera access in the same prompt.
RequestMultiplePermissions() handles exactly that:
Requesting location and camera together
private val multiplePermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
val locationGranted = results[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
val cameraGranted = results[Manifest.permission.CAMERA] ?: false
if (locationGranted && cameraGranted) {
startHikeLoggingWithPhoto()
} else {
showPermissionsNeededMessage()
}
}
private fun requestHikeLoggingPermissions() {
multiplePermissionsLauncher.launch(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA)
)
}
The callback receives a Map<String, Boolean> — one entry per permission
requested — so you can check each one individually rather than assuming an all-or-nothing result.
Permissions TrailLog Actually Needs, Track-Wide
| Permission | Why TrailLog needs it | Covered in |
|---|---|---|
ACCESS_FINE_LOCATION | GPS-tagging trails and live hike tracking | This lesson, and Fused Location Provider |
CAMERA | Capturing a trail photo directly in-app | CameraX |
POST_NOTIFICATIONS (API 33+) | Showing the hike reminder and recording-in-progress notifications | Notifications & Channels |