Android Accessibility

fulvio-ambrosanio-733582-unsplash_meitu_1

公司启用钉钉打卡了,刚开始还挺不适应,总是忘记打卡。

所以就想着,这打卡能不能实现自动化,每天都要记住打卡这个动作,一点儿也不猿。

首先,分析一下,钉钉打卡,必须要在公司附近的范围内,其次,只能是拥有 GPS 定位的钉钉 APP 才行,所以,公司 WiFi 的连接断开,刚好可以作为上下班打卡的时机。

钉钉的打卡页面肯定不会允许第三方应用打开,因此要实现自动打卡功能,肯定需要模拟用户发出点击事件。

能模拟点击事件的,首先想到了Android 辅助功能


AccessibilityService

辅助功能(AccessibilityService)是 Android 系统提供给的一种服务,本身是继承 Service 类的。这个服务提供了增强的用户界面,旨在帮助残障人士或者可能暂时无法与设备充分交互的人们。

当然,现在 AccessibilityService 已经基本偏离了它设计的初衷。

借助 AccessibilityService ,可以实现对页面的监听及模拟点击控制等。


基本使用

使用 AccessibilityService 实际上只需要以下三步即可:

1.继承 AccessibilityService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class AutoPunchCardService : AccessibilityService(){
//可选。系统会在成功连接上你的服务的时候调用这个方法,在这个方法里你可以做一下初始化工作,
//例如设备的声音震动管理,也可以调用setServiceInfo()进行配置工作
override fun onServiceConnected() {
super.onServiceConnected()
LogUtils.d("onServiceConnected")
}
//必须。这个在系统想要中断AccessibilityService返给的响应时会调用。在整个生命周期里会被调用多次。
override fun onInterrupt() {
LogUtils.d("onInterrupt")
}
//通过这个函数可以接收系统发送来的AccessibilityEvent,
//接收来的AccessibilityEvent是经过过滤的,过滤是在配置工作时设置的。
override fun onAccessibilityEvent(event: AccessibilityEvent) {
LogUtils.d("事件--> $event.eventType ,app包名--> $event.packageName")
when (event.eventType) {
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED //收到通知栏消息
-> LogUtils.d("=== 收到通知栏消息")
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED //界面状态改变
-> LogUtils.d("=== 界面状态改变," + event.toString())
AccessibilityEvent.TYPE_VIEW_CLICKED //点击事件
-> LogUtils.d("=== 点击事件" + event.toString())
AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT //文本改变
-> LogUtils.d("=== 文本改变")
}//省略其他的一堆可以监听的事件
}
//可选。在系统将要关闭这个AccessibilityService会被调用。在这个方法中进行一些释放资源的工作
override fun onUnbind(intent: Intent?): Boolean {
LogUtils.d("onUnbind")
return super.onUnbind(intent)
}
}


2.新建配置文件

在资源目录 res 下新建 xml 文件夹,新建 accessibility_service_config.xml文件

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/accessibility_service_description"
android:packageNames="com.alibaba.android.rimet"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:settingsActivity="com.example.android.accessibility.ServiceSettingsActivity"/>

其中 description 是描述;

packageNames 是要监控的 APP 包名;

accessibilityEventTypes 指监控的的事件,typeAllMask / AccessibilityEvent.TYPES_ALL_MASK:全局事件响应


3.AndroidMainifest 中注册

1
2
3
4
5
6
7
8
9
10
11
12
13
<service
android:name=".AutoPunchCardService"
android:description="@string/accessibility_service_description"
android:label="@string/accessibility_service_label"
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_service_config"/>
</service>

这样基本上就算配置完成了,不过,想要运行起来,还得用户手动开启辅助设置。


相关方法

1.服务是否开启

AccessibilityService 的服务想要运行,就得让用户手动开启它,那如何判断开启呢?

这需要通过下面的方法:

1
2
3
4
5
6
7
8
9
10
11
private fun isAccessibilitySettingsOn(context: Context): Boolean {
val service = context.packageName + File.separator + AutoPunchCardService::class.java.canonicalName
val accessibilityEnabled = Settings.Secure.getInt(context.contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED)
val splitter = TextUtils.SimpleStringSplitter(':')
if (accessibilityEnabled != 1) return false
val value = Settings.Secure.getString(context.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) ?: return false
splitter.setString(value)
return splitter.contains(service)
}

2.跳转到无障碍设置界面

如果通过判断,发现用户没有开启,就得让用户去打开它, 无障碍设置界面 的设置一般非常深,用户难以到达,这时就得直接打开 无障碍设置界面 的设置页面了。

1
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))


模拟点击事件

经过上面的步骤,此时可以正常使用 AccessibilityService,现在来模拟点击事件。

首先,需要寻找界面元素信息,

1
AccessibilityNodeInfo rootNode = getRootInActiveWindow()

此方法可以获取当前 Activity 的根节点窗口信息,再通过下面两种方式获取具体的节点信息。

1
2
3
4
//通过文字,获取控件信息
List<AccessibilityNodeInfo> list = rootNode.findAccessibilityNodeInfosByText("工作");
//通过 id ,获取控件信息,注意 ID 的格式
List<AccessibilityNodeInfo> list = rootNode.findAccessibilityNodeInfosByViewId("com.alibaba.android.rimet:id/home_bottom_tab_button_work");

文本内容很容易获取,看界面就知道,但有时候文本控件不可点击,需要点击父控件,因此文本点击一般这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private fun click(viewText: String): Boolean {
val nodeInfo = rootInActiveWindow
try {//点击前滞留1s
Thread.sleep(1000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
if (nodeInfo == null) {
LogUtils.d("点击失败,rootWindow为空")
return false
}
val list = nodeInfo.findAccessibilityNodeInfosByText(viewText)
if (list.isEmpty()) {//没有该文字的控件
LogUtils.d("点击失败," + viewText + "控件列表为空")
return false
} else {
for (info in list) {
if (viewText == info?.text?.toString()) {
return onclick(info) //遍历点击
}
}
return false
}
}
private fun onclick(view: AccessibilityNodeInfo): Boolean {
if (view == null) {
LogUtils.d("node 为空无法点击")
return false
}
if (view.isClickable) {
view.performAction(AccessibilityNodeInfo.ACTION_CLICK)
LogUtils.d("view name" + view.className + "+点击成功")
return true
} else {
if (view.parent == null) {
return false
}
return onclick(view.parent)
}
}

ViewId 一般需要用到工具 Android Device Monitor 来查看,目前,这个工具在 AS 3.1中打不开了,

需要到 Android SDK/tools/monitor 运行,通过 Hierarchy View 查看 ID 后,需要注意 ID 是书写格式,前面有包名。
最后,模拟点击事件

1
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)

还可以模拟 Home,Back 键

1
2
3
4
5
6
//后退键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
//Home键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
//模拟左滑
performGlobalAction(AccessibilityService.GESTURE_SWIPE_LEFT);


后记

AccessibilityService 确实强大,网上一些微信抢红包,答题负责工具,都是借此完成。

但它不是万能的,钉钉上的打卡页面是 WebView ,因此无法完成节点寻找,更不可能模拟点击事件。

因此,想通过模拟点击来完成打卡, AccessibilityService 无法胜任,最后,还是得 Root 。

通过 Root 后执行 shell 命令来完成模拟点击事件。


设置页面的常量表

为了方便,这里列出设置界面所有的 Action 常量

常量字段 示意
ACTION_SETTINGS 系统设置界面
ACTION_APN_SETTINGS APN设置界面
ACTION_LOCATION_SOURCE_SETTINGS 定位设置界面
ACTION_AIRPLANE_MODE_SETTINGS 更多连接方式设置界面
ACTION_DATA_ROAMING_SETTINGS 双卡和移动网络设置界面
ACTION_ACCESSIBILITY_SETTINGS 无障碍设置界面
ACTION_SYNC_SETTINGS 同步设置界面
ACTION_ADD_ACCOUNT 添加账户界面
ACTION_NETWORK_OPERATOR_SETTINGS 选取运营商的界面
ACTION_SECURITY_SETTINGS 安全设置界面
ACTION_PRIVACY_SETTINGS 备份重置设置界面
ACTION_VPN_SETTINGS VPN设置界面,可能不存在
ACTION_WIFI_SETTINGS 无线网设置界面
ACTION_WIFI_IP_SETTINGS WIFI的IP设置
ACTION_BLUETOOTH_SETTINGS 蓝牙设置
ACTION_CAST_SETTINGS 投射设置
ACTION_DATE_SETTINGS 日期时间设置
ACTION_SOUND_SETTINGS 声音设置
ACTION_DISPLAY_SETTINGS 显示设置
ACTION_LOCALE_SETTINGS 语言设置
ACTION_VOICE_INPUT_SETTINGS 辅助应用和语音输入设置
ACTION_INPUT_METHOD_SETTINGS 语言和输入法设置
ACTION_USER_DICTIONARY_SETTINGS 个人字典设置界面
ACTION_INTERNAL_STORAGE_SETTINGS 存储空间设置的界面
ACTION_SEARCH_SETTINGS 搜索设置界面
ACTION_APPLICATION_DEVELOPMENT_SETTINGS 开发者选项设置
ACTION_DEVICE_INFO_SETTINGS 手机状态信息的界面
ACTION_DREAM_SETTINGS 互动屏保设置的界面
ACTION_NOTIFICATION_LISTENER_SETTINGS 通知使用权设置的界面
ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS 勿扰权限设置的界面
ACTION_CAPTIONING_SETTINGS 字幕设置的界面
ACTION_PRINT_SETTINGS 打印设置界面
ACTION_BATTERY_SAVER_SETTINGS 节电助手界面
ACTION_HOME_SETTINGS 主屏幕设置界面


参考

Android AccessibilityService使用注意

(AccessibilityService) Android 辅助功能笔记

Android Accessibility辅助功能类的学习

AccessibilityService从入门到出轨

微信检查被删好友(Android Accessibility 学习实践 )


坚持分享技术,但行好事,莫问前程 ~^o^~