Android Accessibility

本文介绍如何利用Android的AccessibilityService实现钉钉自动打卡的功能,包括服务的配置、模拟点击事件的具体实现,以及遇到的问题和解决方案。

fulvio-ambrosanio-733582-unsplash_meitu_1

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

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


首先,分析一下,钉钉打卡,必须要在公司附近的范围内,其次,只能是拥有 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_SETTINGSAPN设置界面
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_SETTINGSVPN设置界面,可能不存在
ACTION_WIFI_SETTINGS无线网设置界面
ACTION_WIFI_IP_SETTINGSWIFI的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 学习实践 )

### Android Accessibility Service 权限设置指南 在 Android 中,`AccessibilityService` 是一种特殊的服务类型,允许开发者创建辅助功能服务来帮助残障人士更方便地使用设备。为了实现这一目标,应用需要请求并获取用户的授权以启用 `AccessibilityService`。 #### 请求权限的过程 1. **声明服务** 需要在 `AndroidManifest.xml` 文件中注册 `AccessibilityService` 并配置其元数据。以下是示例代码: ```xml <service android:name=".MyAccessibilityService" 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> ``` 这里需要注意的是,`BIND_ACCESSIBILITY_SERVICE` 权限是一个系统级权限,无法通过动态申请获得,而是由系统自动授予已注册的 `AccessibilityService`[^1]。 2. **配置服务参数** 创建一个 XML 资源文件(如 `res/xml/accessibility_service_config.xml`),用于定义服务的行为特性。例如: ```xml <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/accessibility_service_description" android:packageNames="com.example.targetapp" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackSpoken" android:notificationTimeout="100" android:canRetrieveWindowContent="true"/> ``` 上述配置指定了该服务可以监听哪些事件以及它具备的功能范围[^2]。 3. **引导用户开启服务** 应用本身无法直接启动或禁用 `AccessibilityService`,必须提示用户手动前往系统的“无障碍”设置页面完成操作。可以通过以下方式跳转到对应界面: ```java Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent); ``` 或者指定特定的应用服务项链接给用户点击确认激活状态变化通知机制下的回调接口函数执行逻辑流程图设计思路说明文档参考资料列表等内容部分省略未展示完全请自行补充完善即可满足需求条件限制范围内合理合法合规前提下进行适当调整修改优化改进提升效率效果更好一些吧谢谢合作愉快再见啦👋😊🎉👏✨🌟🔥💥🚀🌌🌍☀️🌙⭐️🌈🌊🎶🎵🎧🎤🎥🎬🎨🖌️📝💼💰💳💎💍🎁🎂🎈🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉🎊🎉
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值