前言
本文章旨在提供一种新的思路,在无需 Root 的情况下,实现自动化钉钉定时打卡,更多是为了自己方便而定制开发,所以很多功能的实现局限性较大
MIUI 用户可以直接使用该 APP 实现自动考勤
1.需求分析
公司考勤调整过后,需要使用钉钉一天打四次卡:08:30 前上班卡;12:00 后下班卡;13:30 前上班卡;18:00 后下班卡。防止中午午休时缺卡,现有如下需求:
到点自动启动钉钉进行打卡并息屏
需求具体化,梳理上班、下班考勤流程( 钉钉默认 上班打卡 启用 极速打卡,而 下班打卡 则是 手动打卡 )
涉及功能点
- 亮屏
- 息屏
- 屏幕解锁
- 启动钉钉
- 模拟用户操作
- 保活
2.效果展示
图中展示的下班打卡效果,上班打卡效果则更为简单:唤醒屏幕并解锁,启动钉钉(触发极速打卡)后息屏。
点击跳转下载安装包
3.功能实现
3.1 亮屏
当触发打卡功能时,首先需要判断屏幕是否处于亮屏状态,非亮屏状态则唤醒屏幕
通过 PowerManager 实现,需要声明权限:
示例代码:
private fun wakeUpScreen(context: Context, tag: String) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
//是否需要亮屏唤醒屏幕
if (!powerManager.isInteractive) {
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val wl = pm.newWakeLock(
PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_DIM_WAKE_LOCK,
tag
)
wl.acquire(60 * 1000L /*1 minutes*/)
wl.release()
}
}
3.2 息屏
通过 DevicePolicyManager 实现
1.自定义设备管理器启用状态监听器(点击跳转查看源码)
class LockScreenDeviceAdminReceiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
//TODO 设备管理器已启用
}
override fun onDisabled(context: Context, intent: Intent) {
super.onDisabled(context, intent)
//TODO 设备管理器已禁用
}
}
2.配置清单注册监听器(点击跳转查看源码)
其中 lock_screen 资源文件为 申请设备管理器的具体权限
3.判断是否启用设备管理器 / 启用设备管理器
/**
* 判断是否启用设备管理器
*/
fun isAdminActive(context: Context): Boolean {
val devicePolicyManager =
context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
return devicePolicyManager.isAdminActive(ComponentName(context, LockScreenDeviceAdminReceiver::class.java))
}
/**
* 申请启用设备管理器
*/
fun enableAdmin(context: Context){
val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
intent.putExtra(
DevicePolicyManager.EXTRA_DEVICE_ADMIN,
ComponentName(context, LockScreenDeviceAdminReceiver::class.java)
)
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "锁屏")
context.startActivity(intent)
}
4.使用设备管理器进行锁屏
/**
* 锁屏
*/
fun lockScreen(context: Context) {
val devicePolicyManager =
context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
if (devicePolicyManager.isAdminActive(
ComponentName(
context,
LockScreenDeviceAdminReceiver::class.java
)
)
) {
devicePolicyManager.lockNow()
}
}
3.3 屏幕解锁(通过模拟用户操作实现)
使用 AccessibilityService 模拟用户操作,实现解锁功能
基于笔者的手机系统 MIUI 12 20.7.9 示例,解锁流程为:唤醒屏幕 → 进入负一屏 → 点击“万能遥控”按钮 → 唤起解锁密码输入页面 → 输入解锁密码 → 完成解锁
1.自定义 AccessibilityService (点击跳转查看源码)
/**
* 自定义 AccessibilityService 重写 onAccessibilityEvent 方法
* 根据 AccessibilityEvent 的 eventType 监听系统变化动作:如通知变动、页面变动、点击、长按等
* 再通过 AccessibilityService 来分析当前页面节点,模拟用户操作:滑动、点击/长按指定按钮
*/
class SelfAccessibilityService : AccessibilityService() {
override fun onInterrupt() {
TODO("Not yet implemented")
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
//只会监听在 accessibility-service 的 android:packageNames 中声明的 packageName
val packageName = event!!.packageName.toString()
val eventType = event.eventType
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
//前台页面出现变动:回到桌面、启动新的页面等
if ("com.android.systemui" == packageName) {
//该页面属于系统页面
} else if ("com.alibaba.android.rimet" == packageName) {
//该页面属于钉钉
//当前页面节点信息
val nodeInfo = this.rootInActiveWindow
//目标 view id ,通过 uiautomatorviewer 工具分析布局可以知道第三方 APP 指定页面的 view 的 id、text 及 description
val aimsId = "com.alibaba.android.rimet:id/session_title"
//查询到结果节点集合
val list = nodeInfo
.findAccessibilityNodeInfosByViewId(aimsId)
for (n in list) {
val text = n.text
val description = n.contentDescription
//TODO 与目标 view 的 text、description 进行匹配,匹配成功得到指定 view 节点
//TODO 匹配成功后,调用 AccessibilityNodeInfo 的 performAction(int action) 方法执行相关操作:如点击、长按
}
}
}
}
}
2.配置清单注册无障碍服务(点击跳转查看源码)
其中 accessible_service_config 资源文件为该 无障碍服务的具体配置
3.跳转无障碍服务页面,让用户手动启用无障碍服务
/**
* 跳转系统无障碍服务设置页面
*/
fun openAccessibilityService(context: Context) {
context.startActivity(Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS))
}
在 更多已下载的服务 中找到当前 APP 提供的无障碍服务并启用
息屏、亮屏及解锁功能测试与效果展示(解锁流程的滑动点击及密码输入动作皆借助 AccessibilityService 实现)
3.4 启动钉钉
/**
* 通过指定包名打开APP
* 钉钉包名 com.alibaba.android.rimet
* LaunchHomeActivity
*/
fun openRimet(context: Context): Boolean {
val packageManager = context.packageManager
val pi: PackageInfo?
val packageName = "com.alibaba.android.rimet"
pi = try {
packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
//TODO 未安装钉钉
return false
}
val resolveIntent = Intent(Intent.ACTION_MAIN, null)
resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER)
resolveIntent.setPackage(pi.packageName)
val apps =
packageManager.queryIntentActivities(resolveIntent, 0)
val resolveInfo = apps.iterator().next()
if (resolveInfo != null) {
val className = resolveInfo.activityInfo.name
val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_LAUNCHER)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val cn = ComponentName(packageName, className)
intent.component = cn
context.startActivity(intent)
return true
} else {
//TODO resolveInfo = null
return false
}
}
3.5 保活
在系统资源管理监控越来越严格的现状下,同时为了避免杂草丛生的无良 APP 影响用户使用体验,提出保活这个需求,其实是 不合理 、 不推荐 和 不倡导 的。但确实有可以保活的方法,只是需要用户手动授予更高的权限。
此处基于笔者的手机系统 Android 10 MIUI 12 20.7.9 示例实现保活:
1.授予 APP 自启动权限
2.启用无障碍服务
当 APP 具有自启动权限后,其已启用的无障碍服务就不会随着 APP 被杀死而关闭(退出APP 或 最近任务页面滑除APP),从而实现保活。
4.注意事项
- 目前该 APP 仅适配了 MIUI 系统,仅支持 Android 7.0 或以上的 MIUI 系统
- 目前支持的钉钉版本为 5.1.16
- MIUI 系统下需要授予 APP 后台弹出界面 权限(应用详情页面手动授予)
- 实现保活效果,需要授予 APP 自启动 权限(应用详情页面手动授予)
- 默认手机有设置 锁屏密码,且是纯数字密码
- 手机 负一屏 模式需设置为 标准模式
- 可能会因为手机设置了 防误触,导致亮屏后无法解锁(当手机在口袋里的时候)
- 钉钉内需将 消息 页面的 智能工作助理 置顶
5.开始使用
- 启动 APP,进入 设置 页面
- 点击第一项(设置“ 后台弹出界面 ”权限)下的 跳转设置 进入应用详情页面,分别授予 自启动 和 后台弹出界面 权限后回到 APP
- 点击第二项(获得钉钉启动权限)下的 启动钉钉以获取权限 ,允许启动钉钉后回到 APP
- 启用第三项(相关服务设置)下的 设备管理应用 和 无障碍服务 后回到 APP
- 点击 设置/更新锁屏密码 按钮,输入当前手机的 锁屏密码 并确定(仅保存本地)
- 添加考勤时间
- 回到 APP 首页,点击 测试 按钮,来到功能测试页面,逐一测试功能是否实现
- 测试无误后,回到 APP 首页,点击 启用 按钮,启用自动考勤服务
6.最后
该 APP 为闲余时间做出来利于自己考勤用的,并未做过多的适配,目前仅适用于部分 MIUI 系统机型。如需适配别的手机系统,可自行下载源码调整。
- 项目使用 MVVM + Jetpack 架构 开发,参考博主 却把清梅嗅 的开源项目 MVVM-Architecture 部分代码搭建
- 源码及 Demo 地址:https://github.com/ziwenL/self-service
- 如有更好的见解或建议,欢迎留言