Android 基于 AccessibilityService 实现钉钉自动签到方案
在 Android 平台上利用 AccessibilityService 实现钉钉自动签名的技术方案。文章涵盖了从权限配置、Manifest 注册到核心代码实现的完整流程,包括通过广播接收器监听电话状态来唤醒应用,以及利用无障碍服务模拟点击操作的具体逻辑。内容修正了原有代码中的语法错误,补充了状态机管理、节点查找算法及常见调试问题,并强调了相关的安全与合规注意事项,为移动端自动化开发提供了实用的参考指南。

在 Android 平台上利用 AccessibilityService 实现钉钉自动签名的技术方案。文章涵盖了从权限配置、Manifest 注册到核心代码实现的完整流程,包括通过广播接收器监听电话状态来唤醒应用,以及利用无障碍服务模拟点击操作的具体逻辑。内容修正了原有代码中的语法错误,补充了状态机管理、节点查找算法及常见调试问题,并强调了相关的安全与合规注意事项,为移动端自动化开发提供了实用的参考指南。

在移动办公场景中,自动化操作需求日益增长。Android 系统提供的 AccessibilityService(无障碍服务)允许应用获取当前屏幕内容并模拟用户交互,是实现 UI 自动化测试和辅助功能的核心 API。本文将以钉钉自动签名为例,详细讲解如何利用 AccessibilityService 结合广播接收器,实现通过远程电话触发应用自动完成签到流程。
本方案主要包含三个核心模块:
BroadcastReceiver 监听手机状态(如来电),触发目标应用启动。AccessibilityService 获取窗口节点信息,模拟点击特定文本控件。在 AndroidManifest.xml 中,必须声明必要的权限和服务配置。由于涉及电话状态读取和无障碍服务绑定,需特别注意权限说明。
<!-- 读取手机状态权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 处理拨出电话权限 -->
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<!-- 绑定无障碍服务权限 -->
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
需要注册广播接收器用于监听电话状态,以及无障碍服务用于执行点击操作。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.dingsign">
<!-- 广播接收器:监听电话状态 -->
<receiver android:name=".PhoneReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
<!-- 无障碍服务 -->
<service
android:name=".DingService"
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_service_config" />
</service>
</manifest>
在 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:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_description"
android:notificationTimeout="100"
android:packageNames="com.alibaba.android.rimet" />
为了能够启动钉钉,我们需要一个通用的方法,根据包名查找并启动 Activity。
package com.example.dingsign;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
public class Utils {
public static void openApp(String packageName, Context context) {
PackageManager packageManager = context.getPackageManager();
PackageInfo pi = null;
try {
pi = packageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return;
}
Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null);
resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
resolveIntent.setPackage(pi.packageName);
java.util.List<ResolveInfo> apps = packageManager.queryIntentActivities(resolveIntent, 0);
ResolveInfo ri = apps.iterator().next();
if (ri != null) {
String className = ri.activityInfo.name;
Intent intent = (Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
(packageName, className);
intent.setComponent(cn);
context.startActivity(intent);
}
}
}
当检测到特定号码来电时,唤醒钉钉并开启无障碍服务。
package com.example.dingsign;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.util.Log;
public class PhoneReceiver extends BroadcastReceiver {
private static final String TAG = "PhoneReceiver";
private static boolean mIncomingFlag = false;
private static String mIncomingNumber = null;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_NEW_OUTGOING_CALL.equals(action)) {
mIncomingFlag = false;
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
Log.i(TAG, "call OUT:" + phoneNumber);
} else {
TelephonyManager tManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
(tManager.getCallState()) {
TelephonyManager.CALL_STATE_RINGING:
mIncomingNumber = intent.getStringExtra();
Log.i(TAG, + mIncomingNumber);
(mIncomingNumber != && mIncomingNumber.equals()) {
Utils.openApp(, context);
DingService.instance.setServiceEnable();
}
;
TelephonyManager.CALL_STATE_OFFHOOK:
(mIncomingFlag) {
Log.i(TAG, + mIncomingNumber);
}
;
TelephonyManager.CALL_STATE_IDLE:
(mIncomingFlag) {
Log.i(TAG, );
}
;
}
}
}
}
这是核心部分,通过遍历窗口节点找到特定文本并执行点击。
package com.example.dingsign;
import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class DingService extends AccessibilityService {
private static final String TAG = "DingService";
private boolean isFinish = false;
public static DingService instance;
private int index = 1; // 状态机索引
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (isFinish) return;
AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
if (nodeInfo == null) {
Log.w(TAG, "rootWindow 为空");
return;
}
switch (index) {
case :
OpenHome(event.getEventType(), nodeInfo);
;
:
OpenQianDao(event.getEventType(), nodeInfo);
;
:
doQianDao(event.getEventType(), nodeInfo);
;
:
;
}
}
{
(type == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
List<AccessibilityNodeInfo> homeList = nodeInfo.findAccessibilityNodeInfosByText();
(!homeList.isEmpty()) {
click();
index = ;
Log.i(TAG, );
}
}
}
{
(type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
List<AccessibilityNodeInfo> qianList = nodeInfo.findAccessibilityNodeInfosByText();
(!qianList.isEmpty()) {
click();
index = ;
Log.i(TAG, );
}
}
}
{
(type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
List<AccessibilityNodeInfo> case1 = nodeInfo.findAccessibilityNodeInfosByText();
(!case1.isEmpty()) {
click();
Log.i(TAG, );
}
List<AccessibilityNodeInfo> case2 = nodeInfo.findAccessibilityNodeInfosByText();
(!case2.isEmpty()) {
click();
Log.i(TAG, );
}
List<AccessibilityNodeInfo> case3 = nodeInfo.findAccessibilityNodeInfosByText();
(!case3.isEmpty()) {
Toast.makeText(getApplicationContext(), , Toast.LENGTH_SHORT).show();
click();
isFinish = ;
}
}
}
{
getRootInActiveWindow();
(nodeInfo == ) {
Log.w(TAG, );
;
}
List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(viewText);
(list.isEmpty()) {
Log.w(TAG, + viewText + );
;
} {
list.get();
onclick(view);
}
}
{
(view.isClickable()) {
view.performAction(AccessibilityNodeInfo.ACTION_CLICK);
Log.w(TAG, );
;
} {
view.getParent();
(parent == ) ;
onclick(parent);
}
}
{
.onServiceConnected();
Log.i(TAG, );
Toast.makeText(getApplicationContext(), , Toast.LENGTH_SHORT).show();
instance = ;
}
{
isFinish = ;
Toast.makeText(getApplicationContext(), , Toast.LENGTH_SHORT).show();
index = ;
}
{
}
}
onServiceConnected 不会回调。Thread.sleep(500))。Accessibility Inspector 等工具检查目标控件的 text 属性是否变更。使用无障碍服务进行自动化操作存在一定风险:
本文介绍了基于 Android AccessibilityService 实现自动化签到的完整流程。通过合理设计状态机和节点查找逻辑,可以灵活适配多种 UI 场景。开发者在实际应用中应注重代码健壮性与安全性,确保技术使用的正当性。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online