如何编写基于Android的AccessibilityService的钉钉自动打卡
第一节 缘由与准备
最近有时间空闲,闲来无事,想到使用钉钉打卡有时会迟到,所以周末的时候去看了相关网上资料,做了个demo。
材料:定时器,AccessibilityService
加工方案:使用定时器在签到签退期间内自启,通过AccessibilityService模拟点击:分为签到与签退两种情况。
签到正常流程:工作-》考勤打卡-》(判断是否弹出窗口-是:我知道了否跳过)-》签到。
签到迟到流程:工作-》考勤打卡-》迟到打卡。
签退正常流程:工作-》考勤打卡-》签退。
工艺难点:签到页中嵌套的是基于WebView的页面,一开始以为无法获取节点,想到通过屏幕中的位置去点击那块区域,查看官方文档发现有个方法getAccessibilityNodeProvider(),得到虚拟节点进行模拟点击。
第二节:热火朝天
技能点:判断应用状态,启动指定应用,自定义AccessibilityService控制模拟点击流程
判断应用状态:
public static boolean isBackground(Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses(); for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { if (appProcess.processName.equals(context.getPackageName())) { if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { Log.i("后台", appProcess.processName); return true; }else{ Log.i("前台", appProcess.processName); return false; } } } return false; }
启动指定应用:
public static void doStartApplicationWithPackageName(Context context, String packagename) { // 通过包名获取此APP详细信息,包括Activities、services、versioncode、name等等 PackageInfo packageinfo = null; try { packageinfo = context.getPackageManager().getPackageInfo(packagename, 0); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } if (packageinfo == null) { return; } // 创建一个类别为CATEGORY_LAUNCHER的该包名的Intent Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null); resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER); resolveIntent.setPackage(packageinfo.packageName); // 通过getPackageManager()的queryIntentActivities方法遍历 List<ResolveInfo> resolveinfoList = context.getPackageManager() .queryIntentActivities(resolveIntent, 0); ResolveInfo resolveinfo = resolveinfoList.iterator().next(); if (resolveinfo != null) { // packagename = 参数packname String packageName = resolveinfo.activityInfo.packageName; // 这个就是我们要找的该APP的LAUNCHER的Activity[组织形式:packagename.mainActivityname] String className = resolveinfo.activityInfo.name; // LAUNCHER Intent Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); // 设置ComponentName参数1:packagename参数2:MainActivity路径 ComponentName cn = new ComponentName(packageName, className); intent.setComponent(cn); context.startActivity(intent); } }
自定义AccessibilityService控制模拟点击流程:
1.获取点击控件对象(可以通过文本不推荐通过资源id方式点击)
工作布局的资源ID:
考勤打卡布局的资源ID(这个id是动态生成的8个都是):
考勤打卡布局的资源ID:
2窗口发生变化处理:
@Override public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) { if(isFinish){ return; } AccessibilityNodeInfo nodeInfo = getRootInActiveWindow(); if(nodeInfo == null) { Log.d(TAG, "rootWindow为空"); return ; } switch (accessibilityEvent.getEventType()){ //当窗口的状态发生改变时 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: if (index==1) { //主页点击工作 clickHome(nodeInfo,accessibilityEvent); Log.d(TAG,"工作"); } else if (index==2) { //工作tab点击考勤打卡 clickKaoQing(nodeInfo,accessibilityEvent); Log.d(TAG,"点击考勤打卡"); } else if (index==3) { //开始打卡 clickKaoQingBtn(nodeInfo,accessibilityEvent); Log.d(TAG,"打卡"); } break; } }
3后续处理:
发现考勤打卡页面是基于webview的h5页面,因此暂时没有好的方法,获取webview对象,以及获取虚拟节点。
不过如果可以获取到窗口下的webview对象,那么是可以获取页面的虚拟节点,进行模拟点击。打卡是没问题的,由于现在极速打卡的功能,打开应用自动签到。
参考:
原创