In this article, we’ll take a closer look at the implementation of Notification on Android.
Notification is an API that has been around since The launch of Android and is one of the most commonly used features in applications, so developers should be pretty familiar with it.
In the last few years of Android releases, almost every release has made some changes to the system notification interface and related apis. These changes give developers more control over the notification style of their applications and make notifications easier for users to use.
In this article we take a closer look at Notification.
The developer API
I’m not going to go into too much detail about the basic use of Notification, which is already familiar to many developers and easily searchable on the Web.
Only the new additions to Notification since Android 5.0 are described below.
Heads-up Notification
Heads-up Notification is a new feature on Android 5.0.
When the device is in use (unlocked and the screen is lit), this notification takes the form of a small floating window like this:
This style looks like a compression of the Notification, but heads-up Notification can include an Action Button. The user can click on the Action Button to perform an Action, or remove the notification screen without leaving the current application.
This is a great improvement to the user experience, and the system’s call notification is this form of notification. When the device is in use, this notification will not interfere with the user’s current behavior (the notification interface can be removed directly), and it is convenient for the user to handle the notification (you can directly click the Action Button to handle the notification).
A heads-up Notification is generated as long as the Notification satisfies either of the following two conditions:
- Notification sets fullScreenIntent
- Notification is a high-priority Notification and uses a ring or vibrate
Notification on lock screen
Starting with Android 5.0, notifications can be displayed on the lock screen. Developers can use this feature to implement media play buttons and other common actions. At the same time, however, the user can also set whether to display an app notification on the lock screen.
Developers can use Notification. Builder. SetVisibility (int) method to control the notice shows that the level of detail. This method receives three levels of control:
- VISIBILITY_PUBLIC displays the full contents of the notification
- VISIBILITY_PRIVATE displays the basic information of the notification, such as the icon and title of the notification, but does not display the details
- VISIBILITY_SECRET does not display any of the contents of the notification
Direct reply to Notification
Starting with Android 7.0, users can reply directly from the notification screen. The direct reply button is attached below the notification.
When the user replies via the keyboard, the system attaches the user’s input text to the Intent specified by the developer and then sends it to the corresponding application.
Creating a notification that contains a direct reply button consists of the following steps:
- Create a PendingIntent that fires when the user clicks the Send button after typing. Therefore, we need to set a receiver for this PendingIntent. We can use a BroadcastReceiver to receive it
- Create an instance of the RemoteInput.Builder object. The constructor of this class accepts a string as a Key for the system to put in user-typed text. This key is used to get input at the receiver
- through
Notification.Action.Builder.addRemoteInput()
Method adds the RemoteInput object created in step 1 to notification. Action - Create a Notification containing the Notification.Action you created earlier, and send it
Examples of related code are as follows:
intent = new Intent(context, NotificationBroadcastReceiver.class);
intent.setAction(REPLY_ACTION);
intent.putExtra(KEY_NOTIFICATION_ID, notificationId);
intent.putExtra(KEY_MESSAGE_ID, messageId);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
getApplicationContext(), 100, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
// Key for the string that's delivered in the action's intent.
private static final String KEY_TEXT_REPLY = "key_text_reply";
String replyLabel = getResources().getString(R.string.reply_label);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel(replyLabel)
.build();
// Create the reply action and add the remote input.
Notification.Action action =
new Notification.Action.Builder(R.drawable.ic_reply_icon,
getString(R.string.label), replyPendingIntent)
.addRemoteInput(remoteInput)
.build();
// Build the notification and add the action.
Notification newMessageNotification =
new Notification.Builder(mContext)
.setSmallIcon(R.drawable.ic_message)
.setContentTitle(getString(R.string.title))
.setContentText(getString(R.string.content))
.addAction(action)
.build();
// Issue the notification.
NotificationManager notificationManager =
(NotificationManager) this.getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(notificationId, newMessageNotification);
Copy the code
When the user clicks the reply button, the system will prompt the user to enter:
When the user finishes typing and clicks the Send button, the replyPendingIntent we set will fire. We set up a BroadcastReceiver to handle this Intent, so we can get the user’s input text in the BroadcastReceiver as follows:
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(KEY_REPLY);
}
return null;
}
public void onReceive(Context context, Intent intent) {
if (REPLY_ACTION.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
int messageId = intent.getIntExtra(KEY_MESSAGE_ID, 0);
Toast.makeText(context, "Message ID: " + messageId + "\nMessage: " + message,
Toast.LENGTH_SHORT).show();
}
}
Copy the code
Here are two more things to note for developers:
- After the user clicks the send button, the button changes to a rotating pattern indicating that the action is still in progress. The developer needs to send a new notification to update this status
- Android: Exported = “false” Otherwise, any application can send an Intent to trigger your BroadcastReceiver, which can do harm to your application.
Bundling Notifications
Starting with Android 7.0, the system provides a new way to display continuous notifications: Bundling notifications.
This type of presentation is especially useful for instant messaging applications, which are constantly receiving new messages and sending notifications. This presentation organizes notifications in a hierarchical structure. At the top is a message that displays an overview of the group. When the user expands the group further, more information is displayed. As shown below:
The API for managing this Notification style is provided in the notification. Build class:
Notification.Builder.setGroup(String groupKey)
Group notifications into a group by groupKeyNotification.Builder.setGroupSummary(boolean isGroupSummary)
If isGroupSummary = true, the notification is set to the Summary notification within the groupNotification.Builder.setSortKey(String sortKey)
The system will sort by the sortKey set here
Notification Message Style
Starting with Android 7.0, the system provides the MessagingStyle API to customize notification styles. Developers can customize various labels for notifications, including the conversation Title, attached messages, and Content View for notifications. Here is an example of code:
Notification notification = new Notification.Builder() .setSmallIcon(R.drawable.ic_menu_camera) .setStyle(new Notification.MessagingStyle("Me") .setConversationTitle("Team lunch") .addMessage("Hi", timestamp1, null) // Pass in null for user. .addMessage("What's up?" , timestamp2, "Coworker") .addMessage("Not much", timestamp3, null) .addMessage("How about lunch?" , timestamp4, "Coworker")) .build();Copy the code
The notification appears like this:
Notification bar and notification window
The external interface
The Notification bar is located in the status bar and displays notifications on the left side of the status bar through a series of applied ICONS:
Users can scroll down from the screen to expand the notification window, with the Quick Settings area above and the notification list below. Users can expand the Quick Settings area.
Internal implementation
Now that you know what the notification interface looks like, let’s take a look at how the system implements it.
In the SystemUI implementation, the notification interface is managed through XML Layout files and a series of custom Layout classes.
The entire StatusBar is laid out by the super_status_bar.xml file, whose root element is a custom FrameLayout with the class name StatusBarWindowView. The structure of the layout file is shown below:
Here, we focus on the selected two lines:
- Super_status_bar. XML includes a layout file named status_bar
- Super_status_bar. XML includes a layout file named status_bar_expanded
Here, status_bar is the layout file for the system status bar, and status_bar_expanded is the layout file for the drop-down notification window.
The status_bar.xml layout file structure is shown in the following figure. The root element of this layout file is a custom FrameLayout class named PhoneStatusBarView.
Comparing the layout file with the status bar on the phone, I’m sure it should be easy for readers to understand:
- Notification_icon_area is the area where the system displays the notification icon
- System_icon_area is the area that displays system ICONS, such as Wifi, phone information, and battery
- Clock is the area on the status bar that displays the time
Let’s look at the structure of the layout file status_bar_expanded. The root element of the layout file is a class named NotificationPanelView, which is also a custom FrameLayout.
In this layout file:
- At the top is an element named keyguard_status_view. This is the layout of the status bar on the screen. The content displayed in this status bar is different from that of the normal status bar. You can go back to the corresponding screenshot above to compare the content displayed in different scenarios
- Qs_auto_reinflate_container is the area that displays the Quick Settings. This area actually includes another layout file: qs_panel.xml
- Notification_stack_scroller is really showing notify list, this is a NotificationStackScrollLayout type elements. As you can see from the name, this element is scrollable because the list of notifications can be quite long.
I’ve only scratched the surface of the most important elements of these interfaces, but there are many other elements in the layout. We’re not going to go through all of them. You can use the Layout Inspector on Android Studio to select the com.Android. systemui process and then select StatusBar to analyze each element of the interface in detail. The Layout Inspector looks like this:
Notification from send to display
The Notification sent
With this knowledge of the notification interface layout above, let’s take a look at how notifications sent in the application end up on the system’s notification interface.
Developers send notifications by creating Notification objects. All the details of a Notification are recorded in this object, as shown in the Notification class diagram:
Many of these fields will be familiar to developers as they are the ones we set when we send notifications. The Bundle Extras field needs to be noted here. Bundles store a set of data in key-value pairs that can be passed through IPC. When we built the Notification object with notification. buidler, some custom style values were included in the extras field, such as the following:
public Builder setShowWhen(boolean show) { mN.extras.putBoolean(EXTRA_SHOW_WHEN, show); return this; } public Builder setSmallIcon(Icon icon) { mN.setSmallIcon(icon); if (icon ! = null && icon.getType() == Icon.TYPE_RESOURCE) { mN.icon = icon.getResId(); } return this; } public Builder setContentTitle(CharSequence title) { mN.extras.putCharSequence(EXTRA_TITLE, safeCharSequence(title)); return this; } public Builder setContentText(CharSequence text) { mN.extras.putCharSequence(EXTRA_TEXT, safeCharSequence(text)); return this; } public Builder setContentInfo(CharSequence info) { mN.extras.putCharSequence(EXTRA_INFO_TEXT, safeCharSequence(info)); return this; } public Builder setProgress(int max, int progress, boolean indeterminate) { mN.extras.putInt(EXTRA_PROGRESS, progress); mN.extras.putInt(EXTRA_PROGRESS_MAX, max); mN.extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, indeterminate); return this; } public Builder setStyle(Style style) { if (mStyle ! = style) { mStyle = style; if (mStyle ! = null) { mStyle.setBuilder(this); mN.extras.putString(EXTRA_TEMPLATE, style.getClass().getName()); } else { mN.extras.remove(EXTRA_TEMPLATE); } } return this; }Copy the code
The Notification class is a Parcelable class, which means it can be passed across processes with Binder.
We typically do not create a Notification manually, but rather through the setXXX method in the notification. Builder class (some of which are listed above). This class simplifies the process of creating Notification. The following is the class diagram of notification. Builder.
This class provides a number of setXXX methods that let us set the properties of the Notification, and these methods return the Builder object itself so that we can call it continuously. Finally, we get the constructed Notification object through a build method.
NotificationManagerService
Once the Notification object is constructed, we actually send the Notification through the NotificationManager’s public void notify(int ID, Notification Notification) method (and its overload).
As I’m sure you can imagine, the NotificationManager must also be implemented with Binder.
Yes, really really realize the notifications Service called NotificationManagerService, the Service also in system_server process.
NotificationManager represents the client of the service is used by the application, and NotificationManagerService is located in the system in the process of receiving and processing the request. A large number of system services in the Android system are implemented in this way.
The notify interface eventually calls another interface in the NotificationManager called notifyAsUser to send notifications as follows:
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) { int[] idOut = new int[1]; INotificationManager service = getService(); String PKG = McOntext.getpackagename (); // Fix the notification as best we can. Notification.addFieldsFromContext(mContext, notification); (2) if (notification sound! = null) { notification.sound = notification.sound.getCanonicalUri(); if (StrictMode.vmFileUriExposureEnabled()) { notification.sound.checkFileUriExposed("Notification.sound"); } } fixLegacySmallIcon(notification, pkg); if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { if (notification.getSmallIcon() == null) { throw new IllegalArgumentException("Invalid notification (no valid small icon): " + notification); }} if (localLOGV) log. v(TAG, PKG + ": notify(" + id + ", "+ notification + ")"); final Notification copy = Builder.maybeCloneStrippedForDelivery(notification); try { service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, idOut, user.getIdentifier()); ④ if (localLOGV &&id! = idOut[0]) { Log.v(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code
This code is described as follows:
- Through the remote service interface getService method to obtain NotificationManagerService, The realization of the getService method is by ServiceManager NotificationManagerService Binder object
- Add some additional properties to the Notification through the mContext, which represents the Context in which the Notification interface is invoked, and which is used in the system service to determine who is using the service
- On versions above LOLLIPOP_MR1 (API Level 22), notifications must be set to Small Icon, or an exception will be thrown
- Call NotificationManagerService remote interfaces to really notice sent
Then we have to pay attention to the natural is NotificationManagerService enqueueNotificationWithTag method implementation.
NotificationManagerService related code in the following path: / frameworks/base/services/core/Java/com/android/server/notification
In NotificationManagerService. EnqueueNotificationWithTag method, the user will be sent a Notification object packaging in a StatusBarNotification object:
final StatusBarNotification n = new StatusBarNotification(
pkg, opPkg, id, tag, callingUid, callingPid, 0, notification,
user);
Copy the code
We then wrap the StatusBarNotification in a NotificationRecord object:
final NotificationRecord r = new NotificationRecord(getContext(), n);
Copy the code
Other parameters in the StatusBarNotification constructor describe the identity of the caller who sent the notification, including the package name, the caller’s UID, PID, and so on. The purpose of this identity is that the system can do different things for the identity of the caller. For example, a user may turn off notifications for some applications, and the identity of the caller can determine whether notifications for that application need to be displayed on the notification screen.
NotificationRecord naturally reminds the reader of the ActivityRecord, ProcessRecord, and other structures in ActivityManagerService. These are the corresponding structures used in system services to describe objects in an application.
The following diagram depicts the inclusion relationship of the three structures above:
After creating a NotificationRecord object, the system posts a Runnable Task to send notifications:
final NotificationRecord r = new NotificationRecord(getContext(), n);
mHandler.post(new EnqueueNotificationRunnable(userId, r));
Copy the code
In EnqueueNotificationRunnable, need to do a few things below:
- Processing notification grouping
- Check if the notification has been blocked (by caller’s identity: package name and UID)
- Sort notifications
- Determine whether to update an existing notification or send a new notification
- Call NotificationListeners notifyPostedLocked
- If needed: Handle sound and vibration
There is only NotificationListeners notifyPostedLocked need to explain.
Once a notification is sent to the system, there may be several modules in the system that are interested in it (basically, there are modules that want to display the notification on the notification screen). Sending a notification is an event and processing a notification is a response. To decouple the event from more than one responder, it is natural to use our common listener model (otherwise known as the Observer design pattern).
System, to inform interested listener to express by NotificationListenerService class. And here NotificationListeners. NotifyPostedLocked is for all NotificationListenerService correction notice.
This is one of the most important NotificationListenerService BaseStatusBar. This is the listener that displays the notification on the notification screen.
Notification of the display
The BaseStatusBar callback logic for notification sending is as follows:
public void onNotificationPosted(final StatusBarNotification sbn, final RankingMap rankingMap) { if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn); if (sbn ! = null) { mHandler.post(new Runnable() { @Override public void run() { processForRemoteInput(sbn.getNotification()); String key = sbn.getKey(); 1) mKeysKeptForRemoteInput. Remove (key); boolean isUpdate = mNotificationData.get(key) ! = null; (2) if (! ENABLE_CHILD_NOTIFICATIONS && mGroupManager.isChildInGroupWithSummary(sbn)) { if (DEBUG) { Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); } // Remove existing notification to avoid stale data. if (isUpdate) { removeNotification(key, rankingMap); (3)} else {mNotificationData. UpdateRanking (rankingMap); } return; } if (isUpdate) { updateNotification(sbn, rankingMap); } else { addNotification(sbn, rankingMap, null /* oldEntry */); (4)}}}); }}Copy the code
This code is described as follows:
- Each StatusBarNotification object has a Key value, which is generated based on the identity of the caller and the notification ID set by the caller. When an application sends multiple notifications with the same notification ID, they have the same Key value and can therefore be updated
- MNotificationData (type NotificationData) records all notification lists of the system
- If an existing notification needs to be updated, delete the existing notification first
- AddNotification is an abstract method implemented by subclasses
On mobile devices, the addNotification method is naturally implemented by PhoneStatusBar. In the addNotification method, the updateNotifications method is called to finally display the notification on the notification screen, as shown below:
protected void updateNotifications() {
mNotificationData.filterAndSort();
updateNotificationShade();
mIconController.updateNotificationIcons(mNotificationData);
}
Copy the code
UpdateNotificationShade method here is to inform the display content is added to the notification panel display area: NotificationStackScrollLayout. While mIconController. UpdateNotificationIcons (mNotificationData) is in the region of the notification_icon_area add notification Icon.
The updateNotificationShade code is longer, but the logic is easier to understand. Subject logic is for each create a ExpandableNotificationRow need to display the notification, and then set the corresponding contents and added to the NotificationStackScrollLayout (mStackScroller object).
Browse through this code to see some of the API implementations in the system services we explained in the API section: there are groups to handle notifications, visibility, and so on.
private void updateNotificationShade() { if (mStackScroller == null) return; // Do not modify the notifications during collapse. if (isCollapsing()) { addPostCollapseAction(new Runnable() { @Override public void run() { updateNotificationShade(); }}); return; } ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications(); ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size()); final int N = activeNotifications.size(); for (int i=0; i<N; i++) { Entry ent = activeNotifications.get(i); int vis = ent.notification.getNotification().visibility; // Display public version of the notification if we need to redact. final boolean hideSensitive = ! userAllowsPrivateNotificationsInPublic(ent.notification.getUserId()); boolean sensitiveNote = vis == Notification.VISIBILITY_PRIVATE; boolean sensitivePackage = packageHasVisibilityOverride(ent.notification.getKey()); boolean sensitive = (sensitiveNote && hideSensitive) || sensitivePackage; boolean showingPublic = sensitive && isLockscreenPublicMode(); if (showingPublic) { updatePublicContentView(ent, ent.notification); } ent.row.setSensitive(sensitive, hideSensitive); if (ent.autoRedacted && ent.legacy) { // TODO: Also fade this? Or, maybe easier (and better), provide a dark redacted form // for legacy auto redacted notifications. if (showingPublic) { ent.row.setShowingLegacyBackground(false); } else { ent.row.setShowingLegacyBackground(true); } } if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) { ExpandableNotificationRow summary = mGroupManager.getGroupSummary( ent.row.getStatusBarNotification()); List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(summary); if (orderedChildren == null) { orderedChildren = new ArrayList<>(); mTmpChildOrderMap.put(summary, orderedChildren); } orderedChildren.add(ent.row); } else { toShow.add(ent.row); } } ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>(); for (int i=0; i< mStackScroller.getChildCount(); i++) { View child = mStackScroller.getChildAt(i); if (! toShow.contains(child) && child instanceof ExpandableNotificationRow) { toRemove.add((ExpandableNotificationRow) child); } } for (ExpandableNotificationRow remove : toRemove) { if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) { // we are only transfering this notification to its parent, don't generate an animation mStackScroller.setChildTransferInProgress(true); } if (remove.isSummaryWithChildren()) { remove.removeAllChildren(); } mStackScroller.removeView(remove); mStackScroller.setChildTransferInProgress(false); } removeNotificationChildren(); for (int i=0; i<toShow.size(); i++) { View v = toShow.get(i); if (v.getParent() == null) { mStackScroller.addView(v); } } addNotificationChildrenAndSort(); // So after all this work notifications still aren't sorted correctly. // Let's do that now by advancing through toShow and mStackScroller in // lock-step, making sure mStackScroller matches what we see in toShow. int j = 0; for (int i = 0; i < mStackScroller.getChildCount(); i++) { View child = mStackScroller.getChildAt(i); if (! (child instanceof ExpandableNotificationRow)) { // We don't care about non-notification views. continue; } ExpandableNotificationRow targetChild = toShow.get(j); if (child ! = targetChild) { // Oops, wrong notification at this position. Put the right one // here and advance both lists. mStackScroller.changeViewPosition(targetChild, i); } j++; } // clear the map again for the next usage mTmpChildOrderMap.clear(); updateRowStates(); updateSpeedbump(); updateClearAll(); updateEmptyShadeView(); updateQsExpansionEnabled(); mShadeUpdates.check(); }Copy the code
At this point, a newly sent notification is actually displayed.
The following diagram depicts the flow of a Notification from send to display: