Android通知還能這麼玩?

語言: CN / TW / HK

前言

作為安卓用户,大家是不是一直覺得安卓手機上的通知欄≈垃圾場,除了少數有用的社交通訊通知,其他的都是些垃圾推送通知,好多人會選擇直接關閉,也有人放任不管。

雖然原生Android、第三方Android系統,近幾年都針對通知管理做了優化,但也只是給用户提供了更多選擇,重要還是次要、優先還是靜默等等,還是顯得有點笨拙和難用。

因而市面上出現了一些通知管理的app,它們能更加精準地過濾無用通知,使得通知欄更加清爽乾淨,同時還基於系統通知提供一些實用和有趣的功能。下面讓我們來揭祕下這些功能是如何實現的呢?

如何監聽通知

實際上Android系統提供了相應的能力,讓我們實現通知的監聽。Android4.3加入了通知監聽服務NotificationListenerService,我們開發的APP擁有此權限後便可以監聽當前系統的通知的變化,Android4.4開始還可以獲取通知詳情信息。下面我們來看看具體使用。

實現監聽三步曲

1.創建一個類,繼承NotificationListenerService

```java @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) public class NotificationListener extends NotificationListenerService {

//當系統收到新的通知後出發回調 @Override public void onNotificationPosted(StatusBarNotification sbn) { super.onNotificationPosted(sbn); }

//當系統通知被刪掉後出發回調 @Override public void onNotificationRemoved(StatusBarNotification sbn) { super.onNotificationRemoved(sbn); } } ```

2.在AndroidManifest.xml中註冊Service並聲明相關權限

java <service android:name=".NotificationListener" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> <intent-filter> <action android:name="android.service.notification.NotificationListenerService" /> </intent-filter> </service>

3.在系統“通知使用權”頁面勾選app

完成上面兩步設置,在app安裝到手機上之後,還需要在“通知使用權”頁面勾選app,這個路徑在原生系統上是“設置->隱私->通知使用權”,不過在第三方系統上面路徑不一致,而且比較難找,可以通過以下代碼跳轉過去。

java private void jumpNotificationAccessSetting(Context context) { try { Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } catch(Exception e) { e.printStackTrace(); } }

完成前面三步後,就可以在我們自己的app裏面監聽到系統發出的通知了。NotificationListenerService相關接口和StatusBarNotificationNotification的一些重要字段,大家可以參考附錄1。

通知花樣玩法

通過上面一節的分析,我們知道了如何拿到其他app發送的通知,以及瞭解了系統提供的一些操控通知的方法。基於這些能力,我們能在通知上玩出什麼花樣呢?目前市面上已經有一些app,比如通知過濾、通知語音播報、搶紅包、微信消息防撤回等。下面我們來分析下,這些功能具體是如何實現的。

1.通知過濾

通知過濾是指自動移除符合一定條件的通知,比如內容裏面包含某些關鍵詞的、某些app發送的、某個時間段發送的、手機特定狀態下(熄屏/亮屏,充電/電池)發送的通知。

下圖是“通知濾盒”app裏面過濾愛奇藝裏面內容包含“頭條”的通知,它的過濾條件可以是不同文本內容的組合,包含或者不包含。

下圖是在“一知”app裏面自動過濾廣告營銷類型的通知和自動收納內容資訊的通知。

處理流程

通過NotificationListenerService#onNotificationPosted()接口獲取到通知實例後,我們抽取出通知內容、包名、發送時間等關鍵信息,然後把這些信息輸入到一系列過濾器中,滿足過濾條件的通知就調用cancelNotification移除或者snoozeNotification凍結。

技術點

1.獲取標準通知的文本內容

目前大部分應用發送的通知都是標準通知,獲取文本內容比較容易。

java @Override public void getContent(StatusBarNotification sbn) { Notification notification = sbn.getNotification(); Bundle extras = notification.extras; if (extras != null) { // 獲取通知標題 String title = extras.getString(Notification.EXTRA_TITLE, ""); // 獲取通知內容 String content = extras.getString(Notification.EXTRA_TEXT, ""); } }

2.獲取非標準通知的文本內容

非標準通知是指通過setCustomContentView或者setCustomBigContentView 方法實現的通知,通過常規的內容獲取方法無法獲取到文本信息,我們可以遍歷view的方法來獲取。

主要流程有以下三步: 1. 獲取非標準通知的contentView和bigContentView; 1. 調用RemoteViews#apply方法,把RemoteViews轉成View; 1. 遍歷View,獲取TextView裏面的文本內容。 ```java //獲取notification的view public static View getContentView(Context context, Notification notification) { RemoteViews contentView = null; //獲取contentView if (notification.contentView != null) { contentView = notification.contentView; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { contentView = Notification.Builder.recoverBuilder(context, notification).createContentView(); }

//RemoteViews轉成view
View view = null;
try {
  view = contentView == null ? null : contentView.apply(context, null);
} catch (Throwable e) {
}
return view;

}

//獲取view裏面的文本 public static String getContent(View view) { StringBuilder stringBuilder = new StringBuilder(); traversalView(view, stringBuilder); return stringBuilder.toString(); }

//遍歷View,獲取TextView裏面的文本內容 private static void traversalView(View view, StringBuilder stringBuilder) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; int count = viewGroup.getChildCount();

  for (int i = 0; i < count; ++i) {
    View childView = viewGroup.getChildAt(i);
    traversalView(childView, stringBuilder);
  }
} else {
  if (view instanceof TextView) {
    TextView tv = (TextView) view;
    CharSequence text = tv.getText();
    stringBuilder.append(text);
    stringBuilder.append(";");
  }
}

} ```

3.移除常駐通知

常駐通知是Notification#isClearable()為false的通知,即使我們點擊了“清除全部”按鈕後,也是清除不了的,用户只能通過手動關閉該應用的通知或者通知對應的刪除按鈕來清除。

常見的有音樂類app的通知,更多的是一些為了增加應用入口或者為了進程保活的通知,這類常駐通知我們實際上是最想清除的。

在Android8.0以後系統開放了接口snoozeNotification(String key, long durationMs),它實際上是凍結通知的方法,key是通知的唯一標誌,durationMs是凍結時長,一般設置一個很大的值,就可以實現清除常駐通知的目的,手機重啟之後該凍結操作會失效。

2.微信消息防撤回

微信上的消息撤回功能,可謂是讓人又愛又恨,愛是因為發錯消息能及時撤回,恨則是因為別人也能撤回。相信大家每次看到好友撤回消息的時候,一定會很好奇他/她到底撤回了啥?

想實現微信消息防撤回本身是很難的,之前都是通過root之後裝xposed框架,然後hook微信內部方法來實現的,現在通過通知監聽服務也能實現。

下圖是某個第三方app實現的微信防撤回功能界面,實際上是把可能撤回的消息都給你列了出來,你自己去辨別哪條是撤回消息。

實現方案

實現該功能的前提是微信的通知權限是打開的。當我們收到微信的消息後,提取出key、名字、時間戳等信息,存入數據庫,當用户點擊撤回消息的時候,查詢2分鐘以內的消息記錄(撤回有效時間2分鐘),然後用户根據這個列表判斷哪條是撤回消息(無法精準定位撤回消息)。大致流程如下:

3.通知增能

通知增能主要是給通知擴展一些額外的能力,比如懸浮通知、彈幕通知、語音播報、自定義鈴聲、震動、自動跳轉等。大家可以想想這些功能可以在哪些場景下使用?

彈幕通知:打遊戲、看視頻等全屏狀態下方便查看通知;

自定義鈴聲:給微信、QQ、Soul等IM軟件的不同好友和羣設置不同的鈴聲;

語音播報:在駕駛、散步時不遺漏重要通知;

下圖是一些第三方app提供的通知擴展能力。

懸浮通知                                        自定義鈴聲                                              語音播報

技術方案

我們來揭祕下上面的這些功能是怎麼實現的。第一步是定義匹配規則,比如關鍵詞包含或者正則表達式匹配,然後是規則對應的執行動作,這些動作最後都是調用系統提供的能力。

4.搶紅包

微信搶紅包功能第一步是通過NotificationListenerService監聽到紅包通知,然後跳轉到紅包頁面,最後通過輔助服務AccessibilityService來模擬點擊動作,實現搶紅包功能。技術難點主要是紅包通知的判斷和模擬點擊的實現,模擬點擊涉及到輔助服務相關技術,這裏不展開講了。

紅包通知判斷

主要是判斷通知內容裏面是不是包含“[微信紅包]”。

```java public void processRedPacketNotification(StatusBarNotification sbn) { PendingIntent pendingIntent = null; Notification notification = sbn.getNotification(); Bundle extras = notification.extras; // 獲取通知內容 String content = extras.getString(Notification.EXTRA_TEXT, ""); if (!TextUtils.isEmpty(content) && content.contains("[微信紅包]")) { pendingIntent = notification.contentIntent; }

// 發送pendingIntent打開微信
try {
  if (pendingIntent != null) {
    pendingIntent.send();
  }
} catch (PendingIntent.CanceledException e) {
  e.printStackTrace();
}

} ```

分析下源碼

既然都講這麼多了,我們要不順便分析一下通知發送源碼吧。通知模塊涉及到的主要類有Notification、NotificationChannel、NotificationManager、NotificationManagerService、NotificationListenerService等,這幾個類的關係如下關係如下:

notification.png 1. NotificationManagerService是整個模塊的核心,它在系統啟動時就會被啟動並且運行在後台,負責系統中所有通知的收發、處理、展示、移除等邏輯; 1. NotificaionManager是通知的管理類,負責發送通知、清除通知等,通過Binder方式調用NotificationManagerService; 1. Notification是通知的實體類,裏面定義通知的標題、內容、圖標、跳轉信息等; 調用過程從應用開始,通過NotificationManager.notify()來發出通知,NotificationManager通過binder機制把通知發送到NotificationManagerService中,NotificationManagerService再把通知分發給NotificationListeners中設置的監聽器中,包括SystemUI、系統桌面、運動健康app和其他第三方註冊的app中。

通知發送流程(基於Android10源碼)如下,從發送通知->NotificationManagerService處理->分發到通知監聽服務。

步驟分析

下面的分析會基於上面的流程圖按步驟拆解。

1、NotificationChannel和Notification

java public void sendNotification(View view) { String id = "channel_1"; String des = "des_1"; NotificationChannel channel = new NotificationChannel(id, des, NotificationManager.IMPORTANCE_MIN); notificationManager.createNotificationChannel(channel); Notification notification = new Notification.Builder(MainActivity.this, id) .setContentTitle("你好") .setContentText("您有一條新通知") .setSmallIcon(R.drawable.icon) .setStyle(new Notification.MediaStyle()) .setAutoCancel(false) .build(); notificationManager.notify(1, notification); }

通過NotificationManager創建一個新的或者獲取一個已經創建的通知渠道,然後通過Builder新建一個 Notification 對象,最後通過 NotificationManager#notify() 方法將 Notification 發送出去。

2、NotificationManager#notifyAsUser

java public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) { //獲取系統通知服務,通過Binder方式調用 INotificationManager service = getService(); String pkg = mContext.getPackageName(); try { // 做一些聲音、圖標、contentView的優化和校驗工作。 service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,fixNotification(notification), user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }

notifyAsUser方法裏會調用fixNotification(Notification notification)對通知進行預處理,比如通知小圖標處理,圖片資源裁剪,低內存兼容等,然後直接調用NotificationManagerService#enqueueNotificationWithTag(),這裏通過binder調用進入system_server進程。

3、NotificationManagerService#enqueueNotificationInternal

```java void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int incomingUserId) { // 檢查調用uid是否有權限發送消息 final int notificationUid = resolveNotificationUid(opPkg, pkg, callingUid, userId);

    //檢查分類合法性
    checkRestrictedCategories(notification);

    // 繼續修正通知,主要是對fullScreenIntent處理
fixNotification(notification, pkg, userId);

    ...

     // 獲取通知channel信息,校驗是否規範
    final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg,
            notificationUid, channelId, false /* includeDeleted */);
    if (channel == null) {
        boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)
                == NotificationManager.IMPORTANCE_NONE;

        if (!appNotificationsOff) {
            doChannelWarningToast("Developer warning for package "" + pkg + ""\n" +
                    "Failed to post notification on channel "" + channelId + ""\n" +
                    "See log for more details");
        }
        return;
    }

    //構造StatusBarNotification對象,用來分發給監聽服務,包括SystemUI等
    final StatusBarNotification n = new StatusBarNotification(
            pkg, opPkg, id, tag, notificationUid, callingPid, notification,
            user, null, System.currentTimeMillis());

    //構造NotificationRecord對象,在framework層使用
    final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
 ...

    //檢查應用發送通知的速率、通知總數(單應用最多25)等,決定能否發送
    if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,
            r.sbn.getOverrideGroupKey() != null)) {
        return;
    }

    // 給pendingIntents加白名單,比如省電模式、後台啟動activity等白名單。
    if (notification.allPendingIntents != null) {
        final int intentCount = notification.allPendingIntents.size();
        if (intentCount > 0) {
            final ActivityManagerInternal am = LocalServices
                    .getService(ActivityManagerInternal.class);
            final long duration = LocalServices.getService(
                    DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
            for (int i = 0; i < intentCount; i++) {
                PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                if (pendingIntent != null) {
                    am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
                            WHITELIST_TOKEN, duration);
                    am.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),
                            WHITELIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER
                                    | FLAG_SERVICE_SENDER));
                }
            }
        }
    }

    mHandler.post(new EnqueueNotificationRunnable(userId, r));
}

```

這裏的代碼運行在system_server進程,主要有幾個關鍵點: 1. 獲取通知channel信息,Android 8.0之後不設置channel的通知是無法發送的; 1. 除了系統的通知和已註冊了監聽器的應用外,其他app的通知都會限制通知數上限和通知頻率上限; 1. 將 notification 的 PendingIntent 加入到白名單,比如省電模式、後台啟動activity等白名單; 1. 把notification 進一步封裝為 StatusBarNotificationNotificationRecord,最後封裝到一個異步線程 EnqueueNotificationRunnable 中。 1. StatusBarNotification主要面向客户端,僅包含用户需要知道的信息,如通知包名、id、key等信息,最後回調給監聽者的就是這個對象。 NotificationRecord主要面向框架層,除了持有StatusBarNotification實例外,還封裝了各種通知相關的信息,如channel、sound(通知鈴聲)、vibration(震動效果)等等,這些信息在服務端處理通知的時候需要用到。

4、EnqueueNotificationRunnable

```java protected class EnqueueNotificationRunnable implements Runnable { private final NotificationRecord r; private final int userId;

    EnqueueNotificationRunnable(int userId, NotificationRecord r) {
        this.userId = userId;
        this.r = r;
    };

    @Override
    public void run() {
        synchronized (mNotificationLock) {
            //把通知加到隊列中,ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>()
            mEnqueuedNotifications.add(r);
             //定時取消功能
            scheduleTimeoutLocked(r);

            final StatusBarNotification n = r.sbn;

          //根據key來查找NotificationRecord,有的話保持相同的排序
            NotificationRecord old = mNotificationsByKey.get(n.getKey());
            if (old != null) {
                //複製排序信息
                r.copyRankingInformation(old);
            }

           ......

            //處理NotificationGroup的信息
            handleGroupedNotificationLocked(r, old, callingUid, callingPid);

             .....

            if (mAssistants.isEnabled()) {
                //通知助手(NotificationAssistants),處理通知
                mAssistants.onNotificationEnqueued(r);
                mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
                        DELAY_FOR_ASSISTANT_TIME);
            } else {
                mHandler.post(new PostNotificationRunnable(r.getKey()));
            }
        }
    }
}

`` 主要關注點: 1. 把NotificationRecord加到隊列中; 1. 定時取消功能,構造通知的時候可以通過setTimeout方法設置; 1. 更新NotificationGroup信息; 1. mHandler 是WorkerHandler類的一個實例,在NotificationManagerService#onStart方法中被創建,所以EnqueueNotificationRunnable` 的run方法會運行在system_server的主線程。

5、PostNotificationRunnable

```java protected class PostNotificationRunnable implements Runnable { private final String key;

    PostNotificationRunnable(String key) {
        this.key = key;
    }

    @Override
    public void run() {
        synchronized (mNotificationLock) {
            try {
                NotificationRecord r = null;
                int N = mEnqueuedNotifications.size();
                for (int i = 0; i < N; i++) {
                    final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                    if (Objects.equals(key, enqueued.getKey())) {
                        r = enqueued;
                        break;
                    }
                }

                r.setHidden(isPackageSuspendedLocked(r));
                NotificationRecord old = mNotificationsByKey.get(key);
                final StatusBarNotification n = r.sbn;
                final Notification notification = n.getNotification();

              //根據key查找Record,有就更新,沒有就新增
                int index = indexOfNotificationLocked(n.getKey());
                if (index < 0) {
                    mNotificationList.add(r);
                    mUsageStats.registerPostedByApp(r);
                    r.setInterruptive(isVisuallyInterruptive(null, r));
                } else {
                    old = mNotificationList.get(index);
                    mNotificationList.set(index, r);
                    mUsageStats.registerUpdatedByApp(r, old);
                    // 確保沒有丟失前台服務的flag
                    notification.flags |=
                            old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                    r.isUpdate = true;
                    r.setTextChanged(isVisuallyInterruptive(old, r));
                }

                mNotificationsByKey.put(n.getKey(), r);

                // 前台服務設置flag
                if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) {
                    notification.flags |= Notification.FLAG_ONGOING_EVENT
                            | Notification.FLAG_NO_CLEAR;
                }

                applyZenModeLocked(r);
               //對mNotificationList進行排序
                mRankingHelper.sort(mNotificationList);

                if (notification.getSmallIcon() != null) {
                  //發送通知
                    StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                    mListeners.notifyPostedLocked(r, old);
                    if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) {
                      //發送group通知
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                mGroupHelper.onNotificationPosted(
                                        n, hasAutoGroupSummaryLocked(n));
                            }
                        });
                    }
                } else {
                    //沒有小圖標,移除通知
                    if (old != null && !old.isCanceled) {
                        mListeners.notifyRemovedLocked(r,
                                NotificationListenerService.REASON_ERROR, null);
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                mGroupHelper.onNotificationRemoved(n);
                            }
                        });
                    }

                }

                if (!r.isHidden()) {
                    //處理鈴聲、震動等
                    buzzBeepBlinkLocked(r);
                }
                maybeRecordInterruptionLocked(r);
            } finally {
              //最後移除隊列中的NotificationRecord
                int N = mEnqueuedNotifications.size();
                for (int i = 0; i < N; i++) {
                    final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                    if (Objects.equals(key, enqueued.getKey())) {
                        mEnqueuedNotifications.remove(i);
                        break;
                    }
                }
            }
        }
    }
}

```

主要做了 1. 判斷通知新增還是刷新; 1. 刷新NotificationGroup; 1. 鈴聲、震動處理; 1. 調用NotificationListeners#notifyPostedLocked方法。

6、NotificationListeners

```java private void notifyPostedLocked(NotificationRecord r, NotificationRecord old, boolean notifyAllListeners) { // Lazily initialized snapshots of the notification. StatusBarNotification sbn = r.sbn; StatusBarNotification oldSbn = (old != null) ? old.sbn : null; TrimCache trimCache = new TrimCache(sbn);

        for (final ManagedServiceInfo info : getServices()) {
            boolean sbnVisible = isVisibleToListener(sbn, info);
            boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false;
            ......

            final NotificationRankingUpdate update = makeRankingUpdateLocked(info);

            if (oldSbnVisible && !sbnVisible) {
                final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        //老通知可見,新通知不可見,移除通知
                        notifyRemoved(
                                info, oldSbnLightClone, update, null, REASON_USER_STOPPED);
                    }
                });
                continue;
            }

            final StatusBarNotification sbnToPost = trimCache.ForListener(info);
            //發送通知
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    notifyPosted(info, sbnToPost, update);
                }
            });
        }
    }

private void notifyPosted(final ManagedServiceInfo info, final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) { final INotificationListener listener = (INotificationListener) info.service; StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn); try { listener.onNotificationPosted(sbnHolder, rankingUpdate); } catch (RemoteException ex) { Log.e(TAG, "unable to notify listener (posted): " + listener, ex); } } ```

上面的流程主要是把通知發送給各監聽者: 1. 尋找匹配的ManagedServiceInfo,它裏面封裝了註冊的通知監聽服務信息; 1. 通知監聽器移除不在顯示的Notification; 1. 向監聽器發送新通知的信息; 1. onNotificationPosted傳入的參數是sbnHolder而不是sbn對象,大家可以看到有個get()方法,返回的是真正的sbn對象。app收到sbnHolder後需要再次調用binder調用才能獲得sbn對象。為啥呢?看sbnHolder的註釋:

Wrapper for a StatusBarNotification object that allows transfer across a oneway binder without sending large amounts of data over a oneway transaction.

大體意思就是對sbn對象進行封裝,通過一次one way的binder調用,避免傳輸大量數據。

7、NotificationListenerService

```java public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update) { StatusBarNotification sbn; try { //取出真正的sbn sbn = sbnHolder.get(); } catch (RemoteException e) { Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e); return; }

  ......

        synchronized (mLock) {
            applyUpdateLocked(update);
            if (sbn != null) {
                SomeArgs args = SomeArgs.obtain();
                args.arg1 = sbn;
                args.arg2 = mRankingMap;
                //交由handler處理
       mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED,
                        args).sendToTarget();
            } else {
       mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE,
                        mRankingMap).sendToTarget();
            }
        }

    }


    public void handleMessage(Message msg) {
        if (!isConnected) {
            return;
        }
        switch (msg.what) {
            case MSG_ON_NOTIFICATION_POSTED: {
                SomeArgs args = (SomeArgs) msg.obj;
                StatusBarNotification sbn = (StatusBarNotification) args.arg1;
                RankingMap rankingMap = (RankingMap) args.arg2;
                args.recycle();
                onNotificationPosted(sbn, rankingMap); 
            } 
            break;

```

這裏先通過binder方式從system_server調回到app進程,把sbnHolder實例傳過來了,sbnHolder是IStatusBarNotificationHolder.Stub對象。 然後又通過sbnHolder.get()取出真正的StatusBarNotification實例,從RemoteException的異常類型就可以判斷出它又是一次遠程調用。拿到StatusBarNotification實例後,使用mHandler拋給子類的onNotificationPosted處理,至此各監聽器就能收到新通知了。

總結

通過上面的分析我們揭開了Android通知花樣玩法背後的祕密,知道了第三方app如何監聽通知、取消通知以及從源碼層面分析了從app發送通知->NotificationManagerService處理通知->通知分發到NotificationListenerService的client的整個流程。

當然Android通知管理還可以有更多有趣玩法等待我們去探索!

附錄

附錄1

1、NotificationListenerService

NotificationListenerService主要提供了監聽系統通知發送(onNotificationPosted)和取消(cancelNotification)的能力,而且開放了一些操作通知的接口。

| 方法 | 作用 | | -------------------------------------------------- | ------------------------ | | getActiveNotifications() | 獲取當前通知欄所有通知組 | | cancelAllNotifications() | 刪除系統中所有可被清除的通知(不能清除常駐通知) | | cancelNotification(String key) | 刪除某一個通知 | | snoozeNotification(String key, long durationMs) | 休眠通知,可清除通知欄上常駐通知 | | onNotificationPosted(StatusBarNotification sbn) | 通知發送時回調 | | onNotificationRemoved(StatusBarNotification sbn) | 通知移除時回調 | | onNotificationRankingUpdate(RankingMap rankingMap) | 通知排序發生變化時回調 |

2、StatusBarNotification

StatusBarNotification是我們在NotificationListenerService#onNotificationPosted()接口裏面獲取到的通知實例,它主要有以下重要方法。

| 方法 | 作用 | | ------------------ | ----------------------------------------- | | getId() | 通知的id | | getTag() | 通知的Tag,如果沒有設置返回null | | getKey() | 通知的key,唯一標誌。可以用來指定刪除某條通知 | | getPostTime() | 通知發送的時間 | | getPackageName() | 通知對應的包名 | | isClearable() | 通知是否可被清除,FLAG_ONGOING_EVENT、FLAG_NO_CLEAR | | getNotification() | 獲取通知對象 |

hi, 我是快手電商的小強~

快手電商無線技術團隊正在招賢納士🎉🎉🎉! 我們是公司的核心業務線, 這裏雲集了各路高手, 也充滿了機會與挑戰. 伴隨着業務的高速發展, 團隊也在快速擴張. 歡迎各位高手加入我們, 一起創造世界級的電商產品~

熱招崗位: Android/iOS 高級開發, Android/iOS 專家, Java 架構師, 產品經理(電商背景), 測試開發... 大量 HC 等你來呦~

內部推薦請發簡歷至 >>>我們的郵箱: [email protected] <<<, 備註我的花名成功率更高哦~ 😘