原理:
通过监听窗口改变事件,监听目标应用,通过视图ID(或文本、或描述、或其他如坐标之类的)找到目标视图,使用无障碍动作点击方法点击它
无障碍服务实现:
1、写一个自己的无障碍服务继承AccessibilityService
public class AppWindowChangeService extends AccessibilityService {
private static final String TAG = "MyAppWindowChangeService";
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
if (accessibilityEvent == null || accessibilityEvent.getPackageName() == null) return;
CharSequence packageName = accessibilityEvent.getPackageName();
CharSequence className = accessibilityEvent.getClassName();
//监听当前窗口变化,获取Package
if(accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED){
Log.i(TAG, "onAccessibilityEvent: packageName = "+packageName+", className = "+className);
}
}
@Override
public void onInterrupt() {
Log.e(TAG, "onInterrupt");
}
}
2、AndroidManifest.xml声明这个服务:
<service
android:name=".AppWindowChangeService"
android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|fontScale|locale|uiMode"
android:enabled="true"
android:exported="false"
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/detection_service_config"/>
</service>
3、在xml新建一个配置资源,做这个无障碍服务的相关配置:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeViewClicked"
android:accessibilityFeedbackType="feedbackGeneric"
android:canRetrieveWindowContent="true"
android:accessibilityFlags="flagIncludeNotImportantViews|flagReportViewIds|flagRetrieveInteractiveWindows" />
这里监听类型我还多加了一个typeViewClicked,后面可以用来找你点击的view的相关信息。
启用功能:
自行找到系统设置的无障碍服务功能界面,或者使用代码做跳转到无障碍服务界面:
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.setPackage("com.android.settings");
startActivity(intent);
开启自己app的无障碍服务开关,无障碍服务就会启动起来了。
实践:
举个栗子,监听美团外卖启动页的广告的跳过按钮:
import android.accessibilityservice.AccessibilityService;
import android.os.Handler;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;
public class AppWindowChangeService extends AccessibilityService {
private static final String TAG = "MyAppWindowChangeService";
private static final String TARGET_PACKAGE_NAME = "com.sankuai.meituan.takeoutnew";
private static final String TARGET_VIEW_ID = "com.sankuai.meituan.takeoutnew:id/ll_skip";
private final Runnable runnable = this::findAndClickTargetView;
private final Handler handler = new Handler();
@Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
if (accessibilityEvent == null || accessibilityEvent.getPackageName() == null) return;
CharSequence packageName = accessibilityEvent.getPackageName();
CharSequence className = accessibilityEvent.getClassName();
//监听当前窗口变化,获取Package
if(accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED){
if (TARGET_PACKAGE_NAME.equals(packageName)) {
Log.i(TAG, "Target package opened: " + packageName);
// 查找并点击目标视图
handler.removeCallbacks(runnable);
handler.postDelayed(runnable, 200);
}
} else if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
Log.i(TAG, "onAccessibilityEvent: packageName = "+packageName+", className = "+className);
Log.e(TAG, "onAccessibilityEvent: 点击view:" + className);
AccessibilityNodeInfo source = accessibilityEvent.getSource();
if (source != null) {
String viewId = source.getViewIdResourceName();
CharSequence text = source.getText();
CharSequence contentDescription = source.getContentDescription();
Log.e(TAG, "View ID: " + viewId);
Log.e(TAG, "Text: " + text);
Log.e(TAG, "Content Description: " + contentDescription);
source.recycle(); // 释放资源
}
}
}
private void findAndClickTargetView() {
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
Log.i(TAG, "findAndClickTargetView: rootNode == " + rootNode);
if (rootNode != null) {
AccessibilityNodeInfo targetNode = findNodeById(rootNode, TARGET_VIEW_ID);
Log.i(TAG, "findNodeById: targetNode = "+targetNode);
if (targetNode != null) {
Log.i(TAG, "targetNode != null, start click");
performClick(targetNode);
targetNode.recycle();
}
rootNode.recycle();
}
}
private AccessibilityNodeInfo findNodeById(AccessibilityNodeInfo rootNode, String viewId) {
if (rootNode.getViewIdResourceName() != null && rootNode.getViewIdResourceName().equals(viewId)) {
return AccessibilityNodeInfo.obtain(rootNode);
}
for (int i = 0; i < rootNode.getChildCount(); i++) {
AccessibilityNodeInfo childNode = rootNode.getChild(i);
if (childNode != null) {
AccessibilityNodeInfo result = findNodeById(childNode, viewId);
if (result != null) {
return result;
}
childNode.recycle();
}
}
return null;
}
private void performClick(AccessibilityNodeInfo node) {
if (node != null && node.isClickable()) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
Log.i(TAG, "Clicked on node: " + node);
Toast.makeText(this, "自动点击", Toast.LENGTH_SHORT).show();
} else {
Log.i(TAG, "Node is not clickable: " + node);
}
}
@Override
public void onInterrupt() {
Log.e(TAG, "onInterrupt");
}
}
目标包名和目标viewID是我通过点击的时候输出打印看到的,给他倒推回去记录到代码当中。
查找view的动作需要做延时获取,实测马上去获取是获取不到的。
实际实现中,可以记录多个包名,以及对应的需要点击的视图,做李跳跳的效果。
Android高版本注意:如果打开其他App之后日志不打印,回到自己应用之后才会一次性把之前的动作日志打印出来的情况需要将应用的省电策略改为无限制!!!这个问题网上都没有提到,之前一直不生效困扰了我好久。