天天看點

SystemUI-應用知欄視圖是如何誇程序顯示?

應用知欄視圖是如何誇程序顯示到 SystemUI 的?

跨程序通訊的基礎是 IPC ,通知服務(NotificationManagerService, 簡稱 NMS)也不離開 IPC ,核心架構還是 IPC 架構。

消息通道

  1. 應用做作為通知的發送端, 需要調用 NMS ,發通知。例如:
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);
                 
  1. SystemUI 作為通知的接收放需要注冊監聽器 INotificationListener 是監聽通通知的一個 AIDL 接口,

    NotificationListenerService 是一個監聽管理服務,他的内部類 NotificationListenerWrapper 實作了

INotificationListener 接口。 例如:

/** @hide */
    protected class NotificationListenerWrapper extends INotificationListener.Stub {
        @Override
        public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update) {
                 // 接收通知
                  ....
                 省略了很多代碼
        }

        @Override
        public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update, NotificationStats stats, int reason) {
                // 删除通知
                      ....
                 // 省略了很多代碼
        }
                   

這個通知監聽需要向 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);
      }
             

以上是 Android 為我們提供的通知接收管理服務類, SystemUI 有個NotificationListenerWithPlugins 類繼承了 NotificationListenerService

類。 并在 SystemUI 程序起來的時候調用 registerAsSystemService() 方法完成了注冊:

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

            

這樣通道就建立起來了。

消息傳遞過程,大家可以按照這個思路器走讀源碼

RemoteViews

以上隻是講解了應用怎麼把一個消息傳遞到 SystemUI , 了解 IPC 通訊的不難了解。 而神奇之處在于顯示的視圖布局明明是定義在一個應用中,為何能跨程序顯示到 SystemUI 程序中呢?

發送通知, 傳遞的通知實體是 Notification 的執行個體, Notification 實作了 Parcelable 接口。 Notification 有個 RemoteViews 的成員變量

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

RemoteViews 也實作了 Parcelable 接口, 主要是封裝了通知欄要展示的視圖資訊, 例如, 應用包名、布局ID。我們都知道實作了 Parcelable 這個接口就可以在 IPC 通道上誇程序傳遞。 RemoteView 支援的布局類型也是有限的,例如在 8.0 上僅支援如下類型:

  • 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 攜帶了視圖資訊, 程序間傳遞的并不是真實的視圖對象, 而主要是布局的 id ,那麼顯示在通知欄上的視圖對象又是如何建立出來的呢?

### 通知視圖建立

在通知的接收端建立的,上文說過 NotificationManagerService 内部類 NotificationListenerWrapper 監聽通知消息, 在收到消息之後就在裡面解析消息,并建立視圖了。

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());
              // 建立視圖
              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;
          }

          // ... 省略代碼

      }

      @Override
      public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
              NotificationRankingUpdate update, NotificationStats stats, int reason) {
          StatusBarNotification sbn;
          //... 省略代碼

      }
  }
             

在 maybePopulateRemoteViews 這個方法中會去檢查布局是否要加載, **其實我們比較好奇的是布局資源在應用程序中,

SystemUI 如何加載遠端程序的布局資源?**

有兩個關鍵的資訊: 包名、布局ID。知道了包名 SystemUI 程序是有權限建立對應包名的上下文對象的,進而可以拿到對應應用的

資料總管, 然後就可以加載布局資源建立對象了。 maybePopulateRemoteViews 方法跟蹤下去, 會走到 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;
 }  
 
            

其中 getContextForResources 中的 context 對象就是通過應用包名建立的上下文對象,建立過程:

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;
}    


           

## 隻有 SystemUI 才能接收通知嗎?

答案是否定的, 隻要有權限注冊通知監聽的應用都可以。 具體權限是:

隻要應用有這個權限就可以注冊通知監聽了, 這個權限隻有系統應用才能申請, 也就是說,隻要是系統應用都可以監聽并顯示通知的。 可以寫一個簡單的 demo 測試一下:

一、 申請權限

二、 在布局中定義一個容器來裝遠端通知視圖

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

 </FrameLayout>
 ...
 
            

三、注冊監聽并處理通知顯示邏輯。

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();
        }
    };

    // 調用注冊方法 registerAsSystemService 不是公開的 API 反射

    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();
    }
}    
           

運作起來後,注冊成功, 然後任意應用發通知, 這裡就能顯示出來了。

繼續閱讀