Android无障碍监听通知的实战过程
作者:自动化BUG制造器 发布时间:2022-01-25 16:37:18
监听通知
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 闪烁的逻辑,并且在这个逻辑中,发送出了无障碍消息。
来源:https://juejin.cn/post/7117191453821894686


猜你喜欢
- Redis中opsForValue()方法的使用1、set(K key, V value)新增一个字符串类型的值,key是键,value是值
- Viewpager 横向滑动效果系统就自带了很多种,比如这个效果 那如果做成竖屏的这种效果呢 。我百度过很多,效果都不是很好,有的代码特别多
- 目录安装Nginx准备SpringBoot应用添加网关现如今的项目开发基本都是微服务方式,导致一个系统中会有很多的服务,每个模块都对应着不同
- 死信队列:没有被及时消费的消息存放的队列,消息没有被及时消费有以下几点原因:1.有消息被拒绝(basic.reject/ basic.nac
- 内存映射文件是利用虚拟内存把文件映射到进程的地址空间中去,在此之后进程操作文件,就像操作进程空间里的地址一样了,比如使用c语言的 memcp
- Java 15 在 2020 年 9 月发布,虽然不是长久支持版本,但是也带来了 14 个新功能,这些新功能中有不少是十分实用的。Java
- 前言:阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消
- 无论是Android开发或者是其他移动平台的开发,ListView肯定是一个大咖,那么对ListView的操作肯定是不会少的,上一篇博客介绍
- 一、背景项目中要解析xml,由于Dom4j的诸多优点,我就用Dom4j解析xml,代码如下:public void readXML() {
- SpringBoot默认的页面映射路径(即模板文件存放的位置)为“classpath:/templates/*.html”。静态文件路径为“
- 本文实例为大家分享了Android Studio实现简易计算器App的具体代码,供大家参考,具体内容如下效果演示布局文件<?xml v
- 向右滑动返回,对于屏幕过大的手机来说,在单手操作时,是一个不错的用户体验,用户不必再费力的或者用另一个手去点击屏幕左上角的返回按钮或者,手机
- 本文实例为大家分享了Android实现房贷计算器的具体代码,供大家参考,具体内容如下fangdai(activity)package com
- 本文实例讲述了Java正则验证正整数的方法。分享给大家供大家参考,具体如下:package des;import java.util.reg
- 本文实例为大家分享了MVPXlistView上拉下拉展示的具体代码,供大家参考,具体内容如下抽基类package com.gs.gg.day
- 描述符描述符是你添加到那些定义中来改变他们的意思的关键词。Java 语言有很多描述符,包括以下这些:可访问描述符不可访问描述符应用描述符,你
- RestTemplate 是由 Spring 提供的一个 HTTP 请求工具,它提供了常见的REST请求方案的模版,例如 GET 请求、PO
- 多线程一直是工作或面试过程中的高频知识点,今天给大家分享一下使用 ThreadPoolTaskExecutor 来自定义线程池和实现异步调用
- springboot对压缩请求的处理最近对接银联需求,为了节省带宽,需要对报文进行压缩处理。但是使用springboot自带的压缩设置不起作
- 本文实例为大家分享了C#实现打字小游戏的具体代码,供大家参考,具体内容如下using System;using System.Drawing