本文灵感来源:李跳跳真实好友 app
目录
- 无障碍服务(AccessibilityService)
- 模拟点击
- 组件ID & 组件文本
- 坐标
- 后台保活
- 效果图
- 存在缺陷
- 缺陷一
- 缺陷二
- 缺陷三
- 缺陷四
无障碍服务(AccessibilityService)
无障碍服务(AccessibilityService),是Google推出为了帮助残障用户使用 Android 设备和应用而推出的比较特殊的service
。通过无障碍服务,开发者可提供界面增强功能,来协助残障用户或可能暂时无法与设备进行全面互动的用户完成操作。
无障碍服务虽然不位于Android四大组件之中,但功能给产品带来的影响却不可小觑,例如曾经的自动抢红包功能,甚至因为太过火爆,而被某些手机厂商加入到系统功能之中,那时抢红包靠的就不是手速。其它类似使用无障碍服务开发的产品有微光盲人生活辅助平台
,讯飞心智无障碍助手软件
等。
在App开发过程中,为了使一个service
被视为无障碍服务,必须在清单的application
标签中添加一个service
标签、过滤器,为了与 Android 4.1 及更高版本兼容,还必须添加BIND_ACCESSIBILITY_SERVICE
权限。
<service
android:name=".MyAccessibilityService"
android:enabled="true"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config" />
</service>
从 Android 4.0 开始,需要在清单中添加一个引用配置文件的<meta-data>
标签👆,通过这个标签去引用设置的无障碍服务功能。
accessibility_config.xml
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:packageNames="com.example.android.apis"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"
/>
<!--description:服务的描述文字-->
<!--canPerformGestures:是否支持手势-->
<!--notificationTimeout:事件的发送间隔事件,单位毫秒-->
<!--canRetrieveWindowContent:是否允许读取窗口中的内容-->
<!--accessibilityFeedbackType:事件的反馈类型,暂时不知道用作啥-->
<!--settingsActivity:开启服务界面显示一个设置按钮,可以返回应用指定界面-->
<!--canRequestTouchExplorationMode:在这种模式下,被触摸的项目会被大声说出,用户界面可以被激活通过手势探索-->
<!--accessibilityEventTypes:可以接收的事件类型,例如界面变化、点击等。typeAllMask 接收所有,根据实际情况选择合适的类型,减少电量的消耗-->
配置的全部属性可点击链接前往查看👉AccessibilityService Styleable
创建MyAccessibilityService
类,并继承AccessibilityService
,实现其抽象方法。
class MyAccessibilityService : AccessibilityService() {
// 当系统检测到与无障碍服务指定的事件过滤参数匹配的 AccessibilityEvent 时执行
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
TODO("Not yet implemented")
}
// 当系统要中断服务正在提供的反馈时执行
override fun onInterrupt() {
TODO("Not yet implemented")
}
// 连接上无障碍服务时执行
override fun onServiceConnected() {
super.onServiceConnected()
}
// 可选,关闭无障碍服务时执行
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
}
启动无障碍服务的设置界面。
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
手动启动无障碍功能,onServiceConnected
方法会收到回调,这里可以写启动某个App的代码。
MainActivity.getContext()?.packageManager?.let {
startActivity(it.getLaunchIntentForPackage(packageName))
}
当系统检测到与无障碍服务指定的事件时,执行onAccessibilityEvent
,接下来开始模拟点击操作。
模拟点击
开始模拟点击前,请先安装好
Appium
,可前往观看本人编写的文章进行学习App自动化测试 —— Appium的使用
组件ID & 组件文本
要想拿到组件的ID怎么搞?启动安装好的Appium
,填写以下参数后启动Session。
{
"platformName": "操作系统",
"platformVersion": "操作系统版本号",
"deviceName": "设备名称",
"appPackage": "应用包名(文末有链接)",
"appActivity": "app启动的第一个活动(可阅读参考文档了解如何获取)",
"noReset": "true"
}
启动后会出现这样的界面,操作三个步骤后即可获取组件的ID。
Android的AccessibilityService
类,提供了一个getRootInActiveWindow()
方法,可以根据这个方法获取当前屏幕的根节点,接着就可以根据组件ID & 组件文本去查找到该点击的位置了。
// 根据ID查找组件
val iv_click = rootInActiveWindow.findAccessibilityNodeInfosByViewId("com.daimajia.gold:id/iv_check")
// 根据文本查找组件
val my = rootInActiveWindow.findAccessibilityNodeInfosByText("我的")
当然,不是所有的组件都有一个组件ID,也不是屏幕上只要有文字就一定能识别到并且触发其点击事件的。
坐标
有了以上两个查找界面组件,也可以让脚本自动化的差不多了,但还是有些许小瑕疵,例如,返回上一页的按钮,如果没有组件的ID和文字提示点击该按钮可以返回上一页,那么,以上的两个方法是无法做到点击该按钮的,如图:
而在Android提供的getRootInActiveWindow()
方法中,并没有提供以坐标定位点击的位置,但还是提供了一个叫GestureDescription
的类,让我们去处理坐标点击(滑动)的方案,使用该类就能解决掉组件没有ID或文字导致的无法识别到应该点击哪里的问题,但也因此诞生了一个新的问题:手机分辨率兼容。
后台保活
无障碍服务(AccessibilityService),是一个在后台运行的程序,既然涉及到后台,还得再提一次保活的问题。
在我的Socket 多人聊天室的实现 (含前后端源码讲解)(一)一文中使用的保活方式是在手机前台状态栏显示一个Notification
,除此之外,当然有其它的方式,比如说整个app的弹窗,比较有代表性的腾* 的手机管家、36* 手机助手。
在这里图方便,依旧采用前台状态栏显示Notification
的方式来实现保活,代码如下:
private fun startNotification() {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val notificationId: String = UUID.randomUUID().toString()
val notificationName: String = UUID.randomUUID().toString()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 高版本的模拟器或手机还需要开启渠道才能显示通知
val notificationChannel = NotificationChannel(
notificationId,
notificationName,
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(notificationChannel)
}
val builder = NotificationCompat.Builder(this, notificationId)
builder.setSmallIcon(R.mipmap.ic_launcher)
builder.setContentTitle(getString(R.string.app_name))
builder.setContentText("用来保活的Notification,勿删!!!")
builder.setAutoCancel(false)
notificationManager.notify(0, builder.build())
}
效果图
存在缺陷
缺陷一
无障碍服务只能手动启动,部分品牌手机启用无障碍服务还得输入密码,且每启动一次软件就要开启一次,操作不是很方便。此题无解。
缺陷二
定位所使用的单位是像素(px),不同的手机像素不同,会出现点击的位置出错的问题。关于该问题,目前想到的方法有二:
- 其一,整理出不同分辨率的手机点击的坐标,运行程序时,根据手机分辨率使用对应的坐标方案。
- 其二,图片相似度比对,该方案来自外挂三部曲(三) —— Android 图片相似度对比
缺陷三
当程序自动点击一次并进入下一个界面,会受到页面加载速度的影响,需要加上Thread.sleep(long millis)
来让线程暂缓执行,当几个页面中的某一个页面加载时间过长时,就会出现点击顺序错乱的问题,我现在还没想明白,为啥 李跳跳真实好友 这款app执行的速度那么快,有空真得反编译看一下人家是怎么写的才行,如果正在看文章的你知晓该如何解题,希望您能在评论区留下你的答案。
缺陷四
控件ID值可能会随着app的更新而发生变化,当findAccessibilityNodeInfosByViewId(String str)
找不到对应的控件ID时,脚本的执行计划可能会因此而错乱。
注意:无障碍服务本意是为了帮助残障用户使用 Android 设备和应用而设计出来的,请勿用于违法用途。Android开发者已经够少的了,再少下去就要绝迹了…
本文代码下载链接👉【代码下载】
参考文档
1、无障碍服务
2、查找app的第一个启动页
3、获取App包名的方法
4、创建自己的无障碍服务