4254 words
21 minutes
无障碍开发 1
无障碍开发,编写一个自己的安卓APP并开启无障碍服务,然后APP就可以实现去控制手机中的其他APP。很多的群控软件都是基于无障碍开发实现。
例如:抖音的自动评论工具。
1.常见版本
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 创建项目
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'
3.2 无障碍服务
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时,在安卓手机的无障碍模式中,就会出现我们的服务。
当启动无障碍服务之后,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() {
}
}
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("无障碍", "测试运行中");
}
}
}
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 开始按钮
当启动无障碍服务之后 LuckyAccessibilityService
的 onAccessibilityEvent
方法就会被出发执行(太快了)
所以,可以在我们自己的应用中定义一个按钮,用按钮当开关,来控制无障碍业务代码的执行。
<?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 无障碍:清除应用
先将我们的无障碍应用保护起来,然后用无障碍的代码执行操作:
返回主页
performGlobalAction(GLOBAL_ACTION_HOME);
打开最近应用
performGlobalAction(GLOBAL_ACTION_RECENTS);
清除所有其他的应用
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);
3.9 无障碍:输入直播间
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);
示例代码:
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 无障碍:进入直播间
点击搜索,就可以直接进入直播间。
// 点击搜索,进入直播间
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 无障碍:输入评论
// 找到评论框,并点击
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);
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