Android無障礙監聽通知的實戰過程

監聽通知

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:用於在應用程序進程中展示系統 UI 樣式的 Toast 。
  • NotificationManagerService:通知管理服務。

ToastPresenter

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 的無障礙消息給無障礙服務的。

NotificationManagerService

在通知流程中,是通過 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

這個東西是用來發送通知並進行處理的,例如提示和重排序等。

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無障礙監聽通知內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: