公司启用钉钉打卡了,刚开始还挺不适应,总是忘记打卡。
所以就想着,这打卡能不能实现自动化,每天都要记住打卡这个动作,一点儿也不猿。
首先,分析一下,钉钉打卡,必须要在公司附近的范围内,其次,只能是拥有 GPS 定位的钉钉 APP 才行,所以,公司 WiFi 的连接断开,刚好可以作为上下班打卡的时机。
钉钉的打卡页面肯定不会允许第三方应用打开,因此要实现自动打卡功能,肯定需要模拟用户发出点击事件。
能模拟点击事件的,首先想到了Android 辅助功能 。
AccessibilityService
辅助功能(AccessibilityService)是 Android 系统提供给的一种服务,本身是继承 Service 类的。这个服务提供了增强的用户界面,旨在帮助残障人士或者可能暂时无法与设备充分交互的人们。
当然,现在 AccessibilityService 已经基本偏离了它设计的初衷。
借助 AccessibilityService ,可以实现对页面的监听及模拟点击控制等。
基本使用
使用 AccessibilityService 实际上只需要以下三步即可:
1.继承 AccessibilityService
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文件
<?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 中注册
<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 的服务想要运行,就得让用户手动开启它,那如何判断开启呢?
这需要通过下面的方法:
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.跳转到无障碍设置界面
如果通过判断,发现用户没有开启,就得让用户去打开它, 无障碍设置界面 的设置一般非常深,用户难以到达,这时就得直接打开 无障碍设置界面 的设置页面了。
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
模拟点击事件
经过上面的步骤,此时可以正常使用 AccessibilityService,现在来模拟点击事件。
首先,需要寻找界面元素信息,
AccessibilityNodeInfo rootNode = getRootInActiveWindow()
此方法可以获取当前 Activity 的根节点窗口信息,再通过下面两种方式获取具体的节点信息。
//通过文字,获取控件信息
List<AccessibilityNodeInfo> list = rootNode.findAccessibilityNodeInfosByText("工作");
//通过 id ,获取控件信息,注意 ID 的格式
List<AccessibilityNodeInfo> list = rootNode.findAccessibilityNodeInfosByViewId("com.alibaba.android.rimet:id/home_bottom_tab_button_work");
文本内容很容易获取,看界面就知道,但有时候文本控件不可点击,需要点击父控件,因此文本点击一般这么写:
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 是书写格式,前面有包名。
最后,模拟点击事件
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
还可以模拟 Home,Back 键
//后退键
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使用注意