4254 words
21 minutes
无障碍开发 1

无障碍开发,编写一个自己的安卓APP并开启无障碍服务,然后APP就可以实现去控制手机中的其他APP。很多的群控软件都是基于无障碍开发实现。

例如:抖音的自动评论工具。

image-20240702170022431

1.常见版本#

image-20240702171725022

image-20240702171733937

2.环境准备#

  • Android Studio【2020.3.1.24】

    https://developer.android.google.cn/studio/archive
    
    Android Studio Arctic Fox | 2020.3.1 Patch 2 September 1, 2021
       Windows (64-bit): android-studio-2020.3.1.24-windows.exe (957.2 MB)
            Mac (Intel): android-studio-2020.3.1.24-mac.zip (997.2 MB)
    Mac (Apple silicon): android-studio-2020.3.1.24-mac_arm.dmg (993.3 MB)
    
    其他airtest
    	https://airtest-new.nie.netease.com/update/airtestide
    
  • 一部安卓手机【无需ROOT】

    本节以红米note9 Pro为例
    
  • 抖音 v29.8.0

    https://www.wandoujia.com/apps/7461948/history_v290801
    

3.单机版#

3.1 创建项目#

image-20240703103311205

image-20240703103346105

implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'

3.2 无障碍服务#

image-20240703104640834

app中如果想要支持无障碍服务,需要先进行配置+编写无障碍服务相关的类。

  • 配置【AndroidManifest.xml】

    <!-- 注册辅助功能服务 -->
    <service
             android:name=".LuckyAccessibilityService"
             android:enabled="true"
             android:exported="true"
             android:label="抖音直播助手"
             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/accessibility_config" />
    </service>
    <!-- 注册辅助功能服务 -->
    
  • 配置【xml/accessibility_config】

    <?xml version="1.0" encoding="utf-8"?>
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
        android:accessibilityEventTypes="typeAllMask"
        android:accessibilityFeedbackType="feedbackSpoken"
        android:accessibilityFlags="flagIncludeNotImportantViews|flagReportViewIds"
        android:canPerformGestures="true"
        android:canRetrieveWindowContent="true"
        android:description="@string/accessibility_desc"
        android:notificationTimeout="100" />
    
  • 配置【values/strings.xml】

    <resources>
        <string name="app_name">AutoDemo</string>
        <string name="accessibility_desc">智能一点一点</string>
    </resources>
    
  • LuckyAccessibilityService

package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class LuckyAccessibilityService extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {

    }

    @Override
    public void onInterrupt() {

    }
}

将上述代码编写好,再次启动APP时,在安卓手机的无障碍模式中,就会出现我们的服务。

image-20240703141849036

当启动无障碍服务之后,onAccessibilityEvent 方法就会被触发执行,可以在此处编写控制其他app的代码。

package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

public class LuckyAccessibilityService extends AccessibilityService {
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        Log.e("无障碍","测试运行中");
    }

    @Override
    public void onInterrupt() {

    }
}

image-20240703110340960

package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

public class LuckyAccessibilityService extends AccessibilityService {

    public static boolean running = false;

    @Override
    public void onInterrupt() {

    }
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!running) {
            running = true;
            
            Log.e("无障碍", "测试运行中");
        }
    }
}

image-20240703111820565

3.3 引导服务#

启动APP时,检测如果未开启无障碍服务,则自动进入手机APP的无障碍页面,去开启无障碍服务。

这样就不需要每次都去手动找到无障碍服务再启动了。

package com.nb.autodemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 引导用户开启无障碍
        checkAccessibility();
    }


    /**
     * 引导开启无障碍模式
     */
    private void checkAccessibility() {
        if (!isAccessibilitySettingsOn(getApplicationContext())) {
            // 进入无障碍页面,去开启服务
            startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
        }
    }

    private boolean isAccessibilitySettingsOn(Context mContext) {
        int accessibilityEnabled = 0;
        final String service = getPackageName() + "/" + LuckyAccessibilityService.class.getCanonicalName();
        try {
            accessibilityEnabled = Settings.Secure.getInt(
                    mContext.getApplicationContext().getContentResolver(),
                    android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            Log.e("主界面", "配置文件没找到: " + e.getMessage());
        }

        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
        if (accessibilityEnabled == 1) {
            String settingValue = Settings.Secure.getString(
                    mContext.getApplicationContext().getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                mStringColonSplitter.setString(settingValue);
                while (mStringColonSplitter.hasNext()) {
                    String accessibilityService = mStringColonSplitter.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}

3.4 开始按钮#

当启动无障碍服务之后 LuckyAccessibilityServiceonAccessibilityEvent方法就会被出发执行(太快了)

所以,可以在我们自己的应用中定义一个按钮,用按钮当开关,来控制无障碍业务代码的执行。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="5dp"
        android:text="启动" />

</LinearLayout>
package com.nb.autodemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private Button btnStart;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initListener();
        // 引导用户开启无障碍
        checkAccessibility();
    }


    private void initView() {
        btnStart = findViewById(R.id.btn_start);
    }

    private void initListener() {
        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                start();
            }
        });
    }
    private void start() {
        LuckyAccessibilityService.allowStart = true;
    }

    /**
     * 引导开启无障碍模式
     */
    private void checkAccessibility() {
        if (!isAccessibilitySettingsOn(getApplicationContext())) {
            Toast.makeText(this, "请先打开无障碍服务", Toast.LENGTH_LONG).show();
            startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
        }
    }

    private boolean isAccessibilitySettingsOn(Context mContext) {
        int accessibilityEnabled = 0;
        final String service = getPackageName() + "/" + LuckyAccessibilityService.class.getCanonicalName();
        try {
            accessibilityEnabled = Settings.Secure.getInt(
                    mContext.getApplicationContext().getContentResolver(),
                    android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            Log.e("主界面", "配置文件没找到: " + e.getMessage());
        }

        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
        if (accessibilityEnabled == 1) {
            String settingValue = Settings.Secure.getString(
                    mContext.getApplicationContext().getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (settingValue != null) {
                mStringColonSplitter.setString(settingValue);
                while (mStringColonSplitter.hasNext()) {
                    String accessibilityService = mStringColonSplitter.next();
                    if (accessibilityService.equalsIgnoreCase(service)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

public class LuckyAccessibilityService extends AccessibilityService {

    public static boolean allowStart = false;
    public static boolean running = false;

    @Override
    public void onInterrupt() {

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!allowStart) {
            return;
        }

        if (!running) {
            running = true;

            Log.e("无障碍", "测试运行中");
        }

    }

}

3.5 启动线程执行#

package com.nb.autodemo;

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

import java.util.List;

public class LuckyAccessibilityService extends AccessibilityService {

    public static boolean allowStart = false;
    public static boolean running = false;

    public static Thread runThread = null;

    @Override
    public void onInterrupt() {

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!allowStart) {
            return;
        }

        if (!running) {
            running = true;

            // 创建线程
            runThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    loopTask();
                }
            });
            runThread.start();
        }

    }

    public void loopTask() {
        while (true) {
            try {
                Log.e("无障碍", "开始运行");
                Thread.sleep(2000);

            } catch (Exception ex) {
                 Log.e("异常", ex.toString());
                ex.printStackTrace();
            }
        }
    }
}

3.6 无障碍:清除应用#

image-20240703144542761

先将我们的无障碍应用保护起来,然后用无障碍的代码执行操作:

  • 返回主页

    performGlobalAction(GLOBAL_ACTION_HOME);
    
  • 打开最近应用

    performGlobalAction(GLOBAL_ACTION_RECENTS);
    
  • 清除所有其他的应用 image-20240703143445320 image-20240703143616359

    Log.e("无障碍", "开始运行");
    Thread.sleep(2000);
    
    // 回到首页 Home
    performGlobalAction(GLOBAL_ACTION_HOME);
    Thread.sleep(2000);
    
    // 打开最近应用
    performGlobalAction(GLOBAL_ACTION_RECENTS);
    Thread.sleep(2000);
    
    // 寻找标签 com.android.systemui:id/clearAnimView
    AccessibilityNodeInfo root = getRootInActiveWindow();
    List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId("com.android.systemui:id/clearAnimView");
    AccessibilityNodeInfo node = nodeList.get(0);
    Thread.sleep(2000);
    
    // 点击标签
    node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
    node.recycle();
    

    示例代码:

    package com.nb.autodemo;
    
    import android.accessibilityservice.AccessibilityService;
    import android.util.Log;
    import android.view.accessibility.AccessibilityEvent;
    import android.view.accessibility.AccessibilityNodeInfo;
    
    import java.util.List;
    
    public class LuckyAccessibilityService extends AccessibilityService {
    
        public static boolean allowStart = false;
        public static boolean running = false;
    
        public static Thread runThread = null;
    
        @Override
        public void onInterrupt() {
    
        }
    
        @Override
        public void onAccessibilityEvent(AccessibilityEvent event) {
            if (!allowStart) {
                return;
            }
    
            if (!running) {
                running = true;
    
                // 创建线程
                runThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        loopTask();
                    }
                });
                runThread.start();
            }
    
        }
    
        public void loopTask() {
            while (true) {
                try {
                    Log.e("无障碍", "开始运行");
                    Thread.sleep(2000);
    
                    // 回到首页 Home
                    performGlobalAction(GLOBAL_ACTION_HOME);
                    Thread.sleep(2000);
    
                    // 打开最近应用
                    performGlobalAction(GLOBAL_ACTION_RECENTS);
                    Thread.sleep(2000);
    
                    // 寻找标签 com.android.systemui:id/clearAnimView
                    /*
                    AccessibilityNodeInfo root = getRootInActiveWindow();
                    List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId("com.android.systemui:id/clearAnimView");
                    AccessibilityNodeInfo node = nodeList.get(0);
                    */
                    AccessibilityNodeInfo killAllNode = findNodeById("com.android.systemui:id/clearAnimView");
                    if (killAllNode == null) {
                        continue;
                    }
                    Thread.sleep(2000);
    
                    // 点击标签
                    /*
                    node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    node.recycle();
                    */
                    clickByNodeClickable(killAllNode);
    
    
                } catch (Exception ex) {
                    Log.e("异常", ex.toString());
                    ex.printStackTrace();
                }
            }
        }
    
        /**
         * 根据ID找元素
         */
        private AccessibilityNodeInfo findNodeById(String id) {
            AccessibilityNodeInfo root = getRootInActiveWindow();
            if (root == null) {
                return null;
            }
            List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId(id);
            if (nodeList != null) {
                for (int i = 0; i < nodeList.size(); i++) {
                    AccessibilityNodeInfo node = nodeList.get(i);
                    if (node != null) {
                        return node;
                    }
                }
            }
            return null;
        }
    
        /**
         * 点击节点
         */
        private void clickByNodeClickable(AccessibilityNodeInfo node) {
            if (node.isClickable()) {
                node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                node.recycle();
            } else {
                AccessibilityNodeInfo parent = node.getParent();
                node.recycle();
                clickByNodeClickable(parent);
            }
        }
    }
    

3.7 无障碍:启动抖音#

根据app名称【抖音】寻找app并点击。

package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import java.util.List;

public class LuckyAccessibilityService extends AccessibilityService {

    public static boolean allowStart = false;
    public static boolean running = false;

    public static Thread runThread = null;

    @Override
    public void onInterrupt() {

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!allowStart) {
            return;
        }

        if (!running) {
            running = true;

            // 创建线程
            runThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    loopTask();
                }
            });
            runThread.start();
        }

    }

    public void loopTask() {
        while (true) {
            try {
                Log.e("无障碍", "开始运行");

                // 关闭所有其他应用
                killAllProcess();

                // 寻找抖音并打开
                Thread.sleep(5000);
                if (!launchApplicationByName("抖音", 10)) {
                    Log.e("无障碍", "无法启动抖音");
                    continue;
                }

            } catch (Exception ex) {
                Log.e("异常", ex.toString());
                ex.printStackTrace();
            }
        }
    }

    /**
     * 在页面上寻找名字叫啥的app
     */
    public boolean launchApplicationByName(String name, int retry) throws InterruptedException {

        performGlobalAction(GLOBAL_ACTION_HOME);

        for (int i = 0; i < retry; i++) {
            // 翻看右边的页面
            slideScreen(540, 800, 500, 800, 500L, 200L);
            Thread.sleep(3000);

            // 找抖音APP
            AccessibilityNodeInfo app = findNodeByText(name);
            if (app != null) {
                Thread.sleep(500);
                clickByNodeClickable(app);
                Thread.sleep(5000);
                return true;
            }
        }
        return false;
    }


    /**
     * 滑动屏幕
     */
    private boolean slideScreen(int startX, int startY, int endX, int endY, Long startTime, Long duration) {
        try {
            Path path = new Path();
            path.moveTo(startX, startY);
            path.lineTo(endX, endY);
            GestureDescription.Builder builder = new GestureDescription.Builder();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 500L, 37L)).build();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 1000L, 500L)).build();
            GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, startTime, duration)).build();
            dispatchGesture(description, null, null);
            return true;
        } catch (Exception e) {
            // Log.e(TAG, "滑动屏幕失败,详细:" + e.toString());
            Log.e("无障碍", "滑动屏幕失败");
        }
        return false;
    }

    /**
     * 杀死所有进程(记得提前锁定无障碍应用)
     */
    public void killAllProcess() throws InterruptedException {
        // 回到首页
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);

        // 打开最近应用
        performGlobalAction(GLOBAL_ACTION_RECENTS);
        Thread.sleep(2000);

        String[] clearAppIdArray = new String[]{
                "com.huawei.android.launcher:id/clear_all_recents_image_button", // 华为v30 pro
                "com.hihonor.android.launcher:id/clear_all_recents_image_button", // 荣耀
                "com.android.systemui:id/clearAnimView" // 小米8A
        };
        for (String id : clearAppIdArray) {
            // 根据ID找到节点
            AccessibilityNodeInfo clearIdNode = findNodeById(id);
            if (clearIdNode != null) {
                // 点击标签
                clickByNodeClickable(clearIdNode);
                break;
            }
        }
    }

    /**
     * 根据文本找元素
     */
    private AccessibilityNodeInfo findNodeByText(String text) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByText(text);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }


    /**
     * 根据ID找元素
     */
    private AccessibilityNodeInfo findNodeById(String id) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId(id);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * 点击节点
     */
    private void clickByNodeClickable(AccessibilityNodeInfo node) {
        if (node.isClickable()) {
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            node.recycle();
        } else {
            AccessibilityNodeInfo parent = node.getParent();
            node.recycle();
            clickByNodeClickable(parent);
        }
    }
}

3.8 无障碍:点击搜索#

AccessibilityNodeInfo searchNode = findNodeById("com.ss.android.ugc.aweme:id/kq3");
if (searchNode == null) {
    continue;
}
clickByNodeClickable(searchNode);

image-20240703151118720

3.9 无障碍:输入直播间#

image-20240703152322500

7- #在抖音,记录美好生活#【吕行舞蹈(下午场)】正在直播,来和我一起支持Ta吧。复制下方链接,打开【抖音】,直接观看直播! https://v.douyin.com/i6HN3SCa/ 7@9.com :0pm
Thread.sleep(randomInt(5, 10) * 1000L);
AccessibilityNodeInfo searchTextNode = findNodeById("com.ss.android.ugc.aweme:id/et_search_kw");
Bundle arguments = new Bundle();
String address = "7- #在抖音,记录美好生活#【吕行舞蹈(下午场)】正在直播,来和我一起支持Ta吧。复制下方链接,打开【抖音】,直接观看直播! https://v.douyin.com/i6HN3SCa/ 7@9.com :0pm"
arguments.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, response.address);
searchTextNode.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments);

image-20240703152439429

示例代码:

package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.os.Bundle;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;

import java.util.List;

public class LuckyAccessibilityService extends AccessibilityService {

    public static boolean allowStart = false;
    public static boolean running = false;

    public static Thread runThread = null;

    @Override
    public void onInterrupt() {

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!allowStart) {
            return;
        }

        if (!running) {
            running = true;

            // 创建线程
            runThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    loopTask();
                }
            });
            runThread.start();
        }

    }

    public void loopTask() {
        while (true) {
            try {
                Log.e("无障碍", "开始运行");

                // 关闭所有其他应用
                killAllProcess();

                // 寻找抖音并打开
                Thread.sleep(5000);
                if (!launchApplicationByName("抖音", 10)) {
                    Log.e("无障碍", "无法启动抖音");
                    continue;
                }

                // 点击搜索
                Thread.sleep(5000);
                AccessibilityNodeInfo searchNode = findNodeById("com.ss.android.ugc.aweme:id/kq3");
                if (searchNode == null) {
                    continue;
                }
                clickByNodeClickable(searchNode);


                // 输入直播间分享地址
                Thread.sleep(5 * 1000L);
                AccessibilityNodeInfo searchTextNode = findNodeById("com.ss.android.ugc.aweme:id/et_search_kw");
                Bundle arguments = new Bundle();
                String liveAddress = "7- #在抖音,记录美好生活#【吕行舞蹈(下午场)】正在直播,来和我一起支持Ta吧。复制下方链接,打开【抖音】,直接观看直播! https://v.douyin.com/i6HN3SCa/ 7@9.com :0pm";
                arguments.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, liveAddress);
                searchTextNode.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments);

                Thread.sleep(20 * 1000L);

            } catch (Exception ex) {
                Log.e("异常", ex.toString());
                ex.printStackTrace();
            }
        }
    }

    /**
     * 在页面上寻找名字叫啥的app
     */
    public boolean launchApplicationByName(String name, int retry) throws InterruptedException {

        performGlobalAction(GLOBAL_ACTION_HOME);

        for (int i = 0; i < retry; i++) {
            // 翻看右边的页面
            slideScreen(540, 800, 500, 800, 500L, 200L);
            Thread.sleep(3000);

            // 找抖音APP
            AccessibilityNodeInfo app = findNodeByText(name);
            if (app != null) {
                Thread.sleep(500);
                clickByNodeClickable(app);
                Thread.sleep(5000);
                return true;
            }
        }
        return false;
    }


    /**
     * 滑动屏幕
     */
    private boolean slideScreen(int startX, int startY, int endX, int endY, Long startTime, Long duration) {
        try {
            Path path = new Path();
            path.moveTo(startX, startY);
            path.lineTo(endX, endY);
            GestureDescription.Builder builder = new GestureDescription.Builder();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 500L, 37L)).build();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 1000L, 500L)).build();
            GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, startTime, duration)).build();
            dispatchGesture(description, null, null);
            return true;
        } catch (Exception e) {
            // Log.e(TAG, "滑动屏幕失败,详细:" + e.toString());
            Log.e("无障碍", "滑动屏幕失败");
        }
        return false;
    }

    /**
     * 杀死所有进程(记得提前锁定无障碍应用)
     */
    public void killAllProcess() throws InterruptedException {
        // 1.回到首页 Home
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);

        // 杀死其他应用
        performGlobalAction(GLOBAL_ACTION_RECENTS);
        Thread.sleep(2000);

        String[] clearAppIdArray = new String[]{
                "com.huawei.android.launcher:id/clear_all_recents_image_button", // 华为v30 pro
                "com.hihonor.android.launcher:id/clear_all_recents_image_button", // 荣耀
                "com.android.systemui:id/clearAnimView" // 小米8A
        };
        for (String id : clearAppIdArray) {
            // 根据ID找到节点
            AccessibilityNodeInfo clearIdNode = findNodeById(id);
            if (clearIdNode != null) {
                // 点击标签
                clickByNodeClickable(clearIdNode);
                break;
            }
        }
    }

    /**
     * 根据文本找元素
     */
    private AccessibilityNodeInfo findNodeByText(String text) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByText(text);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }


    /**
     * 根据ID找元素
     */
    private AccessibilityNodeInfo findNodeById(String id) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId(id);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * 点击节点
     */
    private void clickByNodeClickable(AccessibilityNodeInfo node) {
        if (node.isClickable()) {
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            node.recycle();
        } else {
            AccessibilityNodeInfo parent = node.getParent();
            node.recycle();
            clickByNodeClickable(parent);
        }
    }
}

3.10 无障碍:进入直播间#

image-20240703152930495

点击搜索,就可以直接进入直播间。

// 点击搜索,进入直播间
Thread.sleep(2 * 1000L);
AccessibilityNodeInfo doSearchNode = findNodeById("com.ss.android.ugc.aweme:id/zy=");
clickByNodePosition(doSearchNode);


/**
 * 根据坐标 点击
*/
private void clickByNodePosition(AccessibilityNodeInfo node) {

    Rect rc = new Rect();
    node.getBoundsInScreen(rc);
    Path path = new Path();
    path.moveTo(rc.centerX(), rc.centerY());
    GestureDescription.Builder builder = new GestureDescription.Builder();
    GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200)).build();
    dispatchGesture(description, null, null);
}

3.11 无障碍:输入评论#

image-20240703154019005

image-20240703153949119

image-20240703154351840

image-20240703154503343

// 找到评论框,并点击
Thread.sleep(5 * 1000L);
// com.ss.android.ugc.aweme:id/f18

// AccessibilityNodeInfo commentNode = findNodeByText("说点什么...");
AccessibilityNodeInfo commentNode = findNodeById("com.ss.android.ugc.aweme:id/f18");
clickByNodeClickable(commentNode);


// 输入文本
Thread.sleep(5 * 1000L);
AccessibilityNodeInfo sayNode = findNodeById("com.ss.android.ugc.aweme:id/f4f");
AccessibilityNodeInfo child = sayNode.getChild(0);
Bundle arguments1 = new Bundle();
arguments1.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "太漂亮了");
child.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments1);

// 点击发送评论
Thread.sleep(5 * 1000L);
AccessibilityNodeInfo sendNode = findNodeById("com.ss.android.ugc.aweme:id/z0c");
clickByNodeClickable(sendNode);

image-20240703155317625

3.12 整合示例代码#

package com.nb.autodemo;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.GestureDescription;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;

import java.util.List;

public class LuckyAccessibilityService extends AccessibilityService {

    public static boolean allowStart = false;
    public static boolean running = false;

    public static Thread runThread = null;

    @Override
    public void onInterrupt() {

    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!allowStart) {
            return;
        }

        if (!running) {
            running = true;

            // 创建线程
            runThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    loopTask();
                }
            });
            runThread.start();
        }

    }

    public void loopTask() {
        while (true) {
            try {
                Log.e("无障碍", "开始运行");

                // 关闭所有其他应用
                killAllProcess();

                // 寻找抖音并打开
                Thread.sleep(5000);
                if (!launchApplicationByName("抖音", 10)) {
                    Log.e("无障碍", "无法启动抖音");
                    continue;
                }

                // 点击搜索
                Thread.sleep(5000);
                AccessibilityNodeInfo searchNode = findNodeById("com.ss.android.ugc.aweme:id/kq3");
                if (searchNode == null) {
                    continue;
                }
                clickByNodeClickable(searchNode);


                // 输入直播间分享地址
                Thread.sleep(5 * 1000L);
                AccessibilityNodeInfo searchTextNode = findNodeById("com.ss.android.ugc.aweme:id/et_search_kw");
                Bundle arguments = new Bundle();
                String liveAddress = "7- #在抖音,记录美好生活#【吕行舞蹈(下午场)】正在直播,来和我一起支持Ta吧。复制下方链接,打开【抖音】,直接观看直播! https://v.douyin.com/i6HN3SCa/ 7@9.com :0pm";
                arguments.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, liveAddress);
                searchTextNode.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments);

                // 点击搜索,进入直播间
                Thread.sleep(2 * 1000L);
                AccessibilityNodeInfo doSearchNode = findNodeById("com.ss.android.ugc.aweme:id/zy=");
                clickByNodePosition(doSearchNode);


                // 找到评论框,并点击
                Thread.sleep(5 * 1000L);
                // com.ss.android.ugc.aweme:id/f18
                // AccessibilityNodeInfo commentNode = findNodeByText("说点什么...");
                AccessibilityNodeInfo commentNode = findNodeById("com.ss.android.ugc.aweme:id/f18");
                clickByNodeClickable(commentNode);


                // 输入文本
                Thread.sleep(5 * 1000L);
                AccessibilityNodeInfo sayNode = findNodeById("com.ss.android.ugc.aweme:id/f4f");
                AccessibilityNodeInfo child = sayNode.getChild(0);
                Bundle arguments1 = new Bundle();
                arguments1.putString(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "太漂亮了");
                child.performAction(AccessibilityNodeInfoCompat.ACTION_SET_TEXT, arguments1);

                // 点击发送评论
                Thread.sleep(5 * 1000L);
                AccessibilityNodeInfo sendNode = findNodeById("com.ss.android.ugc.aweme:id/z0c");
                clickByNodeClickable(sendNode);

                return;

            } catch (Exception ex) {
                Log.e("异常", ex.toString());
                ex.printStackTrace();
            }
        }
    }


    /**
     * 根据坐标 点击
     */
    private void clickByNodePosition(AccessibilityNodeInfo node) {

        Rect rc = new Rect();
        node.getBoundsInScreen(rc);
        Path path = new Path();
        path.moveTo(rc.centerX(), rc.centerY());
        GestureDescription.Builder builder = new GestureDescription.Builder();
        GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 0, 200)).build();
        dispatchGesture(description, null, null);
    }

    /**
     * 在页面上寻找名字叫啥的app
     */
    public boolean launchApplicationByName(String name, int retry) throws InterruptedException {

        performGlobalAction(GLOBAL_ACTION_HOME);

        for (int i = 0; i < retry; i++) {
            // 翻看右边的页面
            slideScreen(540, 800, 500, 800, 500L, 200L);
            Thread.sleep(3000);

            // 找抖音APP
            AccessibilityNodeInfo app = findNodeByText(name);
            if (app != null) {
                Thread.sleep(500);
                clickByNodeClickable(app);
                Thread.sleep(5000);
                return true;
            }
        }
        return false;
    }


    /**
     * 滑动屏幕
     */
    private boolean slideScreen(int startX, int startY, int endX, int endY, Long startTime, Long duration) {
        try {
            Path path = new Path();
            path.moveTo(startX, startY);
            path.lineTo(endX, endY);
            GestureDescription.Builder builder = new GestureDescription.Builder();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 500L, 37L)).build();
            // GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, 1000L, 500L)).build();
            GestureDescription description = builder.addStroke(new GestureDescription.StrokeDescription(path, startTime, duration)).build();
            dispatchGesture(description, null, null);
            return true;
        } catch (Exception e) {
            // Log.e(TAG, "滑动屏幕失败,详细:" + e.toString());
            Log.e("无障碍", "滑动屏幕失败");
        }
        return false;
    }

    /**
     * 杀死所有进程(记得提前锁定无障碍应用)
     */
    public void killAllProcess() throws InterruptedException {
        // 1.回到首页 Home
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);
        performGlobalAction(GLOBAL_ACTION_HOME);
        Thread.sleep(2000);

        // 杀死其他应用
        performGlobalAction(GLOBAL_ACTION_RECENTS);
        Thread.sleep(2000);

        String[] clearAppIdArray = new String[]{
                "com.huawei.android.launcher:id/clear_all_recents_image_button", // 华为v30 pro
                "com.hihonor.android.launcher:id/clear_all_recents_image_button", // 荣耀
                "com.android.systemui:id/clearAnimView" // 小米8A
        };
        for (String id : clearAppIdArray) {
            // 根据ID找到节点
            AccessibilityNodeInfo clearIdNode = findNodeById(id);
            if (clearIdNode != null) {
                // 点击标签
                clickByNodeClickable(clearIdNode);
                break;
            }
        }
    }

    /**
     * 根据文本找元素
     */
    private AccessibilityNodeInfo findNodeByText(String text) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByText(text);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }


    /**
     * 根据ID找元素
     */
    private AccessibilityNodeInfo findNodeById(String id) {
        AccessibilityNodeInfo root = getRootInActiveWindow();
        if (root == null) {
            return null;
        }
        List<AccessibilityNodeInfo> nodeList = root.findAccessibilityNodeInfosByViewId(id);
        if (nodeList != null) {
            for (int i = 0; i < nodeList.size(); i++) {
                AccessibilityNodeInfo node = nodeList.get(i);
                if (node != null) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * 点击节点
     */
    private void clickByNodeClickable(AccessibilityNodeInfo node) {
        if (node.isClickable()) {
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            node.recycle();
        } else {
            AccessibilityNodeInfo parent = node.getParent();
            node.recycle();
            clickByNodeClickable(parent);
        }
    }
}
1.关于无障碍开发
    - 群控,通过平台手机下发任务,执行任务。
        - 按键精灵
        - 无障碍开发
            - 自己开发一个安卓APP,去控制手机上其他的APP
            - 无障碍服务【安卓APP+服务】 -> 控制其他的APP
    - 专题本质
        - 开发【安卓APP+服务】            -> 单机版
        - 【安卓APP+服务】 + 管理平台     -> 群控版
    - 图例
    - 背景(直播间)

2.单机版

    2.1 准备工具
        - Android Studio
            - 推荐 2020.3.1.24
            - 新版 + AirtestIDE

        - 安卓手机(不需ROOT)
            - 案例:红米Note9 Pro

        - 抖音v29.8.0
            https://www.wandoujia.com/apps/7461948/history_v290801

        注意:AirtestIDE关闭再去调试无障碍应用。

    2.2 创建安卓应用 + 无障碍服务

    2.3 引导无障碍页面
        - 未开启当前【路飞助手】无障碍服务,启动APP引导无障碍页面
        - 已开启

    2.4 开始按钮 + 执行线程

    2.5 无障碍:清除应用

    2.6 无障碍:打开抖音(寻找抖音app+点击)

    2.7 无障碍:搜索+进入直播间
        - 搜索:com.ss.android.ugc.aweme:id/he-
        - 输入:com.ss.android.ugc.aweme:id/et_search_kw  + 输入直播地址
        - 搜索:com.ss.android.ugc.aweme:id/zy=  + 标签找坐标+点击

    2.8 评论
        - 寻找“说点什么”输入框 + 点击
        - 文本输入框 + 输入
        - 发送 + 点击

小结:
    - 无脑自动化
    - 核心:
        - 返回Home
        - 最近打开的应用
        - 杀死其他应用(保护自己应用)
        - 寻找标签(ID+文本)
        - 点击标签(可点击+坐标点击)
        - 输入文本
        - 滑动
    - 群控
    - 其他同学方案:自动化+Mitmproxy
无障碍开发 1
https://zycreverse.netlify.app/posts/无障碍1/
Author
会写点代码的本子画手
Published at
2025-03-26