<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Android 中的 AccessibilityService 可以監聽通知資訊的變化,首先需要建立一個無障礙服務,這個教學可以自行百度。在無障礙服務的組態檔中,需要以下設定:
<accessibility-service ... android:accessibilityEventTypes="其他內容|typeNotificationStateChanged" android:canRetrieveWindowContent="true" />
然後在 AccessibilityService 的 onAccessibilityEvent 方法中監聽訊息:
override fun onAccessibilityEvent(event: AccessibilityEvent?) { when (event.eventType) { AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> { Log.d(Tag, "Notification: $event") } } }
當有新的通知或 Toast 出現時,在這個方法中就會收到 AccessibilityEvent 。
另一種方案是通過 NotificationListenerService 進行監聽,這裡不做詳細介紹了。兩種方案的應用場景不同,推薦使用 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.
從用法中可以看出一個關鍵資訊 -- TYPE_NOTIFICATION_STATE_CHANGED
,通過這個事件型別入手,發現它用於兩個類中:
ToastPresenter 的 trySendAccessibilityEvent 方法中,構建了一個 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); }
這個方法的呼叫在 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); // ... }
而這個方法的呼叫就是在 Toast 中的 TN 類中的 handleShow 方法。
Toast.makeText(this, "", Toast.LENGTH_SHORT).show()
在 Toast 的 show 方法中,獲取了一個 INotificationManager ,這個是 NotificationManagerService 在使用者端暴露的 Binder 物件,通過這個 Binder 物件的方法可以呼叫 NMS 中的邏輯。
也就是說,Toast 的 show 方法呼叫了 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 方法中,最後呼叫:
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 中根據 callback 是否為空產生了不同的 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); } }
兩者的區別是展示物件的不同:
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 呼叫 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);
也就是呼叫到了 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
時,會呼叫 handleShow(token)
這個方法裡面也就是會觸發 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)); } }
本章節最開始介紹到了 ToastPresenter 的 show 方法中會呼叫 trySendAccessibilityEvent 方法,也就是從這個方法傳送型別是 TYPE_NOTIFICATION_STATE_CHANGED
的無障礙訊息給無障礙服務的。
在通知流程中,是通過 NMS 中的 sendAccessibilityEvent 方法來向無障礙傳送訊息的:
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); }
這個方法的呼叫有兩處,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用來處理通知是否應該發出鈴聲、震動或閃爍 LED 的。省略無關邏輯:
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 的呼叫路徑有兩個:
handleRankingReconsideration 方法中 RankingHandlerWorker (這是一個 Handler)呼叫 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 方法中呼叫了 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 的構建和 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 介面中定義的方法,它的實現在 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(); } }
一般傳送一個通知都是通過 NotificationManager 或 NotificationManagerCompat 來傳送的,例如:
NotificationManagerCompat.from(this).notify(1, builder.build());
NotificationManagerCompat 中的 notify 方法本質上呼叫的是 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)
中的實現:
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 相關方法傳送通知,然後觸發了通知是否要觸發鈴聲/震動/LED 閃爍的邏輯,並且在這個邏輯中,傳送出了無障礙訊息。
到此這篇關於Android無障礙監聽通知的文章就介紹到這了,更多相關Android無障礙監聽通知內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45