Irbisa · Cheatsheet

Notifications & Push

NotificationCompat, channels, POST_NOTIFICATIONS, FCM, data vs notification messages, deep links

Middle Developer · 19 questions

  1. 01

    What is a notification channel, and why does raising its importance in code later change nothing?

    A channel is a category every notification must belong to on Android 8+; posting to a channel that was never created is silently dropped, and its importance, sound and vibration freeze at creation because from then on they belong to the user.

    class App : Application() {  override fun onCreate() {    super.onCreate()    // Idempotent: safe on every launch, must run before the first notify()    NotificationManagerCompat.from(this).createNotificationChannel(      NotificationChannelCompat.Builder("delivery_v2", NotificationManagerCompat.IMPORTANCE_HIGH)…
  2. 02

    What is the minimum a NotificationCompat.Builder needs to post a working notification?

    A channel id and a small icon are the hard minimum; a title, text and a content intent for the tap make it useful, and notify(id, notification) posts it.

    val tap = PendingIntent.getActivity(  ctx, 0,  Intent(Intent.ACTION_VIEW, "myapp://orders/42".toUri(), ctx, MainActivity::class.java),  PendingIntent.FLAG_IMMUTABLE)…
  3. 03

    Which notification styles does NotificationCompat offer, and which part of the notification does a style change?

    BigTextStyle, BigPictureStyle and InboxStyle only change the expanded view and keep the collapsed row from setContentTitle and setContentText; MessagingStyle and CallStyle also replace the collapsed layout.

    val release = NotificationCompat.Builder(ctx, "news")  .setSmallIcon(R.drawable.ic_stat_news)  .setContentTitle("Release 4.2 is out")  .setContentText("Offline sync, faster search, 40 fixes")              // collapsed row  .setStyle(NotificationCompat.BigTextStyle().bigText(fullChangelog))  // expanded only  .build()…
  4. 04

    How do you update or remove a notification that is already posted?

    Call notify() again with the same id (and tag) to update it in place, and cancel(id) or cancelAll() to remove it; the id is the notification's identity, so keep it stable per thing you show.

    val nm = NotificationManagerCompat.from(ctx)val base = NotificationCompat.Builder(ctx, "orders")  .setSmallIcon(R.drawable.ic_stat_order)  .setContentTitle("Order #42") // Same id: updates the row in place, quietly…
  5. 05

    What changed with POST_NOTIFICATIONS in Android 13, and what do you do when the user declines?

    Since Android 13 posting notifications needs a runtime permission that is off by default on new installs, so you declare it, ask for it in context with the Activity Result API, and give the content another home when the user says no.

    // AndroidManifest.xml: <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> @Composablefun PriceAlertButton(onEnabled: () -> Unit) {  val ctx = LocalContext.current  val launcher = rememberLauncherForActivityResult(…
  6. 06

    What decides how loud a notification is: channel importance, setPriority, or the FCM message priority?

    On Android 8+ only the channel's importance; setPriority is read only below API 26, and FCM priority controls how fast the message gets through Doze, not how it alerts.

  7. 07

    The notification's content intent points at this receiver. On Android 12+ the screen never opens. Why, and what do you do instead?

    // setContentIntent(PendingIntent.getBroadcast(ctx, 0, openIntent, FLAG_IMMUTABLE))class OpenReceiver : BroadcastReceiver() {  override fun onReceive(ctx: Context, intent: Intent) {    val route = intent.getStringExtra("route")    analytics.log("notification_open", route)    ctx.startActivity(      Intent(ctx, MainActivity::class.java)        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)        .putExtra("route", route)    )  }}

    This is a notification trampoline: for apps targeting API 31+, a receiver or service started by a notification tap is not allowed to start an Activity, so the launch is silently dropped; point the content intent straight at the Activity.

  8. 08

    Find the bug: with two chat notifications in the shade, tapping either one opens the chat that messaged last.

    fun showChatNotification(ctx: Context, chat: Chat, text: String) {  val open = Intent(ctx, MainActivity::class.java)    .putExtra("chat_id", chat.id)  val pi = PendingIntent.getActivity(    ctx, 0, open,    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT  )  val notification = NotificationCompat.Builder(ctx, CHANNEL_CHATS)    .setSmallIcon(R.drawable.ic_stat_chat)    .setContentTitle(chat.title)    .setContentText(text)    .setContentIntent(pi)    .setAutoCancel(true)    .build()  NotificationManagerCompat.from(ctx).notify(chat.id, notification)}

    Every chat gets the same PendingIntent: its identity is the request code plus the intent's component, action and data, never the extras, so FLAG_UPDATE_CURRENT overwrites chat_id and every notification opens the last chat.

  9. 09

    How do you group notifications under a summary so that ten results produce one sound instead of ten?

    Give every notification the same setGroup key, post one extra summary with setGroupSummary(true), and set setGroupAlertBehavior(GROUP_ALERT_SUMMARY) on all of them so only the summary alerts.

    private const val GROUP_SYNC = "sync_results"private const val SUMMARY_ID = 0 fun postSyncResults(ctx: Context, files: List<SyncedFile>) {  val nm = NotificationManagerCompat.from(ctx)  files.forEach { file ->…
  10. 10

    This download updates a progress notification on every chunk. What does the user hear, and why does the bar stay full while the "Done" text never appears?

    suspend fun download(ctx: Context, file: RemoteFile) {  val nm = NotificationManagerCompat.from(ctx)  val builder = NotificationCompat.Builder(ctx, "downloads")  // IMPORTANCE_DEFAULT    .setSmallIcon(R.drawable.ic_stat_download)    .setContentTitle(file.name)   file.chunks().collectIndexed { i, chunk ->    sink.write(chunk)    builder.setProgress(100, (i + 1) * 100 / file.chunkCount, false)    nm.notify(DOWNLOAD_ID, builder.build())  }  builder.setContentText("Done").setProgress(0, 0, false)  nm.notify(DOWNLOAD_ID, builder.build())}

    Every notify() on a sounding channel alerts again, so the phone chimes on each chunk; the system rate-limits updates of an existing notification to about five per second per app and silently drops the rest, including the final "Done" update, because setProgress(0, 0, false) is not exempt.

  11. 11

    In this direct-reply setup getResultsFromIntent returns null, and once that is fixed the reply box keeps spinning. What is wrong?

    val replyPi = PendingIntent.getBroadcast(  ctx, 0, Intent(ctx, ReplyReceiver::class.java), PendingIntent.FLAG_IMMUTABLE)val action = NotificationCompat.Action.Builder(R.drawable.ic_send, "Reply", replyPi)  .addRemoteInput(RemoteInput.Builder(KEY_REPLY).setLabel("Message").build())  .build() class ReplyReceiver : BroadcastReceiver() {  override fun onReceive(ctx: Context, intent: Intent) {    val text = RemoteInput.getResultsFromIntent(intent)      ?.getCharSequence(KEY_REPLY) ?: return    SendReplyWorker.enqueue(ctx, text.toString())  }}

    The PendingIntent is immutable, so the system cannot write the typed text into it; and the receiver never re-posts the notification, which is the only thing that ends the "sending" state of the reply field.

  12. 12

    What does a chat notification need to appear in the Conversations section and support bubbles?

    MessagingStyle with a Person for each message, plus setShortcutId pointing at a long-lived dynamic shortcut for that chat; without the shortcut the notification quietly falls back to the ordinary section.

    val sender = Person.Builder().setName("Dana").setKey("user:88").build()val shortcutId = "chat:88" // 1. Long-lived shortcut: without it there is no Conversations sectionShortcutManagerCompat.pushDynamicShortcut(  ctx,…
  13. 13

    What is the difference between FCM notification messages and data messages, and when does onMessageReceived actually run?

    A message with a notification block is drawn by the FCM SDK itself while your app is in the background, so onMessageReceived is skipped; a data-only message always reaches onMessageReceived, and you build the notification yourself.

    class AppMessagingService : FirebaseMessagingService() {  // Data-only: runs in every app state. With a "notification" block:  // runs only while the app is in the foreground.  override fun onMessageReceived(message: RemoteMessage) {    val convId = message.data["conversation_id"] ?: return    repository.insert(message.data)…
  14. 14

    How do you manage FCM registration tokens over their lifetime, on the client and on the server?

    A token identifies one app install, not a user or a device, and it can change at any time, so the client re-uploads it whenever it differs from the last uploaded value and the server deletes tokens FCM reports as UNREGISTERED.

    class AppMessagingService : FirebaseMessagingService() {  // Install, data clear, restore, rotation; often before anyone signed in  override fun onNewToken(token: String) {    TokenStore.savePending(this, token)    TokenUploadWorker.enqueue(this)   // retryable: the network may be down  }…
  15. 15

    How do you keep a one-time code off the lock screen, and what do the user and the admin control instead of you?

    You control the notification's visibility (VISIBILITY_PRIVATE with a redacted setPublicVersion, or VISIBILITY_SECRET), while whether the lock screen shows content at all belongs to the user and, on a managed device, to the admin.

    fun otpNotification(ctx: Context, code: String, tap: PendingIntent): Notification {  val redacted = NotificationCompat.Builder(ctx, "security")    .setSmallIcon(R.drawable.ic_lock)    .setContentTitle("New sign-in code")   // says what it is, never the code    .setContentIntent(tap)                 // same tap target    .build()…
  16. 16

    What is a full-screen intent, and what changed with USE_FULL_SCREEN_INTENT in Android 14?

    A full-screen intent asks the system to launch an Activity over the lock screen for urgent events such as calls and alarms; since Android 14 its permission is granted by default only to calling and alarm apps, and when denied the notification silently becomes a heads-up.

    fun ensureFullScreenIntent(activity: Activity) {  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return  val nm = activity.getSystemService(NotificationManager::class.java)  if (nm.canUseFullScreenIntent()) return  activity.startActivity(    Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, "package:${activity.packageName}".toUri())…
  17. 17

    A chat push arrives while the user is reading that exact conversation. What should happen, and where in the architecture is that decided?

    Nothing should buzz: the message goes into the repository, the open screen updates from it, and one notification presenter that knows what is on screen decides not to post.

  18. 18

    How do FCM's collapse_key, ttl and message priority affect delivery, and how do you choose them per message type?

    collapse_key lets a newer message replace an undelivered one, ttl caps how long FCM stores it, and priority decides whether it wakes a dozing device now or waits for a maintenance window; get them wrong and chat messages vanish or a "driver arrived" push lands late.

    // Server, Firebase Admin SDK: one shape per message typefun driverArrived(token: String, rideId: String): Message = Message.builder()  .setToken(token)  .putData("ride_id", rideId)  .setAndroidConfig(AndroidConfig.builder()    .setPriority(AndroidConfig.Priority.HIGH)    // wake it out of Doze…
  19. 19

    Users on some OEM phones say notifications never arrive, and you cannot reproduce it. How do you find where they are lost?

    Split the path into segments you can count (accepted by FCM, delivered to the device, received by your process, posted to the shade), because "notifications don't arrive" covers several unrelated failures and only one of them is the OEM's doing.