Inside Android’s Notification System: From notify() to SystemUI

This article dissects the complete Android notification pipeline, explaining how a call to NotificationManager.notify() travels through Binder, system services, and SystemUI to finally render icons, notification rows, or heads‑up banners, while covering channels, permissions, and special edge cases.

AndroidPub
AndroidPub
AndroidPub
Inside Android’s Notification System: From notify() to SystemUI

Every Android app uses NotificationManager.notify() to push messages, but the underlying process from app code to the status‑bar icon or floating banner involves many components.

1. Why dig deeper?

Understanding this flow helps you troubleshoot display issues, optimise performance, and comply with battery and privacy constraints.

2. Entry point – notify()

Typical code to create and post a notification:

val notification = NotificationCompat.Builder(context, "chat_channel")
    .setContentTitle("新消息")
    .setSmallIcon(R.drawable.ic_chat)
    .setContentText("来消息啦~")
    .build()
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(1001, notification)

The public API runs in the app process (NotificationManager.java).

Binder hands the request to INotificationManager.enqueueNotification() in the system_server process.

3. Core data structures and Binder

INotificationManager.aidl

defines enqueueNotification() and cancelNotification(). NotificationRecord stores uid, package name, channel, importance, user, etc. StatusBarNotification is a serialisable wrapper sent to SystemUI.

4. Full notification flow diagram

The diagram below illustrates the end‑to‑end stages.

Application → NotificationManager API (notify(tag, id, Notification))

The app builds a Notification via NotificationCompat.Builder and calls:

NotificationManager.notify(tag, id, notification);

This thin wrapper packages the payload for IPC.

NotificationManager API → NotificationManagerService

The API invokes the AIDL method INotificationManager.enqueueNotification(...), crossing the process boundary into system_server.

Inside NotificationManagerService, enqueueNotificationInternal() validates the caller, parses the channel and importance (Android 8.0+), and wraps the payload into a NotificationRecord with metadata such as publish time and user ID.

NMS → Notification storage

The NotificationRecord is persisted to the SQLite table notification_records, ensuring the notification survives 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 INotificationListener.onNotificationPosted(...). SystemUI then decides whether to show a status‑bar icon, a notification‑panel entry, or a heads‑up banner.

NMS → NotificationListenerService (notifyListeners(NotificationRecord))

Optionally, NMS broadcasts the new record to any NotificationListenerService implementations (e.g., Wear OS apps or accessibility services) for external handling.

SystemUI → display to user

SystemUI instantiates the notification row in NotificationsShadeWindowView, handling layout, icons, text, actions, and grouping. It also uses HeadsUpManager to show a floating UI when the notification’s importance, timing, and device state (e.g., Do Not Disturb) warrant it.

5. System tray and SystemUI integration

6. Advanced scenarios

Offline: FCM stores high‑priority messages and delivers them when the device reconnects.

Doze mode: On Android 23+, low‑priority work is deferred until the device wakes, but urgent notifications can still break through.

Background limits: From Android 26+, long‑running background work requires a foreground service with its own notification, otherwise the system kills the process.

State loss: If SystemUI loses its in‑memory state, it queries NMS for active notifications to rebuild the tray.

7. Notification channels and permissions

NotificationChannel (Android 8.0+)

val channel = NotificationChannel("chat_channel", "聊天提醒", NotificationManager.IMPORTANCE_HIGH).apply {
    description = "聊天应用的通知"
}
notificationManager.createNotificationChannel(channel)

Channel groups

val group = NotificationChannelGroup("social_group", "社交与消息")
val channel = NotificationChannel("social_chat", "聊天", NotificationManager.IMPORTANCE_DEFAULT).apply {
    group = "social_group"
}
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 this permission, the app’s notifications will be blocked.

8. Summary and best practices

Define clear, descriptive channels with proper names and descriptions.

Respect Doze and background limits; use high priority sparingly.

Honor user notification settings to avoid over‑notification.

For periodic notifications (Android 12+), set FLAG_IMMUTABLE.

Test on real devices and consider OEM‑specific SystemUI customisations.

mobile developmentAndroidNotificationManagerNotificationsSystemUI
AndroidPub
Written by

AndroidPub

Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.