前言: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