安卓自动点击

前言:ui自动化目前使用比较广泛,但是各种各样的弹框会阻断自动化流程。如果业务自己写处理逻辑又特别笨重。于是一个独立的能自动处理弹框的app就会很实用。

基本配置:accessibilityservice.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagReportViewIds|flagRequestTouchExplorationMode"
    android:notificationTimeout="100"
    android:canRetrieveWindowContent="true"
    android:canPerformGestures="true"
    android:canRequestEnhancedWebAccessibility="true"
    xmlns:android="http://schemas.android.com/apk/res/android" />

build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.test.helper"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 26
        versionName "2.6"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.alibaba:fastjson:1.2.28'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

代码逻辑:

package com.test.helper;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.os.Build;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class HelperService extends AccessibilityService {

    // 你需要的点击的包package
    public static String[] PACKAGE_NAME = new String[]{"com.xxxx.xxx","com.android.packageinstaller"};
    private static List<AlertDealModel> LABELS = new ArrayList<>();
    private DealAlertUtils dealAlertUtils;
    private long dealAlertTimes=0;
    private long requestAlertDataTimes=0;

    /**
     * 每秒执行一次弹框检测
     */
    private long DEAL_ALERT_INTERVAL_TIME = 1000;

    /**
     * 每10分钟请求一次服务端配置 10分钟*60秒*1000=600000毫秒
     */
    private long REQUEST_CONFIG_INTERVAL_TIME = 600000;

    static {

        LABELS.add(new AlertDealModel(null,null,"确定", "确定") );
        LABELS.add(new AlertDealModel(null,null,"允许", "允许") );
        LABELS.add(new AlertDealModel(null,null,"始终允许", "始终允许") );
        LABELS.add(new AlertDealModel(null,null,"授权", "授权") );
        LABELS.add(new AlertDealModel(null,null,"确认授权", "确认授权") );
        LABELS.add(new AlertDealModel(null,null,"同意", "同意") );
        LABELS.add(new AlertDealModel(null,null,"总是允许", "总是允许") );
        LABELS.add(new AlertDealModel(null,null,"暂不处理", "暂不处理") );
        LABELS.add(new AlertDealModel(null,null,"我知道了", "我知道了") );
        LABELS.add(new AlertDealModel(null,null,"我知道啦", "我知道啦") );
        LABELS.add(new AlertDealModel(null,null,"好的", "好的") );
        LABELS.add(new AlertDealModel(null,null,"取消", "取消") );
        LABELS.add(new AlertDealModel(null,null,"继续访问", "继续访问") );
        LABELS.add(new AlertDealModel(null,null,"跳过广告", "跳过广告") );
        LABELS.add(new AlertDealModel(null,null,"稍后提醒", "稍后提醒") );
        LABELS.add(new AlertDealModel(null,null,"稍后", "稍后") );
    }

    public HelperService() {
    }

    @Override
    protected void onServiceConnected() {
        LogUtils.log("config success!");
        dealAlertUtils = new DealAlertUtils(this);
        DealPermission dealPermission = new DealPermission();
        dealPermission.doDealPermission();

    }


    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        /*
         * 回调方法,当事件发生时会从这里进入,在这里判断需要捕获的内容,
         * 可通过下面这句log将所有事件详情打印出来,分析决定怎么过滤。
         */


        LogUtils.log("event start!!!");

        if(event==null){
            LogUtils.log("event is null!");
            return;
        }
        LogUtils.log("enentInfo:"+ JSON.toJSONString(event));

        retryDealPermission( event, LABELS );
    }


    public void retryDealPermission( AccessibilityEvent event, List<AlertDealModel> labels ){

        dealAlertTimes = System.currentTimeMillis();

        AccessibilityNodeInfo nodeInfo = getNodeInfo(event);

        dealAlertUtils.dealPermission(nodeInfo,labels);
    }


    public AccessibilityNodeInfo getNodeInfo( AccessibilityEvent event ){

        if (Build.VERSION.SDK_INT >= 16) {
            return super.getRootInActiveWindow();
        } else {
            if(event==null){
                return null;
            }
            return event.getSource();
        }
    }



    @Override
    public void onInterrupt() {
        LogUtils.log("AutoInstallServiceInterrupted");
    }


//    private boolean performClick(List<AccessibilityNodeInfo> nodeInfoList, String label ) {
//        for (AccessibilityNodeInfo node : nodeInfoList) {
//            /*
//             * 这里还可以根据node的类名来过滤,大多数是button类,这里也是为了大而全,
//             * 判断只要是可点击的是可用的就点。
//             */
//            if (label.equals(node.getText())&&node.isClickable() && node.isEnabled()) {
//                return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//            }
//        }
//        return false;
//    }

    public interface Callback{
        void callback();
    }


    /**
     * 更新监听配置
     */
    public class PluginServiceInfo implements Callback{

        @Override
        public void callback() {

            AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
            //指定包名
            accessibilityServiceInfo.packageNames = PACKAGE_NAME;

            //指定事件类型
            accessibilityServiceInfo.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                    + AccessibilityEvent.TYPE_WINDOWS_CHANGED;

            accessibilityServiceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
            accessibilityServiceInfo.notificationTimeout = 30;
            setServiceInfo(accessibilityServiceInfo);
            LogUtils.log("plugin serviceInfo success!");
            LogUtils.log("packageName:"+PACKAGE_NAME);
            LogUtils.log("labels:"+LABELS);
        }
    }


    public String requestConfig() {
        LogUtils.log("start request config!");
        InputStreamReader input = null;
        InputStream inputStream = null;
        try {
            requestAlertDataTimes = System.currentTimeMillis();
            // 拉取配置的链接
            String url_s = "https://xxxxxxx";
            URL url = new URL(url_s);
            HttpURLConnection conn = (HttpURLConnection)url.openConnection();
            conn.setConnectTimeout(3000);

            /**
             * 数据不多不用缓存了
             */
            conn.setUseCaches(true);
            conn.connect();
            inputStream = conn.getInputStream();
            input = new InputStreamReader(inputStream);
            BufferedReader buffer = new BufferedReader(input);
            LogUtils.log("responseCode:"+conn.getResponseCode());

            if(conn.getResponseCode() == 200){//200意味着返回的是"OK"
                String inputLine;
                StringBuffer resultData  = new StringBuffer();//StringBuffer字符串拼接很快
                while((inputLine = buffer.readLine())!= null){
                    resultData.append(inputLine);
                }
                String jsonData = resultData.toString();
                LogUtils.log("requestConfigInfo:"+jsonData);
                if(jsonData!=null){
                    JSONObject jsonObject = JSON.parseObject(jsonData);
                    PACKAGE_NAME = jsonObject.getObject("packageNames",String[].class);
                    LABELS = JSON.parseArray(jsonObject.getString("labels"), AlertDealModel.class);
                    jsonObject.getObject("labels",List.class);
                    DEAL_ALERT_INTERVAL_TIME = jsonObject.getLong("dealAlertIntervalTime")==null ? DEAL_ALERT_INTERVAL_TIME : jsonObject.getLong("dealAlertIntervalTime");
                    REQUEST_CONFIG_INTERVAL_TIME = jsonObject.getLong("requestConfigIntervalTime")==null? REQUEST_CONFIG_INTERVAL_TIME:jsonObject.getLong("requestConfigIntervalTime");
                }
                return jsonData;
            } else {
                LogUtils.log("requestConfigError,responseCode:"+conn.getResponseCode());
            }
        } catch(Exception e){
            e.printStackTrace();
            LogUtils.log(e.getMessage());
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
            }catch ( Exception e ){
                LogUtils.log(e.getMessage());
            }
        }

        return null;
    }

    /**
     * 从远端获取配置数据
     */
    public class DealPermission implements Runnable{


        public void doDealPermission(){
            Thread thread = new Thread(this);
            thread.start();
            LogUtils.log("thread start!");
        }

        @Override
        public void run() {

            dealPermission();
        }

        public void dealPermission() {

            while (true) {
                try {
                    if (System.currentTimeMillis() - dealAlertTimes > DEAL_ALERT_INTERVAL_TIME) {
                        retryDealPermission(null, LABELS);
                        if(System.currentTimeMillis()-requestAlertDataTimes > REQUEST_CONFIG_INTERVAL_TIME ){
                            requestConfig();
                        }
                    } else {
                        Thread.sleep(DEAL_ALERT_INTERVAL_TIME);
                    }
                }catch ( Throwable r ){
                    LogUtils.log(r.toString());
                }
            }
        }
    }

}

HelpeModel:

package com.test.helper;

public class AlertDealModel {
    private String resourceId;
    private String className;
    private String dealKey;
    private String targetTag;


    public AlertDealModel(String resourceId, String className, String dealKey, String targetTag) {
        this.resourceId = resourceId;
        this.className = className;
        this.dealKey = dealKey;
        this.targetTag = targetTag;
    }

    public AlertDealModel() {
    }

    public String getResourceId() {
        return resourceId;
    }

    public void setResourceId(String resourceId) {
        this.resourceId = resourceId;
    }

    public String getDealKey() {
        return dealKey;
    }

    public void setDealKey(String dealKey) {
        this.dealKey = dealKey;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getTargetTag() {
        return targetTag;
    }

    public void setTargetTag(String targetTag) {
        this.targetTag = targetTag;
    }
}

弹框处理:

package com.test.helper;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.annotation.TargetApi;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Build;
import android.text.TextUtils;
import android.view.accessibility.AccessibilityNodeInfo;

import com.alibaba.fastjson.JSON;

import java.util.List;


public class DealAlertUtils {
    private AccessibilityService mAccessibilityService;

    public DealAlertUtils(AccessibilityService mAccessibilityService ){

        this.mAccessibilityService = mAccessibilityService;
    }




    public void dealPermission(AccessibilityNodeInfo dialogNodeInfo, List<AlertDealModel> labels ) {
        if (dialogNodeInfo == null) {
            return;
        }

        String identity = getIdentityRootInfo(dialogNodeInfo, false);
        LogUtils.log( "对话框点击:标识," + identity);

        if(!checkNodeInfo(dialogNodeInfo)){
            return;
        }

        if(TextUtils.isEmpty(identity)){
            return;
        }
        if (labels != null && labels.size()>0) {
            try {
                for ( AlertDealModel alertDealModel : labels ) {
                    if (identity.contains(alertDealModel.getTargetTag())) {
                        LogUtils.log( "过滤标识," + alertDealModel.getTargetTag() + ",过滤按钮" + alertDealModel.getDealKey());
                        if (!TextUtils.isEmpty(alertDealModel.getDealKey())) {
                            clickBtnByText(dialogNodeInfo, alertDealModel.getDealKey());
                        }
                    }
                }
            } catch (Exception e) {
                LogUtils.log( e.toString());
            }
        }
    }


    /**
     * 检查拿到的nodeInfo是不是合法
     * @param nodeInfo
     * @return
     */
    public boolean checkNodeInfo( AccessibilityNodeInfo nodeInfo  ){

        if ( nodeInfo == null ) {
            LogUtils.log("<null> event source");
            return false;
        }

//        for( String packageName: AboatHelperService.PACKAGE_NAME ){
//            if( packageName.equals(nodeInfo.getPackageName()) || (!TextUtils.isEmpty(nodeInfo.getPackageName())&&nodeInfo.getPackageName().toString().contains("packageinstaller"))){
//                return true;
//            }
//        }

        LogUtils.log("nodeInfo:"+ JSON.toJSONString(nodeInfo));

        return true;
    }


    /* 获取该节点的唯一标识 */
    public String getIdentityRootInfo(AccessibilityNodeInfo nodeInfo, boolean showResource) {
        //遍历节点所有text
        StringBuffer identityStr = new StringBuffer();
        for (int i = 0, count = nodeInfo.getChildCount(); i < count; i++) {
            AccessibilityNodeInfo val = nodeInfo.getChild(i);
            if (val == null) {
                continue;
            }
            if (val.getChildCount() != 0) {
                //子节点
                identityStr.append(getIdentityRootInfo(val, showResource));
            } else {
                //根节点
                identityStr.append(showResource ? getNodeText(val).getResourceId() : getNodeText(val).getNodeName());
            }
        }
        return identityStr.toString();
    }


    /* 获取node的描述text */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    public NameNode getNodeText(AccessibilityNodeInfo val) {
        val = getOriginNodeInfo(val);
        CharSequence clickText = (val == null ? "null" : val.getText());
        String resourceId = (val == null ? "null" : val.getViewIdResourceName());
        if (clickText == null) {
            clickText = val.getContentDescription();
        }
        if (clickText == null) {
            clickText = "";
        }
        if (resourceId == null) {
            resourceId = "";
        }
        return new NameNode(clickText.toString(), resourceId);
    }


    /* 获取单个节点的原始节点 */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    public AccessibilityNodeInfo getOriginNodeInfo(AccessibilityNodeInfo val) {
        //获取原子节点的值
        while (val != null && val.getChildCount() != 0) {
            val = val.getChild(0);
        }
        if (val != null) {
            LogUtils.log("getOriginNodeInfo:" + val.getClassName() + ":" + val.getViewIdResourceName() + ":" + val.getChildCount());
        }
        return val;
    }



    /* 页面点击按钮 */
    public void clickBtnByText(AccessibilityNodeInfo nodeInfo, String clickVal ) {
        //点掉允许
        for (int i = 0, count = nodeInfo.getChildCount(); i < count; i++) {
            AccessibilityNodeInfo val = nodeInfo.getChild(i);
            NameNode noteTest = getNodeText(val);
            //过滤空
            if (val == null) {
                continue;
            }
            //添加节点
            if (val.getChildCount() == 0 && (noteTest.getNodeName().equals(clickVal) || noteTest.getResourceId().equals(clickVal))) {
                //子节点
                PageCrawlerEntity entity = new PageCrawlerEntity();
                entity.setAccessibilityNodeInfo(val);
                recycleNormalClick(entity);
            }
            //继续遍历
            if (val.getChildCount() != 0) {
                clickBtnByText(val, clickVal);
            }
        }
    }

    /* 普通按钮点击 */
    public void recycleNormalClick(PageCrawlerEntity val ) {
        if (val != null) {
            val.setClickable(true);
            clickByGesture(val.getAccessibilityNodeInfo());
        }
    }


    public void clickByGesture(AccessibilityNodeInfo nodeInfo) {

        //防止空指针
        if (nodeInfo == null){
            return;
        }

        Rect pos = new Rect();
        nodeInfo.getBoundsInScreen(pos);
        int centerY = pos.centerY();

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            GestureDescription.Builder down = new GestureDescription.Builder();
            Path p_down = new Path();
            p_down.moveTo(pos.centerX(), centerY);
            down.addStroke(new GestureDescription.StrokeDescription(p_down, 100, 50));
            mAccessibilityService.dispatchGesture(down.build(), null, null);
            LogUtils.log( "dispatchGesture:"+nodeInfo.toString());
        } else {
            recycleClick(nodeInfo);
        }
    }


    public void recycleClick(AccessibilityNodeInfo nodeInfo) {
        //防止空指针
        if (nodeInfo == null){
            return;
        }
        boolean result = nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
        if (!result) {
            recycleClick(nodeInfo.getParent());
        } else {
            LogUtils.log( "recycleClick:"+nodeInfo.toString());
        }
    }

    /**
     * 根据位置点击
     *
     * @param nodeInfo
     */
    public void longClickByGesture(AccessibilityNodeInfo nodeInfo) {
        Rect pos = new Rect();
        nodeInfo.getBoundsInScreen(pos);
        int centerY = pos.centerY();

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            GestureDescription.Builder down = new GestureDescription.Builder();
            Path p_down = new Path();
            p_down.moveTo(pos.centerX(), centerY);
            down.addStroke(new GestureDescription.StrokeDescription(p_down, 100, 2000));
            mAccessibilityService.dispatchGesture(down.build(), null, null);
        } else {
            recycleLongClick(nodeInfo);
        }
    }

    public void recycleLongClick(AccessibilityNodeInfo nodeInfo) {
        boolean result = nodeInfo.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
        if (!result) {
            recycleLongClick(nodeInfo.getParent());
        }
    }












    public class NameNode {
        private String nodeName;//按钮名称
        private String resourceId;//按钮id

        public NameNode(String nodeName, String resourceId) {
            setNodeName(nodeName);
            setResourceId(resourceId);
        }

        public String getNodeName() {
            return nodeName;
        }

        public void setNodeName(String nodeName) {
            this.nodeName = nodeName;
        }

        public String getResourceId() {
            return resourceId;
        }

        public void setResourceId(String resourceId) {
            this.resourceId = resourceId;
        }
    }
}

页面实例:

package com.test.helper;

import android.view.accessibility.AccessibilityNodeInfo;
import java.io.Serializable;


public class PageCrawlerEntity implements Serializable {
    private AccessibilityNodeInfo accessibilityNodeInfo;
    private boolean clickable;

    public void setClickable(boolean clickable) {
        this.clickable = clickable;
    }

    public boolean isClickable() {
        //设置已点击
        if (clickable) {
            return true;
        }
        //普通是否可点击
        if (accessibilityNodeInfo == null) {
            return false;
        }

        //是否可点击
        if (accessibilityNodeInfo.isClickable()) {
            return true;
        }
        return false;
    }

    public AccessibilityNodeInfo getAccessibilityNodeInfo() {
        return accessibilityNodeInfo;
    }

    public void setAccessibilityNodeInfo(AccessibilityNodeInfo accessibilityNodeInfo) {
        this.accessibilityNodeInfo = accessibilityNodeInfo;
    }
}

日志记录:

package com.test.helper;

import android.util.Log;

public class LogUtils {
    private static final String TAG = "HelperService";


    public static void log(String s) {
        Log.e(TAG, s);
    }
}

安装启动:

package com.test.tools;

import java.util.List;

public class DeviceUtils {
    private static final String ACCESSIBILITY_SERVICE_NAME="com.test.helper/com.test.helper.AboatHelperService";


    public static boolean checkAccessibilityService( String deviceId ){

        if(StringUtil.isBlank(deviceId)){
            return true;
        }
        ProcessBuilder processBuilder = new ProcessBuilder(CmdTemplate.OPEN_ACCESSIBILITY_SERVICE, deviceId, ACCESSIBILITY_SERVICE_NAME);
        ResultDO<List<String>> result = ShellExecutor.executeShellFileWithResult(processBuilder);

        return result.isSuccess();
    }
}

shell执行代码:

 public static ResultDO executeShellFileWithResult( ProcessBuilder processBuilder ) {

        Process process = null;
        List<String> resultList = new ArrayList<>();
        String deviceId = null;
        BufferedReader stdInput =null;
        BufferedReader stdError =null;

        try {
            process = processBuilder.start();
            stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
            stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line;

            while ((line = stdInput.readLine())!= null) {
                resultList.add( line );
                if(line.contains("ScriptEndExecute")){
                    break;
                }
                if( CommonConfigSwitch.shellLogSwitch ) {
                    logger.info(line);
                }
            }

            logger.info("outOf info log");

            while ((line = stdError.readLine())!= null) {
                resultList.add( line );
                if(line.contains("device")){
                    logger.error("[Device-Error-Monitor]:["+line+",deviceId:"+deviceId+"]");
                } else {
                    logger.error(line);
                }
                if(line.contains("timeout")||(line.contains("已终止"))||line.contains("ScriptEndExecute")){
                    break;
                }
            }
            logger.info("outOf error log");
        } catch (Exception e) {
            logger.error("execute shell file err!", e);
            return ResultDO.getErrorResult("执行失败!");
        } finally {
            if(process!=null){
                process.destroy();
            }
            FileUtil.closeQuietly(stdError,stdInput);
        }
        return ResultDO.getSecResult("执行成功!", resultList);
    }

shell代码:

#!/bin/bash


#!/bin/bash

startTime=$(date +%s);
deviceId=$1;
packageName=$2;

ret=`timeout 10 adb -s $deviceId shell settings get secure enabled_accessibility_services`;
echo "current open service:" $ret;

if [[ $ret == *$packageName* ]]
then
    echo $packageName" service already open";
else
    echo $packageName" service start open";
    if [ -z "$ret" ]; then
      echo "current no open service";
      timeout 10 adb -s $deviceId shell settings put secure enabled_accessibility_services "$packageName"
    else
      timeout 10 adb -s $deviceId shell settings put secure enabled_accessibility_services "$ret:$packageName"
    fi
    timeout 10 adb -s $deviceId shell settings put secure accessibility_enabled 1

fi

endTime=$(date +%s)
echo "spendTime>>openServiceTime="$(( $endTime - $startTime ))
echo ScriptEndExecute
exit 0

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值