Notification之 - Android5.0實現原理(二)

Hly_Coder發表於2016-12-06

概述

前文講解了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物件
  2. 將mNotificationData中的每一個Entry物件的row屬性新增到List
  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是一個十分重要的類,裡面有幾個比較重要的資料結構


ArrayMap mEntries = new ArrayMap<>(); //所有Entry的集合
ArrayList mSortedAndFiltered = new ArrayList<>(); //排序後的Entry集合

那這個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 worth thousands of words,   tow pics worth double, lol

類圖

Notification之 - Android5.0實現原理(二)
Notification_class_diagram.jpg

流程圖

Notification之 - Android5.0實現原理(二)
Notification_seq_diagram.jpg

相關閱讀

Notification之----Android5.0實現原理(一)
Notification之----自定義樣式
Notification之----預設樣式
Notification之----任務棧

相關文章