Chapter 28: Notification System¶
The Android notification system is one of the platform's most complex subsystems.
A single notify() call from an application triggers a cascade of permission checks,
channel lookups, signal extraction, ranking, Do Not Disturb filtering, listener
dispatch, and finally UI rendering inside SystemUI. At the time of writing, the core
service NotificationManagerService.java alone exceeds 15,500 lines of code and
coordinates with over 70 helper classes. This chapter traces the full lifecycle of a
notification from the public API down through the server-side pipeline, ranking
engine, attention effects, and into the SystemUI shade.
28.1 Notification Architecture¶
28.1.1 High-Level Overview¶
Android's notification subsystem spans four distinct layers:
Applications
|
v
NotificationManager (android.app.NotificationManager) <-- public SDK API
| (Binder IPC)
v
NotificationManagerService (system_server) <-- policy engine
|
+---> RankingHelper (signal extraction & sort)
+---> PreferencesHelper (channels, groups, per-app prefs)
+---> ZenModeHelper (Do Not Disturb)
+---> NotificationAttentionHelper (sound, vibration, LED)
+---> GroupHelper (auto-grouping)
+---> SnoozeHelper (timed snooze)
+---> ShortcutHelper (conversation shortcuts)
+---> NotificationListeners (listener dispatch)
+---> NotificationAssistants (NAS adjustments)
|
v
SystemUI (notification shade, heads-up, bubbles, status bar icons)
The Binder interface that connects apps to the service is defined as
INotificationManager.aidl. Every notify() and cancel() call from
NotificationManager crosses the Binder into system_server where
NotificationManagerService runs.
Source files:
| Component | Path |
|---|---|
NotificationManager |
frameworks/base/core/java/android/app/NotificationManager.java |
INotificationManager |
frameworks/base/core/java/android/app/INotificationManager.aidl |
NotificationManagerService |
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java |
Notification |
frameworks/base/core/java/android/app/Notification.java |
NotificationChannel |
frameworks/base/core/java/android/app/NotificationChannel.java |
StatusBarNotification |
frameworks/base/core/java/android/service/notification/StatusBarNotification.java |
28.1.2 The Notification Object¶
A Notification object is the core data structure. It carries:
- Small icon (mandatory) -- displayed in the status bar and the shade header.
- Content title and text -- the primary textual content.
- Channel ID (mandatory since API 26) -- the channel that governs behavior.
- PendingIntent -- the tap action (
contentIntent) and optionally adeleteIntent. - Actions -- up to three buttons (including inline reply).
- Style -- one of
BigTextStyle,BigPictureStyle,InboxStyle,MessagingStyle,MediaStyle,CallStyle,DecoratedCustomViewStyle, orDecoratedMediaCustomViewStyle. - Extras -- a
Bundlefor arbitrary key-value pairs. - BubbleMetadata -- optional metadata for bubble presentation.
- Flags -- bit flags like
FLAG_FOREGROUND_SERVICE,FLAG_AUTO_CANCEL,FLAG_GROUP_SUMMARY,FLAG_BUBBLE,FLAG_ONGOING_EVENT,FLAG_NO_CLEAR.
28.1.3 Key Constants and Limits¶
From NotificationManagerService:
// frameworks/base/services/core/java/com/android/server/notification/
// NotificationManagerService.java (lines 473-474)
static final int MAX_PACKAGE_NOTIFICATIONS = 50;
static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f;
A single package may have at most 50 active notifications. The enqueue rate is limited to 5 notifications per second to prevent flooding.
Additional limits enforced by PreferencesHelper:
// frameworks/base/services/core/java/com/android/server/notification/
// PreferencesHelper.java (lines 141-142)
static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000;
static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000;
28.1.4 Architecture Diagram¶
graph TB
subgraph "Application Process"
APP[App Code]
NM[NotificationManager]
end
subgraph "system_server"
NMS[NotificationManagerService]
RH[RankingHelper]
PH[PreferencesHelper]
ZMH[ZenModeHelper]
NAH[NotificationAttentionHelper]
GH[GroupHelper]
SH[SnoozeHelper]
NL[NotificationListeners]
NA[NotificationAssistants]
end
subgraph "SystemUI Process"
SHADE[Notification Shade]
HU[Heads-Up Display]
BUBBLES[Bubble Controller]
SBI[Status Bar Icons]
end
APP -->|"notify()"| NM
NM -->|"Binder IPC"| NMS
NMS --> RH
NMS --> PH
NMS --> ZMH
NMS --> NAH
NMS --> GH
NMS --> SH
NMS --> NL
NMS --> NA
NL -->|"onNotificationPosted()"| SHADE
NL -->|"onNotificationPosted()"| HU
NL -->|"onNotificationPosted()"| BUBBLES
NL -->|"onNotificationPosted()"| SBI
28.1.5 Threading Model¶
NotificationManagerService uses a dedicated worker thread for most operations.
The Binder calls from apps arrive on the Binder thread pool, but the actual
processing is posted to a handler thread:
// NotificationManagerService.java (lines 480-486)
// message codes
static final int MESSAGE_DURATION_REACHED = 2;
static final int MESSAGE_SEND_RANKING_UPDATE = 4;
static final int MESSAGE_LISTENER_HINTS_CHANGED = 5;
static final int MESSAGE_LISTENER_NOTIFICATION_FILTER_CHANGED = 6;
static final int MESSAGE_FINISH_TOKEN_TIMEOUT = 7;
static final int MESSAGE_ON_PACKAGE_CHANGED = 8;
There is also a separate ranking thread:
// NotificationManagerService.java (lines 491-492)
// ranking thread messages
private static final int MESSAGE_RECONSIDER_RANKING = 1000;
private static final int MESSAGE_RANKING_SORT = 1001;
The critical synchronization primitive is mNotificationLock:
// All reads/writes to mNotificationList, mNotificationsByKey, mSummaryByGroupKey,
// and mEnqueuedNotifications must hold mNotificationLock.
28.1.6 Signal Extractor Pipeline¶
The ranking pipeline is driven by a chain of NotificationSignalExtractor
implementations, configured in XML and loaded reflectively:
<!-- frameworks/base/core/res/res/values/config.xml (line 3740) -->
<string-array name="config_notificationSignalExtractors">
<item>com.android.server.notification.NotificationChannelExtractor</item>
<item>com.android.server.notification.NotificationAdjustmentExtractor</item>
<item>com.android.server.notification.BubbleExtractor</item>
<item>com.android.server.notification.ValidateNotificationPeople</item>
<item>com.android.server.notification.PriorityExtractor</item>
<item>com.android.server.notification.ZenModeExtractor</item>
<item>com.android.server.notification.ImportanceExtractor</item>
<item>com.android.server.notification.VisibilityExtractor</item>
<item>com.android.server.notification.BadgeExtractor</item>
<item>com.android.server.notification.CriticalNotificationExtractor</item>
</string-array>
The interface is minimal:
// frameworks/base/services/core/java/com/android/server/notification/
// NotificationSignalExtractor.java
public interface NotificationSignalExtractor {
public void initialize(Context context, NotificationUsageStats usageStats);
public RankingReconsideration process(NotificationRecord notification);
void setConfig(RankingConfig config);
void setZenHelper(ZenModeHelper helper);
}
Each extractor runs in sequence on every notification. If an extractor returns a
non-null RankingReconsideration, it is posted to the ranking thread for deferred
re-evaluation (used by ValidateNotificationPeople for asynchronous contact lookup).
graph LR
NR[NotificationRecord] --> E1[ChannelExtractor]
E1 --> E2[AdjustmentExtractor]
E2 --> E3[BubbleExtractor]
E3 --> E4[ValidateNotificationPeople]
E4 --> E5[PriorityExtractor]
E5 --> E6[ZenModeExtractor]
E6 --> E7[ImportanceExtractor]
E7 --> E8[VisibilityExtractor]
E8 --> E9[BadgeExtractor]
E9 --> E10[CriticalNotificationExtractor]
E10 --> SORTED[Sorted NotificationList]
28.1.7 The NotificationRecord¶
NotificationRecord is the server-side wrapper around StatusBarNotification.
It holds all the intermediate state produced by the signal extractors:
// frameworks/base/services/core/java/com/android/server/notification/
// NotificationRecord.java (lines 108-248, summarized)
public final class NotificationRecord {
private final StatusBarNotification sbn;
private float mContactAffinity;
private boolean mIntercept; // suppressed by DND
private long mRankingTimeMs;
private int mImportance;
private int mSystemImportance;
private int mAssistantImportance;
private float mRankingScore;
private int mCriticality;
private NotificationChannel mChannel;
private ShortcutInfo mShortcutInfo;
private boolean mAllowBubble;
private boolean mShowBadge;
private int mSuppressedVisualEffects;
private ArrayList<Notification.Action> mSystemGeneratedSmartActions;
private ArrayList<CharSequence> mSmartReplies;
private int mUserSentiment;
private boolean mIsInterruptive;
// ... many more fields
}
The Javadoc on the class states the critical threading rule:
These objects should not be mutated unless the code is synchronized on
NotificationManagerService.mNotificationLock, and any modification should be followed by a sorting of that list.
28.2 NotificationManagerService: Enqueue, Post, Cancel¶
28.2.1 Overview of the Notification Lifecycle¶
A notification goes through three distinct phases in the server:
- Enqueue -- validation, channel lookup, rate limiting,
NotificationRecordcreation. - Post -- signal extraction, ranking sort, attention effects, listener dispatch.
- Cancel -- removal from the list, listener notification, history archival.
sequenceDiagram
participant App
participant NM as NotificationManager
participant NMS as NotificationManagerService
participant ENQ as EnqueueNotificationRunnable
participant POST as PostNotificationRunnable
participant Listeners as NotificationListeners
participant SysUI as SystemUI
App->>NM: notify(id, notification)
NM->>NMS: enqueueNotificationWithTag() [Binder]
NMS->>NMS: enqueueNotificationInternal()
Note over NMS: Permission check, channel lookup,<br/>rate limit, fix notification
NMS->>NMS: Create NotificationRecord
NMS->>ENQ: post to handler thread
ENQ->>ENQ: Check snooze state
ENQ->>ENQ: Handle grouped notifications
ENQ->>ENQ: Add to mEnqueuedNotifications
ENQ->>POST: post to handler (or delay 200ms for NAS)
POST->>POST: Check if app banned or blocked
POST->>POST: Add to mNotificationList
POST->>POST: extractSignals() + sort()
POST->>POST: buzzBeepBlink() for attention
POST->>Listeners: notifyListenersPostedAndLogLocked()
Listeners->>SysUI: onNotificationPosted()
28.2.2 Phase 1: Enqueue¶
The entry point for the Binder call is the inner INotificationManager.Stub:
// NotificationManagerService.java (line 4405)
public void enqueueNotificationWithTag(String pkg, String opPkg, String tag,
int id, Notification notification, int userId) throws RemoteException {
enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(),
Binder.getCallingPid(), tag, id, notification, userId,
/* byForegroundService= */ false, /* isAppProvided= */ true);
}
The private enqueueNotificationInternal() method (starting at line 8747) performs
the following steps:
Step 1 -- Validation:
if (pkg == null || notification == null) {
throw new IllegalArgumentException("null not allowed: pkg=" + pkg
+ " id=" + id + " notification=" + notification);
}
Step 2 -- User resolution:
final int userId = ActivityManager.handleIncomingUser(callingPid,
callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
Step 3 -- UID resolution and security check:
notificationUid = resolveNotificationUid(opPkg, pkg, callingUid, userId);
if (notificationUid == INVALID_UID) {
throw new SecurityException("Caller " + opPkg + ":" + callingUid
+ " trying to post for invalid pkg " + pkg);
}
Step 4 -- Foreground service policy check:
final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification(
notification, tag, id, pkg, userId);
Step 5 -- Fix the notification:
The fixNotification() method sanitizes the notification by:
- Stripping invalid actions.
- Enforcing maximum text lengths.
- Setting the
FLAG_NO_CLEARflag for foreground service notifications. - Ensuring the notification has a valid channel ID.
Step 6 -- Channel lookup:
final NotificationChannel channel = getNotificationChannelRestoreDeleted(
pkg, callingUid, notificationUid, channelId, shortcutId);
if (channel == null) {
// ... toast warning, return false
}
Step 7 -- Create NotificationRecord:
final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
r.setIsAppImportanceLocked(mPermissionHelper.isPermissionUserSet(pkg, userId));
r.setPostSilently(postSilently);
r.setFlagBubbleRemoved(false);
r.setPkgAllowedAsConvo(mMsgPkgsAllowedAsConvos.contains(pkg));
Step 8 -- FGS importance floor: If the notification belongs to a foreground service or user-initiated job and the channel importance is MIN or NONE, it is elevated to LOW:
if (notification.isFgsOrUij()) {
if (r.getImportance() == IMPORTANCE_MIN || r.getImportance() == IMPORTANCE_NONE) {
channel.setImportance(IMPORTANCE_LOW);
r.setSystemImportance(IMPORTANCE_LOW);
}
}
Step 9 -- Acquire wake lock and schedule EnqueueNotificationRunnable:
The wake lock timeout is 30 seconds (POST_WAKE_LOCK_TIMEOUT), ensuring the
device stays awake long enough to complete posting.
28.2.3 The EnqueueNotificationRunnable¶
Source: NotificationManagerService.java, line 10054.
This runnable executes on the handler thread, under mNotificationLock:
protected class EnqueueNotificationRunnable implements Runnable {
private final NotificationRecord r;
private final int userId;
// ...
Key operations:
-
Snooze check -- If the notification key was previously snoozed and the snooze timer has not elapsed, re-snooze it immediately:
final long snoozeAt = mSnoozeHelper.getSnoozeTimeForUnpostedNotification( r.getUser().getIdentifier(), r.getSbn().getPackageName(), r.getSbn().getKey()); if (snoozeAt > currentTime) { (new SnoozeNotificationRunnable(r.getSbn().getKey(), snoozeAt - currentTime, null)).snoozeLocked(r); return false; } -
Copy ranking from existing record -- If this is an update to an existing notification, ranking information is preserved:
-
Add to enqueue list:
-
Bubble flags update:
-
Group handling:
-
Dispatch to PostNotificationRunnable, either immediately or with a 200 ms delay if the Notification Assistant Service (NAS) is enabled:
28.2.4 The PostNotificationRunnable¶
Source: NotificationManagerService.java, line 10209.
This is where the notification becomes visible. Under mNotificationLock:
Step 1 -- Find the enqueued record:
NotificationRecord r = findNotificationByListLocked(mEnqueuedNotifications, key);
if (r == null) {
Slog.i(TAG, "Cannot find enqueued record for key: " + key);
return false;
}
Step 2 -- Block check:
if (!(notification.isMediaNotification() || isCallNotificationAndCorrectStyle)
&& (appBanned || isRecordBlockedLocked(r))) {
mUsageStats.registerBlocked(r);
return false;
}
Step 3 -- Add to the main notification list:
int index = indexOfNotificationLocked(n.getKey());
if (index < 0) {
mNotificationList.add(r); // new notification
mUsageStats.registerPostedByApp(r);
} else {
old = mNotificationList.get(index); // update existing
mNotificationList.set(index, r);
mUsageStats.registerUpdatedByApp(r, old);
}
mNotificationsByKey.put(n.getKey(), r);
Step 4 -- Auto-grouping:
The GroupHelper.onNotificationPosted() method decides whether to auto-group:
boolean willBeAutogrouped = mGroupHelper.onNotificationPosted(
r, hasAutoGroupSummaryLocked(r));
if (willBeAutogrouped) {
addAutogroupKeyLocked(key, autogroupName, /*requestSort=*/false);
}
Step 5 -- Signal extraction and sort:
Step 6 -- Attention effects (buzz-beep-blink):
buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
new NotificationAttentionHelper.Signals(
mUserProfiles.isCurrentProfile(r.getUserId()),
mListenerHints));
Step 7 -- Listener dispatch:
if (notification.getSmallIcon() != null) {
notifyListenersPostedAndLogLocked(r, old, mTracker, maybeReport);
posted = true;
} else {
Slog.e(TAG, "Not posting notification without small icon: " + notification);
}
A notification must have a small icon to be posted. Without one, it is rejected at this stage.
Step 8 -- Clean up enqueue list:
for (int i = 0; i < N; i++) {
final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
if (Objects.equals(key, enqueued.getKey())) {
mEnqueuedNotifications.remove(i);
break;
}
}
28.2.5 Phase 3: Cancel¶
Cancellation can originate from many sources:
| Cancel Reason | Constant | Trigger |
|---|---|---|
REASON_CLICK |
1 | User taps the notification |
REASON_CANCEL |
2 | User swipes to dismiss |
REASON_CANCEL_ALL |
3 | User taps "Clear all" |
REASON_APP_CANCEL |
8 | App calls cancel() |
REASON_APP_CANCEL_ALL |
9 | App calls cancelAll() |
REASON_LISTENER_CANCEL |
10 | NotificationListenerService.cancelNotification() |
REASON_PACKAGE_BANNED |
7 | Notifications disabled for package |
REASON_CHANNEL_BANNED |
17 | Channel importance set to NONE |
REASON_SNOOZED |
18 | User snoozes the notification |
REASON_TIMEOUT |
19 | TTL expired (3-day default) |
REASON_GROUP_OPTIMIZATION |
22 | Notification re-grouped |
The main cancel entry point:
// NotificationManagerService.java (line 11608)
void cancelNotification(final int callingUid, final int callingPid,
final String pkg, final String tag, final int id,
final int mustHaveFlags, final int mustNotHaveFlags,
final boolean sendDelete, final int userId, final int reason,
int rank, int count, final ManagedServiceInfo listener) {
mHandler.scheduleCancelNotification(new CancelNotificationRunnable(...));
}
The private cancelNotificationLocked() method (line 11300) performs:
-
Send delete intent if the notification has a
deleteIntent: -
Notify listeners of the removal:
-
Notify GroupHelper for potential un-grouping:
-
Clear attention effects:
-
Update group summary tracking:
-
Archive for history:
28.2.6 TTL and Notification Timeout¶
Notifications have a 3-day time-to-live by default:
// NotificationManagerService.java (line 666)
static final long NOTIFICATION_TTL = Duration.ofDays(3).toMillis();
Notifications older than 14 days at the time of posting are rejected:
// NotificationManagerService.java (line 668)
static final long NOTIFICATION_MAX_AGE_AT_POST = Duration.ofDays(14).toMillis();
The TimeToLiveHelper class schedules an alarm to cancel each notification after
its TTL expires. This prevents zombie notifications from lingering indefinitely.
28.2.7 Rate Limiting¶
The service enforces per-package rate limiting to prevent notification storms:
Toast posting has even stricter rate limits:
// NotificationManagerService.java (lines 543-547)
private static final MultiRateLimiter.RateLimit[] TOAST_RATE_LIMITS = {
MultiRateLimiter.RateLimit.create(3, Duration.ofSeconds(20)),
MultiRateLimiter.RateLimit.create(5, Duration.ofSeconds(42)),
MultiRateLimiter.RateLimit.create(6, Duration.ofSeconds(68)),
};
28.2.8 The Full Post Flow Diagram¶
flowchart TD
A[App calls notify] --> B[enqueueNotificationInternal]
B --> C{pkg == null?}
C -->|Yes| D[throw IllegalArgumentException]
C -->|No| E[Resolve userId and UID]
E --> F[Apply FGS notification policy]
F --> G[fixNotification]
G --> H[Look up NotificationChannel]
H --> I{Channel found?}
I -->|No| J[Show developer warning toast, return]
I -->|Yes| K[Create NotificationRecord]
K --> L[Acquire wake lock]
L --> M[Post EnqueueNotificationRunnable]
M --> N{Snoozed?}
N -->|Yes| O[Re-snooze, return]
N -->|No| P[Copy ranking from old record]
P --> Q[Add to mEnqueuedNotifications]
Q --> R[Handle grouped notifications]
R --> S{NAS enabled?}
S -->|Yes| T[Notify NAS, delay 200ms]
S -->|No| U[Post immediately]
T --> V[PostNotificationRunnable]
U --> V
V --> W{App banned or channel blocked?}
W -->|Yes| X[Register blocked, return]
W -->|No| Y[Add/update in mNotificationList]
Y --> Z[extractSignals + sort]
Z --> AA[buzzBeepBlink attention effects]
AA --> AB{Has small icon?}
AB -->|No| AC[Log error, return]
AB -->|Yes| AD[notifyListenersPosted]
AD --> AE[Remove from mEnqueuedNotifications]
AE --> AF[Release wake lock]
28.3 Notification Channels and Groups¶
28.3.1 Introduction to Channels¶
Since Android 8.0 (API 26), every notification must be assigned to a
NotificationChannel. Channels give users fine-grained control over notification
behavior -- importance, sound, vibration, lights, and badge visibility can all
be configured per-channel. Once created, the channel's behavioral settings
become user-owned: the app cannot programmatically change them.
Source:
frameworks/base/core/java/android/app/NotificationChannel.java
28.3.2 Channel Properties¶
A NotificationChannel has these key properties:
| Property | Method | Description |
|---|---|---|
| ID | getId() |
Immutable string identifier |
| Name | getName() |
User-visible name |
| Description | getDescription() |
User-visible description |
| Importance | getImportance() |
Controls interruption level |
| Sound | getSound() |
Custom notification sound URI |
| Vibration pattern | getVibrationPattern() |
Custom vibration |
| Light color | getLightColor() |
LED color |
| Show badge | canShowBadge() |
App icon badge |
| Bypass DND | canBypassDnd() |
Override Do Not Disturb |
| Lockscreen visibility | getLockscreenVisibility() |
PUBLIC / PRIVATE / SECRET |
| Conversation ID | getConversationId() |
Links to a parent channel for conversations |
| Bubble preference | getAllowBubbles() |
Whether bubbles are allowed |
28.3.3 Importance Levels¶
Importance is the single most consequential property:
| Level | Constant | Value | Behavior |
|---|---|---|---|
| None | IMPORTANCE_NONE |
0 | Blocked -- not shown anywhere |
| Min | IMPORTANCE_MIN |
1 | Silent, no status bar icon |
| Low | IMPORTANCE_LOW |
2 | Silent, shown in shade |
| Default | IMPORTANCE_DEFAULT |
3 | Makes sound |
| High | IMPORTANCE_HIGH |
4 | Makes sound, heads-up display |
| Max | IMPORTANCE_MAX |
5 | Urgent, full-screen intent allowed |
28.3.4 Channel Groups¶
NotificationChannelGroup lets apps organize related channels. Each group can
be independently blocked by the user, which blocks all channels within it.
// App code
NotificationChannelGroup group = new NotificationChannelGroup(
"social", "Social Notifications");
notificationManager.createNotificationChannelGroup(group);
NotificationChannel channel = new NotificationChannel(
"chat", "Chat Messages", NotificationManager.IMPORTANCE_HIGH);
channel.setGroup("social");
notificationManager.createNotificationChannel(channel);
28.3.5 System Reserved Channels¶
The NotificationChannel.SYSTEM_RESERVED_IDS set prevents apps from creating
channels with IDs that the system itself uses. Attempting to create a channel
with a reserved ID is silently ignored.
28.3.6 Channel Storage: PreferencesHelper¶
Server-side channel management lives in PreferencesHelper:
// frameworks/base/services/core/java/com/android/server/notification/
// PreferencesHelper.java (line 128)
public class PreferencesHelper implements RankingConfig {
private static final String TAG = "NotificationPrefHelper";
private final int XML_VERSION;
static final int NOTIFICATION_CHANNEL_COUNT_LIMIT = 5000;
static final int NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT = 6000;
private static final int NOTIFICATION_CHANNEL_DELETION_RETENTION_DAYS = 30;
// ...
}
Channel preferences are serialized to XML and stored per-user. When a channel is deleted, it is retained (soft-deleted) for 30 days to preserve user settings in case the app re-creates it.
28.3.7 The NotificationChannelExtractor¶
The first extractor in the pipeline is NotificationChannelExtractor. It looks
up the channel from PreferencesHelper and attaches it to the NotificationRecord:
// frameworks/base/services/core/java/com/android/server/notification/
// NotificationChannelExtractor.java
public class NotificationChannelExtractor implements NotificationSignalExtractor {
// Sets r.mChannel from PreferencesHelper lookup
}
This must run first because all subsequent extractors depend on channel properties (importance, DND bypass, visibility, etc.).
28.3.8 Classification Channels¶
Recent AOSP versions introduce automatic classification channels for bundling notifications by type:
// NotificationChannel.java
public static final String NEWS_ID = "news";
public static final String PROMOTIONS_ID = "promotions";
public static final String RECS_ID = "recommendations";
public static final String SOCIAL_MEDIA_ID = "social_media";
When the Notification Assistant Service classifies a notification (e.g., as news or promotions), the system may reassign it to one of these channels for more consistent user control.
28.3.9 Channel Lifecycle Diagram¶
stateDiagram-v2
[*] --> Created: App calls createNotificationChannel
Created --> Active: Notification posted on this channel
Active --> UserModified: User changes settings in System Settings
UserModified --> Active: New notification uses modified settings
Active --> Deleted: App calls deleteNotificationChannel
Deleted --> SoftDeleted: Retained for 30 days
SoftDeleted --> Restored: App recreates with same ID
SoftDeleted --> Purged: 30 days elapsed
Restored --> Active
Active --> Blocked: User sets importance to NONE
Blocked --> Active: User re-enables
UserModified --> Blocked: User sets importance to NONE
28.4 Notification Ranking¶
28.4.1 Overview¶
Notification ranking determines the order in which notifications appear in the shade and which ones get to interrupt the user. The ranking pipeline has two stages:
- Signal extraction -- Each
NotificationSignalExtractorwrites signals onto theNotificationRecord. - Sorting -- The comparator uses these signals to produce a final ordering.
Source: frameworks/base/services/core/java/com/android/server/notification/RankingHelper.java
28.4.2 RankingHelper: The Orchestrator¶
// RankingHelper.java (line 37)
public class RankingHelper {
private final NotificationSignalExtractor[] mSignalExtractors;
private final Comparator mPreliminaryComparator;
private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
The extractSignals() method runs every extractor in order:
// RankingHelper.java (line 98)
public void extractSignals(NotificationRecord r) {
final int N = mSignalExtractors.length;
for (int i = 0; i < N; i++) {
NotificationSignalExtractor extractor = mSignalExtractors[i];
try {
RankingReconsideration recon = extractor.process(r);
if (recon != null) {
mRankingHandler.requestReconsideration(recon);
}
} catch (Throwable t) {
Slog.w(TAG, "NotificationSignalExtractor failed.", t);
}
}
}
The sort() method performs a two-pass sort:
// RankingHelper.java (line 113)
public void sort(ArrayList<NotificationRecord> notificationList) {
// Pass 1: Preliminary sort by ranking time
notificationList.sort(mPreliminaryComparator);
// Pass 2: Assign global sort keys with group awareness
for (int i = 0; i < N; i++) {
record.setAuthoritativeRank(i);
// Nominate group proxies (prefer children over summaries)
}
// Global sort key format:
// is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
}
28.4.3 NotificationTimeComparator¶
The preliminary comparator sorts by ranking time (most recent first):
// frameworks/base/services/core/java/com/android/server/notification/
// NotificationTimeComparator.java
public class NotificationTimeComparator implements Comparator<NotificationRecord> {
@Override
public int compare(NotificationRecord left, NotificationRecord right) {
return -1 * Long.compare(left.getRankingTimeMs(), right.getRankingTimeMs());
}
}
28.4.4 Global Sort Key¶
After preliminary sorting, RankingHelper.sort() assigns a global sort key to
each notification. The key encodes multiple dimensions:
This multi-level key ensures:
- Recently-interrupted notifications appear at the top.
- Grouped notifications stay together.
- Within a group, the summary appears before children.
- Within children, the developer-provided sort key is honored.
28.4.5 The Extractors in Detail¶
NotificationChannelExtractor¶
Resolves the NotificationChannel from PreferencesHelper and sets it on the
record. All downstream extractors depend on this.
NotificationAdjustmentExtractor¶
Applies adjustments from the Notification Assistant Service (NAS). Adjustments can modify importance, people references, smart actions, smart replies, and classification type.
// Adjustment keys supported (NotificationManagerService.java, line 510)
static final String[] DEFAULT_ALLOWED_ADJUSTMENTS = new String[] {
Adjustment.KEY_PEOPLE,
Adjustment.KEY_SNOOZE_CRITERIA,
Adjustment.KEY_USER_SENTIMENT,
Adjustment.KEY_CONTEXTUAL_ACTIONS,
Adjustment.KEY_TEXT_REPLIES,
Adjustment.KEY_IMPORTANCE,
Adjustment.KEY_IMPORTANCE_PROPOSAL,
Adjustment.KEY_SENSITIVE_CONTENT,
Adjustment.KEY_RANKING_SCORE,
Adjustment.KEY_NOT_CONVERSATION,
Adjustment.KEY_TYPE,
Adjustment.KEY_SUMMARIZATION
};
BubbleExtractor¶
Determines whether a notification can be presented as a bubble. Requirements:
- Device supports bubbles (
config_supportsBubble). - Not a low-RAM device.
- The notification is a conversation.
- It has a valid
ShortcutInfo. - It is not a foreground service or user-initiated job notification.
- User has enabled bubbles globally and per-app.
// BubbleExtractor.java (line 86)
boolean notifCanPresentAsBubble = canPresentAsBubble(record)
&& !mActivityManager.isLowRamDevice()
&& record.isConversation()
&& record.getShortcutInfo() != null
&& !record.getNotification().isFgsOrUij();
ValidateNotificationPeople¶
Resolves people references (URIs, phone numbers, email addresses) against the
Contacts database. Sets the contactAffinity on the record:
// ValidateNotificationPeople.java (lines 83-95)
static final float NONE = 0f;
static final float VALID_CONTACT = 0.5f;
static final float STARRED_CONTACT = 1f;
This extractor can return a RankingReconsideration for asynchronous contact
resolution, which re-sorts notifications once the lookup completes.
PriorityExtractor¶
Sets the package priority based on channel configuration.
ZenModeExtractor¶
Consults ZenModeHelper.shouldIntercept() to determine if the notification
should be suppressed by DND. Sets mIntercept and mSuppressedVisualEffects
on the record.
ImportanceExtractor¶
Calls record.calculateImportance(), which resolves the final importance from
the channel importance, system importance override, and assistant importance:
VisibilityExtractor¶
Sets the lockscreen visibility based on channel settings and system-wide sensitive content policies.
BadgeExtractor¶
Determines whether the notification should produce an app icon badge, consulting
the channel's canShowBadge() setting and DND suppression.
CriticalNotificationExtractor¶
Marks automotive and critical system notifications for priority placement above all other notifications.
28.4.6 Ranking Reconsideration¶
Some extractors cannot complete synchronously. The RankingReconsideration
mechanism allows deferred re-evaluation:
// RankingHelper.java (line 104)
if (recon != null) {
mRankingHandler.requestReconsideration(recon);
}
The reconsideration runs on the ranking thread (MESSAGE_RECONSIDER_RANKING = 1000).
After it completes, a sort update (MESSAGE_RANKING_SORT = 1001) is triggered
and listeners are notified of the ranking change.
28.4.7 Ranking Pipeline Diagram¶
flowchart LR
subgraph Extractors
CE[ChannelExtractor] --> AE[AdjustmentExtractor]
AE --> BE[BubbleExtractor]
BE --> VNP[ValidateNotificationPeople]
VNP --> PE[PriorityExtractor]
PE --> ZME[ZenModeExtractor]
ZME --> IE[ImportanceExtractor]
IE --> VE[VisibilityExtractor]
VE --> BADGE[BadgeExtractor]
BADGE --> CRIT[CriticalNotificationExtractor]
end
subgraph Sorting
CRIT --> PRELIM["Preliminary Sort<br/>by RankingTime"]
PRELIM --> GSK["Assign Global Sort Keys<br/>with group proxy"]
GSK --> FINAL["Final Sort<br/>by GlobalSortKey"]
end
subgraph Output
FINAL --> RANKING["Ranking Update<br/>sent to Listeners"]
end
VNP -.->|async| RECON["RankingReconsideration<br/>on ranking thread"]
RECON -.->|re-sort| GSK
28.4.8 Interruptiveness¶
A notification is considered "visually interruptive" if it is new or if its
content has changed meaningfully. The isVisuallyInterruptive() method
(line 10480) compares old and new extras for changes in title, text, progress,
and large icon. Interruptive notifications get their ranking time reset, pushing
them to the top of the shade.
28.5 Do Not Disturb (ZenModeHelper and ZenPolicy)¶
28.5.1 Overview¶
Do Not Disturb (DND) in Android is managed by ZenModeHelper, which maintains
the DND state machine, evaluates rules, and tells the notification pipeline which
notifications to intercept. DND is not a simple on/off switch -- it supports
multiple simultaneous rules, each with its own policy.
Source files:
| File | Role |
|---|---|
ZenModeHelper.java |
Central DND state management |
ZenModeFiltering.java |
Notification intercept logic |
ZenModeConditions.java |
Rule condition evaluation |
ZenModeExtractor.java |
Signal extractor integration |
ZenModeConfig.java |
Configuration data model |
ZenPolicy.java |
Per-rule policy specification |
ConditionProviders.java |
Manages condition provider services |
28.5.2 Zen Modes¶
// Settings.Global
ZEN_MODE_OFF = 0; // DND off
ZEN_MODE_IMPORTANT_INTERRUPTIONS = 1; // Priority only
ZEN_MODE_NO_INTERRUPTIONS = 2; // Total silence
ZEN_MODE_ALARMS = 3; // Alarms only
28.5.3 ZenModeHelper Architecture¶
// ZenModeHelper.java (lines 175-211, summarized)
public class ZenModeHelper {
private final ZenModeFiltering mFiltering;
private final ZenModeConditions mConditions;
private final SparseArray<ZenModeConfig> mConfigs; // per-user
protected int mZenMode;
protected NotificationManager.Policy mConsolidatedPolicy;
private ZenDeviceEffects mConsolidatedDeviceEffects;
private DeviceEffectsApplier mDeviceEffectsApplier;
}
The mConsolidatedPolicy merges all active rules into a single effective policy.
This consolidated policy is what ZenModeFiltering uses to make intercept
decisions.
28.5.4 ZenModeConfig and Rules¶
The ZenModeConfig holds:
- The manual rule (user-activated DND).
- A map of
AutomaticZenRuleobjects keyed by rule ID. - Per-rule conditions (time-based, event-based, driving, etc.).
Each AutomaticZenRule has:
- An owner package.
- A
ConditionProvidercomponent. - A
ZenPolicythat describes what the rule allows. - A trigger description.
Rule limit per package:
28.5.5 Automatic Zen Rules and Conditions¶
Automatic rules are activated by ConditionProvider services. Built-in providers:
| Provider | Class | Trigger |
|---|---|---|
| Schedule | ScheduleConditionProvider |
Time-of-day schedules |
| Event | EventConditionProvider |
Calendar events |
| Countdown | CountdownConditionProvider |
Timed DND (e.g., "1 hour") |
Apps can register their own ConditionProviderService to create custom rules
(e.g., "DND when driving").
28.5.6 ZenPolicy¶
ZenPolicy (API 29+) gives fine-grained control per rule:
// android.service.notification.ZenPolicy
public final class ZenPolicy {
// People categories
int mPriorityCategories; // calls, messages, conversations, etc.
int mPriorityCallSenders; // anyone, contacts, starred, none
int mPriorityMessageSenders;
int mConversationSenders;
// Visual effects
int mVisualEffects; // full-screen intents, lights, peek,
// status bar, badge, ambient, notification list
}
28.5.7 The Filtering Decision¶
ZenModeFiltering.shouldIntercept() is the core decision function. It is called
from ZenModeExtractor.process() during signal extraction:
flowchart TD
A[ZenModeExtractor.process] --> B{DND active?}
B -->|No| C[mIntercept = false]
B -->|Yes| D[ZenModeFiltering.shouldIntercept]
D --> E{Total silence?}
E -->|Yes| F[Intercept everything]
D --> G{Alarms only?}
G -->|Yes| H{Is alarm/media?}
H -->|Yes| I[Allow]
H -->|No| J[Intercept]
D --> K{Priority only?}
K -->|Yes| L{Is call?}
L -->|Yes| M[Check call policy]
K -->|Yes| N{Is message?}
N -->|Yes| O[Check message policy]
K -->|Yes| P{Is conversation?}
P -->|Yes| Q[Check conversation policy]
K -->|Yes| R{Can bypass DND?}
R -->|Yes| S[Allow]
R -->|No| T[Intercept]
M --> U{Repeat caller?}
U -->|Yes| V[Allow]
M --> W{Matches sender filter?}
W -->|Yes| X[Allow]
W -->|No| Y[Intercept]
28.5.8 Repeat Caller Detection¶
When DND allows repeat callers, the system tracks recent calls:
If the same caller calls twice within a threshold period (default 15 minutes), the second call is allowed through even if the caller does not match the priority filter.
28.5.9 Device Effects¶
Since Android 15, DND rules can specify ZenDeviceEffects:
Device effects include:
- Grayscale display.
- Suppress ambient display.
- Dim wallpaper.
- Night mode override.
These are applied through a DeviceEffectsApplier, which is set by SystemUI:
// ZenModeHelper.java (line 329)
void setDeviceEffectsApplier(@NonNull DeviceEffectsApplier deviceEffectsApplier) {
mDeviceEffectsApplier = deviceEffectsApplier;
applyConsolidatedDeviceEffects(ORIGIN_INIT);
}
28.5.10 Implicit Rules (Android 15+)¶
Starting with Android V (API 35), app calls to setInterruptionFilter() and
setNotificationPolicy() create implicit AutomaticZenRule objects instead of
directly modifying the global DND state:
// NotificationManagerService.java (line 660)
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L;
This change allows the system to properly track which app activated DND and prevents apps from accidentally overriding each other's DND settings.
28.5.11 Suppressed Visual Effects¶
DND can suppress specific visual effects while still allowing the notification to exist. The suppressed effects bitmap:
| Effect | Constant | Description |
|---|---|---|
| Screen off effects | SUPPRESSED_EFFECT_SCREEN_OFF |
Suppress when screen off |
| Screen on effects | SUPPRESSED_EFFECT_SCREEN_ON |
Suppress when screen on |
| Full-screen intent | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT |
Block FSI |
| Peek (heads-up) | SUPPRESSED_EFFECT_PEEK |
No heads-up |
| Status bar | SUPPRESSED_EFFECT_STATUS_BAR |
No status bar icon |
| Badge | SUPPRESSED_EFFECT_BADGE |
No app badge |
| Ambient | SUPPRESSED_EFFECT_AMBIENT |
No ambient display |
| Notification list | SUPPRESSED_EFFECT_NOTIFICATION_LIST |
Hide from shade |
| Lights | SUPPRESSED_EFFECT_LIGHTS |
No LED |
28.6 Notification Listeners (NotificationListenerService)¶
28.6.1 Overview¶
NotificationListenerService is the mechanism by which apps (including SystemUI)
receive notification events. It is a bound service managed by the
ManagedServices framework. When a listener is enabled, NMS binds to it and
dispatches notification posted, removed, and ranking update events.
Source files:
| File | Role |
|---|---|
NotificationListenerService.java |
SDK API (app-side) |
ManagedServices.java |
Service lifecycle management |
NotificationListeners (inner class in NMS) |
Server-side dispatch |
28.6.2 The ManagedServices Framework¶
NotificationListeners extends ManagedServices, which handles:
- Service discovery via
PackageManagerqueries. - Binding and unbinding.
- User profile awareness (which users the listener serves).
- Persistent configuration in Secure settings.
// NotificationManagerService.java (line 13756)
public class NotificationListeners extends ManagedServices {
static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners";
// ...
}
The config for listeners:
// NotificationListeners (line 13886)
protected Config getConfig() {
Config c = new Config();
c.caption = "notification listener";
c.serviceInterface = NotificationListenerService.SERVICE_INTERFACE;
c.secureSettingName = Secure.ENABLED_NOTIFICATION_LISTENERS;
c.bindPermission =
android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE;
c.settingsAction = Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS;
return c;
}
28.6.3 Binding Flags¶
Notification listeners use specific binding flags to manage memory pressure:
// NotificationListeners (line 13881)
protected long getBindFlags() {
return BIND_AUTO_CREATE
| BIND_FOREGROUND_SERVICE
| BIND_NOT_PERCEPTIBLE // reduce memory pressure
| BIND_ALLOW_WHITELIST_MANAGEMENT
| freezeFlags; // BIND_ALLOW_FREEZE for idle NLS
}
The BIND_NOT_PERCEPTIBLE flag is notable: it tells the system that too many
third-party listeners could cause memory pressure, so they should be treated
as lower priority for OOM adjustment purposes.
28.6.4 Listener Callbacks¶
The listener receives these key callbacks:
// NotificationListenerService.java (SDK API)
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap);
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
int reason);
public void onNotificationRankingUpdate(RankingMap rankingMap);
public void onListenerConnected();
public void onListenerDisconnected();
public void onNotificationChannelModified(String pkg, UserHandle user,
NotificationChannel channel, int modificationType);
28.6.5 Filter Types¶
Listeners can request to receive only specific categories of notifications:
// NotificationListenerService.java
static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1;
static final int FLAG_FILTER_TYPE_ALERTING = 2;
static final int FLAG_FILTER_TYPE_SILENT = 4;
static final int FLAG_FILTER_TYPE_ONGOING = 8;
28.6.6 Trim Levels¶
To reduce IPC overhead, listeners can request trimmed notification data:
static final int TRIM_FULL = 0; // all data
static final int TRIM_LIGHT = 1; // no extras, no large icon
SystemUI uses TRIM_FULL because it needs all notification content for rendering.
Third-party listeners typically use TRIM_LIGHT.
28.6.7 Trusted vs. Untrusted Listeners¶
Recent AOSP versions introduce the concept of "trusted" listeners for sensitive content redaction:
// NotificationListeners (line 13788)
@GuardedBy("mTrustedListenerUids")
private final ArraySet<Integer> mTrustedListenerUids = new ArraySet<>();
When the redactSensitiveNotificationsFromUntrustedListeners flag is enabled,
listeners not in the trusted set receive redacted notification content. SystemUI
is always trusted due to its STATUS_BAR_SERVICE permission.
28.6.8 The Notification Assistant Service (NAS)¶
NotificationAssistantService is a special type of listener that can modify
notifications before they are posted. Only one NAS can be active at a time
(typically the system-provided intelligence service).
The NAS flow:
sequenceDiagram
participant NMS
participant NAS as NotificationAssistantService
participant Listeners
NMS->>NAS: onNotificationEnqueued(sbn)
NAS->>NMS: adjustNotification(adjustment)
Note over NMS: Apply adjustment to NotificationRecord<br/>(importance, smart replies, classification)
NMS->>NMS: PostNotificationRunnable (after 200ms delay)
NMS->>Listeners: onNotificationPosted(sbn, ranking)
Supported adjustment keys:
| Key | Effect |
|---|---|
KEY_IMPORTANCE |
Override importance |
KEY_IMPORTANCE_PROPOSAL |
Suggest importance (user can override) |
KEY_PEOPLE |
Attach people references |
KEY_SNOOZE_CRITERIA |
Suggest snooze options |
KEY_USER_SENTIMENT |
Set user sentiment (positive/neutral/negative) |
KEY_CONTEXTUAL_ACTIONS |
Add smart actions |
KEY_TEXT_REPLIES |
Add smart replies |
KEY_RANKING_SCORE |
Set ranking score |
KEY_SENSITIVE_CONTENT |
Mark as sensitive |
KEY_NOT_CONVERSATION |
Override conversation detection |
KEY_TYPE |
Classify (news, promotions, recommendations) |
KEY_SUMMARIZATION |
AI-generated summary |
28.6.9 Listener Lifecycle Diagram¶
stateDiagram-v2
[*] --> Discovered: PackageManager scan
Discovered --> Enabled: User grants permission
Enabled --> Binding: ManagedServices.bindService
Binding --> Connected: onServiceConnected
Connected --> Active: onListenerConnected + ranking update
Active --> Active: onNotificationPosted/Removed/RankingUpdate
Active --> Rebinding: Process died
Rebinding --> Connected: Auto-reconnect
Active --> Disabled: User revokes permission
Disabled --> [*]
28.7 Conversation Notifications¶
28.7.1 Overview¶
Conversation notifications represent messages from real people. They receive special treatment in the notification shade: a dedicated section, the ability to become bubbles, and richer presentation including the sender's avatar.
A notification qualifies as a conversation if:
- It uses
MessagingStyle. - It references a valid sharing shortcut via
setShortcutId(). - The shortcut has
Persondata attached.
28.7.2 ShortcutInfo Integration¶
The ShortcutHelper class manages the lifecycle of shortcuts referenced by
notifications:
// frameworks/base/services/core/java/com/android/server/notification/
// ShortcutHelper.java (line 48)
public class ShortcutHelper {
private final HashMap<String, HashMap<String, String>> mActiveShortcutBubbles;
private final LauncherApps.ShortcutChangeCallback mShortcutChangeCallback;
}
When a notification is posted, ShortcutHelper queries the shortcut service:
// ShortcutHelper uses these query flags:
FLAG_GET_PERSONS_DATA // include Person objects
FLAG_MATCH_CACHED // match cached shortcuts
FLAG_MATCH_DYNAMIC // match dynamic shortcuts
FLAG_MATCH_PINNED_BY_ANY_LAUNCHER // match pinned shortcuts
The helper also listens for shortcut removal events to clean up associated bubbles.
28.7.3 ValidateNotificationPeople¶
The ValidateNotificationPeople extractor resolves people references against
the contacts database. It uses an LRU cache for performance:
// ValidateNotificationPeople.java (lines 73-74)
private static final int MAX_PEOPLE = 10;
private static final int PEOPLE_CACHE_SIZE = 200;
Contact affinity levels:
NONE (0f)-- no valid contact reference.VALID_CONTACT (0.5f)-- references a real contact.STARRED_CONTACT (1f)-- references a starred/favorite contact.
These affinity values feed into DND filtering decisions (starred contacts can bypass DND) and ranking.
28.7.4 Conversation Detection in NotificationRecord¶
// NotificationRecord.java (relevant fields)
private ShortcutInfo mShortcutInfo;
private boolean mIsNotConversationOverride;
private boolean mPkgAllowedAsConvo;
private boolean mHasSentValidMsg;
private boolean mAppDemotedFromConvo;
A notification is considered a conversation when:
- It has
MessagingStyle. - It has a valid shortcut.
- The NAS has not overridden it with
KEY_NOT_CONVERSATION. - The app has not been demoted from conversation status.
28.7.5 Conversation Channels¶
Each conversation can have its own derived channel. When a user customizes a
conversation's notification settings, the system creates a child channel
linked to the parent via conversationId:
// NotificationChannel.java
public NotificationChannel getConversationId();
public NotificationChannel getParentChannelId();
This allows per-conversation customization (different sound for different contacts) without affecting the parent channel.
28.7.6 MessagingStyle Requirements¶
For optimal conversation display, MessagingStyle should include:
Notification.MessagingStyle style = new Notification.MessagingStyle(sender)
.setConversationTitle("Group Chat Name")
.addMessage("Hello!", timestamp, sender)
.setGroupConversation(true); // for group chats
The Person objects must have:
- A name.
- Optionally an icon (avatar).
- Optionally a URI pointing to a contact.
- A unique key for identity tracking.
28.7.7 Conversation Priority Sections¶
In SystemUI, the notification shade divides notifications into sections:
graph TD
A[Notification Shade] --> B[Incoming / Heads-Up]
A --> C[Priority Conversations]
A --> D[Alerting Notifications]
A --> E[Silent Notifications]
A --> F[Minimized / Ambient]
Priority conversations (those the user has marked as "priority" in settings)
appear above all other notifications. This is controlled by the
NotificationSectionsManager in SystemUI.
28.8 Bubble Notifications¶
28.8.1 Overview¶
Bubble notifications allow conversation notifications to float as circular icons on the screen, expanding into a mini-window when tapped. The bubble system spans three layers:
- NMS --
BubbleExtractordetermines eligibility. - SystemUI -- Bridges notification events to the shell bubble controller.
- WM Shell --
BubbleControllermanages the bubble UI and task lifecycle.
28.8.2 Bubble Eligibility (BubbleExtractor)¶
The BubbleExtractor (a NotificationSignalExtractor) evaluates:
// BubbleExtractor.java (line 86)
boolean notifCanPresentAsBubble = canPresentAsBubble(record)
&& !mActivityManager.isLowRamDevice()
&& record.isConversation()
&& record.getShortcutInfo() != null
&& !record.getNotification().isFgsOrUij();
Additional checks:
userEnabledBubbles-- global setting.appPreference-- per-app bubble preference (NONE, SELECTED, ALL).- Channel-level
getAllowBubbles()setting.
The decision matrix:
| App Pref | Channel Pref | Result |
|---|---|---|
| ALL | ON or default | Allowed |
| ALL | OFF | Denied |
| SELECTED | ON | Allowed |
| SELECTED | OFF or default | Denied |
| NONE | any | Denied |
28.8.3 BubbleMetadata¶
Apps specify bubble behavior through Notification.BubbleMetadata:
Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
pendingIntent, icon)
.setDesiredHeight(600)
.setAutoExpandBubble(true)
.setSuppressNotification(true)
.build();
Key properties:
PendingIntent-- the activity to launch inside the bubble.Icon-- the bubble icon.desiredHeight-- preferred expanded height.autoExpandBubble-- expand immediately on creation.suppressNotification-- hide from the shade (show only as bubble).
28.8.4 WM Shell BubbleController¶
The actual bubble UI lives in the WindowManager Shell module:
Key classes:
| Class | Role |
|---|---|
BubbleController.java |
Main coordinator |
BubbleData.java |
Data model for active bubbles |
Bubble.java |
Single bubble state |
BubbleExpandedView.java |
Expanded view with embedded task |
BubbleTaskView.kt |
Task view for the embedded activity |
BubblePositioner.java |
Position calculations |
BubbleDataRepository.kt |
Persistence |
BubbleStackViewManager.kt |
Stack UI management |
BubbleBarLayerView.java |
Bubble bar for launcher integration |
28.8.5 Bubble Lifecycle¶
stateDiagram-v2
[*] --> NotificationPosted: NMS posts<br/>with BubbleMetadata
NotificationPosted --> BubbleCreated: BubbleExtractor allows
BubbleCreated --> Collapsed: addBubble
Collapsed --> Expanded: tap bubble
Expanded --> Collapsed: tap outside
Collapsed --> Dismissed: drag to dismiss
Expanded --> Dismissed: drag to dismiss
Collapsed --> Overflow: max reached
Overflow --> Collapsed: select from overflow
Dismissed --> [*]
NotificationPosted --> Denied: BubbleExtractor denies
Denied --> ShadeOnly: shade only
28.8.6 Bubble-to-Task Mapping¶
Each expanded bubble hosts an embedded Activity in a task. The BubbleTaskView
creates a virtual display or uses task embedding to render the activity within
the bubble's expanded view:
// BubbleExpandedView.java
// Creates a TaskView that hosts the bubble's activity
// The activity is launched with ActivityOptions specifying the
// bubble display.
The bubble activity must:
- Be declared
resizeableActivity="true". - Allow embedding.
- Have a valid
PendingIntentthat resolves to an activity.
28.8.7 Bubble Persistence¶
The BubbleDataRepository persists bubble state across reboots for ongoing
conversations. This allows bubbles to reappear after a device restart without
the app needing to re-post the notification.
28.8.8 Bubble Integration Flow¶
sequenceDiagram
participant App
participant NMS
participant BubbleExtractor
participant SystemUI
participant BubbleController as WM Shell BubbleController
App->>NMS: notify() with BubbleMetadata
NMS->>BubbleExtractor: process(record)
BubbleExtractor->>BubbleExtractor: Check eligibility
BubbleExtractor->>NMS: setAllowBubble(true/false)
NMS->>SystemUI: onNotificationPosted()
SystemUI->>BubbleController: onEntryAdded(entry)
BubbleController->>BubbleController: Create Bubble object
BubbleController->>BubbleController: Add to BubbleData
BubbleController->>BubbleController: Update stack UI
Note over BubbleController: User taps bubble
BubbleController->>BubbleController: Expand bubble
BubbleController->>BubbleController: Launch activity in TaskView
28.9 Notification UI in SystemUI¶
28.9.1 Overview¶
SystemUI is the primary consumer of notification events. It implements a
NotificationListenerService that receives all notification posted, removed,
and ranking update callbacks. The notification shade, heads-up display, status
bar icons, and lock screen notifications are all rendered by SystemUI.
Key directories:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/
28.9.2 The Notification Pipeline in SystemUI¶
SystemUI uses a modern notification pipeline (sometimes called "new pipeline" or "notif pipeline") for processing notifications. The pipeline stages are:
graph TD
A[NotificationListener] -->|StatusBarNotification| B[Collection]
B --> C[Coordinators & Promoters]
C --> D[GroupManager]
D --> E[SectionsManager]
E --> F[Filtering]
F --> G[Sorting]
G --> H[ShadeListBuilder]
H --> I[NotificationStackScrollLayout]
28.9.3 NotificationStackScrollLayout¶
The NotificationStackScrollLayout is the main scrollable container for
notifications in the shade:
// frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/
// notification/stack/NotificationStackScrollLayout.java
public class NotificationStackScrollLayout extends ViewGroup {
// Custom scrollable ViewGroup that handles:
// - Notification expansion/collapse animation
// - Swipe-to-dismiss gestures
// - Overscroll effects
// - Shade expansion tracking
// - Section headers
// - Grouped notification rendering
}
Its controller:
// NotificationStackScrollLayoutController.java
// Bridges the view with the notification pipeline data
28.9.4 StackScrollAlgorithm¶
The StackScrollAlgorithm computes the position, scale, and alpha of each
notification row based on the current scroll position and shade expansion:
// frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/
// notification/stack/StackScrollAlgorithm.java
This algorithm handles:
- Stacking effect where partially-hidden notifications peek from behind.
- Smooth transitions during shade expansion.
- Gap calculations between sections.
- Rounded corner management for top and bottom items.
28.9.5 Notification Sections¶
Notifications are divided into sections (priority buckets):
// frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/
// notification/stack/NotificationPriorityBucket.kt
The sections in order from top to bottom:
- Heads-Up / Incoming -- currently alerting notifications.
- People (Priority Conversations) -- conversations marked as priority.
- Alerting -- importance HIGH and DEFAULT.
- Silent -- importance LOW and MIN.
- Minimized -- rarely shown.
28.9.6 Notification Row Views¶
Each notification is rendered as an ExpandableNotificationRow:
ExpandableNotificationRow
|
+-- NotificationContentView (collapsed)
| +-- RemoteViews rendered content
|
+-- NotificationContentView (expanded)
| +-- RemoteViews rendered content
|
+-- NotificationContentView (heads-up)
| +-- RemoteViews rendered content
|
+-- NotificationGuts (settings panel)
|
+-- NotificationChildrenContainer (for grouped notifications)
+-- child ExpandableNotificationRows
28.9.7 Notification Styles and Templates¶
The Notification.Style subclasses produce different RemoteViews layouts:
| Style | Collapsed | Expanded | Heads-Up |
|---|---|---|---|
| Default | Title + text | Same | Title + text |
| BigTextStyle | Title + text | Full text | Title + text |
| BigPictureStyle | Title + text | Large image | Title + text |
| InboxStyle | Title + text | Multi-line list | Title + text |
| MessagingStyle | Last message | Conversation history | Last message |
| MediaStyle | Title + text | Up to 5 actions | Title + text |
| CallStyle | Caller info | Call controls | Caller info |
| DecoratedCustomViewStyle | Custom view | Custom view | Custom view |
28.9.8 Heads-Up Notifications¶
Notifications with importance HIGH or above trigger heads-up display. The heads-up manager handles:
- Display duration (typically 5 seconds).
- Multiple heads-up queueing.
- Touch interaction (expand, dismiss).
- Auto-dismiss on user interaction with the device.
Heads-up display is suppressed when:
- The notification shade is already expanded.
- DND suppresses the
SUPPRESSED_EFFECT_PEEKeffect. - The app is in the foreground (configurable).
- The device is in "do not disturb" mode.
28.9.9 Lock Screen Notifications¶
On the lock screen, notification visibility is controlled by:
- Global setting: Show all, hide sensitive, or show none.
- Per-channel visibility: PUBLIC, PRIVATE, or SECRET.
- Work profile policy: Admin can force-hide work notifications.
When content is hidden, the notification shows a generic "Contents hidden"
message. DynamicPrivacyController in SystemUI manages the transition
between public and private content as the user unlocks.
28.9.10 Notification Inflation¶
Notification RemoteViews are inflated asynchronously to avoid janking the
UI thread. The inflation process:
RemoteViewsare extracted from the notification.- An async task inflates the views off the main thread.
- The inflated views are applied to the
NotificationContentView. - If inflation fails (e.g., custom view too large), a fallback minimal layout is shown.
Memory limits for custom views are enforced to prevent malicious apps from causing OOM in SystemUI:
// CustomViewMemorySizeExceededException.kt
// Thrown when a custom RemoteViews exceeds the memory limit
28.9.11 Swipe to Dismiss¶
The NotificationSwipeHelper handles swipe gestures:
- Horizontal swipe dismisses the notification.
- The dismiss sends a
REASON_CANCELback to NMS. - Grouped notifications can be dismissed individually or as a group.
- Ongoing and foreground service notifications resist dismissal.
28.9.12 Notification Shade Architecture Diagram¶
graph TB
subgraph "SystemUI Process"
NLS["NotificationListenerService<br/>Implementation"]
PIPELINE[Notification Pipeline]
SLB[ShadeListBuilder]
NSSL[NotificationStackScrollLayout]
SSA[StackScrollAlgorithm]
SM[SectionsManager]
ENR[ExpandableNotificationRow]
NCV[NotificationContentView]
HU[HeadsUpManager]
BM[BubbleManager]
end
subgraph "system_server"
NMS[NotificationManagerService]
end
NMS -->|Binder: onNotificationPosted| NLS
NLS --> PIPELINE
PIPELINE --> SLB
SLB --> SM
SM --> NSSL
NSSL --> SSA
NSSL --> ENR
ENR --> NCV
NSSL --> HU
NLS --> BM
28.10 Try It¶
28.10.1 Inspecting Active Notifications via ADB¶
Dump the current notification state:
This produces extensive output including:
- All active notifications with their keys, channels, and importance.
- Ranking information.
- Listener state.
- DND configuration.
- Snooze state.
To see just the notification list:
28.10.2 Dumping Notification Channels¶
Or for a specific package:
28.10.3 Inspecting Do Not Disturb State¶
Or through the settings command:
28.10.4 Using the Notification Shell Command¶
NMS includes a shell command interface:
# List all notification channels for a package
adb shell cmd notification list_channels com.example.myapp 0
# Post a test notification
adb shell cmd notification post -t "Test" "tag" "Hello from shell"
# Snooze a notification
adb shell cmd notification snooze --for 60000 <key>
# Unsnooze a notification
adb shell cmd notification unsnooze <key>
Source: frameworks/base/services/core/java/com/android/server/notification/NotificationShellCmd.java
28.10.5 Posting a Notification from Code¶
// Minimal notification with a channel
NotificationChannel channel = new NotificationChannel(
"demo_channel",
"Demo Channel",
NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager nm = getSystemService(NotificationManager.class);
nm.createNotificationChannel(channel);
Notification notification = new Notification.Builder(this, "demo_channel")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Hello")
.setContentText("This is a test notification")
.setAutoCancel(true)
.build();
nm.notify(1, notification);
28.10.6 Posting a Conversation Notification¶
// 1. Create a sharing shortcut
ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "contact_jane")
.setLongLived(true)
.setShortLabel("Jane")
.setPerson(new Person.Builder()
.setName("Jane")
.setKey("jane_key")
.build())
.setIntent(new Intent(Intent.ACTION_DEFAULT))
.build();
ShortcutManager sm = getSystemService(ShortcutManager.class);
sm.addDynamicShortcuts(List.of(shortcut));
// 2. Create channel
NotificationChannel channel = new NotificationChannel(
"messages", "Messages", NotificationManager.IMPORTANCE_HIGH);
nm.createNotificationChannel(channel);
// 3. Build conversation notification
Person sender = new Person.Builder()
.setName("Jane")
.setKey("jane_key")
.setIcon(Icon.createWithResource(context, R.drawable.avatar_jane))
.build();
Notification.MessagingStyle style = new Notification.MessagingStyle(me)
.addMessage("Hey, are you free tonight?", System.currentTimeMillis(), sender);
Notification notification = new Notification.Builder(this, "messages")
.setSmallIcon(R.drawable.ic_message)
.setShortcutId("contact_jane")
.setStyle(style)
.setCategory(Notification.CATEGORY_MESSAGE)
.build();
nm.notify(100, notification);
28.10.7 Creating a Bubble Notification¶
// Build upon the conversation notification above
Intent bubbleIntent = new Intent(context, ChatActivity.class)
.putExtra("contact", "jane");
PendingIntent bubblePendingIntent = PendingIntent.getActivity(
context, 0, bubbleIntent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Notification.BubbleMetadata bubble = new Notification.BubbleMetadata.Builder(
bubblePendingIntent,
Icon.createWithResource(context, R.drawable.avatar_jane))
.setDesiredHeight(600)
.setAutoExpandBubble(false)
.setSuppressNotification(false)
.build();
Notification notification = new Notification.Builder(this, "messages")
.setSmallIcon(R.drawable.ic_message)
.setShortcutId("contact_jane")
.setStyle(style)
.setBubbleMetadata(bubble)
.build();
nm.notify(100, notification);
28.10.8 Implementing a NotificationListenerService¶
public class MyNotificationListener extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
Log.d("NLS", "Posted: " + sbn.getKey()
+ " pkg=" + sbn.getPackageName()
+ " title=" + sbn.getNotification().extras
.getString(Notification.EXTRA_TITLE));
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn,
RankingMap rankingMap, int reason) {
Log.d("NLS", "Removed: " + sbn.getKey() + " reason=" + reason);
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
Log.d("NLS", "Ranking update received");
}
}
AndroidManifest.xml:
<service
android:name=".MyNotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name=
"android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
The user must manually enable the listener in Settings > Notifications > Notification access.
28.10.9 Programmatically Managing DND¶
NotificationManager nm = getSystemService(NotificationManager.class);
// Check if the app has DND access
if (nm.isNotificationPolicyAccessGranted()) {
// Set DND to priority-only
nm.setInterruptionFilter(
NotificationManager.INTERRUPTION_FILTER_PRIORITY);
// Configure DND policy
NotificationManager.Policy policy = new NotificationManager.Policy(
NotificationManager.Policy.PRIORITY_CATEGORY_CALLS
| NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES,
NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS,
NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS);
nm.setNotificationPolicy(policy);
}
28.10.10 Tracing the Notification Pipeline¶
Enable verbose logging for the notification service:
adb shell setprop log.tag.NotificationService VERBOSE
adb shell setprop log.tag.NotificationRecord DEBUG
adb shell setprop log.tag.ZenModeHelper DEBUG
adb shell setprop log.tag.NotifAttentionHelper DEBUG
adb shell setprop log.tag.RankingHelper DEBUG
Then monitor with:
adb logcat -s NotificationService NotificationRecord ZenModeHelper \
NotifAttentionHelper RankingHelper
28.10.11 Inspecting Notification History¶
Android 11+ stores notification history:
Or via the Settings UI: Settings > Notifications > Notification history.
28.10.12 Testing Snooze Behavior¶
# Snooze a notification for 60 seconds
adb shell cmd notification snooze --for 60000 "0|com.example.app|1|null|10088"
# List snoozed notifications
adb shell dumpsys notification snoozed
28.10.13 Debugging Bubble Issues¶
# Check if bubbles are enabled globally
adb shell settings get global notification_bubbles
# Check per-app bubble preference
adb shell dumpsys notification | grep -A 3 "bubble"
# Enable bubble logging
adb shell setprop log.tag.BubbleExtractor DEBUG
adb shell setprop log.tag.Bubbles DEBUG
28.10.14 Observing Attention Effects¶
To debug why a notification does or does not make sound/vibrate:
The attention helper logs detailed reasons for its buzz-beep-blink decisions, including DND suppression, listener hints, and "alert once" flags.
28.10.15 Notification Architecture Exploration Script¶
#!/bin/bash
# Explore the notification subsystem source structure
NOTIF_DIR="frameworks/base/services/core/java/com/android/server/notification"
echo "=== Core Service Files ==="
wc -l $NOTIF_DIR/NotificationManagerService.java
wc -l $NOTIF_DIR/NotificationRecord.java
wc -l $NOTIF_DIR/PreferencesHelper.java
wc -l $NOTIF_DIR/ZenModeHelper.java
wc -l $NOTIF_DIR/RankingHelper.java
echo ""
echo "=== Signal Extractors ==="
grep -l "implements NotificationSignalExtractor" $NOTIF_DIR/*.java
echo ""
echo "=== All Files ==="
ls $NOTIF_DIR/*.java | wc -l
echo "files in the notification server package"
28.10.16 Summary of Key Source Files¶
| File | Lines | Role |
|---|---|---|
NotificationManagerService.java |
~15,500 | Central service |
NotificationRecord.java |
~1,200 | Server-side notification wrapper |
PreferencesHelper.java |
~2,800 | Channel and group storage |
ZenModeHelper.java |
~2,500 | DND state machine |
ZenModeFiltering.java |
~600 | DND intercept decisions |
RankingHelper.java |
~250 | Signal extraction orchestrator |
NotificationAttentionHelper.java |
~1,500 | Sound, vibration, LED |
GroupHelper.java |
~1,400 | Auto-grouping logic |
SnoozeHelper.java |
~600 | Snooze state management |
ShortcutHelper.java |
~350 | Conversation shortcut queries |
ManagedServices.java |
~2,200 | Listener/assistant lifecycle |
ValidateNotificationPeople.java |
~700 | Contact resolution |
BubbleExtractor.java |
~250 | Bubble eligibility |
ImportanceExtractor.java |
~58 | Importance calculation |
ZenModeExtractor.java |
~50 | DND intercept signal |
NotificationShellCmd.java |
~400 | ADB shell interface |
NotificationStackScrollLayout.java |
~5,000+ | SystemUI shade container |
BubbleController.java |
~2,000+ | WM Shell bubble management |
28.10.17 Monitoring Auto-Grouping¶
Auto-grouping bundles notifications from the same package when there are too many individual groups. To observe this:
Auto-grouping triggers when a package has more than
AUTOGROUP_SPARSE_GROUPS_AT_COUNT (6) sparse groups each with fewer than 3
children. The auto-group summary uses the key "ranker_group" (or an
aggregate group key with the prefix "Aggregate_" when force grouping is
active).
28.10.18 Verifying Channel Configuration¶
To verify that your app's notification channels are correctly configured, use the Settings shell command:
# List all channels for a package and user
adb shell cmd notification list_channels <package_name> <user_id>
# Example
adb shell cmd notification list_channels com.google.android.gm 0
You can also check channel importance directly:
28.10.19 Testing the Full Pipeline with a Custom Extractor¶
While the signal extractor pipeline is not extensible by third-party apps, you can modify the configuration for testing purposes on an eng build:
<!-- Modify frameworks/base/core/res/res/values/config.xml -->
<string-array name="config_notificationSignalExtractors">
<!-- Add your custom extractor here -->
<item>com.android.server.notification.NotificationChannelExtractor</item>
<!-- ... existing extractors ... -->
<item>com.example.MyCustomExtractor</item>
</string-array>
Your custom extractor must implement NotificationSignalExtractor and live in
the system_server classpath.
28.10.20 Foreground Service Notification Constraints¶
Foreground service (FGS) notifications have special constraints:
Key behaviors:
- FGS notifications cannot be dismissed by the user (they get
FLAG_NO_CLEAR). - If the channel importance is MIN or NONE, it is elevated to LOW.
- FGS notifications cannot be cancelled by the app while the service runs.
- Starting from Android 14, the system enforces that FGS notifications must be visible (importance > NONE) or the FGS start is rejected.
28.10.21 Notification Permission (Android 13+)¶
Since Android 13 (API 33), posting notifications requires the
POST_NOTIFICATIONS runtime permission. The PermissionHelper tracks this:
# Check if a package has notification permission
adb shell dumpsys notification permissions | grep "com.your.package"
Apps targeting API 33+ must request this permission at runtime. Pre-existing apps (upgraded from older API levels) get the permission granted automatically if they had existing notification channels.
28.11 Notification Attention Effects¶
28.11.1 Overview¶
When a notification is posted, the system decides whether to play a sound,
vibrate the device, or flash the LED. This logic lives in
NotificationAttentionHelper:
Source: frameworks/base/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
28.11.2 The buzzBeepBlink Decision¶
The buzzBeepBlinkLocked() method is the central decision point. It evaluates
many conditions:
- Is the notification intercepted by DND? If intercepted, most effects are suppressed.
- Has the notification already alerted? The
FLAG_ONLY_ALERT_ONCEflag prevents re-alerting on updates. - Is the notification in a group? Group alert behavior can delegate alerting to the summary or children.
- What are the listener hints? SystemUI can disable effects via
HINT_HOST_DISABLE_EFFECTS. - Is the phone in a call? Call notifications may suppress other alerts.
- What is the importance? Only HIGH and above trigger heads-up; only DEFAULT and above trigger sound.
28.11.3 Sound¶
Sound playback uses the IRingtonePlayer service:
// NotificationAttentionHelper uses:
// - Channel's sound URI
// - Channel's audio attributes
// - System default notification sound as fallback
Sound is suppressed when:
- Importance is below DEFAULT.
- The notification has
FLAG_ONLY_ALERT_ONCEand has already alerted. - DND suppresses the notification.
- The screen is off and
SUPPRESSED_EFFECT_SCREEN_OFFis active. - A phone call is active and listener hints disable notification effects.
28.11.4 Vibration¶
Vibration follows a similar decision tree. The vibration pattern comes from
the NotificationRecord, which resolves it from the channel:
// NotificationRecord.java (line 338)
private VibrationEffect getVibrationForChannel(
NotificationChannel channel, VibratorHelper helper, boolean insistent) {
if (!channel.shouldVibrate()) return null;
// Priority: channel vibration effect > channel pattern > sound URI vibration > default
final VibrationEffect vibration = channel.getVibrationEffect();
if (vibration != null) return vibration;
final long[] vibrationPattern = channel.getVibrationPattern();
if (vibrationPattern != null)
return helper.createWaveformVibration(vibrationPattern, insistent);
return helper.createDefaultVibration(insistent);
}
The FLAG_INSISTENT flag causes the vibration to repeat indefinitely until the
user acknowledges it.
28.11.5 LED¶
LED notification lights are controlled through the LightsManager service:
// NotificationRecord.java (line 308)
private Light calculateLights() {
int channelLightColor = getChannel().getLightColor() != 0
? getChannel().getLightColor() : defaultLightColor;
Light light = getChannel().shouldShowLights()
? new Light(channelLightColor, defaultLightOn, defaultLightOff) : null;
// ... pre-channel legacy handling
}
Many modern devices no longer have LED indicators, but the API persists for devices that do support them (and for custom ROM developers).
28.11.6 Full-Screen Intent¶
Notifications with importance HIGH or MAX can include a full-screen intent:
Full-screen intents launch an activity over the lock screen (e.g., for incoming calls or alarms). The system may suppress the full-screen intent and show a heads-up notification instead if the user is actively using the device.
Starting from Android 14, the USE_FULL_SCREEN_INTENT permission is required
and can be revoked by the user.
28.11.7 Attention Effects Flow¶
flowchart TD
A[NotificationRecord posted] --> B{Intercepted by DND?}
B -->|Yes| C[Check suppressed visual effects]
B -->|No| D{Importance >= DEFAULT?}
C --> E{SUPPRESSED_EFFECT_PEEK?}
E -->|Yes| F[No heads-up]
E -->|No| G[Allow heads-up]
D -->|No| H[No sound/vibration]
D -->|Yes| I{"FLAG_ONLY_ALERT_ONCE<br/>and already alerted?"}
I -->|Yes| J[Skip sound/vibration]
I -->|No| K{"Listener hints<br/>disable effects?"}
K -->|Yes| L[Skip effects]
K -->|No| M{Importance >= HIGH?}
M -->|Yes| N[Heads-up + Sound + Vibrate]
M -->|No| O[Sound + Vibrate only]
N --> P{Channel has sound?}
O --> P
P -->|Yes| Q[Play sound via IRingtonePlayer]
P -->|No| R[Skip sound]
N --> S{Channel has vibration?}
O --> S
S -->|Yes| T[Vibrate via VibratorHelper]
S -->|No| U[Skip vibration]
A --> V{Channel has lights?}
V -->|Yes| W[Flash LED via LightsManager]
V -->|No| X[Skip LED]
28.12 Notification History and Persistence¶
28.12.1 Notification History¶
Android maintains a history of dismissed and cancelled notifications:
The Archive class maintains a bounded ring buffer:
// NotificationManagerService.java (line 840)
static class Archive {
final SparseArray<Boolean> mEnabled;
final int mBufferSize;
final LinkedList<Pair<StatusBarNotification, Integer>> mBuffer;
}
When a notification is cancelled, it is recorded in the archive:
// cancelNotificationLocked (line 11408)
if (reason != REASON_CHANNEL_REMOVED) {
mArchive.record(getSbnForArchive(r, reason), reason);
}
28.12.2 NotificationHistoryManager¶
Beyond the in-memory archive, NotificationHistoryManager provides persistent
history using a SQLite database:
// frameworks/base/services/core/java/com/android/server/notification/
// NotificationHistoryManager.java
// NotificationHistoryDatabase.java
The history database stores:
- Package name
- Channel ID
- Notification title and text
- Icons (as bitmaps, with a cleanup job)
- Post time
- Conversation shortcut ID
The NotificationHistoryJobService runs periodic cleanup to remove old entries
and reclaim storage.
28.12.3 Policy File Persistence¶
NMS persists its configuration to an XML policy file:
The policy file contains:
- Zen mode configuration (DND rules, policies).
- Channel and group preferences (via
PreferencesHelper). - Enabled notification listeners.
- Enabled notification assistant.
- Enabled condition providers.
- Snoozed notification state.
- Lockscreen secure notification preference.
The file is loaded during boot:
// NotificationManagerService.java (line 1278)
protected void loadPolicyFile() {
synchronized (mPolicyFile) {
InputStream infile = mPolicyFile.openRead();
readPolicyXml(infile, false /*forRestore*/, USER_ALL, null);
}
}
28.12.4 Backup and Restore¶
Notification settings participate in Android's backup/restore framework. When a user sets up a new device from backup:
- The policy XML is restored.
- Channel preferences are restored per-app.
- DND rules are restored.
- Listener and assistant permissions are restored.
- Managed services are re-enabled.
The NotificationBackupHelper coordinates this process, and
BackupRestoreEventLogger tracks success/failure metrics.
28.13 Notification Snooze¶
28.13.1 Overview¶
Snoozing temporarily removes a notification and re-posts it after a delay.
The SnoozeHelper manages this:
// frameworks/base/services/core/java/com/android/server/notification/
// SnoozeHelper.java (line 57)
public final class SnoozeHelper {
static final int CONCURRENT_SNOOZE_LIMIT = 500;
static final int MAX_STRING_LENGTH = 1000;
}
28.13.2 Snooze Mechanisms¶
There are two types of snooze:
- Duration-based: The notification re-appears after a specified time.
- Criterion-based: The notification re-appears when a condition is met (e.g., arriving at a location).
// SnoozeHelper manages:
// - AlarmManager alarms for timed snooze
// - SnoozeContext for criterion-based snooze
// - Persistence across reboots via XML
28.13.3 Snooze during Enqueue¶
When a notification is enqueued, the system checks if it was previously snoozed:
// EnqueueNotificationRunnable (line 10093)
final long snoozeAt = mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
r.getUser().getIdentifier(),
r.getSbn().getPackageName(), r.getSbn().getKey());
if (snoozeAt > currentTime) {
(new SnoozeNotificationRunnable(r.getSbn().getKey(),
snoozeAt - currentTime, null)).snoozeLocked(r);
return false;
}
This prevents a notification from appearing briefly before being re-snoozed.
28.13.4 Snooze Persistence¶
Snoozed notifications survive reboots. The SnoozeHelper persists state to XML:
On boot, the snoozed notification data is read from the policy file, and alarms are re-scheduled for any still-pending snoozes.
28.14 Auto-Grouping and Force Grouping¶
28.14.1 Traditional Auto-Grouping¶
When an app posts multiple notifications without explicitly grouping them,
NMS auto-groups them under a synthetic group summary. The GroupHelper decides:
Auto-grouping triggers when a package has more than AUTOGROUP_AT_COUNT_DEFAULT
(2) ungrouped notifications.
28.14.2 Force Grouping (Android 16+)¶
Force grouping is a newer mechanism (behind the notificationForceGrouping flag)
that aggressively bundles notifications:
Force grouping:
- Groups sparse groups (packages with many small groups) into aggregate groups.
- Triggers when a package has more than
AUTOGROUP_SPARSE_GROUPS_AT_COUNT(6) sparse groups. - Uses a 3-second delay after posting/removing to let apps complete their group structure before regrouping.
// NotificationManagerService.java (line 571)
private static final long DELAY_FORCE_REGROUP_TIME = 3000;
28.14.3 Auto-Group Summary Properties¶
The auto-group summary inherits properties from its children:
// GroupHelper.java (lines 79-85)
// Flags that all autogroup summaries have
protected static final int BASE_FLAGS =
FLAG_AUTOGROUP_SUMMARY | FLAG_GROUP_SUMMARY | FLAG_LOCAL_ONLY;
// Flag that autogroup summaries inherits if all children have the flag
private static final int ALL_CHILDREN_FLAG = FLAG_AUTO_CANCEL;
// Flags that autogroup summaries inherits if any child has them
private static final int ANY_CHILDREN_FLAGS = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR;
28.14.4 Classification-Based Regrouping¶
When the NAS classifies notifications (as news, promotions, etc.), the system
may regroup them into classification-specific bundles. This is controlled by
the notificationRegroupOnClassification flag.
28.14.5 Grouping Decision Diagram¶
flowchart TD
A[Notification posted] --> B{Has explicit group?}
B -->|Yes| C[Use app's group]
B -->|No| D{Force grouping enabled?}
D -->|Yes| E{Package has >= 6 sparse groups?}
E -->|Yes| F[Aggregate into force group]
E -->|No| G{Package has >= 2 ungrouped notifs?}
D -->|No| G
G -->|Yes| H[Auto-group with ranker_group]
G -->|No| I[Leave ungrouped]
C --> J{App provides group summary?}
J -->|Yes| K[Use app's summary]
J -->|No| L{Auto-generate summary?}
L -->|Yes| M[Create synthetic summary]
L -->|No| N[Use first child as proxy]
28.15 Key Data Structures¶
28.15.1 Core Collections in NMS¶
// NotificationManagerService.java (lines 731-746)
// The definitive list of all active notifications, sorted by rank
final ArrayList<NotificationRecord> mNotificationList;
// Fast lookup by key (pkg|uid|id|tag)
final ArrayMap<String, NotificationRecord> mNotificationsByKey;
// Notifications in the enqueue phase (not yet posted)
final ArrayList<NotificationRecord> mEnqueuedNotifications;
// Auto-bundled summary keys per userId per package/groupKey
final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries;
// Group summary records by groupKey
final ArrayMap<String, NotificationRecord> mSummaryByGroupKey;
28.15.2 Notification Key Format¶
The notification key uniquely identifies a notification:
For example: 0|com.example.app|1|null|10088
28.15.3 Group Key Format¶
The group key uniquely identifies a notification group:
If the notification has an explicit group set via setGroup(), groupId is that
string. Otherwise, it is the notification key itself (each notification is its
own group of one).
28.15.4 The NotificationRecord Field Map¶
| Field | Source | Extractor | Used By |
|---|---|---|---|
mChannel |
PreferencesHelper |
NotificationChannelExtractor |
Importance, sound, vibration |
mContactAffinity |
Contacts DB | ValidateNotificationPeople |
DND filtering, ranking |
mIntercept |
DND policy | ZenModeExtractor |
Listener dispatch |
mImportance |
Channel + system + assistant | ImportanceExtractor |
Ranking, attention |
mPackagePriority |
Channel | PriorityExtractor |
DND bypass |
mAllowBubble |
User + app + channel prefs | BubbleExtractor |
Bubble presentation |
mShowBadge |
Channel | BadgeExtractor |
App icon badge |
mPackageVisibility |
Channel | VisibilityExtractor |
Lockscreen |
mCriticality |
System policy | CriticalNotificationExtractor |
Top-of-shade placement |
mRankingScore |
NAS | NotificationAdjustmentExtractor |
Ranking |
mUserSentiment |
NAS | NotificationAdjustmentExtractor |
Smart actions |
mShortcutInfo |
ShortcutHelper |
Post processing | Conversations, bubbles |
mSuppressedVisualEffects |
DND policy | ZenModeExtractor |
Attention effects |
Summary¶
The Android notification system is a deeply layered pipeline that transforms a
simple notify() call into a carefully ranked, policy-filtered, attention-managed
user experience. The key architectural insights from this chapter:
-
NotificationManagerService is the central hub. At over 15,500 lines, it coordinates permission checks, channel lookups, signal extraction, ranking, DND filtering, attention effects, and listener dispatch.
-
The signal extractor pipeline provides a modular, extensible architecture. Each extractor writes a specific signal onto the
NotificationRecord, and the system can be extended by adding new extractors to the XML configuration. -
Notification channels shift control to users. Once created, channel settings are user-owned, and apps cannot programmatically override them.
-
Do Not Disturb is not a single switch but a rule engine. Multiple
AutomaticZenRuleobjects can be active simultaneously, each with its ownZenPolicy. TheZenModeHelperconsolidates them into a single effective policy. -
Conversation notifications receive first-class treatment through
MessagingStyle+ sharing shortcuts +Persondata, enabling features like priority sections and bubbles. -
Bubbles bridge the notification system and the window manager. Eligibility is determined in NMS, but the UI lifecycle is managed by WM Shell's
BubbleController. -
SystemUI's notification pipeline transforms raw
StatusBarNotificationobjects into a rendered shade through a series of coordinators, filters, sorters, and theNotificationStackScrollLayout. -
The threading model is carefully designed: Binder calls arrive on the Binder pool, processing happens on the handler thread under
mNotificationLock, and ranking reconsideration runs on a separate thread. This prevents the notification system from blocking the main thread or causing deadlocks. -
Auto-grouping and force grouping ensure a clean notification shade even when apps do not properly group their notifications. The
GroupHelperhandles both traditional auto-grouping (2+ ungrouped notifications) and aggressive force-grouping (6+ sparse groups). -
Attention effects (sound, vibration, LED, heads-up) are determined by a complex decision tree in
NotificationAttentionHelperthat considers importance, DND state, listener hints, group alert behavior, and theFLAG_ONLY_ALERT_ONCEflag. -
Notification history is maintained at two levels: an in-memory ring buffer archive for
getHistoricalNotifications()and a persistent SQLite database for the Settings notification history UI.
Appendix A: Complete Extractor Pipeline Reference¶
The following table documents every signal extractor, its dependencies, and
the fields it modifies on NotificationRecord:
| # | Extractor | Depends On | Modifies | May Defer |
|---|---|---|---|---|
| 1 | NotificationChannelExtractor |
None | mChannel |
No |
| 2 | NotificationAdjustmentExtractor |
Channel | mImportance, mRankingScore, mUserSentiment, smart actions/replies, classification |
No |
| 3 | BubbleExtractor |
Channel, Adjustment | mAllowBubble |
No |
| 4 | ValidateNotificationPeople |
Adjustment (for people overrides) | mContactAffinity, mPeopleOverride, phone numbers |
Yes (async contact lookup) |
| 5 | PriorityExtractor |
Channel | mPackagePriority |
No |
| 6 | ZenModeExtractor |
Priority | mIntercept, mSuppressedVisualEffects |
No |
| 7 | ImportanceExtractor |
Channel, System, Assistant | mImportance (final) |
No |
| 8 | VisibilityExtractor |
Importance | mPackageVisibility |
No |
| 9 | BadgeExtractor |
ZenMode | mShowBadge |
No |
| 10 | CriticalNotificationExtractor |
None | mCriticality |
No |
Appendix B: Notification Flags Reference¶
| Flag | Value | Description |
|---|---|---|
FLAG_SHOW_LIGHTS |
0x00000001 | Legacy: request LED |
FLAG_ONGOING_EVENT |
0x00000002 | Cannot be cleared by user |
FLAG_INSISTENT |
0x00000004 | Repeat sound/vibration until acknowledged |
FLAG_ONLY_ALERT_ONCE |
0x00000008 | Alert only on first post, not updates |
FLAG_AUTO_CANCEL |
0x00000010 | Cancel on tap |
FLAG_NO_CLEAR |
0x00000020 | Cannot be cleared by "Clear all" |
FLAG_FOREGROUND_SERVICE |
0x00000040 | Associated with a foreground service |
FLAG_HIGH_PRIORITY |
0x00000080 | Legacy: high priority (use importance instead) |
FLAG_LOCAL_ONLY |
0x00000100 | Do not bridge to remote devices |
FLAG_GROUP_SUMMARY |
0x00000200 | This is a group summary |
FLAG_AUTOGROUP_SUMMARY |
0x00000400 | System-generated group summary |
FLAG_BUBBLE |
0x00001000 | Eligible for bubble presentation |
FLAG_NO_DISMISS |
0x00002000 | Cannot be dismissed at all |
FLAG_FSI_REQUESTED_BUT_DENIED |
0x00004000 | Full-screen intent was requested but denied |
FLAG_USER_INITIATED_JOB |
0x00008000 | Associated with a user-initiated job |
FLAG_PROMOTED_ONGOING |
0x00010000 | Promoted to ongoing status |
FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY |
0x00020000 | Kept alive after user reply |
FLAG_SILENT |
0x00040000 | Forced silent (no sound/vibration) |
Appendix C: Cancel Reason Constants¶
| Constant | Value | Description |
|---|---|---|
REASON_CLICK |
1 | User tapped the notification |
REASON_CANCEL |
2 | User swiped to dismiss |
REASON_CANCEL_ALL |
3 | User tapped "Clear all" |
REASON_ERROR |
4 | Notification was invalid |
REASON_PACKAGE_CHANGED |
5 | Package was updated |
REASON_USER_STOPPED |
6 | User was stopped |
REASON_PACKAGE_BANNED |
7 | Package notifications disabled |
REASON_APP_CANCEL |
8 | App called cancel() |
REASON_APP_CANCEL_ALL |
9 | App called cancelAll() |
REASON_LISTENER_CANCEL |
10 | Listener cancelled the notification |
REASON_LISTENER_CANCEL_ALL |
11 | Listener cancelled all |
REASON_GROUP_SUMMARY_CANCELED |
12 | Group summary removed |
REASON_GROUP_OPTIMIZATION |
13 | Regrouping optimization |
REASON_PACKAGE_SUSPENDED |
14 | Package was suspended |
REASON_PROFILE_TURNED_OFF |
15 | User profile turned off |
REASON_UNAUTOBUNDLED |
16 | Removed from auto-bundle |
REASON_CHANNEL_BANNED |
17 | Channel importance set to NONE |
REASON_SNOOZED |
18 | User snoozed the notification |
REASON_TIMEOUT |
19 | TTL expired |
REASON_CHANNEL_REMOVED |
20 | Channel was deleted |
REASON_CLEAR_DATA |
21 | App data was cleared |
REASON_ASSISTANT_CANCEL |
22 | NAS cancelled the notification |
REASON_LOCKDOWN |
23 | Device entered lockdown mode |
REASON_BUNDLE_DISMISSED |
24 | Bundle was dismissed |
Appendix D: Importance Level Behavioral Matrix¶
| Importance | Sound | Vibration | Heads-Up | Status Bar Icon | Shade | Badge | Full-Screen Intent |
|---|---|---|---|---|---|---|---|
| NONE (0) | No | No | No | No | No | No | No |
| MIN (1) | No | No | No | No | Yes (collapsed) | No | No |
| LOW (2) | No | No | No | Yes | Yes | Yes | No |
| DEFAULT (3) | Yes | Yes | No | Yes | Yes | Yes | No |
| HIGH (4) | Yes | Yes | Yes | Yes | Yes | Yes | If granted |
| MAX (5) | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Notes:
- Vibration follows the system vibration setting and channel configuration.
- Heads-up may be suppressed by DND, listener hints, or active shade expansion.
- Full-screen intent requires the
USE_FULL_SCREEN_INTENTpermission on Android 14+. - Badge visibility also depends on the launcher implementation.
Appendix E: Evolution of the Notification System¶
| Android Version | Key Changes |
|---|---|
| 1.0 (API 1) | Basic notifications with icon, title, text |
| 3.0 (API 11) | Notification.Builder introduced |
| 4.1 (API 16) | Expandable notifications, BigTextStyle, InboxStyle |
| 4.3 (API 18) | NotificationListenerService API |
| 5.0 (API 21) | Heads-up notifications, lockscreen notifications, CATEGORY_*, visibility |
| 7.0 (API 24) | Inline reply, bundled notifications, MessagingStyle |
| 8.0 (API 26) | Notification Channels (mandatory), notification dots, snooze |
| 9.0 (API 28) | MessagingStyle improvements, Person class |
| 10 (API 29) | Bubble notifications, ZenPolicy, conversation shortcuts |
| 11 (API 30) | Conversation section in shade, conversation channels, notification history |
| 12 (API 31) | Custom notification styles restricted, notification trampoline blocked |
| 13 (API 33) | POST_NOTIFICATIONS runtime permission |
| 14 (API 34) | USE_FULL_SCREEN_INTENT permission, FGS type requirements |
| 15 (API 35) | Implicit zen rules, ZenDeviceEffects, sensitive content redaction |
| 16 (API 36) | Force grouping, notification classification, AI summarization |
Appendix F: Source File Index¶
For quick reference, the complete set of source files discussed in this chapter:
Server-side (system_server):
frameworks/base/services/core/java/com/android/server/notification/
NotificationManagerService.java -- Central service (15,500+ lines)
NotificationRecord.java -- Server-side notification wrapper
PreferencesHelper.java -- Channel and group storage
RankingHelper.java -- Signal extraction orchestrator
RankingConfig.java -- Ranking configuration interface
RankingHandler.java -- Ranking thread handler
ZenModeHelper.java -- DND state machine
ZenModeFiltering.java -- DND intercept logic
ZenModeConditions.java -- DND rule conditions
ZenModeExtractor.java -- DND signal extractor
ZenModeEventLogger.java -- DND metrics
NotificationAttentionHelper.java -- Sound, vibration, LED
GroupHelper.java -- Auto-grouping
SnoozeHelper.java -- Snooze management
ShortcutHelper.java -- Conversation shortcuts
ManagedServices.java -- Listener/assistant lifecycle
ConditionProviders.java -- DND condition providers
ValidateNotificationPeople.java -- Contact resolution
NotificationSignalExtractor.java -- Extractor interface
NotificationChannelExtractor.java -- Channel resolution
NotificationAdjustmentExtractor.java -- NAS adjustments
BubbleExtractor.java -- Bubble eligibility
ImportanceExtractor.java -- Importance calculation
PriorityExtractor.java -- Priority extraction
VisibilityExtractor.java -- Lockscreen visibility
BadgeExtractor.java -- App badge control
CriticalNotificationExtractor.java -- Critical notification detection
NotificationTimeComparator.java -- Time-based sorting
GlobalSortKeyComparator.java -- Final sort by global key
NotificationShellCmd.java -- ADB shell interface
NotificationHistoryManager.java -- Persistent history
NotificationHistoryDatabase.java -- SQLite history storage
NotificationUsageStats.java -- Usage statistics
NotificationBackupHelper.java -- Backup/restore
PermissionHelper.java -- POST_NOTIFICATIONS permission
AlertRateLimiter.java -- Rate limiting
TimeToLiveHelper.java -- TTL management
VibratorHelper.java -- Vibration effect creation
SDK API (app-side):
frameworks/base/core/java/android/app/
Notification.java -- Notification data model
NotificationManager.java -- Public API
NotificationChannel.java -- Channel configuration
NotificationChannelGroup.java -- Channel grouping
INotificationManager.aidl -- Binder interface
frameworks/base/core/java/android/service/notification/
NotificationListenerService.java -- Listener API
NotificationAssistantService.java -- Assistant API
ConditionProviderService.java -- DND condition API
StatusBarNotification.java -- Parcelable wrapper
ZenPolicy.java -- Per-rule DND policy
ZenModeConfig.java -- DND configuration
Adjustment.java -- NAS adjustment data
SystemUI:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/
stack/NotificationStackScrollLayout.java -- Main shade container
stack/StackScrollAlgorithm.java -- Position calculation
stack/NotificationSectionsManager.kt -- Section management
stack/NotificationPriorityBucket.kt -- Priority buckets
stack/NotificationSwipeHelper.java -- Swipe-to-dismiss
ConversationNotifications.kt -- Conversation handling
DynamicPrivacyController.java -- Lockscreen privacy
WM Shell Bubbles:
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/
BubbleController.java -- Main bubble coordinator
BubbleData.java -- Bubble data model
Bubble.java -- Single bubble state
BubbleExpandedView.java -- Expanded bubble view
BubbleTaskView.kt -- Embedded activity
BubblePositioner.java -- Position calculations
BubbleDataRepository.kt -- Persistence
Configuration: