1. 程式人生 > >基於AccessibilityService製作的釘釘自動簽到程式

基於AccessibilityService製作的釘釘自動簽到程式

前兩天公司開始宣佈要使用阿里釘釘來簽到啦!!!~~這就意味著,我必須老老實實每天按時簽到上班下班了,這真是一個悲傷的訊息,可是!!!!那麼機智(lan)的我,怎麼可能就這麼屈服!!!阿里釘釘簽到,說到底不就是手機軟體簽到嗎?我就是幹移動開發的,做一個小應用每天自動簽到不就行了:)

說幹就幹,首先分析一下,阿里釘釘的簽到流程:
開啟阿里釘釘->廣告頁停留2S左右->進入主頁->點選“工作”tab->點選“簽到”模組->進入簽到頁面(可能會再次出現廣告和對話方塊)->點選簽到

我們操作手機的過程就是這樣,要實現這些點選,很自然想起了前段時間做的微信搶紅包小應用,利用AccessibilityService服務幫助我們實現這些自動化操作。

以上是分析過程,接下來是我對這個小功能實現的具體方案思路:

將測試手機放公司並且安裝這個應用,通過我遠端的電話撥打或者簡訊傳送到測試手機(只要能產生廣播或者資訊的就行),測試手機接受到廣播資訊,喚醒釘釘,進入釘釘頁面,AccessibilityService開始工作,進行一系列點選簽到操作,結束操作後退出釘釘,簽到完成。

通過以上過程的分析我們大概要用到的知識有以下幾塊:

  1. 喚醒非自己的其他第三方應用
  2. 廣播
  3. AccessibilityService服務

以下是對這三部分程式碼實現:

  • 喚醒第三方應用
package net.fenzz.dingplug;

import
java.util.List; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; public
class Utils { public static void openCLD(String packageName,Context context) { PackageManager packageManager = context.getPackageManager(); PackageInfo pi = null; try { pi = packageManager.getPackageInfo("com.alibaba.android.rimet", 0); } catch (NameNotFoundException e) { } Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null); resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER); resolveIntent.setPackage(pi.packageName); List<ResolveInfo> apps = packageManager.queryIntentActivities(resolveIntent, 0); ResolveInfo ri = apps.iterator().next(); if (ri != null ) { String className = ri.activityInfo.name; Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ComponentName cn = new ComponentName(packageName, className); intent.setComponent(cn); context.startActivity(intent); } } }
  • 接受電話廣播並且喚醒釘釘:

mainifest先註冊監聽器

  <!-- 註冊監聽手機狀態 -->  
        <receiver android:name=".PhoneReceiver">  
            <intent-filter android:priority="1000" >  
                <action android:name="android.intent.action.PHONE_STATE" />  
            </intent-filter>  
        </receiver>  

相關許可權

 <!-- 讀取手機狀態的許可權 -->  
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />  
     <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>

程式碼

package net.fenzz.dingplug;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.app.Service;
import android.util.Log;

public class PhoneReceiver extends BroadcastReceiver {

    private static final String TAG = "message";
    private static boolean mIncomingFlag = false;
    private static String mIncomingNumber = null;

    @Override
    public void onReceive(Context context, Intent intent) {
        // 如果是撥打電話
        if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
            mIncomingFlag = false;
            String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
            Log.i(TAG, "call OUT:" + phoneNumber);

        } else {
            // 如果是來電
            TelephonyManager tManager = (TelephonyManager) context
                    .getSystemService(Service.TELEPHONY_SERVICE);
            switch (tManager.getCallState()) {

            case TelephonyManager.CALL_STATE_RINGING:
                mIncomingNumber = intent.getStringExtra("incoming_number");
                Log.i(TAG, "RINGING :" + mIncomingNumber);
                if(mIncomingNumber!=null&&mIncomingNumber.equals(你的手機號)){
                    Utils.openCLD("com.alibaba.android.rimet", context);
                    DingService.instance.setServiceEnable();
                }
                break;
            case TelephonyManager.CALL_STATE_OFFHOOK:
                if (mIncomingFlag) {
                    Log.i(TAG, "incoming ACCEPT :" + mIncomingNumber);
                }
                break;
            case TelephonyManager.CALL_STATE_IDLE:
                if (mIncomingFlag) {
                    Log.i(TAG, "incoming IDLE");
                }
                break;
            }
        }
    }

}
  • AccessibilityService服務實現:
    相關許可權及註冊:
 <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />

<service
            android:name=".DingService"
            android:enabled="true"
            android:exported="true"
            android:label="@string/app_name"
            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/red_service_config" />
 </service>

需要在res資料夾下新建一個xml資料夾裡面放入一個這樣的xml配置檔案:

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/accessibility_description"
    android:notificationTimeout="100"
    android:packageNames="com.alibaba.android.rimet" 

    />

程式碼:

package net.fenzz.dingplug;

import java.util.ArrayList;
import java.util.List;

import android.accessibilityservice.AccessibilityService;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;

public class DingService extends AccessibilityService {

    private String TAG = getClass().getSimpleName();

    private  boolean  isFinish = false;

    public static DingService instance;
    private int index = 1;

    /**
     * 獲取到簡訊通知
     *  0.喚醒螢幕
     *  1.開啟釘釘
     *  2.確保當前頁是主頁介面
     *  3.找到“工作”tab並且點選
     *  4.確保到達簽到頁面
     *  5.找到簽到按鈕,並且點選
     *  6.判斷簽到是否成功
     *      1.成功,退出程式
     *      2.失敗,返回到主頁,重新從1開始簽到
     */


    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // TODO Auto-generated method stub
//       final int eventType = event.getEventType();
         ArrayList<String> texts = new ArrayList<String>();
            Log.i(TAG, "事件---->" + event.getEventType());


         if(isFinish){
            return; 
         }

         AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
         if(nodeInfo == null) {
                Log.w(TAG, "rootWindow為空");
                return ;
          }
//       nodeInfo.

//       System.out.println("nodeInfo"+nodeInfo);



         System.out.println("index:"+index);
         switch (index) {

        case 1: //進入主頁
             OpenHome(event.getEventType(),nodeInfo);
            break;
        case 2: //進入簽到頁
            OpenQianDao(event.getEventType(),nodeInfo);
            break;
        case 3:
            doQianDao(event.getEventType(),nodeInfo);
            break;

        default:
            break;
        }

    }


    private ArrayList<String> getTextList(AccessibilityNodeInfo node,ArrayList<String> textList){
        if(node == null) {
            Log.w(TAG, "rootWindow為空");
            return null;
        }
        if(textList==null){
            textList = new ArrayList<String>();
        }
        String text = node.getText().toString();
          if(text!=null&&text.equals("")){
              textList.add(text);
          }
//        node.get
        return null;

    }


    private void OpenHome(int type,AccessibilityNodeInfo nodeInfo) {
        if(type == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED){
            //判斷當前是否是釘釘主頁
            List<AccessibilityNodeInfo> homeList = nodeInfo.findAccessibilityNodeInfosByText("工作");
            if(!homeList.isEmpty()){
                //點選
                 boolean isHome = click( "工作");
                 System.out.println("---->"+isHome);
                index = 2;
                System.out.println("點選進入主頁簽到");
            }
        }

    }

    private void OpenQianDao(int type,AccessibilityNodeInfo nodeInfo) {
        if(type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED){
            //判斷當前是否是主頁的簽到頁
            List<AccessibilityNodeInfo> qianList = nodeInfo.findAccessibilityNodeInfosByText("工作");
            if(!qianList.isEmpty()){
                 boolean ret = click( "簽到");
                 index = 3;
                 System.out.println("點選進入簽到頁面詳情");
            }

//           index = ret?3:1;   
        }

    }


    private void doQianDao(int type,AccessibilityNodeInfo nodeInfo) {
        if(type == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED){
            //判斷當前頁是否是簽到頁
            List<AccessibilityNodeInfo> case1 = nodeInfo.findAccessibilityNodeInfosByText("開啟我的簽到之旅");
            if(!case1.isEmpty()){
                click("開啟我的簽到之旅");
                System.out.println("點選簽到之旅");
            }

            List<AccessibilityNodeInfo> case2 = nodeInfo.findAccessibilityNodeInfosByText("我知道了");
            if(!case2.isEmpty()){
                click("我知道了");
                System.out.println("點選我知道對話方塊");
            }
            List<AccessibilityNodeInfo> case3 = nodeInfo.findAccessibilityNodeInfosByText("簽到");
            if(!case3.isEmpty()){
                Toast.makeText(getApplicationContext(), "發現目標啦!!~~", 1).show();
                System.out.println("發現目標啦!");
                click("簽到");
                isFinish = true;
            }
        }

//      if(type == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED){
//          List<AccessibilityNodeInfo> case3 = nodeInfo.findAccessibilityNodeInfosByText("簽到");
//          if(!case3.isEmpty()){
//              Toast.makeText(getApplicationContext(), "發現目標啦!!~~", 1).show();
//          }
//      }

    }


    //通過文字點選
    private boolean click(String viewText){
         AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
        if(nodeInfo == null) {
                Log.w(TAG, "點選失敗,rootWindow為空");
                return false;
        }
        List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(viewText);
        if(list.isEmpty()){
            //沒有該文字的控制元件
             Log.w(TAG, "點選失敗,"+viewText+"控制元件列表為空");
             return false;
        }else{
            //有該控制元件
            //找到可點選的父控制元件
            AccessibilityNodeInfo view = list.get(0);
            return onclick(view);  //遍歷點選
        }

    }

    private boolean onclick(AccessibilityNodeInfo view){
        if(view.isClickable()){
            view.performAction(AccessibilityNodeInfo.ACTION_CLICK);
             Log.w(TAG, "點選成功");
             return true;
        }else{

            AccessibilityNodeInfo parent = view.getParent();
            if(parent==null){
                return false;
            }
            onclick(parent);
        }
        return false;
    }

    //點選返回按鈕事件
    private void back(){
         performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
    }

    @Override
    public void onInterrupt() {
        // TODO Auto-generated method stub

    }

    @Override
    protected void onServiceConnected() {
        // TODO Auto-generated method stub
        super.onServiceConnected();
        Log.i(TAG, "service connected!");
        Toast.makeText(getApplicationContext(), "連線成功!", 1).show();
        instance = this;
    }

    public void setServiceEnable(){
        isFinish = false;
        Toast.makeText(getApplicationContext(), "服務可用開啟!", 1).show();
        index = 1;
    }

}

以上基本是所有程式碼,這個小程式中可以不用Activity元件,也可以加一個小的Activity,用來作為系統的總開關,當然也可以自動檢測時間,來判斷是否開啟服務,這樣就不用Activity了,在這個小例子中,我使用了一個小activity,就放了一個button。

專案原始碼,我稍後上傳!