无障碍服务 AccessibilityService

1、新建一个Service 集成AccessibilityService

1.1、DingService.java

 public class DingService extends AccessibilityService {
     //初始化工作
    public void onServiceConnected() {

    } 
    //获得控件节点
    public void onAccessibilityEvent(AccessibilityEvent event) {
        //获得根节点
        AccessibilityNodeInfo rowNode = getRootInActiveWindow();
        if (rowNode == null) {
            return;
        } else {
         //在这里对根节点遍历等操作,寻找需要的控件,进行操作
         recycle(rowNode);
         }
    }
     /**
     * 中断AccessibilityService的反馈时调用
     */
    @Override
    public void onInterrupt() {

    }
 }

1.2、控件定制化操作

  @SuppressLint("NewApi")
    public void recycle(AccessibilityNodeInfo info) {
        if (info.getChildCount() == 0) {
            click("注册/登录");//遍历到名为“注册/登录”的控件信息,并点击他
              if (info.getClassName().equals("android.widget.EditText"))
              {
                setText(info, "请输入密码","123456");//给内容为“请输入密码”的EditText填充123456数据
            }

    }
    else {//继续遍历
            for (int i = 0; i < info.getChildCount(); i++) {
                if (info.getChild(i) != null) {
                    recycle(info.getChild(i));
                }
            }
        }

}

1.3、通过文字点击

    //通过文字点击
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    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);
            //Log.i(TAG, "点击" + viewText);
            Boolean fal = onclick(view);  //遍历点击
            return fal;

        }
    }
    private boolean onclick(AccessibilityNodeInfo view) {

        if (view.isClickable()) {
            view.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            //Log.i(TAG, "成功");
            return true;
        } else {

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

1.4、通过文字给EditText填充数据

void setText(AccessibilityNodeInfo info, String title, String vaule) {
        if (info == null) {
            return;
        }
        String str = info.getText().toString();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Log.i(TAG, "HintText" + info.getHintText() + "");
        }
        Log.i(TAG, "Text" + info.getText() + "");

        if (str.equals(title)) {
         putClipboard1(info, vaule);
        }
}

    //自动为edittext粘贴上文字内容
    public Boolean putClipboard1(AccessibilityNodeInfo edittext, String text) {
        if (edittext != null) {
            ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
            ClipData clip = ClipData.newPlainText("text", text);
            clipboard.setPrimaryClip(clip);
            //焦点(n是AccessibilityNodeInfo对象)
            edittext.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
            ////粘贴进入内容
            return edittext.performAction(AccessibilityNodeInfo.ACTION_PASTE);
            //发送
            //...
        }
        return false;
    }

1.5、打印界面节点

   private static int tabcount = -1;
    private static StringBuilder sb;

    //打印此时的界面状况,便于分析
    private static void analysisPacketInfo(AccessibilityNodeInfo info, int... ints) {
        if (info == null) {
            return;
        }
        if (tabcount > 0) {
            for (int i = 0; i < tabcount; i++) {
                sb.append("\t\t");
            }
        }
        if (ints != null && ints.length > 0) {
            StringBuilder s = new StringBuilder();
            for (int j = 0; j < ints.length; j++) {
                s.append(ints[j]).append(".");
            }
            sb.append(s).append(" ");
        }
        String name = info.getClassName().toString();
        String[] split = name.split("\\.");
        name = split[split.length - 1];
        if ("TextView".equals(name)) {
            CharSequence text = info.getText();
            sb.append("text:").append(text);
        } else if ("Button".equals(name)) {
            CharSequence text = info.getText();
            sb.append("Button:").append(text);
        } else {
            sb.append(name);
        }
        sb.append("\n");

        int count = info.getChildCount();
        if (count > 0) {
            tabcount++;
            int len = ints.length + 1;
            int[] newInts = Arrays.copyOf(ints, len);

            for (int i = 0; i < count; i++) {
                newInts[len - 1] = i;
                analysisPacketInfo(info.getChild(i), newInts);
            }
            tabcount--;
        }

    }
    public static void printPacketInfo(AccessibilityNodeInfo root) {
        sb = new StringBuilder();
        tabcount = 0;
        int[] is = {};
        analysisPacketInfo(root, is);
        Log.d("节点", sb.toString());
    }
    //查找节点
    public static AccessibilityNodeInfo findNodeByViewName(AccessibilityNodeInfo info, String viewName) {
        String name = info.getClassName().toString();
        String[] split = name.split("\\.");
        name = split[split.length - 1];
        if (name.equals(viewName)) {
            return info;
        } else {

            int count = info.getChildCount();
            if (count > 0) {
                for (int i = 0; i < count; i++) {
                    AccessibilityNodeInfo inf = findNodeByViewName(info.getChild(i), viewName);
                    if (inf != null) {
                        return inf;
                    }
                }
            } else {
                return null;
            }
        }
        return null;
    }
  • 这样我们可以通过前面的0.0.0.1.1直接定位到View
       AccessibilityNodeInfo info = root;
        int[] path = {0, 0, 0, 1, 1};
        for (int i = 0; i < path.length; i++) {
            info = info.getChild(path[i]);
            if (info == null || info.getChildCount() <= 0) {
                return null;
            }
        }
        return info;
  • 当然你有可能不知道0.0.0.1.1对应哪一个视图,可以
        Rect rect = new Rect();
        info.getBoundsInScreen(rect);
        //状态栏的高度
        int h = GUtil.getStatusBarHeight(context.getApplicationContext());
        rect.top -= h;
        rect.bottom -= h;

打印rect,或者直接在全局窗口创建window,显示rect为有色区域.

2、配置

  • manifest
      <!-- AccessibilityService服务实现权限 -->
    <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
        tools:ignore="ProtectedPermissions" />

<!-- 注册监听手机状态Servicer -->
        <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/accessibilityservice" />
        </service>
  • accessibilityservice.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:packageNames="com.hbc.hbc"//com.hbc.hbc是你要监听的应用包名,不写则监听所有包
    android:notificationTimeout="200" />

3、在MainActivity中启动

//在onCreate中调用startAccessibilityService就可以开启服务

    /**
     * 前往设置界面开启服务
     */
    private void startAccessibilityService() {
        new AlertDialog.Builder(this)
                .setTitle("开启辅助功能")
                .setIcon(R.mipmap.ic_launcher)
                .setMessage("使用此项功能需要您开启辅助功能")
                .setPositiveButton("立即开启", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 隐式调用系统设置界面
                        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
                        startActivity(intent);
                    }
                }).create().show();
    }
    /**
     * 判断自己的应用的AccessibilityService是否在运行
     *
     * @return
     */
    private boolean serviceIsRunning() {
        ActivityManager am = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(Short.MAX_VALUE);
        for (ActivityManager.RunningServiceInfo info : services) {
            if (info.service.getClassName().equals(getPackageName() + ".DingService")) {//DingService是你的包名
                return true;
            }
        }
        return false;
    }

AccessibilityNodeInfo支持的操作

  • 监听包过滤,一般在onServiceConnected中进行初始化
    “`
    String[] PACKAGE_NAMES = {“com.hbc.hbc”};//要监听的包名
    AccessibilityServiceInfo info = new AccessibilityServiceInfo();
    info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;

    info.packageNames = PACKAGE_NAMES;
    

    // …配置
    setServiceInfo(info);

 - AccessibilityService本身有方法,模拟返回键,home键等

performGlobalAction(GLOBAL_ACTION_BACK)

 - AccessibilityNodeInfo还可以直接模拟点击,长按等事件。

info.performAction(AccessibilityNodeInfo.ACTION_CLICK);

 **但是,performAction有时候根本没用!!!** 

因为现在很多应用都是混合应用,内容页可能是Html5写的,看起来是按钮,其实就是普通View..他的点击事件不是通过OnClick产生,而是直接判断TouchEvent。AccessibilityNodeInfo没有提供发送down,move,up事件的api。我不能通过这系列模拟所有操作了,替代方案使用root 后的手机,向系统发送全局点击命令。

/*点击某个视图/
public static void perforGlobalClick(AccessibilityNodeInfo info) {
Rect rect = new Rect();
info.getBoundsInScreen(rect);
perforGlobalClick(rect.centerX(), rect.centerY());
}

//点击屏幕指定点
public static void perforGlobalClick(int x, int y) {
execShellCmd(“input tap ” + x + ” ” + y);
}
/**
* 执行shell命令
*
* @param cmd
*/
public static void execShellCmd(String cmd) {

    try {
        // 申请获取root权限,这一步很重要,不然会没有作用
        Process process = Runtime.getRuntime().exec("su");
        // 获取输出流
        OutputStream outputStream = process.getOutputStream();
        DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
        dataOutputStream.writeBytes(cmd);
        dataOutputStream.flush();
        dataOutputStream.close();
        outputStream.close();

// process.waitFor();
} catch (Throwable t) {
t.printStackTrace();
}
}
“`

借鉴资料

https://blog.csdn.net/u013147734/article/details/78490629
https://blog.csdn.net/c794904140/article/details/52153148
https://www.cnblogs.com/itchq/articles/5648657.html

猜你喜欢

转载自blog.csdn.net/m0_37909265/article/details/82190914