Zygote
Notifications & SystemUInerd

The notification pipeline

notify() is asynchronous and crosses Binder twice. Between the call and the pixel there is a service that checks permission, resolves a channel, applies Do Not Disturb and ranking, and hands a parcel to SystemUI.

11 minAPI 21–37 · 17interactivequizverified 2026-10-27

Most developers treat notify() as a function call that puts a row in the shade. It is closer to posting a letter: it returns before anything has happened, and several offices can refuse it along the way.

TL;DR#

  • NotificationManager is a client proxy. The real work happens in NotificationManagerService inside system_server.
  • The call is asynchronous from the app's point of view. notify() returning does not mean the notification was shown.
  • Before anything is drawn, the service checks POST_NOTIFICATIONS, resolves the channel, applies Do Not Disturb and ranking, and notifies listeners.
  • SystemUI inflates the notification's RemoteViews in its own process. Your code does not run there.
  • Any app with a NotificationListenerService grant receives a copy of every notification. Never put secrets in one.

Mental model#

Think of submitting a press release to a newspaper.

You hand it to the front desk (the NotificationManager proxy), which logs it and passes it to the editorial office (NotificationManagerService). The editorial office checks whether you are allowed to publish at all (the permission), which section it belongs in (the channel), whether it is newsworthy enough to interrupt the reader (importance and DND), and where it should appear (ranking). Only then does the typesetter (SystemUI) lay it out.

Your copy is not what gets printed. The editorial office may shorten it, reorder it, or hold it.

Where the analogy breaks#

A newspaper has one edition. Android has a live shade that can re-rank notifications as new ones arrive, so a notification's position can change without anything being republished. Ranking is continuous, not a one-time editorial decision.

The big picture#

Loading diagram…
Text description of this diagram

A ten-step sequence between your app, the NotificationManager client proxy, the NotificationManagerService in system_server, the ranking and policy layer, SystemUI, and any NotificationListenerService. Building the Notification validates the channel and small icon. notify() parcels it over Binder. The call is one-way from the app's point of view. The service checks POST_NOTIFICATIONS; a denial turns the whole call into a silent drop with no exception to the app. It resolves the channel and reads importance, sound, vibration and visibility. It applies Do Not Disturb, bubbles, conversation status and full-screen intent policy. It tells listeners and SystemUI through onNotificationPosted. SystemUI inflates and draws the RemoteViews in its own process. Notification listeners receive a copy. The service may return a sanitised, ranked copy to the app, which is not the object that was sent.

The step that surprises people is the permission check: on API 33+ a denied POST_NOTIFICATIONS makes the entire call a silent no-op. No exception, no log line the app sees, no indication that anything was dropped.

Deep dive#

notify() is a Binder call#

val manager = context.getSystemService(NotificationManager::class.java)
manager.notify(TAG, notificationId, notification)

NotificationManager is a thin wrapper over INotificationManager, an AIDL interface. The call marshals the Notification and crosses into system_server.1

Two consequences:

  • The Notification is Parcelable and size-limited. A notification carrying a full-resolution bitmap can fail the Binder transaction. setLargeIcon() with an un-scaled photo is a common cause of "the notification silently does not appear".
  • The call is one-way. The service may not have processed the notification when notify() returns, which is why posting and immediately querying getActiveNotifications() can be racy.

The permission gate#

if (ContextCompat.checkSelfPermission(
        context,
        Manifest.permission.POST_NOTIFICATIONS,
    ) != PackageManager.PERMISSION_GRANTED
) {
    // notify() will not show anything. Ask the user first.
}

On API 33+, POST_NOTIFICATIONS is a runtime permission.2 A denial means:

  • notify() does not throw.
  • Nothing appears in the shade.
  • The foreground service notification is still shown, because it is part of the service contract.

That last exception is important and frequently misremembered. The foreground service notification is shown regardless; the app's own notifications are not.

Channel resolution#

Every notification names a channel:

val notification = NotificationCompat.Builder(context, CHANNEL_MESSAGES)
    .setSmallIcon(R.drawable.ic_message)
    .setContentTitle("Priya")
    .setContentText("Are we still on for 7?")
    .build()

If the channel does not exist, notify() throws on API 26+.3 This is a validation error at the builder, not a silent drop, and it is the single most common crash when adding channels to an existing app.

The channel's importance decides:

ImportanceBehaviour
IMPORTANCE_HIGHSound and heads-up
IMPORTANCE_DEFAULTSound
IMPORTANCE_LOWNo sound, visible in shade
IMPORTANCE_MINNo sound, collapsed in the shade

Once the user changes a channel's importance, the app cannot raise it back. This is deliberate: the user's decision outranks the app's preference.

Ranking, DND and policy#

Before SystemUI sees anything, the service applies policy:

  • Do Not Disturb can suppress sound, or suppress the notification entirely, depending on the interruption filter and the channel's category.
  • Conversation ranking promotes notifications that carry a valid Person and a conversation shortcut to a dedicated section.
  • Bubbles may convert a notification into a floating window if the user has enabled them for the conversation.
  • Full-screen intent policy decides whether a notification may take over the screen, which is restricted to calling and alarm apps on modern releases.

SystemUI renders a description, not your code#

The notification's content is a RemoteViews — a parcel of layout id and method calls — or a system-drawn layout chosen by the style. SystemUI inflates it in its own process with its own Context.

That is why:

  • Custom layouts only support a small set of views (TextView, ImageView, Button, ProgressBar, Chronometer, and a few layout containers).
  • The app's code does not run while the notification is displayed. Only a PendingIntent fires code, and it fires in the app's process.
  • Custom views are being progressively restricted because they are the part of the system most exposed to a malicious app's layout.

Listeners see everything#

NotificationListenerService is a privileged capability granted by the user in Settings. A service with the grant receives onNotificationPosted and onNotificationRemoved for every notification, including other apps'.4

The security implication is direct: notification content is not private from a determined app the user has granted access to. Do not put tokens, one-time codes or personal data in a notification that does not need it.

The architectural implication is equally interesting: a notification listener can cancel or snooze notifications, which is how smartwatch apps and car head units mirror the shade.

What the pipeline explains about bugs#

Almost every "notification problem" maps to one stage of the pipeline, and knowing the stage tells you where to look.

SymptomLikely stageWhat to check
Nothing appears, no crashPermission gatePOST_NOTIFICATIONS on API 33+
Crash on notifyChannel resolutionThe channel was never created
Appears but never soundsChannel importanceThe channel is IMPORTANCE_LOW or the user muted it
Appears but is not promotedRanking and DNDConversation shortcut missing, or DND active
Truncated or blank contentRenderingRemoteViews too large, or a custom layout using unsupported views
Appears on another deviceListenersA listener app is mirroring it, by design

That table is worth keeping. It turns a vague "notifications are broken" into a specific check, and in practice most reports land in the first two rows.

The post-and-forget model#

The pipeline's asynchrony has a design consequence that is easy to miss: you cannot use a notification as a synchronous signal. Posting one and immediately reading getActiveNotifications() is racy, and a notification posted during onPause may not be visible before the activity is gone.

The supported way to know what happened is getActiveNotifications() after a delay, or a listener for your own package in a test. In production, the only reliable feedback that a notification was shown is the user's reaction, which is a poor API but an honest one.

The id is the identity#

The pair of tag and id identifies a notification for its whole life. Posting again with the same pair updates it in place; posting with a different pair creates a second row. That sounds obvious and it is the source of two common bugs: using id 0 for everything (so a service cannot update one notification without replacing another), and generating a new id on every update (so the shade fills with duplicates).

The pattern that works is a stable id per logical thing — conversationId.hashCode(), or a small constant per notification category — and a tag when two categories share an id space.

How it evolved#

2008–2011Direct posta manager and a service, no channels or ranking
2012–2016Rankingpriority, heads-up, group summaries, listeners
2017–2019Channels and DNDper-channel policy before dispatch
2020–nowPermission and policyruntime permission, conversation ranking, full-screen restrictions
Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

Under the hood#

Curiosity corner#

  • notify() with an id of 0 is allowed, and many apps use it, which makes them impossible to update individually. Use distinct ids per conversation or per state.
  • A notification's PendingIntent runs with the app's identity, not SystemUI's. That is why mutability flags became mandatory on API 31.
  • The notification shade is a window like any other, which is why it can be screenshotted and why its contents appear in screen recordings unless the app sets FLAG_SECURE on its own window — which does not cover the shade.

Try it#

Dump the notification service state

adb shell dumpsys notification | head -80

Example output
NotificationRecord{... pkg=com.example.app id=1 channel=messages importance=4}
See the active notifications for a package

adb shell dumpsys notification --noredact | grep -A6 com.example.app

Example output
NotificationRecord{... pkg=com.example.app}
Check the listener grant

adb shell settings get secure enabled_notification_listeners

Example output
enabled listeners:
  com.example.watch/.NotificationForwarder

Build it#

Post a notification with a large icon from a full-resolution photo and watch it fail to appear. Then scale the bitmap to a thumbnail and watch it work. The size limit is not documented as a number you can rely on; the failure mode is the lesson.

Check yourself#

Check yourself

1 / 12

Put these notification features in the order they were introduced.

Drag, or use the arrows to reorder.

  1. 1POST_NOTIFICATIONS as a runtime permission
  2. 2Direct reply and bundled groups
  3. 3Mandatory notification channels
  4. 4Lock screen notifications and heads-up peeks
  5. 5Expandable styles and actions
0 answered · 0 correct

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsBinder overviewsource.android.comaccessed 2026-10-27↩
  2. docsBehavior changes: Apps targeting Android 13 or higherdeveloper.android.comaccessed 2026-10-27↩
  3. docsCreate and manage notification channelsdeveloper.android.comaccessed 2026-10-27↩
  4. docsNotifications overviewdeveloper.android.comaccessed 2026-10-27↩
  5. docsBehavior changes: Apps targeting Android 12developer.android.comaccessed 2026-10-27↩
  6. docsBehavior changes: Apps targeting Android 11developer.android.comaccessed 2026-10-27↩
  7. docsPerubahan Perilaku Android 8.0developer.android.comaccessed 2026-10-27↩