钉钉自动拍照打卡 App 的实现

code小生,一个专注 Android 领域的技术平台

公众号回复 Android 加入我的安卓技术群

作者:aJIEw
链接:https://www.jianshu.com/p/16cf3ba7e6ff
声明:本文已获aJIEw授权发表,转发等请联系原作者授权

每天上下班使用钉钉拍照打卡是个很烦人的事情,因为我经常会忘记打卡。而且每天要打开手机操作两次,这么机械化的事情,作为一个安卓开发工程师,难道就没有什么办法可以把它给自动化吗?答案当然是 Yes, we can!

00 实现方式的探讨

首先上网搜索了一番,果然我不是第一个觉得打卡这件事很麻烦的人。发现钉钉打卡其实还分很多种,比如公司 WIFI 打卡,如果你们使用的是这种打卡方式,那么钉钉其实是有一个快速打卡(相当于自动打卡)的功能,只要管理员开启这个功能就可以了,我们也就不需要折腾了。

但是,很显然我们拍照打卡是不可能使用这种方案来解决的,于是又看了几篇拍照打卡的文章,发现实现方案大致分为两种:1)使用钉钉打卡的接口;2)模拟屏幕点击完成打卡。

第一种方案需要抓包获取到钉钉的打卡请求的接口,然后我们只要到点按时发起请求就完成打卡了,这种方案感觉难度比较高,而且请求接口也有可能发生变化,所以果断 PASS 了。

第二种方案模拟屏幕点击,想了想感觉还是比较靠谱的,也比较符合我们的需求,我们只要把拍照打卡这一系列的点击屏幕的操作规划好就行了。于是乎,撸起袖子开干!

01 初步尝试

一开始,我参考了这位博主的方案:
https://www.jianshu.com/p/2b416ad0b949

监听闹钟广播,闹钟响起的时候,解锁,并打开钉钉 App,然后模拟屏幕点击,滑动等操作,最后关闭钉钉。

看似很完美的方案对不对,但是也有问题,那就是在我的 Nexus 6P (Android 8.0) 上,根本就没办法监听到闹钟广播好吗!查了下发现,8.0 以后隐式广播被禁止了,真是非常抱歉呢!

钉钉自动拍照打卡 App 的实现_第1张图片

不过也有一些例外,看这里:https://developer.android.com/guide/components/broadcast-exceptions,只有这些列出的隐式广播还是可以继续在 manifest 里注册的。看吧,谷歌爸爸也不是那么霸道呢~
既然闹钟广播监听不到,那就监听可以监听到的呗,比如:

IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_TICK);
registerReceiver(new AutoStartReceiver(), filter);

Intent.ACTION_TIME_TICK 是由系统发出的时间变化的广播(闹钟响起的时候同时也会触发),触发后每分钟发送一次。所以我们可以监听这个广播然后判断当前时间是否是打卡时间,是则执行打卡,否则就忽略。

这种方案经过我的初步尝试后的确可行,但是设置闹钟难免有点麻烦。后来想了想,既然只是自己用,那么干脆就用比较暴力的手段实现:保持 Service 运行,时间一到就执行打卡,嗯~(•̀ᴗ•́)و ̑̑

02 实现策略

首先说明下,我的实现方案肯定不是最好的,而且一些编码的方式也不推荐在实际的项目中使用。

钉钉自动拍照打卡 App 的实现_第2张图片 项目基本结构

我们使用一个 KeepRunningService 来保证应用始终能够在后台运行,最好把应用加入到 Greenify 的白名单中。Service 中只做一件事,注册监听 Intent.ACTION_TIME_TICK 的广播,然后把这个 Service 设置成前台 Service 并每 5 分钟运行一次来达到保持后台运行的目的。

通过 PunchReceiver 来运行 PunchService,PunchService 就是实际进行打卡操作的地方,我们使用 IntentService 来实现。

public class PunchService extends IntentService {

    // ...

    /**
     * 处理打卡,利用 adb 命令点击屏幕完成打卡
     * 不同屏幕坐标位置不同,可以在开发者选项中开启查看屏幕坐标:Developer options -> Input -> Pointer location
     */

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        if (!timeForPunch()) {
            stopSelf();
            return;
        }

        Log.d(this.getClass().getSimpleName(), "onHandleIntent: start punching...");

        // 唤醒屏幕
        wakeUp();
        // 上滑解锁
        swipe("720""2320""720""1320");
        SystemClock.sleep(1000);

        // 输入 PIN 码解锁
        inputPinIfNeeded();
        SystemClock.sleep(3000);

        showToast("打开钉钉");
        startAppLauncher(DD_PACKAGE_NAME);
        SystemClock.sleep(10000);

        showToast("点击中间菜单");
        clickXY("700""2325");
        SystemClock.sleep(5000);

        showToast("点击考勤打卡");
        clickXY("540""1800");
        SystemClock.sleep(10000);

        showToast("点击打卡");
        clickXY("700", punchPositionY);
        SystemClock.sleep(5000);

        showToast("点击拍照");
        clickXY("710""2280");
        SystemClock.sleep(8000);

        showToast("点击 OK");
        clickXY("710""2281");
        SystemClock.sleep(5000);

        showToast("退出钉钉");
        stopApp(DD_PACKAGE_NAME);

        startAppLauncher(getPackageName());
        SystemClock.sleep(3000);

        // 更新 UI
        String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date());
        EventBus.getDefault().post(new PunchFinishedEvent(punchType, currentTime));
        Log.d(this.getClass().getSimpleName(), "onHandleIntent: punch finished");

        close();

        stopSelf();
    }        
}

以上是核心代码,点击以及滑动操作都是通过 adb 命令完成的(需要 root 权限):

    /**
     * 滑动屏幕
     */

    public static void swipe(String x1, String y1, String x2, String y2) {
        String cmd = String.format("input swipe %s %s %s %s \n", x1, y1, x2, y2);
        exec(cmd);
    }

    /**
     * 点击
     */

    public static void clickXY(String x, String y) {
        Log.d(AppUtil.class.getSimpleName(), "clickXY: " + x + ", " + y);
        String cmd = String.format("input tap %s %s \n", x, y);
        exec(cmd);
    }

    /**
     * 执行 ADB 命令
     */

    public static void exec(String cmd) {
        try {
            if (os == null) {
                os = Runtime.getRuntime().exec("su").getOutputStream();
            }
            os.write(cmd.getBytes());
            os.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

打卡完成后会提示,而且主页面提供手动打卡的方式,基本上和自动打卡操作是一样的,点击后会直接打开钉钉进行打卡,以防如果自动打卡不起作用(服务被杀死)的情况。

钉钉自动拍照打卡 App 的实现_第3张图片 打卡完成

项目地址:https://github.com/aJIEw/AutoPunchDing

当然这个实现方式还是很原始的,如果想要使用的话还是得自己改代码,比如点击位置以及打卡时间。另外,闹钟其实可以不设置,因为只要应用不被杀死就可以自动打卡,但是最好还是设置一下,因为根据我一个礼拜下来的使用体验来看,偶尔还是会不能自动打卡的。最理想的方式是快到打卡时间的时候把应用打开一下,但是要是能记得就不需要这个 app 了,所以还是设闹钟吧(•̀ᴗ•́)و ̑̑

参考文章:钉钉自动打卡的一种实现
https://www.jianshu.com/p/2b416ad0b949

钉钉自动拍照打卡 App 的实现_第4张图片

分享技术我是认真的

你可能感兴趣的:(钉钉自动拍照打卡 App 的实现)