每天上下班使用钉钉拍照打卡是个很烦人的事情,因为我经常会忘记打卡。而且每天要打开手机操作两次,这么机械化的事情,作为一个安卓开发工程师,难道就没有什么办法可以把它给自动化吗?答案当然是 Yes, we can!
00 实现方式的探讨
首先上网搜索了一番,果然我不是第一个觉得打卡这件事很麻烦的人。发现钉钉打卡其实还分很多种,比如公司 WIFI 打卡,如果你们使用的是这种打卡方式,那么钉钉其实是有一个快速打卡(相当于自动打卡)的功能,只要管理员开启这个功能就可以了,我们也就不需要折腾了。但是,很显然我们拍照打卡是不可能使用这种方案来解决的,于是又看了几篇拍照打卡的文章,发现实现方案大致分为两种:1)使用钉钉打卡的接口;2)模拟屏幕点击完成打卡。
第一种方案需要抓包获取到钉钉的打卡请求的接口,然后我们只要到点按时发起请求就完成打卡了,这种方案感觉难度比较高,而且请求接口也有可能发生变化,所以果断 PASS 了。第二种方案模拟屏幕点击,想了想感觉还是比较靠谱的,也比较符合我们的需求,我们只要把拍照打卡这一系列的点击屏幕的操作规划好就行了。于是乎,撸起袖子开干!
01 初步尝试
一开始,我参考了这位博主的方案:
监听闹钟广播,闹钟响起的时候,解锁,并打开钉钉 App,然后模拟屏幕点击,滑动等操作,最后关闭钉钉。
看似很完美的方案对不对,但是也有问题,那就是在我的 Nexus 6P (Android 8.0) 上,根本就没办法监听到闹钟广播好吗!查了下发现,8.0 以后隐式广播被禁止了,真是非常抱歉呢!
不过也有一些例外,看这里:Implicit Broadcast Exceptions,只有这些列出的隐式广播还是可以继续在 manifest 里注册的。看吧,谷歌爸爸也不是那么霸道呢~
既然闹钟广播监听不到,那就监听可以监听到的呗,比如:
1 2 3
| IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_TIME_TICK); registerReceiver(new AutoStartReceiver(), filter);
|
Intent.ACTION_TIME_TICK
是由系统发出的时间变化的广播(闹钟响起的时候同时也会触发),触发后每分钟发送一次。所以我们可以监听这个广播然后判断当前时间是否是打卡时间,是则执行打卡,否则就忽略。
这种方案经过我的初步尝试后的确可行,但是设置闹钟难免有点麻烦。后来想了想,既然只是自己用,那么干脆就用比较暴力的手段实现:保持 Service 运行,时间一到就执行打卡,嗯~(•̀ᴗ•́)و ̑̑
02 实现策略
首先说明下,我的实现方案肯定不是最好的,而且一些编码的方式也不推荐在实际的项目中使用。
我们使用一个 KeepRunningService
来保证应用始终能够在后台运行,最好把应用加入到 Greenify 的白名单中。Service 中只做一件事,注册监听 Intent.ACTION_TIME_TICK
的广播,然后把这个 Service 设置成前台 Service 并每 5 分钟运行一次来达到保持后台运行的目的。
通过 PunchReceiver
来运行 PunchService
,PunchService
就是实际进行打卡操作的地方,我们使用 IntentService
来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| public class PunchService extends IntentService {
@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);
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);
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 权限):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
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); }
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(); } }
|
打卡完成后会提示,而且主页面提供手动打卡的方式,基本上和自动打卡操作是一样的,点击后会直接打开钉钉进行打卡,以防如果自动打卡不起作用(服务被杀死)的情况。
项目地址:aJIEw/AutoPunchDing
当然这个实现方式还是很原始的,如果想要使用的话还是得自己改代码,比如点击位置以及打卡时间。另外,闹钟其实可以不设置,因为只要应用不被杀死就可以自动打卡,但是最好还是设置一下,因为根据我一个礼拜下来的使用体验来看,偶尔还是会不能自动打卡的。最理想的方式是快到打卡时间的时候把应用打开一下,但是要是能记得就不需要这个 app 了,所以还是设闹钟吧(•̀ᴗ•́)و ̑̑
参考文章:钉钉自动打卡的一种实现