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.
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.aidldefines 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.
AndroidPub
Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
