Notifications & Permissions

Android Development โ€” Production & Publishing
Course 3 ยท Chapter 4 ยท Notifications & Permissions

๐Ÿ”” Notifications & Permissions

A background sync worker (last chapter) that finishes successfully but never tells the user anything is often incomplete โ€” notifications are how Android apps communicate outside their own UI. Since Android 13, showing one requires a runtime permission, which is this chapter's second half: requesting permissions, and handling a user saying no gracefully rather than assuming yes.

๐Ÿ“ป Notification Channels โ€” Required Since Android 8

Every notification must belong to a channel โ€” a named category the user can individually enable, disable, or customize (sound, vibration, priority) from system settings, independent of every other channel the app has:

val channel = NotificationChannel( "sync_updates", "Sync Updates", NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "Notifications about background data sync" } val notificationManager = context.getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel)

Channels exist because a single blanket "allow notifications: yes/no" switch was too coarse โ€” a user might want message notifications but not promotional ones from the same app. Creating a channel is idempotent (calling createNotificationChannel again with the same ID is a no-op if it already exists), so it's typically done once, early in the app's lifecycle โ€” often inside the Application class from Course 2 Chapter 5's Hilt setup.

๐Ÿ“จ Building and Showing a Notification

val notification = NotificationCompat.Builder(context, "sync_updates") .setSmallIcon(R.drawable.ic_sync) .setContentTitle("Sync complete") .setContentText("Your notes are up to date") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .build() if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notification) }

The permission check before .notify(...) isn't optional defensive code โ€” without it, calling notify without the granted permission throws a SecurityException and crashes the app on Android 13+. This is the bridge into the chapter's second half: showing a notification and requesting permission to do so are two genuinely separate steps.

๐Ÿ” Runtime Permissions โ€” Declared Isn't Enough

A "dangerous" permission (camera, location, notifications, and others) must be declared in AndroidManifest.xml and explicitly granted by the user at runtime โ€” declaring it alone only makes the permission requestable, not automatically granted:

// AndroidManifest.xml โ€” necessary but not sufficient on its own <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@Composable fun NotificationPermissionRequester() { val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { // Now safe to call NotificationManagerCompat.notify(...) } else { // Handled below โ€” don't just silently give up } } Button(onClick = { launcher.launch(Manifest.permission.POST_NOTIFICATIONS) }) { Text("Enable Notifications") } }

rememberLauncherForActivityResult is Compose's bridge to Android's activity-result APIs โ€” launcher.launch(...) triggers the actual system permission dialog, and the lambda passed to rememberLauncherForActivityResult receives the user's choice as a plain Boolean, which then drives what the UI does next.

๐Ÿšซ Handling Denial Gracefully

A denied permission isn't an error state to hide from โ€” the app should keep working, just without that specific capability, and should explain why the permission matters before re-asking:

shouldShowRequestPermissionRationale

Returns true if the user denied once but hasn't permanently blocked it โ€” the signal to show an explanatory message ("We use this to remind you about tasks") before asking again, rather than immediately re-prompting with no context.

Permanently Denied

After a user selects "Don't ask again" (or denies twice on some Android versions), the system stops showing the permission dialog entirely โ€” the only path forward is directing the user to the app's system settings screen manually.

โš  The App Must Function Without the Permission

A denied notification permission shouldn't mean the app is unusable โ€” sync (last chapter) should still work, tasks should still save; the user simply won't be notified about it. Designing every permission-gated feature as an enhancement layered on top of a working core, rather than a hard requirement, is the difference between a graceful degradation and an app that feels broken the moment someone taps "Deny."

Android Permissions vs Browser Permissions

Browser (Notification API, Geolocation API)Android
RequestingNotification.requestPermission() / navigator.geolocationActivityResultContracts.RequestPermission()
Result"granted" / "denied" / "default"A Boolean (granted or not)
Re-prompting after denialBrowser-controlled; often can't re-prompt at all from JSPossible, guided by shouldShowRequestPermissionRationale
Permanent blockUser must change it in browser site settingsUser must change it in Android app settings

The underlying philosophy is identical to what's likely already familiar from the browser's own geolocation or notification prompts โ€” the platform, not the app, controls whether and when to ask, and a denial is a real, permanent-feeling answer the app has to respect and design around.

๐Ÿ’ป Coding Challenges

Challenge 1: A Notification Channel and a Notification

Create a notification channel named "task_reminders" with IMPORTANCE_DEFAULT, and a function showTaskReminder(context: Context, taskTitle: String) that builds and shows a notification using that channel, checking the POST_NOTIFICATIONS permission before calling notify().

Goal: Practice the full channel-then-notification setup.

โ†’ Solution

Challenge 2: Requesting the Permission from Compose

Write a composable that checks whether POST_NOTIFICATIONS is already granted (using ContextCompat.checkSelfPermission), and if not, shows a Button that requests it via rememberLauncherForActivityResult, updating a remembered Boolean state based on the result.

Goal: Practice the check-then-request pattern for a runtime permission.

โ†’ Solution

Challenge 3: Graceful Denial Handling

Extend Challenge 2: if the permission is denied, show explanatory text ("Notifications help you stay on top of tasks") only when shouldShowRequestPermissionRationale(activity, permission) returns true, and a different message ("Enable notifications in Settings") when it returns false after a denial. Confirm the rest of the screen (e.g. a placeholder Text saying "Tasks: 3") remains visible regardless of permission state.

Goal: Practice designing a feature that degrades gracefully rather than blocking the whole screen on a denied permission.

โ†’ Solution

๐Ÿ’ก Every Dangerous Permission Follows This Same Shape

Camera, location, contacts, microphone โ€” every runtime-requestable permission uses the identical check โ†’ request โ†’ handle-denial-gracefully pattern from this chapter, just with a different permission string and a different fallback behavior. Learning this pattern once genuinely transfers to any permission a future feature might need.

๐ŸŽฏ What's Next

Next chapter: App Security โ€” ProGuard/R8, certificate pinning, secure storage, and biometric authentication.