溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android無障礙監(jiān)聽通知怎么實現(xiàn)

發(fā)布時間:2022-07-08 09:54:34 來源:億速云 閱讀:195 作者:iii 欄目:開發(fā)技術

本篇內(nèi)容主要講解“Android無障礙監(jiān)聽通知怎么實現(xiàn)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Android無障礙監(jiān)聽通知怎么實現(xiàn)”吧!

監(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)聽通知邏輯

從用法中可以看出一個關鍵信息 -- TYPE_NOTIFICATION_STATE_CHANGED ,通過這個事件類型入手,發(fā)現(xiàn)它用于兩個類中:

  • ToastPresenter:用于在應用程序進程中展示系統(tǒng) 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 中根據(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 調用 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) 這個方法里面也就是會觸發(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 方法中會調用 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);
}

這個方法的調用有兩處,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用來處理通知是否應該發(fā)出鈴聲、震動或閃爍 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

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

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 接口中定義的方法,它的實現(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 方法本質上調用的是 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 相關方法發(fā)送通知,然后觸發(fā)了通知是否要觸發(fā)鈴聲/震動/LED 閃爍的邏輯,并且在這個邏輯中,發(fā)送出了無障礙消息。

到此,相信大家對“Android無障礙監(jiān)聽通知怎么實現(xiàn)”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關內(nèi)容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI