Android6.0 SystemUI中的Notification流程

来源:互联网 发布:iphone照片如何导进mac 编辑:程序博客网 时间:2024/05/15 01:20

前言:最近在做 基于Android6.0的SystemUI开发,其中有一个功能决定通过系统的Notification通道来实现,在网上找了些资料,都没找到自己想要的东西,所以就只能自己慢慢看源码,现在把自己看懂的部分整理一下,有不对或者不详尽的地方,希望大家指正和补充。PS:此文需要配合安卓6.0SystemUI源码食用。

在安卓app中调用调用NotificationManager.notify(int id,Notification notification)方法可以发出和更新Notification跨进程在SystemUI的通知栏中进行显示,调用NotificationManager.cancel(int id)可以取消通知。我这里关注的是,整个流程走到SystemUI之后,里面是怎么对通知进行管理和显示的,确切的说是Notification中的自定义RemoteView部分,因为我需要利用这部分来实现我修改后的SystemUI的某个功能。

下面先上类图和添加新通知的时序图,更新通知和移除通知一来比添加新通知简单二来很多流程会和添加新通知殊途同归,就不贴了

SystemUI中关于Notification的类图

这里写图片描述
SystemUI中添加新通知的时序图

新Notification的添加

不管是发出一个新的通知还是对已经存在的通知进行更新,调用的都是NotificationManager.notify(int id,Notification notification)。最后走到SystemUI的时候首先调用BaseStatusBar中的成员变量mNotificationListener的onNotificationPosted(final StatusBarNotification sbn,final RankingMap rankingMap)方法。
其中BaseStatusBar是一个抽象类,是所有状态栏的基类,位于SystemUI工程下,包路径为com.android.systemui.statusbar。它的成员变量mNotificationListener是一个NotificationListenerService类的对象,该类不在SystemUI工程下,我们暂不关注。
来看mNotificationListener对onNotificationPosted(final StatusBarNotification sbn,final RankingMap rankingMap)方法的实现。

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() {                         String key = sbn.getKey();                        boolean isUpdate = mNotificationData.get(key) != null;                        .                        .                        .                        if (isUpdate) {                            updateNotification(sbn, rankingMap);                        } else {                            addNotification(sbn, rankingMap, null /* oldEntry */);                        }                    }                });            }        }

首先来看方法中的两个参数:1.StatusBarNotification sbn;2.RankingMap rankingMap。
StatusBarNotification点进去看,发现其实是由Notification组装而成,里面比较重要的属性有String pkg,int id,String key,Notification notification,保存着通知的内容,发出通知的报名信息,以及id等。StatusBarNotification 具体的组装生成过程不是在SystemUI包中进行,暂不关注。
RankingMap则是NotificationListenerService的一个静态内部类,里面保存着所有Notification相关的信息,具体的我们先往下看。
在onNotificationPosted()方法中,先对传过来的sbn进行空判断,不为空,则用mHandler发一个新的runnable来处理。

String key = sbn.getKey();boolean isUpdate = mNotificationData.get(key) != null;

先拿到sbn里面key属性,再根据这个key去mNotificationData取对象。
mNotificationData是BaseStatusBar的一个protected成员变量,可被子类继承,自己本身的类是NotificationData,位于SystemUI工程下的com.android.systemui.statusbar。它的get(key)方法如下:

public Entry get(String key) {    return mEntries.get(key);}

返回了一个Entry对象mEntries.get(key),我们来看看这个Entry是什么。Entry是NotificationData的一个内部类。其中包含的几个重要的属性的属性:

public String key;public StatusBarNotification notification;public StatusBarIconView icon;public ExpandableNotificationRow row; // the outer expanded view

显然,我们的Notification先被封装成StatusBarNotification传到SystemUI,然后在SystemUI中又再一次被封装进这个Entry中。

然后我们再看看这个mEntries,这是NotificationData的一个成员变量:

private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();

暂时没看到这个map的数据是在哪里添加的。
我们回到onNotificationPosted方法,跳过一部分代码,直接来到重点。

if (isUpdate) {      updateNotification(sbn, rankingMap); } else {      addNotification(sbn, rankingMap, null /* oldEntry */);}

如果mNotificationData能通过sbn的key拿到的Entry不为空,说明这个通知已经存在了,isUpdate为true走更新流程,否则走添加流程。到此,onNotificationPosted方法就结束了。

我们先来看添加流程addNotification(sbn, rankingMap, null /* oldEntry */)。
这在BaseStatusBar中是一个抽象方法,具体逻辑有它的子类去实现,由于我们主要关注的是手机,所以去到PhoneStatusBar,位于com.android.systemui.statusbar.phone。
在PhoneStatusBar中,这个方法是被这么实现的

public void addNotification(StatusBarNotification notification, RankingMap ranking,            Entry oldEntry) {        if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey());        Entry shadeEntry = createNotificationViews(notification);        if (shadeEntry == null) {            return;        }        .        .        .        addNotificationViews(shadeEntry, ranking);        // Recalculate the position of the sliding windows and the titles.        setAreThereNotifications();}

首先通过传来的StatusBarNotification notification封装构造出一个Entry对象(注意,这的notification是StabusBarNotification不是Notification,也就是onNotificationPosted传入的sbn)

Entry shadeEntry = createNotificationViews(notification);

跟过去看createNotificationViews(notification)方法,这里又跳回了状态栏的基类BaseStatusBar。

    protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) {        if (DEBUG) {            Log.d(TAG, "createNotificationViews(notification=" + sbn);        }        final StatusBarIconView iconView = createIcon(sbn);        if (iconView == null) {            return null;        }        // Construct the expanded view.        NotificationData.Entry entry = new NotificationData.Entry(sbn, iconView);        if (!inflateViews(entry, mStackScroller)) {            handleNotificationError(sbn, "Couldn't expand RemoteViews for: " + sbn);            return null;        }        return entry;    }

这里面的重点是inflateViews(entry, mStackScroller)。第二个参数mStackScroller,就是在Android6.0原生SystemUI中的下拉通知栏里面所有通知以及一些其他view的父view,是BaseStatusBar中一个成员变量。
跟过去看方法细节,方法太长,挑重点。

    protected boolean inflateViews(Entry entry, ViewGroup parent) {        PackageManager pmUser = getPackageManagerForUser(                entry.notification.getUser().getIdentifier());        int maxHeight = mRowMaxHeight;        final StatusBarNotification sbn = entry.notification;        RemoteViews contentView = sbn.getNotification().contentView;        if (contentView == null) {            return false;        }        ExpandableNotificationRow row;        if (entry.row != null) {            row = entry.row;            entry.reset();        } else {            // create the row view            LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);            row = (ExpandableNotificationRow)inflater.inflate(R.layout.status_bar_notification_row,parent, false);        }        NotificationContentView contentContainer = row.getPrivateLayout();        NotificationContentView contentContainerPublic = row.getPublicLayout();        row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);        mNotificationClicker.register(row, sbn);        // set up the adaptive layout        View contentViewLocal = null;        View bigContentViewLocal = null;        View headsUpContentViewLocal = null;        try {            contentViewLocal = contentView.apply(                    sbn.getPackageContext(mContext),                    contentContainer,                    mOnClickHandler);            if (bigContentView != null) {                bigContentViewLocal = bigContentView.apply(                        sbn.getPackageContext(mContext),                        contentContainer,                        mOnClickHandler);            }            if (headsUpContentView != null) {                headsUpContentViewLocal = headsUpContentView.apply(                        sbn.getPackageContext(mContext),                        contentContainer,                        mOnClickHandler);            }        }        catch (RuntimeException e) {            final String ident = sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId());            Log.e(TAG, "couldn't inflate view for notification " + ident, e);            return false;        }        if (contentViewLocal != null) {            contentViewLocal.setIsRootNamespace(true);            contentContainer.setContractedChild(contentViewLocal);        }        if (bigContentViewLocal != null) {            bigContentViewLocal.setIsRootNamespace(true);            contentContainer.setExpandedChild(bigContentViewLocal);        }        if (headsUpContentViewLocal != null) {            headsUpContentViewLocal.setIsRootNamespace(true);            contentContainer.setHeadsUpChild(headsUpContentViewLocal);        }        return true;    }

这个方法很长,内容很多,这里我就挑我看懂的我感兴趣的讲。

int maxHeight = mRowMaxHeight;final StatusBarNotification sbn = entry.notification;RemoteViews contentView = sbn.getNotification().contentView;RemoteViews bigContentView = sbn.getNotification().bigContentView;RemoteViews headsUpContentView = sbn.getNotification().headsUpContentView;

首先,mRowMaxHeight就是一条通知在通知栏中显示的最高高度,这个值的初始化是在PhoneStatusBar的loadDimens中自资源文件中加载而来,所以需要修改通知的高度的可以修改SystemUI工程下的对应dimens资源条目:notification_min_height,notification_max_height,notification_mid_height(事实上,我就是这么干的)。
然后RemoteViews contentView = sbn.getNotification().contentView,这个就是我们在app中使用自定义通知的时候给notification赋值的contentView。

继续回到inflateViews

ExpandableNotificationRow row;if (entry.row != null) {    row = entry.row;    hasUserChangedExpansion = row.hasUserChangedExpansion();    userExpanded = row.isUserExpanded();    userLocked = row.isUserLocked();    entry.reset();    if (hasUserChangedExpansion) {        row.setUserExpanded(userExpanded);    } } else {    // create the row view    LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);        row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row,                    parent, false);    row.setExpansionLogger(this, entry.notification.getKey());    row.setGroupManager(mGroupManager);}

这个ExpandableNotificationRow row就是最终添加到通知栏上的通知对应的view,它的布局文件是R.layout.status_bar_notification_row。
然后往下

NotificationContentView contentContainer = row.getPrivateLayout();NotificationContentView contentContainerPublic = row.getPublicLayout();

其实这两个就是对应布局文件中的

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

其中id为expanded的就是我们的自定义通知的父布局。

继续inflateViews

row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);mNotificationClicker.register(row, sbn);

第一句就是实现了焦点的策略,row(也就是我们的通知)有子控件需要焦点则把焦点交给子控件,否则给row。
然后我们看一下mNotificationClicker.register(row, sbn)。
这个NotificationClicker是BaseStatusBar的一个私有成员,它所归属的类,是一个BaseStatusBar的内部类,它就是row的监听类,实现的功能是row被点击时,启动相应的pendingIntent,详细代码就不贴了。

继续inflateViews

        View contentViewLocal = null;        View bigContentViewLocal = null;        View headsUpContentViewLocal = null;        try {            contentViewLocal = contentView.apply(                    sbn.getPackageContext(mContext),                    contentContainer,                    mOnClickHandler);            if (bigContentView != null) {                bigContentViewLocal = bigContentView.apply(                        sbn.getPackageContext(mContext),                        contentContainer,                        mOnClickHandler);            }            if (headsUpContentView != null) {                headsUpContentViewLocal = headsUpContentView.apply(                        sbn.getPackageContext(mContext),                        contentContainer,                        mOnClickHandler);            }        }        catch (RuntimeException e) {            final String ident = sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId());            Log.e(TAG, "couldn't inflate view for notification " + ident, e);            return false;        }        if (contentViewLocal != null) {            contentViewLocal.setIsRootNamespace(true);            contentContainer.setContractedChild(contentViewLocal);        }        if (bigContentViewLocal != null) {            bigContentViewLocal.setIsRootNamespace(true);            contentContainer.setExpandedChild(bigContentViewLocal);        }        if (headsUpContentViewLocal != null) {            headsUpContentViewLocal.setIsRootNamespace(true);            contentContainer.setHeadsUpChild(headsUpContentViewLocal);        }

这部分实现的是,把remoteView加载出来,装进row中相应的地方。

再往下就是加载公共notification的部分,通知中提供的icon,ticketText等属性,这部分就会加载一个固定的公共布局,把这些属性填到布局的相应位置。

至此inflateViews()结束,Entry生成完毕,Entry中的row生成完毕。

回到addNotification(),往下走到addNotificationViews(shadeEntry, ranking)这个shadeEntry就是刚生成的Entry,这个方法是BaseStatusBar的方法,里面很简单,就两句代码。

    protected void addNotificationViews(Entry entry, RankingMap ranking) {        if (entry == null) {            return;        }        // Add the expanded view and icon.        mNotificationData.add(entry, ranking);        updateNotifications();    }

我们先看 mNotificationData.add(entry, ranking)

    public void add(Entry entry, RankingMap ranking) {        mEntries.put(entry.notification.getKey(), entry);        updateRankingAndSort(ranking);        mGroupManager.onEntryAdded(entry);    }

把entry和对应的key存到mEntries,和最开始在onNotificationPosted方法中的boolean isUpdate = mNotificationData.get(key) != null相呼应了。

接下来看updateNotifications(),这个方法在BaseStatusBar是抽象方法,我们看它在PhoneStatusBar的实现

    protected void updateNotifications() {        mNotificationData.filterAndSort();        updateNotificationShade();        mIconController.updateNotificationIcons(mNotificationData);    }

重点在updateNotificationShade()(实际上,我就是在这个方法里进行了一些修改达到我想要的需求),太长,就不全贴了

    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++) {            .            .            .            if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) {                .                .                .            } else {                toShow.add(ent.row);            }        }        ArrayList<View> 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(child);            }        }        for(View remove : toRemove){            mStackScroller.remove(remove);        }        for(int i = 0; i < toShow.size();i++){            View v = toShow.get(i);            if(v.getParent() == null){                mStackScroller.addView(v);            }        }        .        .        .    }

显示做mStackScroller的空判断,然后是通知栏动画状态的判断。一切OK,就:
1.mNotificationData获取数据Entry集合,构造一个大小和这个Entry集合一样的ExpandableNotificationRow集合toShow
2.遍历entry,把entry.row添加到toshow里面
3.原有的通知,但是toshow里没有的则移除,然后toshow里没添加上的添加上去

至此我们的通知就成功地添加到通知栏了。

Notification的更新

 if (isUpdate) {      updateNotification(sbn, rankingMap);  } else {      addNotification(sbn, rankingMap, null /* oldEntry */);  }

isUpdate为true既是Notification的更新,走 updateNotification(sbn, rankingMap),方法在BaseStatusBar类中。

    public void updateNotification(StatusBarNotification notification, RankingMap ranking) {        //先通过新来的notification的key从mNotificationData获取旧的entry,然后通过一系列的比对,来判断同一notification的新旧之间是否存在改变,如果有,则刷新        final String key = notification.getKey();        Entry entry = mNotificationData.get(key);        if (entry == null) {            return;        }        Notification n = notification.getNotification();        if (DEBUG) {            logUpdate(entry, n);        }        boolean applyInPlace = shouldApplyInPlace(entry, n);        boolean shouldInterrupt = shouldInterrupt(entry, notification);        boolean alertAgain = alertAgain(entry, n);        entry.notification = notification;        mGroupManager.onEntryUpdated(entry, entry.notification);        boolean updateSuccessful = false;        if (applyInPlace) {            try {                if (entry.icon != null) {                    // Update the icon                    //icon部分是刷新公共布局通知                    final StatusBarIcon ic = new StatusBarIcon(                            notification.getUser(),                            notification.getPackageName(),                            n.getSmallIcon(),                            n.iconLevel,                            n.number,                            n.tickerText);                    entry.icon.setNotification(n);                    if (!entry.icon.set(ic)) {                        handleNotificationError(notification, "Couldn't update icon: " + ic);                        return;                    }                }                //刷新自定义部分                updateNotificationViews(entry, notification);                updateSuccessful = true;            }            catch (RuntimeException e) {                // It failed to apply cleanly.                Log.w(TAG, "Couldn't reapply views for package " + n.contentView.getPackage(), e);            }        }        if (!updateSuccessful) {            if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key);            //如果刷新不成功,直接生成一个新的entry            final StatusBarIcon ic = new StatusBarIcon(                    notification.getUser(),                    notification.getPackageName(),                    n.getSmallIcon(),                    n.iconLevel,                    n.number,                    n.tickerText);            entry.icon.setNotification(n);            entry.icon.set(ic);            inflateViews(entry, mStackScroller);        }        updateHeadsUp(key, entry, shouldInterrupt, alertAgain);        //刷新ranking中的数据,以及UI        mNotificationData.updateRanking(ranking);        //这个方法开始,往下就和添加新的通知一样了        updateNotifications();        // Update the veto button accordingly (and as a result, whether this row is        // swipe-dismissable)        updateNotificationVetoButton(entry.row, notification);        if (DEBUG) {            // Is this for you?            boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);            Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");        }        setAreThereNotifications();    }

先通过新来的notification的key从mNotificationData获取旧的entry,然后通过一系列的比对,来判断同一notification的新旧之间是否存在改变,如果有,则刷新。
刷新分两部分,一部分是刷新公共布局的通知,另一部分是updateNotificationViews(entry, notification)刷新自定义通知。

private void updateNotificationViews(Entry entry, StatusBarNotification notification) {        final RemoteViews contentView = notification.getNotification().contentView;        final RemoteViews bigContentView = notification.getNotification().bigContentView;        final RemoteViews headsUpContentView = notification.getNotification().headsUpContentView;        final Notification publicVersion = notification.getNotification().publicVersion;        final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView                : null;        System.out.println("updateNotificationViews=== notification==" + notification);        // Reapply the RemoteViews        contentView.reapply(mContext, entry.getContentView(), mOnClickHandler);        if (bigContentView != null && entry.getExpandedContentView() != null) {            bigContentView.reapply(notification.getPackageContext(mContext),                    entry.getExpandedContentView(),                    mOnClickHandler);        }        View headsUpChild = entry.getHeadsUpContentView();        if (headsUpContentView != null && headsUpChild != null) {            headsUpContentView.reapply(notification.getPackageContext(mContext),                    headsUpChild, mOnClickHandler);        }        if (publicContentView != null && entry.getPublicContentView() != null) {            publicContentView.reapply(notification.getPackageContext(mContext),                    entry.getPublicContentView(), mOnClickHandler);        }        // update the contentIntent        mNotificationClicker.register(entry.row, notification);        entry.row.setStatusBarNotification(notification);        entry.row.notifyContentUpdated();        entry.row.resetHeight();    }

这个方法主要是把新notification中的contentview等赋值到旧entry的row中对应的属性,然后把新的notification也赋值到entry中,还刷新了点击意图。entry.getContentView()等方法实际返回的是entry里面的row的对应子view。

刷新成功得到一个新的entry,如果刷新失败就用之前已经分析过的inflateViews()生成一个新的entry。然后拿这个新的entry刷新mNotificationData中的数据以及刷新UI。到这里走到updateNotifications()方法就和前面添加新通知又殊途同归了。

移除通知

首先还是BaseStatusBar中的mNotificationListener但是和notification的添加/更新不同的是,走的不再是onNotificationPosted方法,而是onNotificationRemoved

 public void onNotificationRemoved(StatusBarNotification sbn,                final RankingMap rankingMap) {            if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);            if (sbn != null) {                final String key = sbn.getKey();                mHandler.post(new Runnable() {                    @Override                    public void run() {                        removeNotification(key, rankingMap);                    }                });            }        }

这个方法就是很简单地拿个key,然后走removeNotification(key, rankingMap)方法,这个方法在BaseStatusBar中是个抽象方法,看PhoneStatusBar中的实现

    public void removeNotification(String key, RankingMap ranking) {        boolean deferRemoval = false;        if (mHeadsUpManager.isHeadsUp(key)) {            deferRemoval = !mHeadsUpManager.removeNotification(key);        }        if (key.equals(mMediaNotificationKey)) {            clearCurrentMediaNotification();            updateMediaMetaData(true);        }        if (deferRemoval) {            mLatestRankingMap = ranking;            mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));            return;        }        StatusBarNotification old = removeNotificationViews(key, ranking);        if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);        if (old != null) {            if (CLOSE_PANEL_WHEN_EMPTIED && !hasActiveNotifications()                    && !mNotificationPanel.isTracking() && !mNotificationPanel.isQsExpanded()) {                if (mState == StatusBarState.SHADE) {                    animateCollapsePanels();                } else if (mState == StatusBarState.SHADE_LOCKED) {                    goToKeyguard();                }            }        }        setAreThereNotifications();    }

这里面的重点是removeNotificationViews(key, ranking)方法,这个方法是在BaseStatusBar中定义的

    protected StatusBarNotification removeNotificationViews(String key, RankingMap ranking) {        NotificationData.Entry entry = mNotificationData.remove(key, ranking);        if (entry == null) {            Log.w(TAG, "removeNotification for unknown key: " + key);            return null;        }        updateNotifications();        return entry.notification;    }

里面逻辑也很简单,根据key,从mNotificationData移除entry,然后就是走回我们熟悉的updateNotifications()刷新UI。