===============================================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秒的延遲時間,畢竟頁面跳轉是須要時間的,對於手機性能差的手機相應的時間能夠再延遲一些.
五. 功能測試
到這裏,咱們的自動打卡程序基本就已經實現了,固然,上面只是實現自動打卡的核心代碼.還有不少的拓展空間,好比能夠加上一個任務請求線程,實如今特定時間,來實現打上班卡仍是打下班卡,以及打卡成功以後及時的郵件通知到手機上.也能夠經過服務器來定時啓動程序,控制腳本程序啥時候運行,啥時候不運行.發揮你的想象吧!