Blog Infos
Author
Published
Topics
, , , ,
Author
Published

Every Android app uses NotificationManager.notify() to surface messages, alerts, and updates. But what really happens behind the scenes—from your app code to the little icon or heads-up banner in your status bar? In this blog, we’ll follow the exact sections from our video and dive deep into each stage of the notification pipeline.

📺 Watch the video explanation on youtube

 

1. Intro: Why This Deep Dive?

You’ve probably called notify() hundreds of times—but have you ever wondered what happens next?
Understanding this flow helps you debug display issues, optimize performance, and respect battery & privacy boundaries.

2. The Entry Point — notify()

Your app constructs and posts a notification like this:

val notification = NotificationCompat.Builder(context, "chat_channel")
    .setContentTitle("New Message")
    .setContentText("You have a new DM.")
    .setSmallIcon(R.drawable.ic_chat)
    .setAutoCancel(true)
    .build()

val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(1001, notification)
  • Facade APINotificationManager.notify() lives in your app’s process (NotificationManager.java).
  • Binder Hand-Off: Under the hood, it calls INotificationManager.enqueueNotification() to cross into the system_server process.
3. Core Data Structures & Binder
4. Full Notification Flow Explained

App → NotificationManager API (notify(tag, id, Notification))
Your application constructs a Notification (via NotificationCompat.Builder) and calls:

NotificationManager.notify(tag, id, notification);

This is a framework façade — a thin client-side wrapper that packages up your payload and marshals it for IPC.

NotificationManager API → NotificationManagerService
Under the hood, the API invokes the AIDL interface INotificationManager.enqueueNotification(...).
This call crosses process boundaries (from your app’s process into system_server) via Binder IPC.

Inside NotificationManagerServiceenqueueNotificationInternal() performs:

  • Validation: Ensures the calling app’s UID matches the package name.
  • Channel & Importance Resolution (Android 8+).
  • Wrapping: Converts the raw Notification into an internal NotificationRecord, adding metadata like postTimeuserId, ongoing flags, etc.

NMS → Notification Storage
NMS writes (or updates) the NotificationRecord in its on-disk SQLite table (notification_records), ensuring notifications survive reboots or service crashes.

NMS → StatusBarService/SystemUI
After persistence, NMS calls StatusBarService.enqueueNotificationRecord(...).
StatusBarService packages the record into a StatusBarNotification and invokes the registered callback in SystemUI (IStatusBar.onNotificationPosted(...)).
SystemUI then decides whether to show a status-bar icon, a shade entry, or a heads-up banner.

NMS → NotificationListenerService (notifyListeners(NotificationRecord))
Optionally, NMS also broadcasts the new record to any NotificationListenerService implementations (e.g., Wear OS companion apps or accessibility services), enabling external reactions like wearable sync or auto-reply.

SystemUI → User (render icon, shade, heads-up)
SystemUI inflates UI components:

  • A small icon in the status bar
  • An expandable row in the notification shade (with action buttons)
  • A temporary heads-up banner for high-importance alerts

NotificationListenerService → External Service
Each NotificationListenerService receives the same event and can forward data to external devices, log analytics, or trigger custom behaviors (e.g., automatically replying to messages).

5. System Tray & SystemUI Integration Explained

The sequence diagram above illustrates how SystemUI — the Android system-chrome app — registers with and receives notifications from the framework services. Let’s break down each phase:

Initialization / Boot

  • SystemUI (App) starts in its own process (com.android.systemui).
  • During device boot (or SystemUI restart), it calls StatusBarService.registerStatusBar(IStatusBar.Stub).
  • This binder registration hands over a callback object (IStatusBar.Stub) so that StatusBarService can later notify SystemUI of new events.

Notification Posted

  • NotificationManagerService (NMS) in the system_server process enqueues a new notification record via StatusBarService.enqueueNotificationRecord().
  • StatusBarService (SBS) then requests an icon slot from WindowManagerService (requestStatusBarIcon()) to reserve space in the status bar.
  • Next, SBS invokes the registered callback IStatusBar.onNotificationPosted(...) in SystemUI (onNotificationPosted()), passing a StatusBarNotification object containing all necessary data.

Permission & Channel Checks

As part of its enqueueNotificationRecord() handling, StatusBarService enforces two critical checks before informing SystemUI:

  • Notification Permission: On Android 13+ (API 33), it verifies the posting app still holds the POST_NOTIFICATIONS runtime permission.
  • Channel Enabled: It confirms that the notification’s channel is still enabled and not blocked by the user in Settings.

Rendering in SystemUI

Upon receiving the callback, SystemUI takes over:

  • It inflates the notification row in the NotificationShadeWindowView, handling layout, icons, text, action buttons, and grouping logic.
  • It also triggers heads-up UI via HeadsUpManager, which decides whether to show a floating banner based on importance, timing, and current device state (e.g., ongoing calls or DND mode).

Finally, the notification appears on the screen — either as a small icon, a shade entry, or a heads-up alert.

6. Beyond the Basics: Edge Cases

Android’s power-saving and security features introduce a few wrinkles:

  • Offline: Firebase Cloud Messaging (FCM) will hold onto high-priority messages and retry delivery when you come back online.
  • Doze Mode: On API 23+, low-priority work gets delayed until your device wakes up, but urgent notifications still break through.
  • Background Limits: Since API 26, long-running background work requires a foreground service (and its own notification), or the system will kill your process.
  • App Data Cleared: If SystemUI loses its in-memory state, it rebuilds the notification shade by querying NMS for active notifications.
7. Notification Channels & Permissions

 

// Android 8.0+ NotificationChannel
val channel = NotificationChannel(
  "chat", "Chat Messages", NotificationManager.IMPORTANCE_HIGH
).apply {
  description = "DM alerts"
}
notificationManager.createNotificationChannel(channel)

 

Channel Groups:

val group = NotificationChannelGroup("social", "Social & Messaging") 
notificationManager.createNotificationChannelGroup(group) 
channel.group = "social" 
notificationManager.createNotificationChannel(channel)

Runtime Permission (Android 13+):

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {   
requestPermissions(
    arrayOf(
      Manifest.permission.POST_NOTIFICATIONS
    ),
    REQUEST_CODE_NOTIFY
  )
}

Without POST_NOTIFICATIONS, your app’s notifications are silently dropped.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

8. Summary & Best Practices
  • Define meaningful channels with clear names & descriptions.
  • Respect Doze & background limits; use high-priority sparingly.
  • Group low-importance notifications to avoid shade clutter.
  • Use FLAG_IMMUTABLE for PendingIntent (Android 12+).
  • Test on real devices and across OEM SystemUI customizations.

By mastering these internals, you’ll create more reliable, battery-friendly, and user-respecting notifications. Happy coding!

🔗 Connect with me:
LinkedIn · Twitter · Book a 1:1 session

This article was previously published on proandroiddev.com.

Menu