Skip to content
All posts

When to ask for notification permission on Android (and how to recover from Deny)

A practical guide to Android's POST_NOTIFICATIONS runtime permission — when to ask, how to prime the request, and what to do when someone denies it.

MFKAPPS 5 min read

Since Android 13, notifications aren’t granted by default — the user has to say yes to POST_NOTIFICATIONS explicitly, the same way they’d say yes to camera or location access. For an app where notifications carry the entire value — a hydration reminder, a medication alert — that single system dialog is the difference between the app working and the app being an icon that occasionally opens. Get the timing wrong and most people tap “Don’t allow” out of reflex, before they’ve seen a reason to say yes. Get it right and the same dialog becomes routine. The permission itself doesn’t change; the request around it does.

The dialog only fires once

POST_NOTIFICATIONS behaves like every other Android runtime permission: the system shows its dialog once per install. If the user denies it, the next call to requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) returns false silently — no dialog, no error, just an immediate denial. That’s the part that trips people up during testing: a second ActivityResultLauncher request looks like it’s doing nothing, when in fact it’s doing exactly what it’s supposed to.

val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) {
        scheduleDailyReminder()
    } else {
        showNotificationsDisabledState()
    }
}

Because there’s only one real shot at the system dialog, when you call launch() matters more than how you call it.

Don’t ask on first launch

The reflex is to request every permission the app will ever need during onboarding, so the rest of the app can assume they’re all granted. For notifications specifically, this is close to the worst possible timing. On first launch the user has no context for why a reminder app wants to interrupt them later — they’ve seen the icon for ten seconds. A permission dialog with no preceding reason reads as friction, and friction at that exact moment gets a reflexive “Don’t allow” more often than a considered one.

The fix is to ask at the point where the permission’s value is self-evident. In Hydrame, that’s the moment someone finishes setting a daily water goal — they’ve just told the app when they want to drink more, so a reminder isn’t an interruption, it’s the feature they asked for. In OldSchool, it’s right after adding the first medication and its schedule. The request follows an action that implies the user already wants what the permission enables; the system dialog just formalizes it.

A priming screen beats a naked system dialog

The system permission dialog is terse by design — a title, an app name, Allow and Don’t allow. It can’t explain why, and Android gives you no way to customize its copy. What you control is the screen shown immediately before it: a single sentence of context, framed around the user’s own goal rather than the app’s need.

"I'll remind you to drink water on the schedule you just set.
 Android needs your permission to show these reminders."

 [ Turn on reminders ]   [ Not now ]

This is sometimes called a “pre-permission” or “soft-ask” screen, and its value isn’t cosmetic — it changes what the system dialog means when it appears a moment later. Without it, the OS dialog is the first and only explanation the user gets. With it, the OS dialog is just a confirmation of something they already agreed to. A “Not now” on the priming screen also costs nothing: it doesn’t touch the real permission at all, so the one system attempt is still available later, once the user has spent more time in the app and has more reason to trust the ask.

Denial isn’t final — shouldShowRequestPermissionRationale

Once someone denies the system dialog, ActivityCompat.shouldShowRequestPermissionRationale() tells you whether it’s worth showing your own rationale again before the next request, or whether the user has denied it permanently (checked “Don’t ask again,” or denied it once already on Android’s newer, stricter re-ask rules).

fun onReminderCardTapped(activity: Activity) {
    val granted = ContextCompat.checkSelfPermission(
        activity, Manifest.permission.POST_NOTIFICATIONS
    ) == PackageManager.PERMISSION_GRANTED

    when {
        granted -> scheduleDailyReminder()
        ActivityCompat.shouldShowRequestPermissionRationale(
            activity, Manifest.permission.POST_NOTIFICATIONS
        ) -> showRationaleThenRequest()
        else -> openAppNotificationSettings(activity)
    }
}

When it returns false after a prior denial, the system won’t show its dialog again no matter how you call launch() — the only path back is the app’s notification settings page, reached with an Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) scoped to your package. Don’t hide this behind a generic “check your settings” message; the intent takes the user directly to the right screen, no navigating required.

Design for “no” as a real state, not a bug

Some users will deny it and mean it — build the app so that’s a supported state, not a broken one. Hydrame still logs water without reminders; the streak and the history just don’t get a nudge. OldSchool keeps the in-app medication list fully usable, and surfaces a persistent, dismissible banner — “Reminders are off” — rather than pretending the feature exists when it can’t fire. A silent failure, where a reminder was scheduled but never appears because the permission was never granted, is worse than an honest missing feature: it looks like the app is unreliable rather than like a choice the user made.

The one system dialog is worth the setup

POST_NOTIFICATIONS isn’t a hard API — registerForActivityResult and a single permission string cover the mechanics in a few lines. What actually determines the grant rate is everything around those lines: asking after the moment the value became obvious, priming with one sentence of plain-language context, and having a real answer for denial instead of assuming it won’t happen. For any app where the reminder is the product, that’s not polish — it’s the feature working at all.