How is the Application bar view displayed in SystemUI?

Cross-process communication is based on IPC, notification service (NotificationManagerService, NMS) don’t leave the IPC, core architecture or the IPC architecture.

News channel

  1. As the sender of notifications, the application invokes the NMS to send notifications. Such as:
String channelId = "channel_1"; String tag = "ailabs"; int id = 10086; int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = new NotificationChannel(channelId, "123", importance); NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); manager.createNotificationChannel(channel); Notification notification = new Notification.Builder(MainActivity.this, channelId) .setCategory(Notification.CATEGORY_MESSAGE) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("This is a content title") .setContentText("This is a content text") .setAutoCancel(true) .build(); RemoteViews RemoteViews = new RemoteViews(getPackageName(), r.layout.layout_remoteviews); notification.contentView = remoteViews; manager.notify(tag, id , notification);Copy the code
  1. SystemUI as notification of receiving put need to register the listener INotificationListener is listening to the notice of an AIDL interface, NotificationListenerService is a monitoring management services, His inner class NotificationListenerWrapper has been realized

INotificationListener interface. Such as:

/** @hide */ protected class NotificationListenerWrapper extends INotificationListener.Stub { @Override public void OnNotificationPosted (IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update) {/ / receiving notice... Omitted a lot of code} @ Override public void onNotificationRemoved (IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update, NotificationStats stats, int "reason) {/ / delete inform... // omit a lot of code}Copy the code

This notification listener needs to be registered with the NMS:

   @SystemApi
      public void registerAsSystemService(Context context, ComponentName componentName,
              int currentUser) throws RemoteException {
          if (mWrapper == null) {
              mWrapper = new NotificationListenerWrapper();
          }
          mSystemContext = context;
          INotificationManager noMan = getNotificationInterface();
          mHandler = new MyHandler(context.getMainLooper());
          mCurrentUser = currentUser;
          noMan.registerListener(mWrapper, componentName, currentUser);
      }
  Copy the code

Above is the Android provide notification of receiving management services for our class, a NotificationListenerWithPlugins SystemUI inherited NotificationListenerService class. RegisterAsSystemService () is called when the SystemUI process is up:

NotificationListenerWithPlugins mNotificationListener = new NotificationListenerWithPlugins();
mNotificationListener.registerAsSystemService();

 Copy the code

So the passage is set up.

Message passing process, you can follow this train of thought to read the source code

RemoteViews

This is just how an application passes a message to SystemUI. It’s easy to understand IPC communication. The magic is that the layout of the view displayed is defined in an application, so how can it be displayed across processes in SystemUI?

Notifications are sent, and the Notification entity passed is an instance of Notification, which implements the Parcelable interface. Notification has a RemoteViews member variable

 
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews);
notification.contentView = remoteViews;
Copy the code

RemoteViews also implements the Parcelable interface, which encapsulates the view information to be displayed in the notification bar, such as application package name and layout ID. We all know that the Parcelable interface can be implemented to pass processes on the IPC channel. RemoteView also supports a limited number of layout types; for example, on 8.0 only the following types are supported:

  • android.widget.AdapterViewFlipper
  • android.widget.FrameLayout
  • android.widget.GridLayout
  • android.widget.GridView
  • android.widget.LinearLayout
  • android.widget.ListView
  • android.widget.RelativeLayout
  • android.widget.StackView
  • android.widget.ViewFlipper

RemoteView carries information about the view, and is passed between processes not the actual view object, but mainly the layout ID, so how can the view object displayed on the notification bar be created?

## Notify view creation

Created in the notification of the receiving end, the said NotificationListenerWrapper NotificationManagerService inner class monitor notification message, after receiving the message is inside parse the messages, and create a view.

protected class NotificationListenerWrapper extends INotificationListener.Stub {

@Override public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); } catch (RemoteException e) { Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e); return; } try { // convert icon metadata to legacy format for older clients createLegacyIconExtras(sbn.getNotification()); // Create view maybePopulateRemoteViews(sbn.getNotification()); maybePopulatePeople(sbn.getNotification()); } catch (IllegalArgumentException e) { // warn and drop corrupt notification Log.w(TAG, "onNotificationPosted: can't rebuild notification from " + sbn.getPackageName()); sbn = null; } / /... Omit code} @ Override public void onNotificationRemoved (IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update, NotificationStats stats, int reason) { StatusBarNotification sbn; / /... Omit code}}Copy the code

In the maybePopulateRemoteViews method, we will check whether the layout is loaded. ** In the application process, how does SystemUI load the layout resources of the remote process? **

There are two key pieces of information: package name and layout ID. Knowing the package name, the SystemUI process has permission to create context objects corresponding to the package name, and then can get the corresponding application resource manager, and then can load layout resources to create objects. The maybePopulateRemoteViews method follows and goes to RemoteViews

private View inflateView(Context context, RemoteViews rv, ViewGroup parent) { // RemoteViews may be built by an application installed in another // user. So build a context that loads resources from that user but // still returns the current users userId so settings like data / time formats // are  loaded without requiring cross user persmissions. final Context contextForResources = getContextForResources(context); Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources); // If mApplyThemeResId is not given, Theme.DeviceDefault will be used. if (mApplyThemeResId ! = 0) { inflationContext = new ContextThemeWrapper(inflationContext, mApplyThemeResId); } LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); // Clone inflater so we load resources from correct context and // we don't add a filter to the static version returned by getSystemService. inflater = inflater.cloneInContext(inflationContext); inflater.setFilter(this); View v = inflater.inflate(rv.getLayoutId(), parent, false); v.setTagInternal(R.id.widget_frame, rv.getLayoutId()); return v; }Copy the code

GetContextForResources is a context object created by the application package name.

private static ApplicationInfo getApplicationInfo(String packageName, int userId) { if (packageName == null) { return null; } // Get the application for the passed in package and user. Application application = ActivityThread.currentApplication(); if (application == null) { throw new IllegalStateException("Cannot create remote views out of an aplication."); } ApplicationInfo applicationInfo = application.getApplicationInfo(); if (UserHandle.getUserId(applicationInfo.uid) ! = userId || ! applicationInfo.packageName.equals(packageName)) { try { Context context = application.getBaseContext().createPackageContextAsUser( packageName, 0, new UserHandle(userId)); applicationInfo = context.getApplicationInfo(); } catch (NameNotFoundException nnfe) { throw new IllegalArgumentException("No such package " + packageName); } } return applicationInfo; }Copy the code

Can only SystemUI receive notifications?

The answer is no, as long as the application has permission to register notification listening. The specific permission is: as long as the application has this permission can register the notification listening, this permission can only apply for the system application, that is, as long as the system application can listen and display the notification. Write a simple demo to test this:

1. Apply for permission

Define a container in the layout to hold the remote notification view

. <FrameLayout android:layout_width="match_parent" android:layout_height="92px" android:id="@+id/notification"> </FrameLayout> ...Copy the code

Register to listen and process notification display logic.

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ViewGroup notificationContainer = findViewById(R.id.notification); NotificationListenerService listenerService = new NotificationListenerService() { @SuppressLint("LongLogTag") @Override public void onNotificationPosted(StatusBarNotification sbn) { super.onNotificationPosted(sbn); Log.d("NotificationListenerService", "onNotificationPosted" + sbn); if (sbn.getNotification().contentView ! = null) { View view = sbn.getNotification().contentView.apply(MainActivity.this, null); notificationContainer.addView(view); view.setVisibility(View.VISIBLE); Log.d("NotificationListenerService", "add contentView"); } if (sbn.getNotification().bigContentView ! = null) { View view = sbn.getNotification().bigContentView.apply(MainActivity.this, null); notificationContainer.addView(view); view.setVisibility(View.VISIBLE); Log.d("NotificationListenerService", "add bigContentView"); } if (sbn.getNotification().headsUpContentView ! = null) { sbn.getNotification().headsUpContentView.apply(MainActivity.this, null); Log.d("NotificationListenerService", "add headsUpContentView"); } } @SuppressLint("LongLogTag") @Override public void onNotificationRemoved(StatusBarNotification sbn) { super.onNotificationRemoved(sbn); Log.d("NotificationListenerService", "onNotificationRemoved" + sbn); } @SuppressLint("LongLogTag") @Override public void onListenerConnected() { super.onListenerConnected(); Log.d("NotificationListenerService", "onNotificationRemoved"); } @Override public void onListenerDisconnected() { super.onListenerDisconnected(); }}; // Call registerAsSystemService which is not a public API reflection try {Method Method = NotificationListenerService.class.getMethod("registerAsSystemService", Context.class, ComponentName.class, int.class); method.setAccessible(true); method.invoke(listenerService, this, new ComponentName(getPackageName(), getClass().getCanonicalName()), -1); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }}Copy the code

Once it’s up and running, it’s registered, and then any application sends a notification, it shows up here.