Notification之----Android5.0实现原理(二)

概述

前文讲解了Notification的构造,现在来讲讲notification的发送,以及公布前文留下的疑问(自定义view不论高度是多高,最后只能显示为64dp,why?)

NotificationManager

在Notification构造完成后,会调用NotificationManager的notify方法来发送通知,我们就来看看该方法
frameworks/base/core/java/android/app/NotificationManager.java

public void notify(String tag, int id, Notification notification)
{
    ...
    INotificationManager service = getService();
    ...
    service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
        stripped, idOut, UserHandle.myUserId());
    ...
}

可以看出NotificationManager只是一个空壳,没有做什么实际上的事情,只是把notify的动作交给了service来做。
为了主干的清晰,直接给出enqueueNotificationWithTag的实现在NotificationManagerService中

NotificationManagerService

frameworks/base/services/java/com/android/server/NotificationManagerService.java

public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id,
        Notification notification, int[] idOut, int userId) throws RemoteException {
    enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(),
            Binder.getCallingPid(), tag, id, notification, idOut, userId);
}

所以重要的是enqueueNotificationInternal方法

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
        final int callingPid, final String tag, final int id, final Notification notification,
        int[] idOut, int incomingUserId) {
    ...
    
    if (!isSystemNotification && !isNotificationFromListener) {
        ...
        //MAX_PACKAGE_NOTIFICATIONS = 50;
        if (count >= MAX_PACKAGE_NOTIFICATIONS) {
            return;
        }
    }
    
    ...
        
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            synchronized (mNotificationList) {
                ...
                // blocked apps
                //如果用户设置了该引用不显示通知,并且不是系统通知的话,直接将该通知打分为-1000
                if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) {
                    if (!isSystemNotification) {
                        //JUNK_SCORE = -1000;
                        r.score = JUNK_SCORE;
                    }
                }

                //SCORE_DISPLAY_THRESHOLD = -20;
                //打分小于阈值的通知不显示
                if (r.score < SCORE_DISPLAY_THRESHOLD) {
                    // Notification will be blocked because the score is too low.
                    return;
                }

                //垃圾通知,也不会显示
                if (isNotificationSpam(notification, pkg)) {
                    mArchive.record(r.sbn);
                    return;
                }

                ...
                //只显示有图标的通知
                if (notification.icon != 0) {
                    StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                    mListeners.notifyPostedLocked(n, oldSbn);
                }
                ...
                //声音,震动,闪光灯的控制
                buzzBeepBlinkLocked(r);
            }
        }
    });
}

可以看到要想发出通知必须得满足以下几个条件

  1. 非系统应用,最多只能发送50个通知消息
  2. 用户设置了允许应用发送通知
  3. 被系统判定为非垃圾通知(该功能是cm自己添加的,系统中会有一个数据库,然后根据通知栏的Extra信息来匹配,如果成功则判定为垃圾通知,但是该功能现在并没有实现)
  4. 通知必须得有icon

检查通过后再使用notifyPostedLocked方法做真正的发送动作。buzzBeepBlinkLocked很简单,不浪费篇幅叙述了。

INotificationListener

notifyPostedLocked方法最后调用notifyPosted方法,我们直接来看看该方法

private void notifyPosted(final ManagedServiceInfo info,
    final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
    final INotificationListener listener = (INotificationListener)info.service;
    ...
    listener.onNotificationPosted(sbnHolder, rankingUpdate);
    ...
}

这里有一个INotificationListener对象,一看到以I开头的就可以知道,这里肯定又是一个IPC通信。
查看源码可以知道,onNotificationPosted的实现是在SystemUI进程中,也就是我们的状态栏进程。

BaseStatusBar

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java

@Override
public void onNotificationPosted(final StatusBarNotification sbn,
        final RankingMap rankingMap) {
    mHandler.post(new Runnable() {
        @Override
        public void run() {
             ...
             boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
                            || isHeadsUp(sbn.getKey());
             ...
            if (isUpdate) {
                updateNotification(sbn, rankingMap);
            } else {
                addNotification(sbn, rankingMap);
            }
        }
    });
}

状态栏会根据通知的唯一key值来判断该通知是否是更新还是新增的。
我们以新增的为例来讲.addNotification是一个抽象方法,实现是在BaseStatusBar的子类PhoneStatusBar

PhoneStatusBar

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java

public void addNotification(StatusBarNotification notification, RankingMap ranking) {
    ...
    Entry shadeEntry = createNotificationViews(notification);
    if (shadeEntry == null) {
        return;
    }
    ...
    addNotificationViews(shadeEntry, ranking);
    ...
}

该方法做了2个重要的事情,一个就是创建Entry实例,另外一个就是将Entry添加到状态栏上,然后就显示完成了。
因为createNotificationViews的实现是在父类中,并且该方法十分重要,所以我们先跳过该方法。
先把Entry理解成一条通知,来讲addNotificationViews的实现。

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

先直接将得到的Entry添加到mNotificationData里面
最终updateNotifications会调用PhoneStatusBar中的updateNotificationShade方法

private void updateNotificationShade() {
    ...
    ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
    ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
    ...
    for (int i=0; i<N; i++) {
       Entry ent = activeNotifications.get(i);
       ...
       toShow.add(ent.row);
    }

    for (int i=0; i<toShow.size(); i++) {
            View v = toShow.get(i);
            if (v.getParent() == null) {
                mStackScroller.addView(v);
            }
    }
    ...
}
  1. 从mNotificationData对象中获取一个list<Entry>对象
  2. 将mNotificationData中的每一个Entry对象的row属性添加到List<ExpandableNotificationRow>中
  3. 将ExpandableNotificationRow添加到mStackScroller里面

这个mStackScroller是NotificationStackScrollLayout的对象,而这个NotificationStackScrollLayout是一个继承自ViewGroup的,也就是我们下拉状态栏看到的整片view的根view.
那么ExpandableNotificationRow也就是对应着每一个通知了. ExpandableNotificationRow是继承自FrameLayout的

我们前面说到把Entry先理解为一条通知,看到这里,其实添加的是Entry对象里面的row属性到界面上,也就是ExpandableNotificationRow

createNotificationViews

这个是解答开头疑问的关键。 该方法是BaseStatusBar类的方法。

protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) {
    ...
    // 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;
}

这里首先实例化了NotificationData的内部类Entry。
NotificationData是一个十分重要的类,里面有几个比较重要的数据结构
<pre>
ArrayMap<String, Entry> mEntries = new ArrayMap<>(); //所有Entry的集合
ArrayList<Entry> mSortedAndFiltered = new ArrayList<>(); //排序后的Entry集合
</pre>
那这个Entry到底是个什么东西呢?先来看看这个类的定义

public static final class Entry {
       ...
       public ExpandableNotificationRow row; // the outer expanded view
       public View expanded; // the inflated RemoteViews
       public View expandedPublic; // for insecure lockscreens
       public View expandedBig;
       ...
 }

从定义里面可以看出,一个Entry对应了一条通知栏的所有Data信息,其中比较重要的是row属性,前面已经碰到过了。最后添加界面上的也就是这个row。
inflateViews方法里面,这个row会被赋值,我们来看看row是怎么被赋值的

private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent, boolean isHeadsUp) {
    ...
    //contentView和bigContentView是我们构造Notification时传过来的view
    RemoteViews contentView = sbn.getNotification().contentView;
    RemoteViews bigContentView = sbn.getNotification().bigContentView;
    ...
    ExpandableNotificationRow row;
    ...
    //使用指定view填充
    row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row,
                    parent, false);
    ...
    //这个expanded view就是我们在下拉状态栏中看到的每一条view,这里命名为expanded 应该是状态栏展开,而不是通知展开
    //NotificationContentView是继承自FrameLayout的,会根据不同状态来控制显示哪个view(默认通知/展开通知)
    NotificationContentView expanded =
                (NotificationContentView) row.findViewById(R.id.expanded);
    ...

    //给每一条通知设置onClick的点击事件,以来相应我们设置的动作.
    PendingIntent contentIntent = sbn.getNotification().contentIntent;
    final View.OnClickListener listener = makeClicker(contentIntent, sbn.getKey(),
                    isHeadsUp);
    row.setOnClickListener(listener);
    ...

    ///////关键////////////
    View contentViewLocal = null;
    View bigContentViewLocal = null;
    //将构造通知栏时设置的contentView & bigContentView(RemoteView)转换为view
    contentViewLocal = contentView.apply(mContext, expanded,
                    mOnClickHandler, themePackageName);
    if (bigContentView != null) {
       bigContentViewLocal = bigContentView.apply(mContext, expanded,
                          mOnClickHandler, themePackageName);
    }
    ...
    //因为expanded 是一个FrameLayout的ViewGroup,所以往里面塞了2个view
    expanded.setContractedChild(contentViewLocal);
    expanded.setExpandedChild(bigContentViewLocal);
}

看完上面的代码,先来坐个小节,整理下思路。在Entry.row添加到屏幕上前,做了如下的属性赋值

  1. inflate布局文件status_bar_notification_row(这是每个通知栏的根view)
  2. 给根view设置监听器
  3. 将在构造通知过程中的bigContentView 和 contentView 塞到通知栏的根view里面

到这里,一个通知栏从初始化到显示的流程就讲完了,但是最开头的疑问不是还没有解答吗?来看答案

答案

contentView固定高度

expanded.setContractedChild方法前,传递进来的ContentView都还是自义定的view,没有做高度限制或者系统默认的view. 最后显示的时候却被限制了,说明在setContractedChild方法里做了手脚

public void setContractedChild(View child) {
    ...
    sanitizeContractedLayoutParams(child);
    addView(child);
    ...
}
private void sanitizeContractedLayoutParams(View contractedChild) {
    LayoutParams lp = (LayoutParams) contractedChild.getLayoutParams();
    lp.height = mSmallHeight;
    contractedChild.setLayoutParams(lp);
}

可以看到在sanitizeContractedLayoutParams方法里面,不论传递进来的contentView有多高最后的会被改成mSmallHeight的高度。这个mSmallHeight的值就是在SystemUI里面配置的,64dp

bigview最大高度

expanded.setExpandedChild的方法里面却没有做最大高度的限制,那么最大高度是在哪限制的呢?
这个时候就要看看ExpandableNotificationRow这个根view了
ExpandableNotificationRow继承自ExpandableView,来看看onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //mMaxNotificationHeight是systemui中配置的值,256dp
    int ownMaxHeight = mMaxNotificationHeight;
    ...
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        int childHeightSpec = newHeightSpec;
        ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
        if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
            if (layoutParams.height >= 0) {
                // An actual height is set
                childHeightSpec = layoutParams.height > ownMaxHeight
                    ? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
                    : MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
            }
            child.measure(
                    getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
                    childHeightSpec);
            int childHeight = child.getMeasuredHeight();
            maxChildHeight = Math.max(maxChildHeight, childHeight);
        } else {
            mMatchParentViews.add(child);
        }
    }
    int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight;
    newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
    for (View child : mMatchParentViews) {
        child.measure(getChildMeasureSpec(
                widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
                newHeightSpec);
    }
   ...

如果bigviewlayoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT则高度就是newHeightSpec。这个newHeightSpec要么是ownMaxHeight 要么是maxChildHeight,而这2个值的最大值就是256dp
如果bigviewlayoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT,最大值也是maxChildHeight 也就是256dp

注意: 这里并没有显示bigview的最小高度,所以bigview的高度范围是可以在(0,256dp ] 区间的

最后

a pic is worth a thousands words,   tow pics worth double, lol

类图
Notification_class_diagram.jpg
流程图
Notification_seq_diagram.jpg

相关阅读

Notification之---NotificationListenerService5.0实现原理
Notification之----Android5.0实现原理(一)
Notification之----自定义样式
Notification之----默认样式
Notification之----任务栈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容