Android輔助功能開發(fā)——一鍵優(yōu)化

個(gè)人博客地址:https://blog.yookingh.cn
該文章地址:https://blog.yookingh.cn/dev/200424-accessibilityService.html

前言

公司項(xiàng)目需求:為App提供一鍵優(yōu)化功能(一鍵獲取相關(guān)所有權(quán)限)喊暖,確保App在優(yōu)化過后能夠常駐手機(jī)伟桅,為用戶提供服務(wù)渐裸÷央梗——商戶類App忘蟹,用戶非主動(dòng)關(guān)閉時(shí)常駐手機(jī)合情合理

閱讀源碼

  1. getWindows()和getRootInActiveWindow()

    List<AccessibilityNodeInfo> getWindows() 即:獲取當(dāng)前頁面所有的節(jié)點(diǎn)——包括頂部狀態(tài)欄铣揉、底部系統(tǒng)按鈕等各自的root節(jié)點(diǎn)窜骄。

    AccessibilityNodeInfo getRootInActiveWindow()即:獲取當(dāng)前活動(dòng)頁面(Activity)中的root節(jié)點(diǎn)娶牌。

    getRootInActiveWindow()方法在頁面未加載完成時(shí)可能為null奔浅,而getWindows()一直有值。

  2. AccessibilityNodeInfo

    AccessibllityNodeInfo類是含有鏈表結(jié)構(gòu)的類(應(yīng)該是這樣稱呼吧)诗良。

    譬如:

    class AccessibilityNodeInfo implements Parcelable{
        ...
        public AccessibilityNodeInfo getParent() {
            throw new RuntimeException("Stub!");
        }
        
        public AccessibilityNodeInfo getChild(int index) {
            throw new RuntimeException("Stub!");
        }
        ...
    }
    

    AccessibllityNodeInfo頁包含了對View的操作(僅包含有用到的部分):

    class AccessibilityNodeInfo implements Parcelable{
        ...
        //點(diǎn)擊事件
        public static final int ACTION_CLICK = 16;
        //朝后滾動(dòng) performAction返回false即滑動(dòng)到頂部
        public static final int ACTION_SCROLL_BACKWARD = 8192;
        //朝前滾動(dòng) performAction返回false即滑動(dòng)到底部
        public static final int ACTION_SCROLL_FORWARD = 4096;
        ...
     public boolean performAction(int action) {
            throw new RuntimeException("Stub!");
        }
    
        public boolean performAction(int action, Bundle arguments) {
            throw new RuntimeException("Stub!");
        }
        ...
    }
    
  3. 全局事件

    public abstract class AccessibilityService extends Service {
        ...
        //返回鍵
        public static final int GLOBAL_ACTION_BACK = 1;
        //Home鍵
        public static final int GLOBAL_ACTION_HOME = 2;
        //菜單鍵
        public static final int GLOBAL_ACTION_RECENTS = 3;
        ...
        public final boolean performGlobalAction(int action) {
            throw new RuntimeException("Stub!");
        }
        ...
    }
    

封裝工具

了解完AccessibilityService后汹桦,可以對Accessibility進(jìn)行簡單的封裝,方便使用鉴裹。

  1. 獲取root節(jié)點(diǎn)

    由上可知舞骆,在尋找目標(biāo)的root節(jié)點(diǎn)這方面钥弯,getRootInActiveWindow()會(huì)根據(jù)有優(yōu)勢,所以

    /**
     * @return 獲取root節(jié)點(diǎn)
     */
    private AccessibilityNodeInfo findRoot() {
       return getRootInActiveWindow();
    }
    
  2. 獲取所有當(dāng)前Activity可見節(jié)點(diǎn)

    遍歷多叉樹

    /**
     * 遍歷多叉樹 全遍歷
     */
    private List<AccessibilityNodeInfo> iteratorTree(AccessibilityNodeInfo parent) {
        List<AccessibilityNodeInfo> childList = new ArrayList<>();
        if (parent == null) return childList;
        for (int i = 0; i < parent.getChildCount(); i++) {
            AccessibilityNodeInfo child = parent.getChild(i);
            childList.add(child);
            if (child.getChildCount() > 0) {
                //利用遞歸方法
                childList.addAll(iteratorTree(child));
            }
        }
        return childList;
    }
    

    獲取全部可見節(jié)點(diǎn)

    /**
     * 遍歷當(dāng)前頁面所有節(jié)點(diǎn)
     */
    private List<AccessibilityNodeInfo> findAllView() {
        AccessibilityNodeInfo parent = findRoot();
        List<AccessibilityNodeInfo> list = new ArrayList<>();
        list.add(parent);
        list.addAll(iteratorTree(parent));
        return list;
    }
    
  3. 查找第一個(gè)匹配節(jié)點(diǎn)

    馬后炮:在Activity中督禽,可能存在ListView脆霎、RecyclerView滑動(dòng)控件使得目標(biāo)節(jié)點(diǎn)出現(xiàn)超出屏幕情況,所以我們應(yīng)當(dāng)先在當(dāng)前可見區(qū)域內(nèi)查找是否有目標(biāo)節(jié)點(diǎn)狈惫,如果沒有睛蛛,則向下滾動(dòng),再次查找目標(biāo)節(jié)點(diǎn)——直至滑動(dòng)到屏幕底部胧谈。為防止當(dāng)前界面并非從最頂部開始向下滑動(dòng)忆肾,故應(yīng)該再向上滑動(dòng)到頂部一次。

    查找滑動(dòng)組件代碼如下:

    private static final List<String> SCROLL_CLASS_NAME_ARRAY = Arrays.asList(
                "android.widget.ListView",
                "android.support.v7.widget.RecyclerView",
                "androidx.recyclerview.widget.RecyclerView"
    );
    /**
     * 查找可滑動(dòng)的view
     */
    private List<AccessibilityNodeInfo> findCanScrollView() {
        List<AccessibilityNodeInfo> viewList = findAllView();
        List<AccessibilityNodeInfo> scrollViewList = new ArrayList<>();
        for (AccessibilityNodeInfo info : viewList) {
            if (info != null)
                //info.isScrollable判斷是否為可滾動(dòng)控件
                if (info.isScrollable()) {
                    //其中滾動(dòng)控件還包含了spinner菱肖、viewpage等,而這些控件目前業(yè)務(wù)中并沒有需要(且會(huì)影響業(yè)務(wù)內(nèi)容)客冈,因此做了個(gè)限定
                    for (String scrollClassName : SCROLL_CLASS_NAME_ARRAY) {
                        if (scrollClassName.equals(info.getClassName().toString())) {
                            scrollViewList.add(info);
                            setLog("------------------------------------------------------------");
                            setLog("查詢到可滑動(dòng)組件" + info.getViewIdResourceName());
                            setLog("查詢到可滑動(dòng)組件" + info.getClassName());
                            setLog("------------------------------------------------------------");
                        }
                    }
                }
        }
        return scrollViewList;
    }
    

    遍歷多叉樹,根據(jù)條件查尋當(dāng)前可見部分的節(jié)點(diǎn)

    /**
     * 遍歷多叉樹 查詢方法
     * 找到控件會(huì)拋出異常稳强,異常中包含目標(biāo)數(shù)據(jù)
     */
    private void findIteratorTree(AccessibilityNodeInfo parent, String text) throws StopIteratorException {
        if (parent != null)
            for (int i = 0; i < parent.getChildCount(); i++) {
                AccessibilityNodeInfo child = parent.getChild(i);
                if (child != null)
                    if (multiCriteriaMatching(child, text)) {
                        throw new StopIteratorException(child);
                    } else if (child.getChildCount() > 0) {
                        findIteratorTree(child, text);
                    }
            }
    }
    
    /**
     * 自定義報(bào)錯(cuò) 用于中斷遞歸
     */
    private static class StopIteratorException extends RuntimeException {
        private AccessibilityNodeInfo info;
        private StopIteratorException(AccessibilityNodeInfo info) {
            this.info = info;
        }
        private AccessibilityNodeInfo getInfo() {
            return info;
        }
    }
    
    /**
     * 多條件匹配 multiCriteriaMatching
     *
     * @param info 目前包含getViewIdResourceName场仲、getClassName、getText
     * @param text 匹配詞
     * @return 返回匹配結(jié)果
     */
    private boolean multiCriteriaMatching(AccessibilityNodeInfo info, String text) {
        return text.equals(info.getViewIdResourceName())
                || text.equals(info.getClassName() == null ? null : info.getClassName().toString())
                || text.equals(info.getText() == null ? null : info.getText().toString());
    }
    

    查找第一個(gè)匹配節(jié)點(diǎn)代碼如下:

    /**
     * @param text 要查找目標(biāo)組件的標(biāo)識(shí) (className/resourceId/text)
     * @return 返回查找到的目標(biāo)組件 null則表示查找失敗
     */
    private AccessibilityNodeInfo findFirst(String text) {
        AccessibilityNodeInfo info = null;
        try {
            findIteratorTree(findRoot(), text);
    
            int searchNum = 0;
            boolean scroll = true, isForward = true;
            List<AccessibilityNodeInfo> scrollViewList = new ArrayList<>();
            while (scroll) {//利用關(guān)鍵字scroll和Exception跳出循環(huán)
                //找不到控件退疫,向下滑動(dòng)繼續(xù)找
                if (scrollViewList.size() == 0) {
                    if (searchNum < MAX_SEARCH_NUM) {
                        ++searchNum;
                        scrollViewList.addAll(findCanScrollView());
                    } else {
                        //找不到可滑動(dòng)組件
                        setLog("找不到可滑動(dòng)組件渠缕,結(jié)束循環(huán)體,查找次數(shù):" + MAX_SEARCH_NUM);
                        scroll = false;
                    }
                }
    
                if (scrollViewList.size() != 0) {
                    //如果查找到滑動(dòng)組件蹄咖,則滑動(dòng)滑動(dòng)組件
                    //下褐健、上各滑動(dòng)一次,確保找到控件
                    for (AccessibilityNodeInfo scrollViewInfo : scrollViewList) {
                        if (isForward) {
                            if (!scrollViewInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
                                isForward = false;//滑動(dòng)到底部澜汤,修改滑動(dòng)狀態(tài)蚜迅,再滑一次
                            }
                        } else {
                            if (!scrollViewInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)) {
                                scroll = false;
                            }
                        }
                    }
                }
                //滑動(dòng)后 睡眠 之后查找組件
                SystemClock.sleep(TIME_SLEEP);
                findIteratorTree(findRoot(), text);
            }
        } catch (StopIteratorException e) {
            info = e.getInfo();
            if (info != null) {
                setLog("------------------------------------------------------------");
                setLog("查詢到目標(biāo)組件id:" + info.getViewIdResourceName());
                setLog("查詢到目標(biāo)組件text:" + info.getText());
                setLog("查詢到目標(biāo)組件clsName:" + info.getClassName());
                setLog("------------------------------------------------------------");
            }
        }
        return info;
    }
    

    補(bǔ)充說明:(可不看)在查看AccessibilityNodeInfo類中有如下方法

    class AccessibilityNodeInfo implements Parcelable{
        ...
        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) {
            throw new RuntimeException("Stub!");
        }
    
        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(String viewId) {
            throw new RuntimeException("Stub!");
        }
        ...
    }
    

    由此聯(lián)想到findIteratorTree(AccessibilityNodeInfo parent, String text)或許可以改成:(以下代碼未嘗試,只是臨時(shí)想到的改進(jìn)方案)

    private AccessibilityNodeInfo findIteratorTree(AccessibilityNodeInfo parent, String text){
        List<AccessibilityNodeInfo> infoList = new ArrayList<>();
        infoList.addAll(parent.findAccessibilityNodeInfosByText(text));
        if(info.size() == 0){
            infoList.addAll(parent.findAccessibilityNodeInfosByViewId(text));
        }
        AccessibilityNodeInfo info = null;
        if(infoList.size() > 0){
         info = infoList.get(0);
        }
        return info;
    }
    
  4. 點(diǎn)擊事件

    找到目標(biāo)節(jié)點(diǎn)后俊抵,自然是要對目標(biāo)節(jié)點(diǎn)進(jìn)行操作——這里僅封裝點(diǎn)擊事件

    /**
     * 點(diǎn)擊事件
     *
     * @param info 要點(diǎn)擊的目標(biāo)View
     * @return 點(diǎn)擊結(jié)果(成功/失斔弧)
     */
    private boolean clickView(AccessibilityNodeInfo info) {
        if (info != null)
            if (info.isClickable()) {
                info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                return true;
            } else {
                AccessibilityNodeInfo parent = info.getParent();
                if (parent != null) {
                    boolean b = clickView(parent);
                    parent.recycle();
                    return b;
                }
            }
        return false;
    }
    

    將查詢節(jié)點(diǎn)與點(diǎn)擊節(jié)點(diǎn)組合起來就是:

    /**
     * 點(diǎn)擊事件(點(diǎn)擊本身)
     *
     * @param text 要點(diǎn)擊的目標(biāo)節(jié)點(diǎn)標(biāo)識(shí)
     * @return 點(diǎn)擊結(jié)果(true/false)
     */
    public boolean clickFirstView(String text) {
        AccessibilityNodeInfo info = findFirst(text);
        if (info == null) {
            setLog(text + "找不到匹配的控件");
            return false;
        }
        return clickView(info);
    }
    

    那如果是CheckBox之類的點(diǎn)擊事件(如下圖)應(yīng)該怎么處理?


    1.jpg

    首先轉(zhuǎn)化為查詢該CheckBox的同級節(jié)點(diǎn)徽诲,再通過該同級節(jié)點(diǎn)反查父節(jié)點(diǎn)刹帕,之后再查詢父節(jié)點(diǎn)中的子節(jié)點(diǎn)中包含isCheckable的節(jié)點(diǎn):

    private AccessibilityNodeInfo findBrotherCheckBox(AccessibilityNodeInfo child) {
        AccessibilityNodeInfo checkBoxInfo = null;
        AccessibilityNodeInfo parent = child.getParent();
        if (parent != null) {
            List<AccessibilityNodeInfo> infoList = iteratorTree(parent);
            setLog("------------------------------------------------------------");
            for (AccessibilityNodeInfo info : infoList) {
                if (info.isCheckable()) {
                    checkBoxInfo = info;
                    setLog("------------------------------------------------------------");
                    setLog("查詢到目標(biāo)組件id:" + info.getViewIdResourceName());
                    setLog("查詢到目標(biāo)組件text:" + info.getText());
                    setLog("查詢到目標(biāo)組件clsName:" + info.getClassName());
                    setLog("------------------------------------------------------------");
                    break;
                }
            }
            setLog("------------------------------------------------------------");
        }
        return checkBoxInfo;
    }
    

    CheckBox節(jié)點(diǎn)中有isChecked字段用于判斷是( true )否( false )選中,因此有:

    /**
     * checkBox的點(diǎn)擊事件(帶選中狀態(tài))
     *
     * @param brotherText 要點(diǎn)擊目標(biāo)的兄弟節(jié)點(diǎn)標(biāo)識(shí)
     * @param setChecked  將checkBox的狀態(tài)修改為true/false
     * @return 返回點(diǎn)擊事件觸發(fā)結(jié)果(true/false)
     */
    public boolean clickCheckBox(String brotherText, boolean setChecked) {
        AccessibilityNodeInfo brotherInfo = findFirst(brotherText);
        if (brotherInfo == null) {
            setLog(brotherText + "找不到匹配的控件");
            return false;
        }
        AccessibilityNodeInfo checkBoxInfo = findBrotherCheckBox(brotherInfo);
        if (checkBoxInfo == null) {
            setLog(brotherText + "找不到匹配的checkBox控件");
            return false;
        }
        if (checkBoxInfo.isChecked() != setChecked) {
            return clickView(checkBoxInfo);
        } else {
            setLog("checkBox控件為目標(biāo)狀態(tài)谎替,不點(diǎn)擊");
        }
        return false;
    }
    
  5. 補(bǔ)充

    靜態(tài)變量偷溺、日志與返回

    //靜態(tài)變量
    private static final int TIME_SLEEP = 300;//單位毫秒
    private static final int MAX_SEARCH_NUM = 20;//查找循環(huán)體次數(shù)
    
    //日志
    protected abstract boolean isDebugger();
    private void setLog(String msg) {
        if (isDebugger()) {
            String tag = "AManagerLog";
            Log.i(tag, msg);
        }
    }
    
    //系統(tǒng)返回鍵
    public void goBack() {
        performGlobalAction(GLOBAL_ACTION_BACK);
    }
    

    以上就是整個(gè)工具的封裝了

如何使用

  1. 輔助功能觸發(fā)點(diǎn)的設(shè)置

    輔助功能顧名思義,就是輔助用戶使用當(dāng)前App钱贯,甚至可以延伸為使用當(dāng)前手機(jī)挫掏。比如:

    • 監(jiān)測App接收到的通知消息,根據(jù)消息對手機(jī)進(jìn)行輔助操作
    • 監(jiān)測文件下載狀態(tài)秩命,根據(jù)下載狀態(tài)進(jìn)行自動(dòng)安裝尉共、打開等操作
    • 當(dāng)用戶點(diǎn)擊了某個(gè)位置時(shí)褒傅,根據(jù)點(diǎn)擊的位置做出反饋
    • 為用戶自動(dòng)開啟相關(guān)權(quán)限等
    • ...

    這時(shí)候:監(jiān)測到App接收通知消息和監(jiān)測到文件下載狀態(tài)即為觸發(fā)點(diǎn);用戶點(diǎn)擊事件即為出發(fā)點(diǎn)袄友;而開啟權(quán)限殿托,則可以是在輔助功能權(quán)限開啟的時(shí)候。

    觸發(fā)點(diǎn)為輔助功能權(quán)限開啟:輔助功能權(quán)限開啟的時(shí)候會(huì)調(diào)用初始化Service方法:onServiceConnected()剧蚣,這里可以作為一個(gè)監(jiān)測點(diǎn)支竹,利用sendBroadcast(Intent intent)服務(wù)啟動(dòng)的消息廣播出去。而廣播接收器就是真實(shí)的觸發(fā)點(diǎn)券敌。

    觸發(fā)點(diǎn)為某種狀態(tài)

    if(該狀態(tài)可回調(diào)){
        if(輔助功能權(quán)限開啟){
            直接執(zhí)行輔助功能
        }else{
            彈出無障礙設(shè)置頁面唾戚,引導(dǎo)用戶開啟輔助功能權(quán)限-觸發(fā)點(diǎn)轉(zhuǎn)為輔助功能權(quán)限開啟時(shí)
        }
    }else{
        監(jiān)測狀態(tài)改變引發(fā)的界面變化
        ...
    }
    

    觸發(fā)點(diǎn)為用戶點(diǎn)擊事件:如果點(diǎn)擊的是自己的App,在點(diǎn)擊事件觸發(fā)輔助功能即可待诅。如果點(diǎn)擊的不為自己的App,可以在onAccessibilityEvent(AccessibilityEvent event)觸發(fā)熊镣。

  2. 輔助功能執(zhí)行

    輔助功能是一個(gè)富有想象力的功能卑雁,TA的執(zhí)行能力取決于腦洞。比如模擬用戶點(diǎn)擊绪囱,讓用戶的操作更快捷(比如自動(dòng)搶紅包测蹲、一鍵連招等)或者取代繁瑣操作(自動(dòng)下載并安裝等)甚至也可以是智能分詞翻譯(錘子BigBang)等等...

    這里以“取代繁瑣操作”為例——征得用戶同意后,替用戶勾選一些無法直接申請的權(quán)限:

    任務(wù):AssignmentEntity

    public class AssignmentEntity {
        private Queue<StepEntity> queue = new LinkedList();  //步驟隊(duì)列
        private String name;                             //任務(wù)名稱
    
        public AssignmentEntity() {
        }
    
        public String getName() {
            return this.name;
        }
    
        public AssignmentEntity setName(String name) {
            this.name = name;
            return this;
        }
    
        public Queue<StepEntity> getQueue() {
            return this.queue;
        }
    
        /**
         * 添加步驟
         **/
        public AssignmentEntity addStep(StepEntity entity) {
            this.queue.add(entity);
            return this;
        }
    }
    

    具體步驟:StepEntity

    public class StepEntity {
        public static final int TYPE_INTENT = 0;    //頁面跳轉(zhuǎn)
        public static final int TYPE_CLICK = 1;     //點(diǎn)擊事件
        public static final int TYPE_CHECKED = 2;   //選中事件
        public static final int TYPE_BACK = 3;      //返回事件
        private String name;      //目標(biāo)控件名稱
        private int type;         //事件類型
        private boolean checked;  //選中事件-true:選中-false:反選
        private Intent intent;    //跳轉(zhuǎn)事件-Intent
    
        public StepEntity() {
        }
    
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getType() {
            return this.type;
        }
    
        public void setType(int type) {
            this.type = type;
        }
    
        public Intent getIntent() {
            return this.intent;
        }
    
        public void setIntent(Intent intent) {
            this.intent = intent;
        }
    
        public boolean isChecked() {
            return this.checked;
        }
    
        public void setChecked(boolean checked) {
            this.checked = checked;
        }
    }
    

    步驟輔助類StepHelper

    public class StepHelper {
        public StepHelper() {
        }
    
        /**
         * 跳轉(zhuǎn)事件
         **/
        public static StepEntity intentStep(Intent intent) {
            return getStep("頁面跳轉(zhuǎn)", 0, intent, (Boolean)null);
        }
    
        /**
         * 點(diǎn)擊事件
         **/
        public static StepEntity clickStep(String text) {
            return getStep(text, 1, (Intent)null, (Boolean)null);
        }
    
        /**
         * 選中事件
         **/
        public static StepEntity checkStep(String text, boolean setChecked) {
            return getStep(text, 2, (Intent)null, setChecked);
        }
    
        /**
         * 返回事件
         **/
        public static StepEntity backStep() {
            return getStep("返回上一頁", 3, (Intent)null, (Boolean)null);
        }
    
        private static StepEntity getStep(String name, int type, @Nullable Intent intent, @Nullable Boolean setChecked) {
            StepEntity step = new StepEntity();
            step.setName(name);
            step.setType(type);
            if (0 == type) {
                if (intent == null) {
                    throw new IllegalArgumentException("代碼錯(cuò)誤鬼吵,Type為TYPE_INTENT時(shí)Intent參數(shù)不能為空");
                }
    
                step.setIntent(intent);
            } else if (2 == type) {
                if (setChecked == null) {
                    throw new IllegalArgumentException("代碼錯(cuò)誤扣甲,Type為TYPE_CHECKED時(shí)setChecked參數(shù)不能為空");
                }
    
                boolean b = setChecked;
                step.setChecked(b);
            }
    
            return step;
        }
    }
    

    添加任務(wù)示例:

    public class AssignmentFactory {
        ...
        public static Queue<AssignmentEntity> create() {
            Queue<AssignmentEntity> queue = new LinkedList<>();
            if (RomUtils.isHuawei()) {
                queue.addAll(HuaweiFactory.create());
            } else if (RomUtils.isMiui()) {
                queue.addAll(XiaomiFactory.create());
            } else if (RomUtils.isOppo()) {
    
            } else if (RomUtils.isMeizu()) {
                queue.addAll(MeizuFactory.create());
            }
            return queue;
        }
        ...
            
        static class HuaweiFactory {
            
            private static int getVersion() {
                int version = -1;
                try {
                    String systemProperty = RomUtils.getSystemProperty("ro.build.version.emui");
                    if (systemProperty != null) {
                        String trim = systemProperty.replace("EmotionUI", "").replace("_", "").trim();
                        if (trim.contains(".")) {
                            trim = trim.substring(0, trim.indexOf("."));
                        }
                        version = Integer.valueOf(trim);
                    }
                } catch (Exception ignored) {
    
                }
                return version;
            }
    
            static Queue<AssignmentEntity> create() {
                L.i("手機(jī)版本號(hào):" + getVersion()); //目前適配10
                Queue<AssignmentEntity> queue = new LinkedList<>();
                queue.add(ignoreBatteryOptimization());
                queue.addAll(newMessageNotification());
                queue.add(selfStarting());
                return queue;
            }
    
            //忽略電池優(yōu)化
            private static AssignmentEntity ignoreBatteryOptimization() {
                AssignmentEntity assignment = new AssignmentEntity();
                assignment.setName("忽略電池優(yōu)化");
    
                assignment.addStep(StepHelper.intentStep(IntentUtils.hightPowerManger()))
                        .addStep(StepHelper.clickStep("不允許"))
                        .addStep(StepHelper.clickStep("所有應(yīng)用"))
                        .addStep(StepHelper.clickStep(BaseApplication.getInstance().getAppName()))
                        .addStep(StepHelper.clickStep("不允許"))
                        .addStep(StepHelper.clickStep("確定"))
                        .addStep(StepHelper.backStep());
    
                return assignment;
            }
    
            //新消息通知
            private static Queue<AssignmentEntity> newMessageNotification() {
                AssignmentEntity ae1 = new AssignmentEntity();
                ae1.setName("新消息通知");
                AssignmentEntity ae2 = new AssignmentEntity();
                ae2.setName("通知鎖屏顯示");
    
                ae1.addStep(StepHelper.intentStep(IntentUtils.huaweiNotification()))
                        .addStep(StepHelper.clickStep(BaseApplication.getInstance().getAppName()))
                        .addStep(StepHelper.checkStep("允許通知", true))
                        .addStep(StepHelper.backStep());
                ae2.addStep(StepHelper.clickStep("鎖屏通知"))
                        .addStep(StepHelper.clickStep("顯示所有通知"))
                        .addStep(StepHelper.clickStep("更多通知設(shè)置"))
                        .addStep(StepHelper.checkStep("通知亮屏提示", true))
                        .addStep(StepHelper.backStep())
                        .addStep(StepHelper.backStep());
    
                Queue<AssignmentEntity> queue = new LinkedList<>();
                queue.add(ae1);
                queue.add(ae2);
                return queue;
            }
    
            //自啟動(dòng)
            private static AssignmentEntity selfStarting() {
                AssignmentEntity assignment = new AssignmentEntity();
                assignment.setName("自啟動(dòng)");
    
                assignment.addStep(StepHelper.intentStep(IntentUtils.huaweiStartupNormalApp()))
                        .addStep(StepHelper.checkStep(BaseApplication.getInstance().getAppName(), true))
                        .addStep(StepHelper.checkStep(BaseApplication.getInstance().getAppName(), false))
                        .addStep(StepHelper.checkStep("允許自啟動(dòng)", true))
                        .addStep(StepHelper.checkStep("允許關(guān)聯(lián)啟動(dòng)", true))
                        .addStep(StepHelper.checkStep("允許后臺(tái)活動(dòng)", true))
                        .addStep(StepHelper.clickStep("確定"))
                        .addStep(StepHelper.backStep());
    
                return assignment;
            }
        }
        
        ...
    }
    

    執(zhí)行任務(wù)示例:

    public class AssignmentFactory {
        ...
        public static void run(Activity activity, Queue<AssignmentEntity> queue) {
            L.i("隊(duì)列開始執(zhí)行");
            int assignmentSize = queue.size();
    
            CompositeDisposable co = new CompositeDisposable();
            Disposable subscribe = Observable.create(
                    (ObservableOnSubscribe<AssignmentEntity>) emitter -> {
                        int progressSize = 0;
                        for (int i = 0; i < assignmentSize; i++) {
                            AssignmentEntity entity = queue.poll();
                            if (entity != null) {
                                Queue<StepEntity> stepQueue = entity.getQueue();
                                if (stepQueue != null) {
                                    int stepSize = stepQueue.size();
                                    for (int j = 0; j < stepSize; j++) {
                                        progressSize++;
                                        emitter.onNext(entity);
                                    }
                                }
                            }
                        }
                        //FloatWindowView.getInstance().progressBar.setMax(progressSize);
                        emitter.onComplete();
                    })
                    .subscribeOn(Schedulers.io())//執(zhí)行在io線程
                    .observeOn(Schedulers.io())//回調(diào)在io線程  //主線程阻塞將無法更新ui AndroidSchedulers.mainThread()
                    .subscribe(
                            assignment -> {
                                //FloatWindowView.getInstance().progressAdd();
                                //L.i("當(dāng)前進(jìn)度:" + assignment.getName() + "===" + FloatWindowView.getInstance().getProgress());
                                poll(activity, assignment.getQueue().poll());
                            },
                            throwable -> L.e("", throwable),
                            () -> //FloatWindowView.getInstance().stopFloatWindow()
                    );
            co.add(subscribe);
        }
    
        private static void poll(Activity activity, StepEntity poll) {
            if (poll == null) return;
            switch (poll.getType()) {
                case StepEntity.TYPE_INTENT://跳轉(zhuǎn)事件
                    activity.startActivity(poll.getIntent());
                    break;
                case StepEntity.TYPE_CLICK://點(diǎn)擊事件
                    MyAccessibilityService.getInstance().clickFirstView(poll.getName());
                    break;
                case StepEntity.TYPE_CHECKED://選中事件
                    MyAccessibilityService.getInstance().clickCheckBox(poll.getName(), poll.isChecked());
                    break;
                case StepEntity.TYPE_BACK://返回事件
                    MyAccessibilityService.getInstance().goBack();
                    break;
            }
            SystemClock.sleep(POST_DELAY_MILLIS);
        }
        ...
    }
    

    鏈接

    完整demo:Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市齿椅,隨后出現(xiàn)的幾起案子琉挖,更是在濱河造成了極大的恐慌,老刑警劉巖涣脚,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件示辈,死亡現(xiàn)場離奇詭異,居然都是意外死亡遣蚀,警方通過查閱死者的電腦和手機(jī)矾麻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芭梯,“玉大人险耀,你說我怎么就攤上這事【链” “怎么了甩牺?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芒涡。 經(jīng)常有香客問我柴灯,道長卖漫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任赠群,我火速辦了婚禮羊始,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘查描。我一直安慰自己突委,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布冬三。 她就那樣靜靜地躺著匀油,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勾笆。 梳的紋絲不亂的頭發(fā)上敌蚜,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機(jī)與錄音窝爪,去河邊找鬼弛车。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蒲每,可吹牛的內(nèi)容都是我干的纷跛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼邀杏,長吁一口氣:“原來是場噩夢啊……” “哼贫奠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起望蜡,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤唤崭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后泣特,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浩姥,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年状您,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勒叠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡膏孟,死狀恐怖眯分,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柒桑,我是刑警寧澤弊决,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響飘诗,放射性物質(zhì)發(fā)生泄漏与倡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一昆稿、第九天 我趴在偏房一處隱蔽的房頂上張望纺座。 院中可真熱鬧,春花似錦溉潭、人聲如沸净响。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽馋贤。三九已至,卻和暖如春畏陕,著一層夾襖步出監(jiān)牢的瞬間配乓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工蹭秋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扰付,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓仁讨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親实昨。 傳聞我的和親對象是個(gè)殘疾皇子洞豁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359