40

应用知栏视图是如何夸进程显示到 SystemUI 的?

 5 years ago
source link: http://www.liuguangli.win/archives/806?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

应用知栏视图是如何夸进程显示到 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);
      // 通知栏要显示的视图布局
      RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_remoteviews);
      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();
      notification.contentView = remoteViews;
      manager.notify(tag, id , notification);
  2. 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 进程起来的时候调用 unregisterAsSystemService() 方法完成了注册:

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

这样通道就建立起来了。

7Ff2U3u.jpg!web

消息传递过程,大家可以按照这个思路器走读源码

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

运行起来后,注册成功, 然后任意应用发通知, 这里就能显示出来了。

mmeeeyM.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK