Android通知還能這麼玩?
前言
作為安卓使用者,大家是不是一直覺得安卓手機上的通知欄≈垃圾場,除了少數有用的社交通訊通知,其他的都是些垃圾推送通知,好多人會選擇直接關閉,也有人放任不管。
雖然原生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
相關介面和StatusBarNotification
和Notification
的一些重要欄位,大家可以參考附錄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
等,這幾個類的關係如下關係如下:
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 進一步封裝為 StatusBarNotification
和 NotificationRecord
,最後封裝到一個非同步執行緒 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] <<<, 備註我的花名成功率更高哦~ 😘