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

标签:android,无障碍,监听
0
投稿

猜你喜欢

  • 详解用RxJava实现事件总线(Event Bus)

    2022-02-13 16:43:18
  • Android中使用imageviewswitcher 实现图片切换轮播导航的方法

    2023-08-08 16:12:55
  • JAVA实现SOCKET多客户端通信的案例

    2023-08-26 19:10:35
  • Java 梳理总结关于static关键字常见问题

    2021-12-11 11:49:01
  • Mybatis实现增删改查

    2022-05-26 22:00:20
  • Spring boot集成Kafka消息中间件代码实例

    2022-11-06 21:53:48
  • Android Parcleable接口的调用源码层分析

    2023-07-09 20:43:48
  • 浅谈对c# 面向对象的理解

    2021-06-18 18:15:52
  • Android使用注解代替枚举节省系统内存开销的方法

    2021-09-28 18:46:06
  • C# 7.2中结构体性能问题的解决方案

    2022-08-12 23:04:26
  • Java 实战项目锤炼之校园宿舍管理系统的实现流程

    2023-10-15 07:00:58
  • C# 一个WCF简单实例

    2023-07-12 20:53:43
  • Java中的Map集合简单汇总解析

    2023-08-22 19:12:07
  • Android利用RecyclerView实现列表倒计时效果

    2023-09-04 05:29:27
  • java实现合并单元格的同时并导出excel示例

    2022-05-15 04:22:38
  • android 获取上一个activity返回值的方法

    2023-06-15 13:22:22
  • Springboot添加支付接口

    2023-06-21 00:10:20
  • 2020最新eclipse安装过程及细节

    2023-11-06 09:25:56
  • Java对zip,rar,7z文件带密码解压实例详解

    2023-11-29 05:08:32
  • 详解java 客户端链接不上redis解决方案

    2023-11-12 10:12:15
  • asp之家 软件编程 m.aspxhome.com