Android无障碍监听通知的实战过程

目录
  • 监听通知
  • 无障碍服务监听通知逻辑
    • ToastPresenter
    • NotificationManagerService
      • PostNotificationRunnable
  • 总结

监听通知

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无障碍监听通知内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android 无障碍全局悬浮窗实现示例

    目录 无障碍添加 UI 配置分析 Type Flag LayoutInDisplayCutoutMode Android 无障碍的全局悬浮窗可以在屏幕上添加 UI 供用户进行快捷操作,可以展示在所有应用程序之上长期展示.另一方面,在一些自动化场景下,可以用来屏蔽用户行为,防止用户手动操作打断自动化流程. 无障碍添加 UI 无障碍服务添加 UI 十分简单,使用 LayoutInflater 在 AccessibilityService 的 onServiceConnected 添加一个 UI: /

  • Android实现自动点击无障碍服务功能的实例代码

    ps: 不想看代码的滑到最下面有apk包百度网盘下载地址 1. 先看效果图 不然都是耍流氓 2.项目目录 3.一些配置 build.gradle plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-android-extensions' } android { compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applic

  • Android无障碍监听通知的实战过程

    目录 监听通知 无障碍服务监听通知逻辑 ToastPresenter NotificationManagerService PostNotificationRunnable 总结 监听通知 Android 中的 AccessibilityService 可以监听通知信息的变化,首先需要创建一个无障碍服务,这个教程可以自行百度.在无障碍服务的配置文件中,需要以下配置: <accessibility-service ... android:accessibilityEventTypes="其他

  • Android编程监听APK安装与删除等过程的方法

    本文实例讲述了Android编程监听APK安装与删除等过程的方法.分享给大家供大家参考,具体如下: 软件下载后的一系列动作监听:先前是通过Service监听扫描获取状态,以后用这个方法测试使用 import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.widget.Toast; public class getBro

  • Android来电监听和去电监听实现代码

    我觉得写文章就得写得有用一些的,必须要有自己的思想,关于来电去电监听将按照下面三个问题展开 1.监听来电去电有什么用? 2.怎么监听,来电去电监听方式一样吗? 3.实战,有什么需要特别注意地方? 监听来电去电能干什么 1.能够对监听到的电话做个标识,告诉用户这个电话是诈骗.推销.广告什么的 2.能够针对那些特殊的电话进行自动挂断,避免打扰到用户 来电去电的监听方式(不一样的方式) 1.来电监听(PhoneStateListener) 来电监听是使用PhoneStateListener类,使用方式

  • Android 滑动监听RecyclerView线性流+左右划删除+上下移动

    废话不多说了,直接给大家贴代码了.具体代码如下所示: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_wid

  • Android ScreenLockReceiver监听锁屏功能示例

    本文实例讲述了Android ScreenLockReceiver监听锁屏功能.分享给大家供大家参考,具体如下: 监听屏幕锁屏状态(注册接受者--执行业务--注销接受者) public class AppLockService extends Service { private ActivityManager am; private KeyguardManager keyguardManager; private LockScreenReceiver receiver; @Override pu

  • Android 滑动监听的实例详解

    Android 滑动监听的实例详解 摘要: ScollBy,ScollTo是对内容的移动,view.ScollyBy是对view的内容的移动 view,ScollTo是对内容的移动(移动到指定位置),view.ScollyBy是对view的内容的移动(移动距离) 在次activity中,当手指点击TextView ,此时是ViewGroup 响应还是TextView响应呢? 代码实践: 在activity中重写onTouchEvent(): public boolean onTouchEvent

  • Android onKeyDown监听返回键无效的解决办法

     Android onKeyDown监听返回键无效的解决办法 当我们的Activity继承了TabActivity,在该类中重写onKeyDown是监听不到返回键的, 具体解决方法如下: 重写dispatchKeyEvent /** * 退出 */ @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getAc

  • Android编程监听网络连接状态改变的方法

    本文实例讲述了Android编程监听网络连接状态改变的方法.分享给大家供大家参考,具体如下: BroadcastReceiver public class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // TODO Auto-generated method stub //Toast.makeText(context, intent

  • Android ListView监听滑动事件的方法(详解)

    ListView的主要有两种滑动事件监听方法,OnTouchListener和OnScrollListener 1.OnTouchListener OnTouchListener方法来自View中的监听事件,可以在监听三个Action事件发生时通过MotionEvent的getX()方法或getY()方法获取到当前触摸的坐标值,来对用户的滑动方向进行判断,并可在不同的Action状态中做出相应的处理 mListView.setOnTouchListener(new View.OnTouchLis

  • Android 解决监听home键的几种方法

    Android 解决监听home键的几种方法 前言: 以下两种方法可以完美解决监听back键,home键,多任务键(最近任务键). 一.使用注册广播监听home键.多任务键 演示图 创建一个广播代码如下: class InnerRecevier extends BroadcastReceiver { final String SYSTEM_DIALOG_REASON_KEY = "reason"; final String SYSTEM_DIALOG_REASON_RECENT_APP

随机推荐