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.

Note: These are correct, working permission-handling patterns, but this website's own project doesn't run a live Android Studio build, so no system permission dialog actually appears on this page.

Permissions TrailLog Actually Needs, Track-Wide

PermissionWhy TrailLog needs itCovered in
ACCESS_FINE_LOCATIONGPS-tagging trails and live hike trackingThis lesson, and Fused Location Provider
CAMERACapturing a trail photo directly in-appCameraX
POST_NOTIFICATIONS (API 33+)Showing the hike reminder and recording-in-progress notificationsNotifications & Channels
Up next: Handling permissions correctly is one piece of a bigger picture. Security Best Practices rounds up the rest — protecting secrets, encrypting sensitive local data, and enforcing HTTPS.