Android無障礙監(jiān)聽通知的實(shí)戰(zhàn)過程
監(jiān)聽通知
Android 中的 AccessibilityService 可以監(jiān)聽通知信息的變化,首先需要?jiǎng)?chuàng)建一個(gè)無障礙服務(wù),這個(gè)教程可以自行百度。在無障礙服務(wù)的配置文件中,需要以下配置:
<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") } } }
當(dāng)有新的通知或 Toast 出現(xiàn)時(shí),在這個(gè)方法中就會(huì)收到 AccessibilityEvent 。
另一種方案是通過 NotificationListenerService 進(jìn)行監(jiān)聽,這里不做詳細(xì)介紹了。兩種方案的應(yīng)用場景不同,推薦使用 NotificationListenerService 而不是無障礙服務(wù)。stackoverflow 上一個(gè)比較好的回答:
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.
無障礙服務(wù)監(jiān)聽通知邏輯
從用法中可以看出一個(gè)關(guān)鍵信息 -- TYPE_NOTIFICATION_STATE_CHANGED
,通過這個(gè)事件類型入手,發(fā)現(xiàn)它用于兩個(gè)類中:
- ToastPresenter:用于在應(yīng)用程序進(jìn)程中展示系統(tǒng) UI 樣式的 Toast 。
- NotificationManagerService:通知管理服務(wù)。
ToastPresenter
ToastPresenter 的 trySendAccessibilityEvent 方法中,構(gòu)建了一個(gè) 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); }
這個(gè)方法的調(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); // ... }
而這個(gè)方法的調(diào)用就是在 Toast 中的 TN 類中的 handleShow 方法。
Toast.makeText(this, "", Toast.LENGTH_SHORT).show()
在 Toast 的 show 方法中,獲取了一個(gè) INotificationManager ,這個(gè)是 NotificationManagerService 在客戶端暴露的 Binder 對(duì)象,通過這個(gè) Binder 對(duì)象的方法可以調(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ū)別是展示對(duì)象的不同:
TextToastRecord 因?yàn)?ITransientNotification 為空,所以它是通過 mStatusBar 進(jìn)行展示的:
@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; } }
這個(gè) callback 最在
Toast.show()
時(shí)傳進(jìn)去的 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 來傳遞了一個(gè)類型是 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; } } } };
而這個(gè) Handler 在處理 SHOW
時(shí),會(huì)調(diào)用 handleShow(token)
這個(gè)方法里面也就是會(huì)觸發(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 方法中會(huì)調(diào)用 trySendAccessibilityEvent 方法,也就是從這個(gè)方法發(fā)送類型是 TYPE_NOTIFICATION_STATE_CHANGED
的無障礙消息給無障礙服務(wù)的。
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); }
這個(gè)方法的調(diào)用有兩處,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用來處理通知是否應(yīng)該發(fā)出鈴聲、震動(dòng)或閃爍 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)用路徑有兩個(gè):
handleRankingReconsideration 方法中 RankingHandlerWorker (這是一個(gè) 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
這個(gè)東西是用來發(fā)送通知并進(jìn)行處理的,例如提示和重排序等。
PostNotificationRunnable 的構(gòu)建和 post 在 EnqueueNotificationRunnable 中。在 EnqueueNotificationRunnable 的 run 最后,進(jìn)行了 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 接口中定義的方法,它的實(shí)現(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ā)送一個(gè)通知都是通過 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)
中的實(shí)現(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ā)鈴聲/震動(dòng)/LED 閃爍的邏輯,并且在這個(gè)邏輯中,發(fā)送出了無障礙消息。
總結(jié)
到此這篇關(guān)于Android無障礙監(jiān)聽通知的文章就介紹到這了,更多相關(guān)Android無障礙監(jiān)聽通知內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Retrofit網(wǎng)絡(luò)請(qǐng)求和響應(yīng)處理重點(diǎn)分析講解
這篇文章主要介紹了Retrofit網(wǎng)絡(luò)請(qǐng)求和響應(yīng)處理重點(diǎn)分析,在使用?Retrofit發(fā)起網(wǎng)絡(luò)請(qǐng)求時(shí),我們可以通過定義一個(gè)接口并使用Retrofit的注解來描述這個(gè)接口中的請(qǐng)求,Retrofit會(huì)自動(dòng)生成一個(gè)實(shí)現(xiàn)該接口的代理對(duì)象2023-03-03Android 設(shè)置應(yīng)用全屏的兩種解決方法
本篇文章小編為大家介紹,Android 設(shè)置應(yīng)用全屏的兩種解決方法。需要的朋友參考下2013-04-04Android波紋擴(kuò)散效果之仿支付寶咻一咻功能實(shí)現(xiàn)波紋擴(kuò)散特效
這篇文章主要介紹了Android波紋擴(kuò)散效果之仿支付寶咻一咻功能實(shí)現(xiàn)波紋擴(kuò)散特效的相關(guān)資料,需要的朋友可以參考下2016-02-02Android如何實(shí)現(xiàn)NFC讀取卡片信息
這篇文章主要介紹了Android如何實(shí)現(xiàn)NFC讀取卡片信息問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11Flutter 實(shí)現(xiàn)進(jìn)度條效果
在一些上傳頁面炫酷的進(jìn)度條效果都是怎么實(shí)現(xiàn)的,今天小編通過本文給大家分享Flutter 一行代碼快速實(shí)現(xiàn)你的進(jìn)度條效果,感興趣的朋友一起看看吧2020-05-05Android獲取LinearLayout的寬度和高度示例代碼
這篇文章主要介紹了android獲取LinearLayout的寬度和高度,如果想直接獲取在布局文件中定義的組件的寬度和高度,可以直接使用View.getLayoutParams().width和View.getLayoutParams().height,本文結(jié)合示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08WheelView實(shí)現(xiàn)上下滑動(dòng)選擇器
這篇文章主要為大家詳細(xì)介紹了WheelView實(shí)現(xiàn)上下滑動(dòng)選擇器的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12android 獲取視頻,圖片縮略圖的具體實(shí)現(xiàn)
android 獲取視頻,圖片縮略圖的具體實(shí)現(xiàn),需要的朋友可以參考一下2013-06-06android實(shí)現(xiàn)藍(lán)牙文件發(fā)送的實(shí)例代碼,支持多種機(jī)型
這篇文章主要介紹了android實(shí)現(xiàn)藍(lán)牙文件發(fā)送的實(shí)例代碼,有需要的朋友可以參考一下2014-01-01