在手机的更多设置或者高级设置中,我们会发现有个无障碍的功能,很多人不知道这个功能具体是干嘛的,包括我们开发也很少接触这部分功能,以至于对这块不甚了解。前段时间在同事的安利下去了解了下这部分功能。在这里和大家浅谈下自己对这个功能的理解和部分运用。这边打算从 “是什么,为什么,怎么用,好不好”几个方面来说
是什么(应用场景,定义,作用)
为什么(原理及源码解析)
怎么用(如何开发无障碍服务)
怎么防(防止无障碍服务外挂的一些做法)
为了更好的介绍辅助功能服务,这里先看下应用场景
辅助功能服务一般用于操作自动化和辅助操作
● 操作自动化,通过辅助功能服务来代替用户执行连续性的操作,重复性的操作,或者特殊场景的操作(例如自动抢红包,自动点赞,自动回复,自动搜索更优惠商品)
WechatHelper https://github.com/coder-pig/WechatHelper
● 辅助操作,帮助无法和设备完全交互的用户(例如患有视力问题或正在忙而无法操作手机的用户)执行操作(例如talkback(视力低弱辅助),随选听读,语音操作)
无障碍服务是一种应用程序,给有残疾的用户或暂时无法与设备完全交互的用户提供了更好的无障碍用户交互功能。比如驾驶、照顾小孩或者在吵闹的派对上可能需要额外或者替代的交互反馈。
Android提供标准的辅助功能服务,包括TalkBack,开发人员可以创建和分发自己的服务。
在Android 4.0以前,Accessibility功能单一,仅能过单向获取窗口信息(获取输入框内容);
在Android 4.0及以后,Accessibility增加了与窗口元素的双向交互,可以操作窗口元素(点击按钮)。
在Android 4.0之前,无障碍服务事件在提供有关用户选择的用户界面控件的信息时,只提供了有限的上下文信息。在许多情况下,缺少的上下文信息可能对理解所选控件的含义至关重要。
Android 4.0通过基于视图层次结构组合可访问性事件,显着扩展了辅助功能服务可以获得的有关用户界面交互的信息量。
辅助功能服务可以代表用户执行操作,包括更改输入焦点和选择(激活)用户界面元素。在Android
4.1(API级别16)中,操作范围已扩展为包括滚动列表和与文本字段交互。
因为无障碍服务具有强大的界面监听能力和替代用户操作的能力,谷歌建议辅助功能服务仅应用于帮助残障用户使用Android设备和应用。
在我们开发者看来,无障碍服务显然能做的更多,例如微信抢红包应用,自动点赞,自动回复等
android 辅助功能google官方示例 https://github.com/googlesamples/android-BasicAccessibility
Q:为什么辅助功能可以监听用户的操作,界面变化,并根据需要进行反馈
A:辅助功能通过在后台中运行无障碍服务,通过AccessibilityEvent接收指定事件的回调,这样的事件表示用户在界面中的一些状态转换,例如:焦点改变了,一个按钮被点击,等等。
简单的说无障碍就是一个后台监控服务,当你监控的内容发生改变时,就会调用后台服务的回调方法
拿一个具体的例子来看,这是一个抢红包的外挂,把WeChat称作Target
APP,就是被监控的APP,当跳出来一个红包,触发了一个AccessibilityEvent,system_server中的AccessibilityManagerService将AccessibilityEvent分发给有AccessibilityService的APP,称为Accessibility
APP,这个AccessibilityService受到这个AccessibilityEvent后,会找到这个页面的Open
Button,模拟点击。
而在程序内部,这个过程其实就是三个类之间的交互 AccessibilityManager(AM):发送AccessibilityEventAccessibilityManagerService(AMS):分发事件AccessibilityService(AS):进行回应
看到AccessibilityManagerService这样的起名,就很容易联想到,这是一个Binder通信的过程,AM通过IAccessibilityManager(AMS的本地Binder)与AMS跨进程通信。AMS通过IAccessibilityManagerClient(AM的本地Binder)与AM通信。
AM是什么时候与AMS进行通信的,查看源码可以知道,AM的设计其实是一个单例模式,每个app进程都会有一个AM,而AM在构建的时候,即getInstance的时候,就会调用tryConnectToServiceLocked()的方法,连接AMS,得到AMS的代理后,把自己的代理也设置给AMS,这样AM就可以和AMS进行通信了
那么AS又是什么时候和AMS有联系的呢,这又是一个跨进程binder通信的过程。
无障碍服务是很强大的服务,需要我们进到设置中开启这个服务。绑定Service。
这里我们结合时序图进行说明。
Settings->Accessibility->enable(enableAccessibilityServiceLocked())
Settings->Accessibility->disable(disableAccessibilityServiceLocked())
Some RegisterBroadcastReceivers (registerBroadcastReceivers())
当用户在设置->无障碍里面选择了开启或关闭一个辅助功能,会导致一些系统状态会变化;Accessibility APP的安装状态会以BroadcastReceivers的方式会通知状态改变;还有其他的一些状态改变。这些变化最终会调用到AMS的onUserStateChangedLocked()方法。
RegisterBroadcastReceivers 很多情况简单列为这四种,安装app,更新app。强制关闭app,删除app。
onUserStateChangedLocked方法中,有比较多的方法调用,都是一些特定状态的更新,但我们这次只用关注updateServicesLocked这个方法,是处理无障碍服务绑定的
updateServicesLocked这个方法涉及到多个AMS类内部的集合,遍历如上图左边所见
这个方法会遍历ams中的mInstalledServices,看名字可以知道是已经安装的无障碍服务列表。
然后根据enableServices,判断是否已经启用,如果启用则通过mComponentNameToServiceMap判断是否为空,为空就会new一个AccessibilityServiceConnection, 调用其bindlock方法,绑定
未启用就通过mComponentNameToServiceMap判断是否为空,不为空就调用其unbindLocked(),解绑
接下来的过程其实就是跨进程的Binder通信。AS会通过onBind(Intent intent)这个函数返回一个IAccessibilityServiceClientWrapper对象给AccessibilityServiceConnection,这个对象就是AS的本地Binder,AccessibilityServiceConnection通过这个本地Binder去和AS通信。
然后AccessibilityServiceConnection会在onServiceConnected中调用方法,把自己的代理传到AS中。
讲完服务是什么时候绑定的,怎么建立通信的。接下来说下怎么监听事件的,AccessibilityEvent 是从哪里传递出来的
这里分为触发 分发 回调三个部分
先说触发,以点击为例
首先需要知道View都有接入 AccessibilityEventSource接口 分有两个方法。用于发送无障碍事件。
回归到图的部分
● View.java -- performClick() 被点击调用
● View.java -- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 发送类型为click的事件
● View.java -- sendAccessibilityEventInternal() 创建了一个AM的实例(app单例),判断是否enable状态,创建AM实例时会通过binder绑定远程AMS
● View.java -- sendAccessibilityEventUnchecked(AccessibilityEvent) 这个Event方法是根据type调用AccessibilityEvent.obtain(type)创建的
● View.java -- sendAccessibilityEventUncheckedInternal() 会调用onInitializeAccessibilityEvent()初始化一些event信息,比如className/packageName/source等,然后会调用getParent().requestSendAccessibilityEvent(this,event)将event分发给ParentView。requestSendAccessibilityEvent会不断往上调用getParent().requestSendAccessibilityEvent(this,event),ViewRootImpl.requestSendAccessibilityEvent不管ParentView是哪一个,最终会到View层次中的顶层,也就是ViewRootImpl
● ViewRootImpl -- requestSendAccessibilityEvent() 在这里就会调用AccessibilityManager.sendAccessibilityEvent(event);
● AccessibilityManager -- sendAccessibilityEvent(event); 获取远程的AMS 通过getServiceLocked()获取本地Binder,然后通过service.sendAccessibilityEvent(dispatchedEvent, userId)去调用AMS的sendAccessibilityEvent方法。
然后是分发,接着刚才的AccessibilityManagerService.java-- sendAccessibilityEvent()说
● AccessibilityManagerService.java --sendAccessibilityEvent()
● AccessibilityManagerService.java --notifyAccessibilityServicesDelayedLocked(),AMS会维护一个绑定AS的List(mBoundServices),List中每一个AccessibilityServiceConnection对应一个绑定的AS,因此遍历mBoundServices,获取其中的serviceconection 然后去到AccessibilityServiceConnection的notifyAccessibilityEvent()函数。
● AbstractAccessibilityServiceConnection.java-- notifyAccessibilityEvent()
● AbstractAccessibilityServiceConnection.java-- notifyAccessibilityEventInternal() 在notifyAccessibilityEventInternal()中,listener是AS的本地Binder(IAccessibilityServiceClient类型),最终是回调到了AS的onAccessibilityEvent()。到这里Dispatch的部分就结束了。
剩下回调,回调没什么好说的,就是调用方法。
而onAccessibilityEvent()方法是继承了AccessibilityService必须重写的。
1.辅助功能通过在后台中运行无障碍服务,通过AccessibilityEvent接收指定事件的回调
2.Accessibility服务框架类似于hook在Android View组件树中的一套实现,它并不是独立的一套机制,而是”寄生”在View的显示、事件分发的流程中。
● 功能实现依赖于ViewRootImpl, ViewGroup, View视图层级管理的基本架构。在视图变化时发出事件、当收到视图操作请求时也能够作出响应。
● system_server在实现该功能的过程中扮演着中间人的角色。当被监听APP视图变化时,APP首先会发出事件到system_server,随后再中转到监听者APP端。当监听者APP想要执行视图操作时,也是首先在system_server中找到对应的客户端binder proxy,再调用相应接口调用到被监听APP中。完成相关操作后,通过已经获取到的监听APP binder proxy句柄,直接binder call到对应的监听客户端。
3.无障碍权限十分重要,切记不可滥用,APP自身也需要有足够的安全意识,防止恶意应用通过该服务获取用户隐私信息
刚刚说到,为什么辅助功能可以监听用户的操作,界面变化,并根据需要进行反馈,那怎么使用呢,我们继续探究
继承AccessibilityService 类,重写onServiceConnected()方法、onAccessibilityEvent()方法和onInterrupt()方法
● onServiceConnected (可选)系统成功绑定该服务时被触发,也就是当你在设置中开启相应的服务,系统成功的绑定了该服务时会触发,通常我们可以在这里做一些初始化操作
● onUnbind(Intent intent) (可选)系统要关闭该服务是,将调用此方法。主要用来释放资源。
● onAccessibilityEvent(AccessibilityEvent event) 有关AccessibilityEvent事件的回调函数,系统通过sendAccessibiliyEvent()不断的发送AccessibilityEvent到此处
● onInterrupt 系统需要中断AccessibilityService反馈时,将调用此方法。AccessibilityService反馈包括服务发起的震动、音频等行为。
像其他Service服务一样,需要在AndroidManifest.xml中声明该服务.除此之外,该服务还必须配置以下几项,否则都会使该服务没有反应:
●配置
●声明BIND_ACCESSIBILITY_SERVICE权限,以便系统能够绑定该服务(4.0版本后要求)
●android:label:在无障碍列表中显示该服务的名字
配置用来接受指定类型的事件,监听指定package,检索窗口内容,获取事件类型的时间等等。其配置服务参数有两种方法:
● 方法一:安卓4.0之后可以通过meta-data标签指定xml文件进行配置
accessibilityEventTypes:表示该服务对界面中的哪些变化感兴趣,即哪些事件通知,比如窗口打开,滑动,焦点变化,长按等。具体的值可以在AccessibilityEvent类中查到,如typeAllMask表示接受所有的事件通知
accessibilityFeedbackType:表示反馈方式,比如是语音播放,还是震动
canRetrieveWindowContent:表示该服务能否访问活动窗口中的内容。也就是如果你希望在服务中获取窗体内容,则需要设置其值为true
description:对该无障碍功能的描述
notificationTimeout:接受事件的时间间隔,通常将其设置为100即可
packageNames:表示对该服务是用来监听哪个包的产生的事件
● 方法二:通过代码动态配置参数(setServiceInfo(AccessibilityServiceInfo))
AccessibilityServiceInfo类被用于配置AccessibilityService信息,该类中包含了大量用于配置的常量字段及用来xml属性,常见的有:accessibilityEventTypes,canRequestFilterKeyEvents,packageNames等等
https://developer.android.google.cn/reference/android/accessibilityservice/AccessibilityServiceInfo
https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityNodeInfo
https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityWindowInfo
https://developer.android.google.cn/reference/android/view/accessibility/AccessibilityEvent
在设置->辅助功能中便可以找到我们的服务.该服务默认处在关闭状态,需要手动开启.
也可以在app中跳转到设置页。
onAccessibilityEvent(AccessibilityEvent event)是该服务的核心方法,其中参数event封装来自界面相关事件的信息,比如我们可以获得该事件的事件类型,进而根据起类型选择不同的处理方式:
常见种类如图
获取了界面窗口变化后,这个时候就要获取控件的节点。整个窗口的节点本质是个树结构,通过以下操作节点信息
获取窗口节点(根节点)
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
获取指定子节点(控件节点)
//通过文本找到对应的节点集合
List
//通过控件ID找到对应的节点集合,如com.tencent.mm:id/gd
List
AccessibilityService 获取View的Id
获取id的一些方法
● layoutInspector
● Uiautomator viewer
随缘,各种原因,成功率很低。截图失败或者超时
● hierarchy view
相对可靠。
可以在命令行工具中,执行如下命令 得到相关文件
adb shell uiautomator dump
执行成功 系统会返回 UI hierchary dumped to: /mnt/sdcard/window_dump.xml
当然 这个文件地址也是可以更改的。 adb shell uiautomator dump [file]
当我们获取了节点信息之后,对控件节点进行模拟点击、长按等操作,AccessibilityNodeInfo类提供了performAction()方法让我们执行模拟操作,具体操作可看官方文档介绍,这里列举常用的操作
//模拟点击
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//模拟长按
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
//模拟获取焦点
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
//模拟粘贴
accessibilityNodeInfo.performAction(AccessibilityNodeInfo.ACTION_PASTE);
接下来用例子说明下怎么使用无障碍服务
平时我们点击安装包进行安装的时候,会弹出界面让我们确认。一般来说我们就是点击下一步,安装,然后安装完成后点击打开或者完成。
如果用无障碍服务的话,是可以在点击安装包的时候直接自动帮忙点了下一步,安装之类的。
具体做法就是在 onAccessibilityEvent()中,判断是否安装程序进程,然后进行下相应的点击动作,代码如右图所示 这边用的手机是小米,所以是小米的packgeInstaller的判断。如果是的话,就开始执行找控件点击的逻辑
小作业试验:完成自动回复的功能
首先我们知道一般软件,如微信接收信息时是有通知的,显示在通知栏中,所以我们的事件监听需要监听通知栏变化
通知的点击打开是可以进入到相应的聊天页面的。
如果想在聊天页面实现回复,就是在输入框中输入文字,然后点击发送就可以,剩下的回调由微信的程序完成
可以简单实现下,如果真的想通过无障碍服务实现自动回复,逻辑会严谨很多,首先判断依据需要跳转聊天页的依据就不会这么简单。然后是场景也有多种区分,如后台,息屏,跳转失败,代理清除通知等
回复的逻辑也会复杂很多,接入半自动回复机器人接口不用说,还有查找输入框控件失败,群聊,多条信息,是否需要执行自动回复的逻辑判断等
通过原理和源码查看,我们知道了无障碍服务是内嵌到整个android的view层类里的,但是还是有缺陷的
如通过Onclick实现的点击事件,可以捕捉到,但通过onTouchEvent实现的的点击事件,捕捉不到
如有获取节点文字的方法,但没有提供获取图片数据的方法,导致外挂抢票实现验证码输入不太可能。
那么我们可以怎么防御呢,有以下一些做法
AccessibilityManager有提供获取安装了的无障碍服务和开启的无障碍的服务的方法。从而让我们可以知道用户有哪些无障碍服务,及运行的服务有哪些。帮助我们确定自动点击的外挂来源,从而在打开外挂后有一个警告
了解AccessibilityServices源码之后,我们知道其内部核心原理就是调用TextView的findViewsWithText方法。只要复写这个方法,就可以屏蔽文案检查
AccessibilityServices执行点击事件最终在调用View的mOnClickListener。可以利用onTouch代替onClick屏蔽服务调用点击的动作
某些微信红包插件会监控Notification的弹出,那么我们是否可以随意发送这样的Event出来,从而混干扰外挂插件的运行逻辑。这种做法大部分情况比较鸡肋,也看外挂同学的逻辑严不严谨
这个其实是继第一种方式后的操作,在收集好已知外挂的信息后,设立黑名单,遍历系统内部所有已安装的app,鉴别package name 和app name。执行相应处理
Ø官方介绍
https://developer.android.com/guide/topics/ui/accessibility/services
Øandroid 辅助功能google官方示例
https://github.com/googlesamples/android-BasicAccessibility
ØWechatHelper 微信助手
https://github.com/coder-pig/WechatHelper
ØAccessibilityService分析与防御
https://lizhaoxuan.github.io/2018/01/27/AccessibilityService%E5%88%86%E6%9E%90%E4%B8%8E%E9%98%B2%E5%BE%A1/