Common Permission Dialogs Bugs and How to Catch Them

Common Permission Dialogs Bugs and How to Catch Them

January 27, 2026 · 18 min read · Common Issues

Common Permission Dialogs Bugs and How to Catch Them

Permission dialogs are the gatekeepers between an app’s functionality and the device’s protected resources. When they behave incorrectly, users see confusing prompts, lose trust, or abandon the app entirely. This guide walks through the most frequent permission‑dialog bugs, explains why they appear, shows how they look to real people, and gives concrete steps to reproduce, detect, fix, and prevent each issue. The article ends with a test matrix, a symptom‑to‑fix reference table, and a short checklist you can bookmark for your next release.

---

1. Why Permission Dialogs Fail: Core Concepts

Before diving into individual bug patterns, it helps to understand the mechanics that underlie every runtime permission request on Android (the principles apply similarly to iOS, but the examples focus on Android because it exposes the most variability).

1.1. The Permission Lifecycle

  1. Declared in manifest.
  2. Runtime checkContextCompat.checkSelfPermission() returns PERMISSION_GRANTED or PERMISSION_DENIED.
  3. Rationale decision – If the user has previously denied and checked “Don’t ask again”, shouldShowRequestPermissionRationale() returns false.
  4. Request launchActivityResultContracts.RequestPermission (or the legacy requestPermissions()) shows the system dialog.
  5. Result callbackonRequestPermissionsResult() or the ActivityResult API receives the user’s choice.
  6. Persistence – The grant/deny state is stored by the system and survives process death, but not app reinstall unless the permission is declared android:requestLegacyExternalStorage="true" (legacy) or the user resets it via Settings.

1.2. Common Failure Points

Understanding these points makes it easier to spot where a bug originates and how to test for it.

---

2. Bug Pattern 1: Permission Denied After Grant

2.1. What Happens

The user grants a permission (e.g., LOCATION) via the system dialog, but a few seconds later the app behaves as if the permission is still denied—features that rely on the permission are disabled, or the app immediately re‑shows the request.

2.2. Root Cause

Most often this is a state‑synchronization bug: the app caches the permission status in a variable or singleton at launch and never updates it after the asynchronous callback finishes. If the cache is read before the callback runs, the stale “denied” value is used.

2.3. How to Reproduce

  1. Launch the app fresh (no prior grant).
  2. Trigger a feature that needs the permission (e.g., tap “Enable Location”).
  3. When the system dialog appears, press Allow.
  4. Immediately navigate away from the screen that initiated the request (e.g., press Back) and then return to the feature.
  5. Observe that the feature is still disabled or the dialog appears again.

2.4. Detection Strategies

2.5. Fix


class LocationViewModel : ViewModel() {
    private val _hasLocation = MutableLiveData(false)
    val hasLocation: LiveData<Boolean> = _hasLocation

    fun requestLocation() {
        if (ContextCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED) {
            _hasLocation.value = true
        } else {
            requestPermissionLauncher.launch(
                Manifest.permission.ACCESS_FINE_LOCATION
            )
        }
    }

    private val requestPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        _hasLocation.value = granted
    }
}

2.6. Prevention

---

3. Bug Pattern 2: Dialog Shown Multiple Times

3.1. What Happens

After the user grants a permission, the system dialog reappears on every subsequent app launch or even within the same session, creating a frustrating loop.

3.2. Root Cause

The app incorrectly treats a temporary denial as a reason to re‑request every time the feature is accessed, ignoring the fact that the user already granted it. This often stems from calling requestPermissions() inside onResume() or a ViewPager page change without checking the current state first.

3.3. How to Reproduce

  1. Install the app, launch it, and deny a permission (e.g., CAMERA) when prompted.
  2. Immediately grant it in the next prompt (press Allow).
  3. Close the app (swipe away from recent) and relaunch.
  4. Observe that the permission dialog appears again despite the grant being recorded in Settings.

3.4. Detection Strategies

3.5. Fix


fun maybeRequestPermission(
    activity: FragmentActivity,
    permission: String,
    rationale: String,
    requestCode: Int
) {
    if (ContextCompat.checkSelfPermission(activity, permission) ==
        PackageManager.PERMISSION_GRANTED) {
        return
    }
    if (activity.shouldShowRequestPermissionRationale(permission)) {
        // Show custom rationale UI (Snackbar, dialog)
        showRationaleDialog(activity, rationale) { 
            requestPermissionLauncher.launch(permission) 
        }
    } else {
        requestPermissionLauncher.launch(permission)
    }
}

3.6. Prevention

---

4. Bug Pattern 3: Permission Request Shown in Wrong Context

4.1. What Happens

The permission dialog appears while the user is on a completely unrelated screen (e.g., settings page) or after they have navigated away from the feature that triggered it. The user may think the request is spurious and deny it out of confusion.

4.2. Root Cause

A broadcast receiver, foreground service, or WorkManager triggers the request based on a global event (e.g., network change) without checking whether the app is currently in the foreground or whether the relevant UI is visible. The request is posted to the system UI thread, which shows it atop whatever activity is currently resumed.

4.3. How to Reproduce

  1. Start a background task that requests the ACCESS_FINE_LOCATION permission when the device switches from Wi‑Fi to mobile data.
  2. Connect to Wi‑Fi, launch the app, navigate to the Settings screen.
  3. Switch to mobile data (enable airplane mode then disable Wi‑Fi).
  4. Observe the location permission dialog appearing over the Settings screen.

4.4. Detection Strategies

4.5. Fix


fun Context.isAppInForeground(): Boolean {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    val runningApp = activityManager.getRunningAppProcesses()
        ?.firstOrNull { it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND }
    return runningApp?.processName == packageName
}

4.6. Prevention

---

5. Bug Pattern 4: Dialog Blocks UI Thread / Causes ANR

5.1. What Happens

When the permission dialog is shown, the app becomes unresponsive for a few seconds, leading to an “App Not Responding” (ANR) dialog or a noticeable lag in UI interactions.

5.2. Root Cause

Calling the permission request synchronously on the main thread (e.g., using ActivityCompat.requestPermissions() inside a clickListener that performs heavy work before the call) or performing expensive operations inside the result callback (e.g., initializing a camera, starting a location update) before returning control to the system.

5.3. How to Reproduce

  1. Add a heavy computation (e.g., JSON parsing of a 10 MB file) to the click handler that also requests the CAMERA permission.
  2. Tap the button; observe the UI freeze for the duration of the computation before the system dialog appears.
  3. With profiling enabled (adb shell am profile start), you’ll see the main thread blocked.

5.4. Detection Strategies

5.5. Fix


button.setOnClickListener {
    // Fast check + request
    if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        return@setOnClickListener
    }
    // Permission already granted – do heavy work off‑main
    lifecycleScope.launch(Dispatchers.IO) {
        val photo = takeHighResPhoto()   // suspending function
        withContext(Dispatchers.Main) {
            showPhoto(photo)
        }
    }
}

5.6. Prevention

---

6. Bug Pattern 5: Missing Rationale Leading to Denial

6.1. What Happens

The user sees a system permission dialog with no explanation of why the app needs the permission, feels uneasy, and selects Deny. The app then loses functionality, and the user may not realize they can re‑enable it later.

6.2. Root Cause

The app calls requestPermissions() directly without first invoking shouldShowRequestPermissionRationale() to determine if a custom explanation is warranted. On Android, the system only shows a rationale when the user has previously denied the permission *and* has not checked “Don’t ask again”. Skipping this step wastes an opportunity to build trust.

6.3. How to Reproduce

  1. Launch the app, deny a permission (e.g., RECORD_AUDIO) when first prompted.
  2. Immediately trigger the feature again; the system dialog appears without any context.
  3. Observe that the user is likely to deny again because they don’t see why the app needs the mic.

6.4. Detection Strategies

6.5. Fix


private fun requestMicrophonePermission() {
    if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
        startRecording()
        return
    }

    if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
        // Show custom explanation
        Snackbar.make(findViewById(android.R.id.content),
            "We need the mic to add voice notes to your memos.",
            Snackbar.LENGTH_INDEFINITE)
            .setAction("OK") {
                requestPermissionLauncher.launch(
                    Manifest.permission.RECORD_AUDIO
                )
            }
            .show()
    } else {
        // User has checked “Don’t ask again”; direct to settings
        showPermissionSettingsRationale()
    }
}

6.6. Prevention

---

7. Bug Pattern 6: Permission Revoked at Runtime Not Handled

7.1. What Happens

While the app is running, the user goes to Settings and revokes a permission (e.g., turns off Location). The app continues to assume the permission is still granted, leading to crashes, silent failures, or misleading UI states (e.g., a map showing a stale location).

7.2. Root Cause

The app only checks permission status at launch or when a feature is first used, and never listens for runtime revocation events. Android does not broadcast a revocation, so the app must poll or rely on callbacks from the APIs that use the permission (e.g., FusedLocationProviderClient returns a failure result).

7.3. How to Reproduce

  1. Start the app and grant LOCATION permission.
  2. Begin a location‑tracking feature (see a live dot on the map).
  3. Open Settings → Apps → [Your App] → Permissions → toggle Location off.
  4. Return to the app; observe that the map stops updating, but no error is shown, or the app throws a SecurityException when trying to request updates.

7.4. Detection Strategies

7.5. Fix


fusedLocationClient.lastLocation.addOnCompleteListener { task ->
    if (!task.isSuccessful) {
        when (task.exception) {
            is SecurityException -> {
                // Permission likely revoked
                showPermissionMissingSnackbar()
            }
            else -> // handle other errors
        }
    } else {
        // success – update UI
    }
}

7.6. Prevention

---

8. Bug Pattern 7: Incorrect Handling of “Don’t Ask Again”

8.1. What Happens

The user denies a permission and checks Don’t ask again. The app continues to show the system dialog on every launch, or worse, it crashes because it assumes the permission will eventually be granted.

8.2. Root Cause

The app ignores the return value of shouldShowRequestPermissionRationale(). When it returns false, the correct action is to explain why the permission is needed and direct the user to Settings, not to re‑show the system prompt. Some developers mistakenly treat false as “still ask later”.

8.3. How to Reproduce

  1. Launch the app and deny a permission (e.g., READ_CONTACTS) while ticking Don’t ask again.
  2. Close the app and relaunch.
  3. Observe that the permission dialog appears again (incorrect) or that the app crashes when trying to read contacts.

8.4. Detection Strategies

8.5. Fix


if (ContextCompat.checkSelfPermission(this,
        Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {

    if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
        showRationaleDialog() { requestPermissionLauncher.launch(Manifest.permission.READ_CONTACTS) }
    } else {
        // User has chosen “Don’t ask again”
        showSettingsGuidance()
    }
}

The showSettingsGuidance() function launches an intent to the app’s settings screen where the user can manually toggle the permission.

8.6. Prevention

---

9. Bug Pattern 8: Permission Dialog Appears Behind Other UI

9.1. What Happens

The system permission dialog is displayed, but it is obscured by a full‑screen loading spinner, a dialog from a third‑party library, or a system overlay (e.g., chat heads). The user taps elsewhere, thinks the app is frozen, and may force‑close it.

9.2. Root Cause

The app shows its own UI (often a ProgressBar or a custom dialog) before requesting the permission, and does not dismiss it when the system dialog appears. Because the system dialog is shown at the window level TYPE_APPLICATION_OVERLAY, it can still be covered if the app’s window has a higher z‑order or if the app uses WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE incorrectly.

9.3. How to Reproduce

  1. Trigger a feature that first shows a full‑screen loading indicator (e.g., while fetching a token).
  2. Immediately after the indicator appears, request the CAMERA permission.
  3. Observe that the loading indicator stays on top and the permission dialog is partially or fully hidden.

9.4. Detection Strategies

9.5. Fix


fun requestCameraPermission() {
    // Hide any full‑screen blockers first
    binding.progressBar.visibility = View.GONE

    if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
        startCameraPreview()
    } else {
        if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
            showRationale { requestPermissionLauncher.launch(Manifest.permission.CAMERA) }
        } else {
            showSettingsGuidance()
        }
    }
}

9.6. Prevention

---

10. Bug Pattern 9: Permission Request Triggered by Background Service

10.1. What Happens

A background service (e.g., a JobIntentService or WorkManager) decides it needs the READ_PHONE_STATE permission to read the device ID and calls requestPermissions() directly. Because the service has no UI context, the system shows the dialog, but the user may be in another app, leading to confusion and a high denial rate.

10.2. Root Cause

Missing foreground guard: background components lack a visible Activity or Fragment to host the permission rationale, yet they still attempt to request runtime permissions. The system allows the request, but the resulting dialog is detached from any app‑owned context.

10.3. How to Reproduce

  1. Start a background worker that periodically checks for telephony changes and, if the permission is missing, calls requestPermissions().
  2. Put the app in the background (press Home).
  3. Wait for the worker to fire; observe the permission dialog appearing over whatever app is currently in the foreground (e.g., Chrome).
  4. Return to your app; notice that the dialog is still present, but the user may have already interacted with it blindly.

10.4. Detection Strategies

10.5. Fix


class PhoneStateWorker(appContext: Context, params: WorkerParameters) :
    CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        val hasPermission = ContextCompat.checkSelfPermission(
            applicationContext,
            Manifest.permission.READ_PHONE_STATE
        ) == PackageManager.PERMISSION_GRANTED

        if (!hasPermission) {
            // Defer to UI
            val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
            prefs.edit().putBoolean("pending_phone_state", true).apply()
            return Result.retry()   // let the OS retry later; UI will handle it
        }
        // Proceed with work
        // ...
        return Result.success()
    }
}

In the main Activity’s onResume():


override fun onResume() {
    super.onResume()
    val prefs = PreferenceManager.getDefaultSharedPreferences(this)
    if (prefs.getBoolean("pending_phone_state", false)) {
        prefs.edit().remove("pending_phone_state").apply()
        maybeRequestPhoneStatePermission()
    }
}

10.6. Prevention

---

11. Bug Pattern 10: Localization Issues in Dialog Text

11.1. What Happens

The permission dialog’s title or message is truncated, overlaps with the “Allow/Deny” buttons, or shows English text in a non‑English locale because the app supplies a custom rationale string that is not properly localized.

11.2. Root Cause

Developers sometimes hard‑place the rationale text in a layout or call setMessage() on an AlertDialog with a string literal, bypassing the strings.xml resource system. When the app runs on a device with a different language, the literal remains unchanged, causing layout overflow or confusing wording.

11.3. How to Reproduce

  1. Set the device language to Spanish (or any right‑to‑left language).
  2. Trigger a feature that shows a custom rationale dialog before the system permission request.
  3. Observe that the dialog’s text is either in English, exceeds the view bounds, or misaligns the buttons.

11.4. Detection Strategies

11.5. Fix

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