SystemUI 开发之通知的实现逻辑(四)

2023-03-02 12:56:54 浏览数 (1)

0x00 介绍

今天我们来看一下 SystemUI中系统通知的实现流程,希望能解决一个问题:系统通知是如何完成监听然后显示在通知栏的? 在前面的《SystemUI开发之启动流程(一)》一文中,我们了解到 SystemUI 这个应用是由 SystemServer 启动起来的。它启动了 SystemUIService 这个 Android 服务,然后再由这个服务分别启动了 SystemUI定义的各种服务组件 例如 SystemBars, StatusBar, PowerUI, VolumeUI等等组件。本文将此出发看看系统通知是如何实现的。这里会涉及以下一些知识点:

  • SystemUI 应用是如何监听系统通知的
  • 通知的 UI是如何构建的

本文的代码是基于Android 10 来分析

0x01 监听系统通知的实现过程

StatusBar 的创建

在看系统通知的实现流程之前我们先来回顾一下 SystemUI组件的构建流程。注意这些服务组件就是前文《SystemUI 开发之服务组件概览(二)》提到在 config.xml中定义的,例如

代码语言:javascript复制
<string name="config_statusBarComponent" translatable="false">com.android.systemui.statusbar.phone.StatusBar</string>

<string-array name="config_systemUIServiceComponents" translatable="false">
    ...省略一些代码
    <item>com.android.systemui.volume.VolumeUI</item>
    <item>com.android.systemui.stackdivider.Divider</item>
    <item>com.android.systemui.SystemBars</item>
    ...
</string-array>

需要注意的是这些服务组件是普通 Java类,跟 Android四大组件服务是不一样的概念,它们都是扩展自 SystemUI这个基类。

这里我们先关注 SystemBars这个组件的创建

代码语言:javascript复制
/**
 * Ensure a single status bar service implementation is running at all times, using the in-process
 * implementation according to the product config.
 */
public class SystemBars extends SystemUI {
    //...省略一些代码

    @Override
    public void start() {
        if (DEBUG) Log.d(TAG, "start");
        createStatusBarFromConfig();
    }


    private void createStatusBarFromConfig() {
        if (DEBUG) Log.d(TAG, "createStatusBarFromConfig");
        final String clsName = mContext.getString(R.string.config_statusBarComponent);
        ...省略代码
        Class<?> cls = null;
        try {
            cls = mContext.getClassLoader().loadClass(clsName);
        } catch (Throwable t) {
            throw andLog("Error loading status bar component: "   clsName, t);
        }
        try {
            mStatusBar = (SystemUI) cls.newInstance();
        } catch (Throwable t) {
            throw andLog("Error creating status bar component: "   clsName, t);
        }
        ...省略代码
        // 这里执行SystemUI的start方法
        mStatusBar.start();
        ...省略代码
    }

    //...省略一些代码
}

可以看到这个类非常的简单,就是通过它创建了 StatusBar,这个就是手机里面的状态栏。在 start()方法里面执行了 createStatusBarFromConfig()方法,然后读取 xml文件中的类名配置,通过反射创建了 StatusBar类。

有些人可能会疑问为何不直接创建 StatusBar呢? 确实是可以的,但是这样做的好处是比较灵活,不同的设备可以配置不同的 StatusBar实现,比如平板,车机系统等等。

接下来我们看看 StatusBar 的实现。

如果你看源码,会发现这个类很大,有4000多行的代码,但这里只展示与本文相关的关键流程

代码语言:javascript复制
public class StatusBar extends SystemUI ... {

    // ...省略一些代码
    // 这个就是监听系统通知的接口
    protected NotificationListener mNotificationListener;
    // 系统通知 Binder 接口
    protected IStatusBarService mBarService;

    @Override
    public void start() {
        // ...省略一些代码
        mNotificationListener =  Dependency.get(NotificationListener.class);
        mNotificationListener.registerAsSystemService();
        // ...省略一些代码

        // 这里获取系统的状态栏服务 StatusBarService
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));

        //...省略代码

        // Connect in to the status bar manager service
        mCommandQueue = getComponent(CommandQueue.class);
        mCommandQueue.addCallback(this);

        RegisterStatusBarResult result = null;
        try {
            result = mBarService.registerStatusBar(mCommandQueue);
        } catch (RemoteException ex) {
            ex.rethrowFromSystemServer();
        }

        // 将StatusBar在windows显示出来
        createAndAddWindows(result);

        // ...省略一些代码
    }

    // ...省略一些代码

    public void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
        // 创建StatusBar 的View
        makeStatusBarView(result);
        mNotificationShadeWindowController.attach();
        mStatusBarWindowController.attach();
    }
}

我们应该已经了解到所有 SystemUI的服务组件,执行方法都是从 start()方法开始的。 在这个方法里面通过 Dependency组件获取到 mNotificationListener实例,通过它进行注册系统通知的监听,这一步非常关键,这里涉及到 Binder的通信,这里建立了SystemUI应用和系统服务的通信通道。 此外还通过了 mBarServicemCommandQueue注册给了 mBarService,用于建立系统状态栏服务与 SystemUI的通信。不过这个状态栏不是本文的重点。

NotificationListener
代码语言:javascript复制
/**
 * This class handles listening to notification updates and passing them along to
 * NotificationPresenter to be displayed to the user.
 */
@SuppressLint("OverrideAbstract")
public class NotificationListener extends NotificationListenerWithPlugins {

    //...省略代码
    // 用于处理通知添加、移除、更新的操作
    private final NotificationEntryManager mEntryManager =
            Dependency.get(NotificationEntryManager.class);
    @Override
    public void onListenerConnected() {
        //...省略代码
        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
        onSilentStatusBarIconsVisibilityChanged(noMan.shouldHideSilentStatusBarIcons());
    }

    @Override
    public void onNotificationPosted(final StatusBarNotification sbn,
            final RankingMap rankingMap) {
        // 通知到达,StatusBarNotification 是通知的数据
        if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
            // 在主线程中处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                // ...省略代码
                // 更新或者添加通知,使用NotificationEntryManager来处理
                if (isUpdate) {
                    mEntryManager.updateNotification(sbn, rankingMap);
                } else {
                    mEntryManager.addNotification(sbn, rankingMap);
                }
            });
        }
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
            int reason) {
        // 移除通知
        if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
            final String key = sbn.getKey();
            // 主线程操作,使用NotificationEntryManager来处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                mEntryManager.removeNotification(key, rankingMap, reason);
            });
        }
    }

    @Override
    public void onNotificationRankingUpdate(final RankingMap rankingMap) {
        // 更新通知内容
        if (rankingMap != null) {
            RankingMap r = onPluginRankingUpdate(rankingMap);
            // 主线程操作,使用NotificationEntryManager来处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                mEntryManager.updateNotificationRanking(r);
            });
        }
    }

}

代码比较简单,它是扩展于 NotificationListenerWithPlugins,用于监听系统通知的发送、移除、更新的回调,主要是通过 NotificationEntryManager接口执行对通知的各种操作。

NotificationListenerWithPlugins是继承于 NotificationListenerService

这个接口在

frameworks/base/core/java/android/service/notification/NotificationListenerService.java

它是 frameworks提供的,如果一个应用要监听系统通知,那么就必须继承它。我们接着看它的实现。

NotificationListenerService
代码语言:javascript复制
public abstract class NotificationListenerService extends Service {

    /**
     * Directly register this service with the Notification Manager.
     *
     * <p>Only system services may use this call. It will fail for non-system callers.
     * Apps should ask the user to add their listener in Settings.
     *
     * @param context Context required for accessing resources. Since this service isn't
     *    launched as a real Service when using this method, a context has to be passed in.
     * @param componentName the component that will consume the notification information
     * @param currentUser the user to use as the stream filter
     * @hide
     * @removed
     */
    @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);
    }

    /**
     * Directly unregister this service from the Notification Manager.
     *
     * <p>This method will fail for listeners that were not registered
     * with (@link registerAsService).
     * @hide
     * @removed
     */
    @SystemApi
    public void unregisterAsSystemService() throws RemoteException {
        if (mWrapper != null) {
            INotificationManager noMan = getNotificationInterface();
            noMan.unregisterListener(mWrapper, mCurrentUser);
        }
    }
}

看源码这个代码量也很大,不过我们知道它是一个 Android四大组件的 Service,有 registerAsSystemService()方法用于注册系统服务。这个方法是通过 INotificationManager传递 NotificationListenerWrapper来实现注册的。 我们会发现 NotificationListenerWrapper继承自 INotificationListener.Stub,它也是通过 Binder来进行通信的。

到这里小结一下:在构建 StatusBar过程中,通过 NotificationListener完成了系统通知的监听的注册。

现在我们回来看看通知的控件是如何创建的

0x02 通知控件的创建

当收到系统通知时,在 NotificationListeneronNotificationPosted()方法会执行,这时候就会在主线程中执行添加或更新的操作,这里我们关注添加的操作。

代码语言:javascript复制
@Override
    public void onNotificationPosted(final StatusBarNotification sbn,
            final RankingMap rankingMap) {
        // 通知到达,StatusBarNotification 是通知的数据
        if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
            // 在主线程中处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                // ...省略代码
                // 更新或者添加通知,使用NotificationEntryManager来处理
                if (isUpdate) {
                    mEntryManager.updateNotification(sbn, rankingMap);
                } else {
                    mEntryManager.addNotification(sbn, rankingMap);
                }
            });
        }
    }

收到通知到达,就通过 NotificationEntryManager来进行操作,现在来看看 NotificationEntryManager中的实现

代码语言:javascript复制
public class NotificationEntryManager implements ...{

    // ...省略代码

    // 添加通知
    public void addNotification(StatusBarNotification notification, RankingMap ranking) {
        try {
            addNotificationInternal(notification, ranking);
        } catch (InflationException e) {
            handleInflationException(notification, e);
        }
    }

    private void addNotificationInternal(StatusBarNotification notification,
            NotificationListenerService.RankingMap rankingMap) throws InflationException {
        String key = notification.getKey();
        // ...省略代码
        // 更新数据
        mNotificationData.updateRanking(rankingMap);
        NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
        rankingMap.getRanking(key, ranking);

        // ...省略代码
        // Construct the expanded view.
        // 异步创建控件
        requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
                REASON_CANCEL));

        // ...省略代码
    }
    private void bindRow(NotificationEntry entry, ExpandableNotificationRow row) {
        mListContainer.bindRow(row);
        mNotificationRemoteInputManager.bindRow(row);
        row.setOnActivatedListener(mPresenter);
        entry.setRow(row);
        mNotifBindPipeline.manageRow(entry, row);
        mBindRowCallback.onBindRow(row);
    }

    // ...省略代码
}

这里的逻辑也比较清楚, addNotification()中直接使用 addNotificationInternal(),此方法又通过 NotificationRowBinder进行异步创建控件

这是一个接口,具体实现是在

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java

代码语言:javascript复制
/**
     * Inflates the views for the given entry (possibly asynchronously).
     */
    @Override
    public void inflateViews(
            NotificationEntry entry,
            NotificationRowContentBinder.InflationCallback callback)
            throws InflationException {
        ViewGroup parent = mListContainer.getViewParentForNotification(entry);
        // 如果通知本身就存在,执行更新操作
        if (entry.rowExists()) {
            mIconManager.updateIcons(entry);
            ExpandableNotificationRow row = entry.getRow();
            row.reset();
            updateRow(entry, row);
            inflateContentViews(entry, row, callback);
        } else {
            // 首次添加通知
            mIconManager.createIcons(entry);
            // 通过RowInflaterTask来异步创建控件,这个控件的名称ExpandableNotificationRow
            new RowInflaterTask().inflate(mContext, parent, entry,
                    row -> {
                        // Setup the controller for the view.
                        // 将通知数据与控件进行绑定并更新
                        bindRow(entry, row);
                        updateRow(entry, row);
                    });
        }
    }

inflateViews()方法中会根据当前的通知是否存在进行更新或者添加,首次添加通知时会使用一个异步构建控件的接口来创建 ExpandableNotificationRow实例,这个就是在通知栏中显示的具体控件。

看看 RowInflaterTask.inflate的实现

代码语言:javascript复制
/**
     * Inflates a new notificationView. This should not be called twice on this object
     */
    public void inflate(Context context, ViewGroup parent, NotificationEntry entry,
            RowInflationFinishedListener listener) {
        if (TRACE_ORIGIN) {
            mInflateOrigin = new Throwable("inflate requested here");
        }
        mListener = listener;
        AsyncLayoutInflater inflater = new AsyncLayoutInflater(context);
        mEntry = entry;
        entry.setInflationTask(this);
        inflater.inflate(R.layout.status_bar_notification_row, parent, this);
    }

这里使用了 AsyncLayoutInflater这个异步创建布局的接口来实现的

这个接口是 androidx里面的标准接口,对一些复杂的控件可以使用此接口提高创建的效率

https://developer.android.com/reference/androidx/asynclayoutinflater/view/AsyncLayoutInflater

另外我们看到一条通知的静态布局文件 status_bar_notification_row.xml

代码语言:javascript复制
<!-- extends FrameLayout -->
<com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:clickable="true"
    >

    <!-- Menu displayed behind notification added here programmatically -->

    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
        android:id="@ id/backgroundNormal"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
        android:id="@ id/backgroundDimmed"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.android.systemui.statusbar.notification.row.NotificationContentView
        android:id="@ id/expanded"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" />

    <com.android.systemui.statusbar.notification.row.NotificationContentView
        android:id="@ id/expandedPublic"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    ...省略代码

</com.android.systemui.statusbar.notification.row.ExpandableNotificationRow>

它的布局结构中顶层是扩展自 FrameLayoutExpandableNotificationRow,然后是两个自定义的背景图 NotificationBackgroundView,还有两个跟通知内容相关的 NotificationContentView。如果需要自定义这里的布局或修改一些样式,可以关注这些类的实现。

0x03 总结

最后通过一张简单的流程来总结一下

SystemServer启动了 SystemUIService,然后通过反射创建在 xml中配置的 SystemUI服务组件列表,这其中就包括了 StatusBar组件,这是系统状态栏。 这个状态栏会与系统的状态栏服务 StatusBarService建立 Binder通信。同时还会使用 NotificatioinListener注册系统通知的监听。 当收到通知后相应的回调接口 onNotificationPosted会被执行,然后使用 NotificationEntryManager对相应的通知进行处理。

0x04 引用

  • https://cs.android.com/android/platform/superproject/ /android-10.0.0_r10:frameworks/base/packages/SystemUI/

0 人点赞