Hi,你们好,我是承香墨影!java
Android 的辅助模式(Accessibility)功能很是的强大。基本上被获取到受权以后,能够监听手机上的任何事件,例如:屏幕点击、窗口的变化、以及模拟点击、模拟系统按键等等。node
比较常见的实际使用例子,就是通常应用市场,会推荐开启辅助模式,以便在安装 Apk 的时候,自动帮你点击“下一步”和“安装”按钮。还有个例子就是微信抢红包插件,也是基于它来实现的。android
Accessibility 的权限很是的高,基本上你受权开启某个别人提供的 AccessibilityService 以后,他就能够干不少事情而不让你知道,而这些是不须要 Root 权限的。因此通常小体量的产品,可能支持它并无什么用,由于信任度过低了,大部分用户根本不会打开。比较常见的就是一些工具类的 App,帮用户节省一些点击的时间。程序员
虽然不少时候,Accessibility 不会被用在商业产品上,可是这并不妨碍咱们使用 Accessibility 来作一些有意思的功能。小程序
辅助模式是能够支持第三方开发,也就是咱们能够按照文档对其进行支持,只要用户受权开启此服务,咱们就能够利用 Accessibility 提供的一些标准 Api 实现不少有意思的功能。微信
若是你想要使用辅助模式,你还须要以下步骤:ide
接下来咱们一步一步讲解这里的步骤和细节。工具
辅助模式,本质上仍是一个服务,咱们若是想要支持它,首先须要继承 AccessibilityService 这个类。布局
AccessibilityService 类提供了不少须要重写的方法,其中有两个是强制重写的:学习
public abstract void onAccessibilityEvent(AccessibilityEvent event);
public abstract void onInterrupt();
复制代码
当开启了某个 AccessibilityService 服务以后,系统会在该服务监听的事件发生的时候,回调它的 onAccessibilityEvent()
方法,并将该事件的信息当参数传递过去,若是你监听的事件足够多,它就会被频繁调用。
而 onInterrupt()
方法会在系统事件被打断的时候回调,也是会被频繁调用,通常咱们不须要作额外处理。
一般咱们只须要在 onAccessibilityEvent()
方法中,编写核心逻辑便可,其余的方法,只是辅助使用。
当建立一个 AccessibilityService 以后,咱们还须要对其进行一些基本的配置,不然在系统设置的“无障碍”中,是看不到咱们编写的服务的。
配置 AccessibilityService 有两种方式,
可是其实有一些属性是只能经过 XML 配置文件进行配置的,Java 代码只是让某一些配置项更灵活了而已,后面会细说。
一、xml 配置文件
想要使用 XML 配置文件,首先须要建立一个 res/xml 的目录,并在其内建立一个 xml 文件,文件名随意无要求,内部定义一个 accessibility-service
标签,在其中设定 AccessibilityService 的各项配置。例如我这里建立一个 accessibility_config.xml
的文件,后面会用到这个文件。
XML 配置 AccessibilityService 是咱们一个比较经常使用的配置方法,很是清晰且方便。
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackAllMask" android:accessibilityFlags="flagReportViewIds" android:canRetrieveWindowContent="true" android:packageNames="com.forwarding.wechat" android:description="@string/accessbility_desc" android:notificationTimeout="100" />
复制代码
例如上面就是一个常见的配置,若是没有特殊要求的话,直接复制过去,修改一些个别参数就可使用。
各项属性的含义:
node.getSource()
方法会调用失败。这些可配置的参数,系统都提供了可选的配置参数,正常不须要额外定制的时候,使用上面默认的配置便可,若是有定制须要,仍是查阅官方文档得到最全的介绍。
AccessibilityService:
https://developer.android.com/reference/android/accessibilityservice/AccessibilityService
二、Java 代码中动态配置
除了 XML 文件配置的方式,咱们还能够经过重写 AccessibilityService 的 onServiceConnected()
方法,咱们首先须要构建一个 AccessibilityServiceInfo 对象,经过它的标准 Api 进行配置,再使用 setServiceInfo()
方法将它设置给辅助模式。
onServiceConnected()
会在应用成功链接到此辅助服务的时候系统调用,通常在其中作一些初始化的操做便可。
override fun onServiceConnected() {
super.onServiceConnected()
var serviceInfo = AccessibilityServiceInfo()
serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK
serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
serviceInfo.notificationTimeout = 100
serviceInfo.packageNames = arrayOf("com.forwarding.wechat")
serviceInfo.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
setServiceInfo(serviceInfo)
}
复制代码
这里提供的例子,其实和前面使用 XML 配置的效果一直。推荐使用 XML 的配置方式,会更清晰且灵活,并且像 description 这种属性,在 AccessibilityServiceInfo 中,并无提供有效的相似 setDescription() 方法,这一点也确实是设计如此,毕竟服务没有运行,就不存在描述信息,在系统设置的“无障碍”页面,就读取不到。
也就是说即使是使用 setServiceInfo()
方法动态设置,也逃不脱使用 XML 配置文件的方式,我仍是强烈建议都使用 XML 配置文件的方式配置辅助服务,主要是为了省事。
本质上 AccessibilityService 仍是一个 Service,使用它咱们还须要在清单文件中配置它。
<service android:label="承香墨影的辅助工具" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:name=".WeForwardServer">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_config"/>
</service>
复制代码
这就是一个标准的 Service,其中 label
会被解析在系统设置的“辅助模式中显示”,而 intent-filter
和 meta-data
按照格式写就行了,没什么缘由。
meta-data
中,经过 android:resource
属性指定的就是咱们在第二步编辑的配置文件路径,指定它就行了。
以上步骤都完成以后,你就能够在系统的“无障碍”设置里,看到你编写的辅助模式的开关了。
默认为关闭状态,打开它的时候,你会收到一个警告弹窗,说明当前你正在开启一个无障碍的服务,它有哪些权限,这个对话框,咱们是控制不了的。
注意这里的 Title 就是清单文件里配置的 android:label
,而描述就是 XML 配置文件里的 android:description
信息。
当你在系统设置里,能看到此开关的时候,就说明你的辅助模式的服务,配置的没问题了,接下来就要思考如何使用它。
前面提到,在 AccessibilityService 里,咱们最须要关注的就是 onAccessibilityEvent()
方法,它会在咱们监听的事件发生的时候,被系统回调,并传递过来该事件相关的信息。
接下来咱们看看如何在 onAccessibilityEvent()
回调方法里,编写具体的逻辑。
接下来 "程序员思惟" 要上线了,把大象关冰箱,须要几步。咱们接下来来拆分辅助模式的步骤。
onAccessibilityEvent()
会被回调屡次,而咱们只须要处理咱们关心的事件,其余的忽略过滤掉便可。很简单对不对,接下来咱们细细的说下,这些步骤相关的方法和属性。
当 onAccessibilityEvent()
被系统回调的时候,同时也会传递过来一个 AccessibilityEvent 对象,它其中包含了不少与当前事件相关的信息,有兴趣能够看看源码,咱们这里只关注最须要的几个属性。
一、eventType 判断事件类型
经过 eventType 来判断事件的类型,咱们能够利用 getEventType()
方法获取到它。
这些事件都很好辨认,例如:TYPE_NOTIFICATION_STATE_CHANGED 是一个窗口 View 发生了变化,TYPE_VIEW_CLICKED 是某个 View 发生了一次点击事件等等。
二、packageName 判断事件发生的 App
经过 getPackageName()
方法,判断出事件发生在那个 App 里的。
三、className 判断当前发生事件的是那个类
经过 getClassName()
判断当前发生事件的是那个类,例如 页面的显示,className 可能指向一个 Activity,一个按钮的点击,className 可能指向的是一个 Button,这些都是根据实际场景区分的。
四、text 判断当前事件触发源上的 Text
经过 getText()
获取当前事件源的 text 属性,多是 TextView 的 Text,也多是 Activity 的 Label 属性,依然是根据实际状况区分。
通常咱们能够经过以上几种方式,猜想是不是咱们须要监听的事件,下一步就是咱们找到咱们要操做的源。
一般咱们是使用辅助模式去操做页面上的某个元素,那这一步,就是为了找到它。
在辅助模式下,页面上的每一个元素,其实都是一个个 AccessibilityNodeInfo 节点,它是一个相似树形的结构,其内和咱们真实 App 内的布局层级是一致的,可是并不能将它单纯的理解成一个 ViewTree。
既然是树形结构,咱们首先要获取到根节点的 NodeInfo,能够经过如下两个方式获取:
这两个方法都会返回一个 AccessibilityNodeInfo 对象。getSource()
是AccessibilityEvent 的方法,它可用的前提是前面配置 android:canRetrieveWindowContent
的时候,被设置为 True。因此我推荐使用 getRootInActiveWindow()
方法来获取。这两个方法仍是略微有些差别,有兴趣能够打断点看看信息,可是大多数状况下,对咱们使用者来讲是一致的。
得到根节点的 AccessibilityNodeInfo 以后,就能够经过它找到咱们想操做的关键节点,在 AccessibilityNodeInfo 中,提供了如下两个方法来找到关键节点。
一个是依赖 ViewId,另一个是依赖 Text 信息。
使用 ViewId 查找关键节点是稳妥的方案,而使用 Text 去查找,可能会找不到。
不管经过哪一种方式查找 关键节点 ,都是存在能找到多个 NodeInfo 的可能的,因此这两个方法干脆的都返回了一个 List<AccessibilityNodeInfo>
,因此须要咱们经过其余条件再过滤一遍,一般就是经过 Text 信息过滤。
var mNodeInfo = rootInActiveWindow
var listItem = mNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/lp")
for (item in listItem) {
if (item.text.toString().equals("承香墨影")){
nodeClick(item)
}
}
复制代码
若是是使用 findXxxByText()
的方法的话,还须要注意它实际上不是经过相似 ==
或者 equals()
的方法来查找子节点的,而是经过相似 contain()
的方式,因此只要节点的 text 属性包含查找的内容,都会被找到,这个咱们额外还须要增长判断条件。
若是这些方法都试过,仍是找不到关键节点,能够经过遍历的方式查找。
AccessibilityNodeInfo 既然是一个树状结构,也提供了咱们遍历树的方法。
经过 getChild()
和 getChildCount()
两个方法,咱们是能够对整个 ViewNodeTree 进行遍历,来找到咱们关注的关键节点,这是一个最后的方案,并不推荐使用。
辅助模式通常都是帮助咱们响应一些事件,而这些事件大致上,能够分为两类。
对于全局系统事件,其实咱们并不须要第二步找到的关键节点。AccessibilityService 提供了一个 performGlobalAction()
方法,咱们能够经过该方法,操做一些全局的系统事件,例如:模拟返回键点击、模拟 HOME 键点击、锁屏等等。
// 返回键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
// HOME键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
复制代码
这些事件被封装在 AccessibilityService 中,以 GLOBAL_
为前缀,看看属性说明就懂了。
除了全局系统事件以外,一般咱们就是想操做第二步拿到的关键节点。
在 AccessibilityNodeInfo 中,提供了一个 performAction()
的方法,能够经过该方法,对关键节点传递一个咱们须要的事件。
这些事件都被定义在 AccessibilityNodeInfo 中,以 ACTION_
为前缀定义。例如:ACTION_CLICK 是一个点击事件,ACTION_SET_TEXT 设置一个输入。
这里仅介绍一些比较常见的操做,更多的操做也是相似的使用方式。
1. View 的点击
找到关键节点以后,就能够发送 AccessibilityNodeInfo.ACTION_CLICK
模拟对这个 View 的点击操做。
可是有时候它是不生效的,主要缘由是由于你找到的这个关键节点,它的 isClickable()
为 false。
例如微信的这个公众号分享弹窗,若是咱们想要查找“发送给朋友”,其实最好的办法是找到这个 TextView 控件所表明的关键节点(NodeInfo),而后对它进行点击。而实际上这个 TextView 是不具备点击效果的,它的 isClickable()
为 false。
这个时候能够想一个折中的方案,去找关键节点(NodeInfo)的父节点,再去判断它是否可点击,可点击则点击它,不然继续向上找。
private fun nodeClick(node : AccessibilityNodeInfo?){
var clickNode = node;
while (clickNode!=null){
if(clickNode.isClickable){
clickNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break;
}
clickNode = node?.parent
}
}
复制代码
虽然 AccessibilityNodeInfo 其实也开放了 setClickable()
方法,可是我不建议操做它,有些时候会抛出一个异常,不太稳定。
2. EditText 输入文字
对 EditText 输入文字,最少须要两个参数,关键节点和输入的文字。这就须要用到 performAction()
的另一个重载方法,它容许额外在传递一个 Bundle 来指定参数。
private fun nodeSetText(node : AccessibilityNodeInfo?,text:String){
var argument = Bundle()
argument.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,text)
node?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,argument)
}
复制代码
全部支持定义的额外参数,都被定义在 AccessibilityNodeInfo 中,并以 ACTION_ARGUMENT_
为前缀定义。
3. ListView 的滚动
AccessibilityNodeInfo 其实只能操做当前屏幕下可见的 节点,因此碰上 ListView 或者 RecycleView 这种列表,就须要对它进行滚动。
滚动的事件有两种:
private fun nodeScrollList(node : AccessibilityNodeInfo?){
node?.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}
复制代码
一个前进一个后退,足够使用了。
在使用完 AccessibilityNodeInfo 以后,别忘了还须要调用 recycle()
方法,释放资源。
nodeInfo.recycle();
复制代码
辅助模式如何使用,到如今已经讲解的很是清楚了,后面基本上就是靠本身的想象力来作小功能了。
利用辅助模式,发挥想象力,你也能够作出不少有意思的功能。
公众号后台回复成长『成长』,将会获得我准备的学习资料,也能回复『加群』,一块儿学习进步;你还能回复『提问』,向我发起提问。
推荐阅读: