Android 实现钉钉自动打卡

前言

  本文章旨在提供一种新的思路,在无需 Root 的情况下,实现自动化钉钉定时打卡,更多是为了自己方便而定制开发,所以很多功能的实现局限性较大
   MIUI 用户可以直接使用该 APP 实现自动考勤

1.需求分析

  公司考勤调整过后,需要使用钉钉一天打四次卡:08:30 前上班卡;12:00 后下班卡;13:30 前上班卡;18:00 后下班卡。防止中午午休时缺卡,现有如下需求:

到点自动启动钉钉进行打卡并息屏

  需求具体化,梳理上班、下班考勤流程( 钉钉默认 上班打卡 启用 极速打卡,而 下班打卡 则是 手动打卡 )

上下班打卡流程示意图

涉及功能点

  • 亮屏
  • 息屏
  • 屏幕解锁
  • 启动钉钉
  • 模拟用户操作
  • 保活

2.效果展示

到点唤醒屏幕并解锁,启动钉钉打卡后息屏,全程无手动操作

  图中展示的下班打卡效果,上班打卡效果则更为简单:唤醒屏幕并解锁,启动钉钉(触发极速打卡)后息屏。
  点击跳转下载安装包
安装包下载二维码

3.功能实现

3.1 亮屏

  当触发打卡功能时,首先需要判断屏幕是否处于亮屏状态,非亮屏状态则唤醒屏幕
  通过 PowerManager 实现,需要声明权限:

    <!-- 允许应用程序使用电源管理器的屏幕锁功能 -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />

  示例代码:

    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.配置清单注册监听器(点击跳转查看源码)

        <!--设备管理器-->
        <receiver
            android:name=".service.LockScreenDeviceAdminReceiver"
            android:description="@string/app_name"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_DEVICE_ADMIN">
            <meta-data
                android:name="android.app.device_admin"
                android:resource="@xml/lock_screen" />
            <intent-filter>
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
            </intent-filter>
        </receiver>

  其中 lock_screen 资源文件为 申请设备管理器的具体权限

<?xml version="1.0" encoding="UTF-8"?>
<!--该文件路径 res/xml/lock_screen.xml -->
<device-admin xmlns:android="http://schemas.android.com/apk/res/android" >
    <uses-policies>
        <!-- 锁定屏幕 -->
        <force-lock />
    </uses-policies>
</device-admin>

  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.配置清单注册无障碍服务(点击跳转查看源码)​​​​​​​

        <!--无障碍服务-->
        <service
            android:name=".service.SelfAccessibilityService"
            android:label="@string/accessibility_service"
            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/accessible_service_config" />
        </service>

  其中 accessible_service_config 资源文件为该 无障碍服务的具体配置

<?xml version="1.0" encoding="utf-8"?>
<!--该文件路径 res/xml/accessible_service_config.xml -->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_service_description"
    android:notificationTimeout="100"
    android:packageNames="com.android.systemui,com.alibaba.android.rimet" />

  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),从而实现保活。

杀死APP后,前台服务依然被无障碍服务唤醒

4.注意事项

  • 目前该 APP 仅适配了 MIUI 系统,仅支持 Android 7.0 或以上的 MIUI 系统
  • 目前支持的钉钉版本为 5.1.16
  • MIUI 系统下需要授予 APP 后台弹出界面 权限(应用详情页面手动授予)
  • 实现保活效果,需要授予 APP 自启动 权限(应用详情页面手动授予)
  • 默认手机有设置 锁屏密码,且是纯数字密码
  • 手机 负一屏 模式需设置为 标准模式
  • 可能会因为手机设置了 防误触,导致亮屏后无法解锁(当手机在口袋里的时候)
  • 钉钉内需将 消息 页面的 智能工作助理 置顶

5.开始使用

  1. 启动 APP,进入 设置 页面
  2. 点击第一项(设置“ 后台弹出界面 ”权限)下的 跳转设置 进入应用详情页面,分别授予 自启动 和 后台弹出界面 权限后回到 APP
  3. 点击第二项(获得钉钉启动权限)下的 启动钉钉以获取权限 ,允许启动钉钉后回到 APP
  4. 启用第三项(相关服务设置)下的 设备管理应用 和 无障碍服务 后回到 APP
  5. 点击 设置/更新锁屏密码 按钮,输入当前手机的 锁屏密码 并确定(仅保存本地)
  6. 添加考勤时间
  7. 回到 APP 首页,点击 测试 按钮,来到功能测试页面,逐一测试功能是否实现
  8. 测试无误后,回到 APP 首页,点击 启用 按钮,启用自动考勤服务

6.最后

  该 APP 为闲余时间做出来利于自己考勤用的,并未做过多的适配,目前仅适用于部分 MIUI 系统机型。如需适配别的手机系统,可自行下载源码调整。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

友情链接更多精彩内容