===============================================php
[TOC]java
有天早晨下大雨,小编虽然出门早却仍是路上堵的迟到了,心中一句XXX崩腾而过啊,这月全勤又没了,无奈之余想起既然技术能解决一切,那能不能搞个自动打卡的功能(好像有点做弊的嫌疑...哈O(∩_∩)O哈哈~),这样之后就不用在考虑会迟到了!因而一个邪恶的程序就诞生了.node
大体须要准备如下东西:android
项目的核心在于利用程序模拟人工打卡操做,须要用到android模拟点击功能的相关api,目前比较经常使用的黑科技主要是如下两种:web
AccessibilityService原本是作一些辅助功能的,提供了一系列的事件回调,帮助咱们指示一些用户及界面的状态变化,主要给残障人群提供帮助.手机上的全部操做都会经过onAccessibilityEvent方法返回,咱们能够利用该原理作到模拟点击咱们须要的操做程序. 不过,如今AccessibilityService已经基本偏离了它设计的初衷,至少在国内是这样,愈来愈多的App借用AccessibilityService来实现了一些其它功能,甚至是灰色产品。json
基于UIAutomation的用户界面自动化测试框架,能够跨应用工做,谷歌亲生的. UIAutomation在Android4.3发布时有了新版本,官方简介 Android4.3以前:使用inputManager或者更早的WindowsManager来注入KeyEventapi
固然,除了以上两种,还有其余的一些能实现模拟点击的框架,这里我就不一一赘述了,今天咱们要用的就是利用AccessibilityService 辅助功能来实现咱们的自动打卡功能.bash
1.继承AccessibilityService类,监听手机运行状态信息服务器
public class MainAccessService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
//手机的全部操做信息都会经过这个方法回调
}
@Override
public void onInterrupt() {
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
}
复制代码
2.配置AccessibilityService,建立accessibility_service_config.xml文件架构
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" //过滤全部时间 android:accessibilityFlags="flagReportViewIds" //辅助服务额外的flag信息 android:accessibilityFeedbackType="feedbackSpoken"//事件的反馈类型 android:notificationTimeout="100" //通知超时时间 android:canRetrieveWindowContent="true" //是否能够获取窗口内容 />
复制代码
3.AndroidManifest引用建立的配置文件(如下是配置必须)
<service android:name=".MainAccessService"
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/accessibility_service_config"/>
</service>
复制代码
4.在设置中打开辅助功能服务
检查辅助服务是否开启
private void openAccessSettingOn(){
if (!isAccessibilitySettingsOn(getApplicationContext())) {
Toast.makeText(getApplicationContext(), "请开启辅助服务", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
}
}
private boolean isAccessibilitySettingsOn(Context mContext) {
int accessibilityEnabled = 0;
// TestService为对应的服务
final String service = getPackageName() + "/" + MainAccessService.class.getCanonicalName();
// com.z.buildingaccessibilityservices/android.accessibilityservice.AccessibilityService
try {
accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
if (accessibilityEnabled == 1) {
String settingValue = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
if (settingValue != null) {
mStringColonSplitter.setString(settingValue);
while (mStringColonSplitter.hasNext()) {
String accessibilityService = mStringColonSplitter.next();
if (accessibilityService.equalsIgnoreCase(service)) {
return true;
}
}
}
}
return false;
}
复制代码
配置好AccessibilityService服务后,接下来咱们就能够在onAccessibilityEvent方法中写咱们自动化脚本的逻辑了.具体流程看第一节中的图.
AccessibilityNodeInfo node=getRootInActiveWindow();
if (node == null || !Comm.launcher_PakeName.equals(node.getPackageName().toString())) {
throw new Exception("程序不在初始化启动器页面,抛出异常");
}
复制代码
注意上面的手动异常和下面全部的手动抛出异常到最后是会有大做用的,后面会讲到.
AccessibilityNodeInfo node=getRootInActiveWindow();
int m = 10;
while (m > 0) {
LogUtil.D("循环--" + node);
if (node != null && Comm.dingding_PakeName.equals(node.getPackageName().toString())) {
node = getRootInActiveWindow(); //刷新根页面节点
LogUtil.D("已进入app" + node);
break;
} else {
startApplication(getApplicationContext(), Comm.dingding_PakeName);
}
sleepT(1000); //1秒钟启动一次
if (node != null) {
node = refshPage();
}
m--;
}
if (m <= 0) {
throw new Exception("进入钉钉主页异常");
}
复制代码
这里我用了10次循环去尝试启动钉钉,,假如10次以后都没有进入钉钉或者已进入钉钉,都将抛出异常,这次脚本终止.(目的是防止出现启动时卡死,致使脚本也卡死)
经过Android SDK的uiautomatorviewer工具(在tools文件夹下,须要手机root,studio的sdk可能和elipse的不一样),查看页面的节点信息,以下图:
能够获得底部绝对布局的资源id是com.alibaba.android.rimet:id/home_bottom_tab_root,并且这个id是惟一的,也就是说咱们只要找到这个节点的资源id,就表明已经进入了钉钉程序的主页了.
具体代码:
String resId="com.alibaba.android.rimet:id/home_bottom_tab_root";
AccessibilityNodeInfo info=getRootInActiveWindow();
List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByViewId(resId);
if(list==null||list.size()==0){
throw new Exception("已进入app,未找到主页节点");
}
复制代码
到这一步,咱们程序已进入钉钉主页,接下来须要进入考勤打卡所在的工做页面 在底部选项卡中,找到工做按钮布局所在的资源id(com.alibaba.android.rimet:id/home_bottom_tab_button_work),点击工做页按钮,进入工做页,以下图:
具体代码
String resId="com.alibaba.android.rimet:id/home_bottom_tab_button_work";
AccessibilityNodeInfo info=getRootInActiveWindow();
List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByViewId(resId);
if(list==null||list.size()==0){
throw new Exception("已进入主页,未找到工做页按钮");
}else{
list.get(0).performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
复制代码
到这一步,咱们程序默认已经在工做页面了,接下来须要作的就是点击考勤打卡选项,进入考勤页面. 这里有些许的复杂,由于不能直接找到考勤打卡所在布局的id,只能先查找其所在的父布局的id(com.alibaba.android.rimet:id/oa_fragment_gridview),而后再找到考勤打卡的节点.
具体代码:
String resId="com.alibaba.android.rimet:id/oa_fragment_gridview";
AccessibilityNodeInfo info=getRootInActiveWindow();
List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByViewId(resId);
if(list!=null||list.size()!=0){
AccessibilityNodeInfo node = list.get(0);
if (node != null || node.getChildCount() >= 8) {
node = node.getChild(7);
if (node != null) { //已找到考勤打卡所在节点,进行点击操做
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}else{
throw new Exception("已进入工做页,但未找到考勤打卡节点");
}
}else{
throw new Exception("已进入工做页,但未找到考勤打卡节点");
}
}else{
throw new Exception("已进入工做页,但未找到相关节点");
}
复制代码
到这一步,咱们程序认为已经进入了考勤打卡页面了,接下来咱们须要再确认一下目前所在节点是否是考勤打卡页面的节点. 这个页面是一个webview页面,因此判断是否已进入考勤打卡界面,咱们只要找到了webview布局的一个惟一资源id标识便可(com.alibaba.android.rimet:id/webview_frame),
代码:
String resId="com.alibaba.android.rimet:id/webview_frame";
AccessibilityNodeInfo info=getRootInActiveWindow();
List<AccessibilityNodeInfo> list = info.findAccessibilityNodeInfosByViewId(resId);
if(list==null||list.size()==0){
throw new Exception("进入考勤打卡页面异常");
}
复制代码
到这一步,程序已确认进入考勤打卡页面,能够开始执行打卡操做.按照咱们一些的步骤,打卡操做只须要你找到相应的打卡按钮节点,而后经过节点的点击操做接口,可是很不幸的是,因为考勤打卡页面时webview页面,咱们不能定位到详细的打卡按钮所在的节点(准确来讲有时能够,有时不能够,并且这状况发生在同一台手机上,差点把小编折腾死,只能用最坏状况操做了),由于咱们根本找不到他的资源id,咱们惟一能找到的只能是他的父节点(com.alibaba.android.rimet:id/webview_frame),而后并没卵用!
不过方法老是有的! 既然咱们不能定位节点,但咱们能够定位坐标啊,恰好tap命令能够模拟点击屏幕坐标!!!瞬间感受本身是个天才!!
咱们只须要找到上班打卡和下班打卡两个按钮所在的坐标(不一样分辨率的手机会有不一样),而后使用adb命令直接模拟点击便可!
点击坐标方法
public static void clickXy(String x,String y){
String cmd = "input tap "+x+" "+y ;
try {
execRootCmdSilent( cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
/** * 执行命令但不关注结果输出 */
private static int execRootCmdSilent(String cmd) {
int result = -1;
DataOutputStream dos = null;
try {
Process p = Runtime.getRuntime().exec("su");
dos = new DataOutputStream(p.getOutputStream());
dos.writeBytes(cmd + "\n");
dos.flush();
dos.writeBytes("exit\n");
dos.flush();
p.waitFor();
result = p.exitValue();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (dos != null) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
复制代码
模拟点击了打卡界面以后,若是操做成功,默认会出现一个打卡成功的弹窗,咱们能够根据这个弹窗来判断是否打卡成功
因为这个弹窗也不能找到相关的id的详细节点,并且也不能经过text去查找,因此这里先经过递归方法拿到全部的几点,而后判断每一个节点的content-desc是否包含打卡成功的字样,若是有,咱们就默认打卡成功!
首先找出全部节点
//递归获取全部节点
private List<AccessibilityNodeInfo> getAllNode(AccessibilityNodeInfo node, List<AccessibilityNodeInfo> list) {
if (list == null) {
list = new ArrayList<>();
}
if (node != null && node.getChildCount() != 0) {
for (int i = 0; i < node.getChildCount(); i++) {
AccessibilityNodeInfo info = node.getChild(i);
if (node != null) {
list.add(info);
node = info;
}
}
} else {
return list;
}
return getAllNode(node, list);
}
复制代码
判断节点是否包含打卡成功字样
//检查是否打卡成功
AccessibilityNodeInfo node = getRootInActiveWindow();
//查询全部的根节点,假若有弹窗,说明打卡成功
List<AccessibilityNodeInfo> list = getAllNode(node, null);
LogUtil.D("全部节点个数-->" + list.size());
if (list != null) {
for (AccessibilityNodeInfo info : list) {
String className = info.getClassName().toString();
if ("android.app.Dialog".equals(className)) {
//说明多是打卡致使的成功弹窗
AccessibilityNodeInfo nodeInfo = info.getChild(0);
if (nodeInfo != null) {
nodeInfo = nodeInfo.getChild(1);
if (nodeInfo != null) {
String des = nodeInfo.getContentDescription().toString();
if (des.contains("打卡成功")) {
//这里作你想作的事,好比发个邮件通知一下
return;
}
}
}
}
}
}
复制代码
每次模拟点击以后,都要判断一下是否有打卡成功弹窗,最多尝试10次
//已进入打卡页面,执行打卡操做
int j = 10;
while (j >= 0) {
LogUtil.D("尝试打卡操做->" + j);
if(DoDaKa(order)){ //这里封装了一下,这是模拟点击以后,判断弹窗打卡成功的方法
//这里能够发送邮件
return;
}
sleepT(2000);
j--;
}
复制代码
在上述流程中,基本每一步都抛出了大量异常,出现异常,即表明程序没有按照咱们设定的流程走,这时咱们就须要去修正.一旦出现异常,咱们让脚本回到初始状态,也就是最初的桌面状态.android能够经过回退键来恢复到桌面.
代码:
//程序异常时的操做方法
private void AppCallBack() {
int i = 10; //最多尝试10次回退操做
while (true) {
//执行回退操做
AccessibilityNodeInfo node = getRootInActiveWindow();
if (i < 0) { //10次还未到桌面
//说明可能卡住了,没法回退,强行中止程序进程
CMDUtil.stopProcess(node.getPackageName().toString());
break;
}
LogUtil.D("执行回退操做");
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
if (node != null && Comm.launcher_PakeName.equals(node.getPackageName().toString())) {
//已回退到启动页,退出循环
LogUtil.D("桌面");
break;
}
i--;
sleepT(1000); //睡眠一秒
}
}
复制代码
TIPS: 上溯全部流程的每一步,咱们最好都加上1-2秒的延迟时间,毕竟页面跳转是须要时间的,对于手机性能差的手机相应的时间能够再延迟一些.
五. 功能测试
到这里,咱们的自动打卡程序基本就已经实现了,固然,上面只是实现自动打卡的核心代码.还有不少的拓展空间,好比能够加上一个任务请求线程,实如今特定时间,来实现打上班卡仍是打下班卡,以及打卡成功以后及时的邮件通知到手机上.也能够经过服务器来定时启动程序,控制脚本程序啥时候运行,啥时候不运行.发挥你的想象吧!