網(wǎng)站首頁 編程語言 正文
監(jiān)聽通知
Android 中的 AccessibilityService 可以監(jiān)聽通知信息的變化,首先需要創(chuàng)建一個無障礙服務,這個教程可以自行百度。在無障礙服務的配置文件中,需要以下配置:
<accessibility-service ... android:accessibilityEventTypes="其他內(nèi)容|typeNotificationStateChanged" android:canRetrieveWindowContent="true" />
然后在 AccessibilityService 的 onAccessibilityEvent 方法中監(jiān)聽消息:
override fun onAccessibilityEvent(event: AccessibilityEvent?) { when (event.eventType) { AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> { Log.d(Tag, "Notification: $event") } } }
當有新的通知或 Toast 出現(xiàn)時,在這個方法中就會收到 AccessibilityEvent 。
另一種方案是通過 NotificationListenerService 進行監(jiān)聽,這里不做詳細介紹了。兩種方案的應用場景不同,推薦使用 NotificationListenerService 而不是無障礙服務。stackoverflow 上一個比較好的回答:
It depends on WHY you want to read it. The general answer would be Notification Listener. Accessibility Services are for unique accessibility services. A user has to enable an accessibility service from within the Accessibility Service menu (where TalkBack and Switch Access are). Their ability to read notifications is a secondary ability, to help them achieve the goal of creating assistive technologies (alternative ways for people to interact with mobile devices).
Whereas, Notification Listeners, this is their primary goal. They exist as part of the context of an app and as such don't need to be specifically turned on from the accessibility menu.
Basically, unless you are in fact building an accessibility service, you should not use this approach, and go with the generic Notification Listener.
無障礙服務監(jiān)聽通知邏輯
從用法中可以看出一個關(guān)鍵信息 -- TYPE_NOTIFICATION_STATE_CHANGED
,通過這個事件類型入手,發(fā)現(xiàn)它用于兩個類中:
- ToastPresenter:用于在應用程序進程中展示系統(tǒng) UI 樣式的 Toast 。
- NotificationManagerService:通知管理服務。
ToastPresenter
ToastPresenter 的 trySendAccessibilityEvent 方法中,構(gòu)建了一個 TYPE_NOTIFICATION_STATE_CHANGED
類型的消息:
public void trySendAccessibilityEvent(View view, String packageName) { if (!mAccessibilityManager.isEnabled()) { return; } AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setClassName(Toast.class.getName()); event.setPackageName(packageName); view.dispatchPopulateAccessibilityEvent(event); mAccessibilityManager.sendAccessibilityEvent(event); }
這個方法的調(diào)用在 ToastPresenter 中的 show 方法中:
public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, @Nullable ITransientNotificationCallback callback) { // ... trySendAccessibilityEvent(mView, mPackageName); // ... }
而這個方法的調(diào)用就是在 Toast 中的 TN 類中的 handleShow 方法。
Toast.makeText(this, "", Toast.LENGTH_SHORT).show()
在 Toast 的 show 方法中,獲取了一個 INotificationManager ,這個是 NotificationManagerService 在客戶端暴露的 Binder 對象,通過這個 Binder 對象的方法可以調(diào)用 NMS 中的邏輯。
也就是說,Toast 的 show 方法調(diào)用了 NMS :
public void show() { // ... INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; final int displayId = mContext.getDisplayId(); try { if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) { if (mNextView != null) { // It's a custom toast service.enqueueToast(pkg, mToken, tn, mDuration, displayId); } else { // It's a text toast ITransientNotificationCallback callback = new CallbackBinder(mCallbacks, mHandler); service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback); } } else { service.enqueueToast(pkg, mToken, tn, mDuration, displayId); } } catch (RemoteException e) { // Empty } }
這里是 enqueueToast 方法中,最后調(diào)用:
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration, int displayId, @Nullable ITransientNotificationCallback textCallback) { // ... record = getToastRecord(callingUid, callingPid, pkg, token, text, callback, duration, windowToken, displayId, textCallback); // ... }
getToastRecord 中根據(jù) callback 是否為空產(chǎn)生了不同的 Toast :
private ToastRecord getToastRecord(int uid, int pid, String packageName, IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration, Binder windowToken, int displayId, @Nullable ITransientNotificationCallback textCallback) { if (callback == null) { return new TextToastRecord(this, mStatusBar, uid, pid, packageName, token, text,duration, windowToken, displayId, textCallback); } else { return new CustomToastRecord(this, uid, pid, packageName, token, callback, duration, windowToken, displayId); } }
兩者的區(qū)別是展示對象的不同:
-
TextToastRecord 因為 ITransientNotification 為空,所以它是通過 mStatusBar 進行展示的:
@Override public boolean show() { if (DBG) { Slog.d(TAG, "Show pkg=" + pkg + " text=" + text); } if (mStatusBar == null) { Slog.w(TAG, "StatusBar not available to show text toast for package " + pkg); return false; } mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback); return true; }
-
CustomToastRecord 調(diào)用 ITransientNotification 的 show 方法:
@Override public boolean show() { if (DBG) { Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback); } try { callback.show(windowToken); return true; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show custom toast " + token + " in package " + pkg); mNotificationManager.keepProcessAliveForToastIfNeeded(pid); return false; } }
這個 callback 最在
Toast.show()
時傳進去的 TN :TN tn = mTN; service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
也就是調(diào)用到了 TN 的 show 方法:
@Override @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); }
TN 的 show 方法中通過 mHandler 來傳遞了一個類型是 SHOW
的消息:
mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; break; } case CANCEL: { handleHide(); // Don't do this in handleHide() because it is also invoked by // handleShow() mNextView = null; try { getService().cancelToast(mPackageName, mToken); } catch (RemoteException e) { } break; } } } };
而這個 Handler 在處理 SHOW
時,會調(diào)用 handleShow(token)
這個方法里面也就是會觸發(fā) ToastPresenter 的 show 方法的地方:
public void handleShow(IBinder windowToken) { // If a cancel/hide is pending - no need to show - at this point // the window token is already invalid and no need to do any work. if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) { return; } if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; // 【here】 mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY, mHorizontalMargin, mVerticalMargin, new CallbackBinder(getCallbacks(), mHandler)); } }
本章節(jié)最開始介紹到了 ToastPresenter 的 show 方法中會調(diào)用 trySendAccessibilityEvent 方法,也就是從這個方法發(fā)送類型是 TYPE_NOTIFICATION_STATE_CHANGED
的無障礙消息給無障礙服務的。
NotificationManagerService
在通知流程中,是通過 NMS 中的 sendAccessibilityEvent 方法來向無障礙發(fā)送消息的:
void sendAccessibilityEvent(Notification notification, CharSequence packageName) { if (!mAccessibilityManager.isEnabled()) { return; } AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setPackageName(packageName); event.setClassName(Notification.class.getName()); event.setParcelableData(notification); CharSequence tickerText = notification.tickerText; if (!TextUtils.isEmpty(tickerText)) { event.getText().add(tickerText); } mAccessibilityManager.sendAccessibilityEvent(event); }
這個方法的調(diào)用有兩處,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用來處理通知是否應該發(fā)出鈴聲、震動或閃爍 LED 的。省略無關(guān)邏輯:
int buzzBeepBlinkLocked(NotificationRecord record) { // ... if (!record.isUpdate && record.getImportance() > IMPORTANCE_MIN && !suppressedByDnd) { sendAccessibilityEvent(notification, record.getSbn().getPackageName()); sentAccessibilityEvent = true; } if (aboveThreshold && isNotificationForCurrentUser(record)) { if (mSystemReady && mAudioManager != null) { // ... if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) { if (!sentAccessibilityEvent) { sendAccessibilityEvent(notification, record.getSbn().getPackageName()); sentAccessibilityEvent = true; } // ... } else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) { hasValidSound = false; } } } // ... }
buzzBeepBlinkLocked 的調(diào)用路徑有兩個:
-
handleRankingReconsideration 方法中 RankingHandlerWorker (這是一個 Handler)調(diào)用 handleMessage 處理
MESSAGE_RECONSIDER_RANKING
類型的消息:@Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_RECONSIDER_RANKING: handleRankingReconsideration(msg); break; case MESSAGE_RANKING_SORT: handleRankingSort(); break; } }
handleRankingReconsideration 方法中調(diào)用了 buzzBeepBlinkLocked :
private void handleRankingReconsideration(Message message) { // ... synchronized (mNotificationLock) { // ... if (interceptBefore && !record.isIntercepted() && record.isNewEnoughForAlerting(System.currentTimeMillis())) { buzzBeepBlinkLocked(record); } } if (changed) { mHandler.scheduleSendRankingUpdate(); } }
PostNotificationRunnable 的 run 方法。
PostNotificationRunnable
這個東西是用來發(fā)送通知并進行處理的,例如提示和重排序等。
PostNotificationRunnable 的構(gòu)建和 post 在 EnqueueNotificationRunnable 中。在 EnqueueNotificationRunnable 的 run 最后,進行了 post:
public void run() { // ... // tell the assistant service about the notification if (mAssistants.isEnabled()) { mAssistants.onNotificationEnqueuedLocked(r); mHandler.postDelayed(new PostNotificationRunnable(r.getKey()), DELAY_FOR_ASSISTANT_TIME); } else { mHandler.post(new PostNotificationRunnable(r.getKey())); } }
EnqueueNotificationRunnable 在 enqueueNotificationInternal 方法中使用,enqueueNotificationInternal 方法是 INotificationManager 接口中定義的方法,它的實現(xiàn)在 NotificationManager 中:
public void notifyAsPackage(@NonNull String targetPackage, @Nullable String tag, int id, @NonNull Notification notification) { INotificationManager service = getService(); String sender = mContext.getPackageName(); try { if (localLOGV) Log.v(TAG, sender + ": notify(" + id + ", " + notification + ")"); service.enqueueNotificationWithTag(targetPackage, sender, tag, id, fixNotification(notification), mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } @UnsupportedAppUsage public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) { INotificationManager service = getService(); String pkg = mContext.getPackageName(); try { if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, fixNotification(notification), user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
一般發(fā)送一個通知都是通過 NotificationManager 或 NotificationManagerCompat 來發(fā)送的,例如:
NotificationManagerCompat.from(this).notify(1, builder.build());
NotificationManagerCompat 中的 notify 方法本質(zhì)上調(diào)用的是 NotificationManager:
// NotificationManagerCompat public void notify(int id, @NonNull Notification notification) { notify(null, id, notification); } public void notify(@Nullable String tag, int id, @NonNull Notification notification) { if (useSideChannelForNotification(notification)) { pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification)); // Cancel this notification in notification manager if it just transitioned to being side channelled. mNotificationManager.cancel(tag, id); } else { mNotificationManager.notify(tag, id, notification); } }
mNotificationManager.notify(tag, id, notification)
中的實現(xiàn):
public void notify(String tag, int id, Notification notification) { notifyAsUser(tag, id, notification, mContext.getUser()); } public void cancel(@Nullable String tag, int id) { cancelAsUser(tag, id, mContext.getUser()); }
串起來了,最終就是通過 NotificationManager 的 notify 相關(guān)方法發(fā)送通知,然后觸發(fā)了通知是否要觸發(fā)鈴聲/震動/LED 閃爍的邏輯,并且在這個邏輯中,發(fā)送出了無障礙消息。
總結(jié)
原文鏈接:https://juejin.cn/post/7117191453821894686
相關(guān)推薦
- 2022-08-19 Mac上Chrome瀏覽器快捷鍵匯總
- 2022-07-15 SQL?Server中的游標介紹_MsSql
- 2023-02-15 Python二進制轉(zhuǎn)化為十進制數(shù)學算法詳解_python
- 2023-10-27 解決webpack打包后圖片加載失敗的bug(適用于所有本地靜態(tài)資源)
- 2022-06-06 一文搞懂Redis中String數(shù)據(jù)類型_Redis
- 2022-04-01 K8s 創(chuàng)建Pod 的時候出現(xiàn) Error: Error response from daemon:
- 2022-06-27 Android音視頻開發(fā)之MediaCodec的使用教程_Android
- 2021-12-13 linux下AutoFs掛載服務安裝教程_Linux
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支