Android實現釘釘自動打卡功能(AccessibilityService版本)

Android實現釘釘自動打卡功能(AccessibilityService版本)

===============================================php

目錄


[TOC]java

爲何要作這個項目?

有天早晨下大雨,小編雖然出門早卻仍是路上堵的遲到了,心中一句XXX崩騰而過啊,這月全勤又沒了,無奈之餘想起既然技術能解決一切,那能不能搞個自動打卡的功能(好像有點做弊的嫌疑...哈O(∩_∩)O哈哈~),這樣之後就不用在考慮會遲到了!因而一個邪惡的程序就誕生了.node

一. 項目需求

項目功能:
  1. 程序啓動後,一直後臺運行,自動啓動釘釘,並進入相應的打卡頁面進行打卡(須要用到模擬點擊功能).
  2. 程序的執行時間段爲上午8-9點爲上班打卡,18-19點爲下班打卡(時間段根據需求便可).
  3. 確認打卡成功以後程序進入休眠狀態,等待下次指令.
  4. 程序必須24小時處於激活狀態,避免被系統清理
項目流程:

WX20180727-192204@2x.png

二. 資源準備

大體須要準備如下東西:android

  1. 一臺空閒的andorid手機,能root最好.
  2. 下載釘釘,登錄帳號
  3. 手機設置充電不鎖屏,而且鏈接了相應的打卡wifi.

三. 核心代碼架構

項目的核心在於利用程序模擬人工打卡操做,須要用到android模擬點擊功能的相關api,目前比較經常使用的黑科技主要是如下兩種:web

AccessibilityService

AccessibilityService原本是作一些輔助功能的,提供了一系列的事件回調,幫助咱們指示一些用戶及界面的狀態變化,主要給殘障人羣提供幫助.手機上的全部操做都會經過onAccessibilityEvent方法返回,咱們能夠利用該原理作到模擬點擊咱們須要的操做程序. 不過,如今AccessibilityService已經基本偏離了它設計的初衷,至少在國內是這樣,愈來愈多的App借用AccessibilityService來實現了一些其它功能,甚至是灰色產品。json

UiAutomator

基於UIAutomation的用戶界面自動化測試框架,能夠跨應用工做,谷歌親生的. UIAutomation在Android4.3發佈時有了新版本,官方簡介 Android4.3以前:使用inputManager或者更早的WindowsManager來注入KeyEventapi


固然,除了以上兩種,還有其餘的一些能實現模擬點擊的框架,這裏我就不一一贅述了,今天咱們要用的就是利用AccessibilityService 輔助功能來實現咱們的自動打卡功能.bash


四. 功能實現


4.1 配置AccessibilityService,監聽手機操做

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;

    }

複製代碼

4.2 實現自動化打卡流程


配置好AccessibilityService服務後,接下來咱們就能夠在onAccessibilityEvent方法中寫咱們自動化腳本的邏輯了.具體流程看第一節中的圖.

4.2.1 保證手機處於桌面(如下是部分核心代碼)
AccessibilityNodeInfo node=getRootInActiveWindow();
if (node == null || !Comm.launcher_PakeName.equals(node.getPackageName().toString())) {
                throw new Exception("程序不在初始化啓動器頁面,拋出異常");
            }

複製代碼

注意上面的手動異常和下面全部的手動拋出異常到最後是會有大做用的,後面會講到.

4.2.2 啓動釘釘
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次以後都沒有進入釘釘或者已進入釘釘,都將拋出異常,這次腳本終止.(目的是防止出現啓動時卡死,致使腳本也卡死)

4.2.3 判斷是否位於釘釘主頁面

經過Android SDK的uiautomatorviewer工具(在tools文件夾下,須要手機root,studio的sdk可能和elipse的不一樣),查看頁面的節點信息,以下圖:

12.jpg

能夠獲得底部絕對佈局的資源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,未找到主頁節點");

}

複製代碼
4.2.4 進入工做頁面

到這一步,咱們程序已進入釘釘主頁,接下來須要進入考勤打卡所在的工做頁面 在底部選項卡中,找到工做按鈕佈局所在的資源id(com.alibaba.android.rimet:id/home_bottom_tab_button_work),點擊工做頁按鈕,進入工做頁,以下圖:

CB989BA02A7AB74285EA28D92A998E19.jpg

具體代碼

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);
}

複製代碼
4.2.5 已進入工做頁,查找考勤打卡按鈕,進行點擊操做,進入考勤打卡頁面

到這一步,咱們程序默認已經在工做頁面了,接下來須要作的就是點擊考勤打卡選項,進入考勤頁面. 這裏有些許的複雜,由於不能直接找到考勤打卡所在佈局的id,只能先查找其所在的父佈局的id(com.alibaba.android.rimet:id/oa_fragment_gridview),而後再找到考勤打卡的節點.

33.jpg

具體代碼:

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("已進入工做頁,但未找到相關節點");
        }
複製代碼
4.2.6 確認已考勤打卡頁面

到這一步,咱們程序認爲已經進入了考勤打卡頁面了,接下來咱們須要再確認一下目前所在節點是否是考勤打卡頁面的節點. 這個頁面是一個webview頁面,因此判斷是否已進入考勤打卡界面,咱們只要找到了webview佈局的一個惟一資源id標識便可(com.alibaba.android.rimet:id/webview_frame),

55.jpg

代碼:

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("進入考勤打卡頁面異常");
}

複製代碼
4.2.7 執行打卡操做

到這一步,程序已確認進入考勤打卡頁面,能夠開始執行打卡操做.按照咱們一些的步驟,打卡操做只須要你找到相應的打卡按鈕節點,而後經過節點的點擊操做接口,可是很不幸的是,因爲考勤打卡頁面時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;
    }

複製代碼
4.2.8 肯定打卡成功

模擬點擊了打卡界面以後,若是操做成功,默認會出現一個打卡成功的彈窗,咱們能夠根據這個彈窗來判斷是否打卡成功

因爲這個彈窗也不能找到相關的id的詳細節點,並且也不能經過text去查找,因此這裏先經過遞歸方法拿到全部的幾點,而後判斷每一個節點的content-desc是否包含打卡成功的字樣,若是有,咱們就默認打卡成功!

66.jpg

首先找出全部節點

//遞歸獲取全部節點
    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--;
        }

複製代碼
4.2.9 異常處理

在上述流程中,基本每一步都拋出了大量異常,出現異常,即表明程序沒有按照咱們設定的流程走,這時咱們就須要去修正.一旦出現異常,咱們讓腳本回到初始狀態,也就是最初的桌面狀態.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秒的延遲時間,畢竟頁面跳轉是須要時間的,對於手機性能差的手機相應的時間能夠再延遲一些.


五. 功能測試

到這裏,咱們的自動打卡程序基本就已經實現了,固然,上面只是實現自動打卡的核心代碼.還有不少的拓展空間,好比能夠加上一個任務請求線程,實如今特定時間,來實現打上班卡仍是打下班卡,以及打卡成功以後及時的郵件通知到手機上.也能夠經過服務器來定時啓動程序,控制腳本程序啥時候運行,啥時候不運行.發揮你的想象吧!

相關文章
相關標籤/搜索