├── settings.gradle
├── app
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ └── strings.xml
│ │ ├── xml
│ │ │ └── qianghongbao.xml
│ │ └── layout
│ │ │ └── main_activity.xml
│ │ ├── java
│ │ └── com
│ │ │ └── wang
│ │ │ └── hongbaotest
│ │ │ ├── MyApp.java
│ │ │ ├── Utils.java
│ │ │ ├── TouchView.java
│ │ │ ├── MainActivity.java
│ │ │ ├── AbstractTF.java
│ │ │ └── HongBaoService.java
│ │ └── AndroidManifest.xml
└── build.gradle
├── .gitignore
└── README.md
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weimingjue/AccessibilityExample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | HongBaoTest
3 | 辅助功能测试
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.bat
3 | *.apk
4 | *.zip
5 | *.properties
6 | *.hprof
7 | /captures
8 | ~$*
9 | gradlew
10 | .DS_Store
11 | .idea
12 | .gradle
13 | gradle
14 | build
--------------------------------------------------------------------------------
/app/src/main/java/com/wang/hongbaotest/MyApp.java:
--------------------------------------------------------------------------------
1 | package com.wang.hongbaotest;
2 |
3 | import android.app.Application;
4 |
5 | public class MyApp extends Application {
6 | public static MyApp mApp;
7 |
8 | @Override
9 | public void onCreate() {
10 | super.onCreate();
11 | mApp = this;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/qianghongbao.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 运行app即可立即跳到辅助功能页面,选择开启"HongBaoTest"的辅助功能,然后打开微信的聊天界面即可看到提示语"找到wx的表情图标"
2 |
3 | 如果报错无法运行请修改根目录build.gradle的classpath为你自己能运行的项目版本
4 |
5 | 如果APP崩溃过或APP在开启辅助功能的时候进行覆盖安装,大概率会出现辅助功能直接无效的情况(就是"红包锁定中"这句话都出不来),重启手机即可(以后需要注意,这是Android的通病)
6 |
7 | 由于国内第三方厂商各种奇葩定制,demo可能会出现以下问题:
8 |
9 | 1.打开微信好友页看不到提示语,解决方法以下几种:
10 |
11 | ①一键清理所有的app(包括demo),重新运行app
12 |
13 | ②上述操作无效的话,重启手机,重新运行app
14 |
15 | ③上述操作依然无效的话,可能是第三方厂商屏蔽了Toast,请开启悬浮窗权限(华为最奇葩的定制)或者直接查看Logcat的日志打印
16 |
17 | 2.手势发送失败:重启手机即可
18 |
19 | 辅助功能出问题万能解决方式:重启手机、debug、看日志
20 |
21 |
22 | 博客:https://blog.csdn.net/weimingjue/article/details/82744146
--------------------------------------------------------------------------------
/app/src/main/java/com/wang/hongbaotest/Utils.java:
--------------------------------------------------------------------------------
1 | package com.wang.hongbaotest;
2 |
3 | import android.widget.Toast;
4 |
5 | import java.util.Collection;
6 |
7 | public class Utils {
8 |
9 | public static void toast(CharSequence cs) {
10 | Toast.makeText(MyApp.mApp, cs, Toast.LENGTH_SHORT).show();
11 | }
12 |
13 | //集合是否是空的
14 | public static boolean isEmptyArray(Collection list) {
15 | return list == null || list.size() == 0;
16 | }
17 |
18 | public static boolean isEmptyArray(T[] list) {
19 | return list == null || list.length == 0;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | android {
3 | compileSdkVersion 27
4 | defaultConfig {
5 | applicationId "com.wang.hongbaotest"
6 | minSdkVersion 19
7 | targetSdkVersion 27
8 | versionCode 1
9 | versionName "1.0"
10 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | }
16 | }
17 | }
18 |
19 | dependencies {
20 | implementation fileTree(dir: 'libs', include: ['*.jar'])
21 | api 'com.android.support:appcompat-v7:27.0.2'
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
16 |
17 |
21 |
22 |
27 |
28 |
33 |
34 |
39 |
40 |
45 |
46 |
47 |
48 |
53 |
54 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wang/hongbaotest/TouchView.java:
--------------------------------------------------------------------------------
1 | package com.wang.hongbaotest;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.graphics.Canvas;
6 | import android.graphics.Paint;
7 | import android.graphics.Path;
8 | import android.os.Build;
9 | import android.util.AttributeSet;
10 | import android.view.MotionEvent;
11 | import android.view.View;
12 |
13 | /**
14 | * Created by cloudplug on 18/4/27.
15 | * 手指触摸的点
16 | */
17 | public class TouchView extends View {
18 |
19 | public final Path mPath = new Path();
20 |
21 | private final Paint mPaint = new Paint();
22 |
23 | public TouchView(Context context) {
24 | this(context, null, 0);
25 | }
26 |
27 | public TouchView(Context context, AttributeSet attrs) {
28 | this(context, attrs, 0);
29 | }
30 |
31 | public TouchView(Context context, AttributeSet attrs, int defStyleAttr) {
32 | super(context, attrs, defStyleAttr);
33 | initData();
34 | }
35 |
36 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
37 | public TouchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
38 | super(context, attrs, defStyleAttr, defStyleRes);
39 | initData();
40 | }
41 |
42 | public void initData() {
43 | mPaint.setAntiAlias(true);
44 | mPaint.setStrokeWidth(10);
45 | mPaint.setTextAlign(Paint.Align.CENTER);
46 | mPaint.setStyle(Paint.Style.STROKE);
47 | mPaint.setColor(0xff333333);
48 | setBackgroundColor(0x44000000);
49 | }
50 |
51 | @Override
52 | protected void onDraw(Canvas canvas) {
53 | super.onDraw(canvas);
54 | canvas.drawPath(mPath, mPaint);
55 | }
56 |
57 | @Override
58 | public boolean onTouchEvent(MotionEvent event) {
59 | switch (event.getAction()) {
60 | case MotionEvent.ACTION_DOWN:
61 | mPath.reset();
62 | mPath.moveTo(event.getX(), event.getY());
63 | break;
64 | case MotionEvent.ACTION_MOVE:
65 | case MotionEvent.ACTION_UP:
66 | case MotionEvent.ACTION_CANCEL:
67 | mPath.lineTo(event.getX(), event.getY());
68 | break;
69 | default:
70 | break;
71 | }
72 | invalidate();
73 | return true;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wang/hongbaotest/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.wang.hongbaotest;
2 |
3 | import android.accessibilityservice.AccessibilityService;
4 | import android.accessibilityservice.GestureDescription;
5 | import android.annotation.TargetApi;
6 | import android.app.Activity;
7 | import android.content.Intent;
8 | import android.graphics.Path;
9 | import android.graphics.Rect;
10 | import android.os.Build;
11 | import android.os.Bundle;
12 | import android.provider.Settings;
13 | import android.util.DisplayMetrics;
14 | import android.util.Log;
15 | import android.view.View;
16 | import android.view.accessibility.AccessibilityNodeInfo;
17 | import android.widget.Toast;
18 |
19 | public class MainActivity extends Activity {
20 |
21 |
22 | private static final String TAG = MainActivity.class.getSimpleName();
23 |
24 | @Override
25 | protected void onCreate(Bundle savedInstanceState) {
26 | super.onCreate(savedInstanceState);
27 | setContentView(R.layout.main_activity);
28 | final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
29 |
30 | //屏幕横滑手势
31 | findViewById(R.id.bt_main_ShouShi).setOnClickListener(new View.OnClickListener() {
32 | @TargetApi(Build.VERSION_CODES.N)
33 | @Override
34 | public void onClick(View v) {
35 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
36 | Toast.makeText(MainActivity.this, "7.0及以上才能使用手势", Toast.LENGTH_SHORT).show();
37 | return;
38 | }
39 | Path path = new Path();
40 | path.moveTo(displayMetrics.widthPixels / 2, displayMetrics.heightPixels * 2 / 3);//从屏幕的2/3处开始滑动
41 | path.lineTo(10, displayMetrics.heightPixels * 2 / 3);
42 | final GestureDescription.StrokeDescription sd = new GestureDescription.StrokeDescription(path, 0, 500);
43 | HongBaoService.mService.dispatchGesture(new GestureDescription.Builder().addStroke(sd).build(), new AccessibilityService.GestureResultCallback() {
44 | @Override
45 | public void onCompleted(GestureDescription gestureDescription) {
46 | super.onCompleted(gestureDescription);
47 | Toast.makeText(MainActivity.this, "手势成功", Toast.LENGTH_SHORT).show();
48 | }
49 |
50 | @Override
51 | public void onCancelled(GestureDescription gestureDescription) {
52 | super.onCancelled(gestureDescription);
53 | Toast.makeText(MainActivity.this, "手势失败,请重启手机再试", Toast.LENGTH_SHORT).show();
54 | }
55 | }, null);
56 | }
57 | });
58 |
59 | //点击指定控件
60 | findViewById(R.id.bt_main_DianJi).setOnClickListener(new View.OnClickListener() {
61 | @Override
62 | public void onClick(View v) {
63 | AccessibilityNodeInfo ces = HongBaoService.mService.findFirst(AbstractTF.newText("测试控件", true));
64 | if (ces == null) {
65 | Utils.toast("找测试控件失败");
66 | return;
67 | }
68 | HongBaoService.clickView(ces);
69 | }
70 | });
71 |
72 | //用手势长按指定控件
73 | findViewById(R.id.bt_main_ChangAn).setOnClickListener(new View.OnClickListener() {
74 | @TargetApi(Build.VERSION_CODES.N)
75 | @Override
76 | public void onClick(View v) {
77 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
78 | Toast.makeText(MainActivity.this, "7.0及以上才能使用手势", Toast.LENGTH_SHORT).show();
79 | return;
80 | }
81 | AccessibilityNodeInfo ces = HongBaoService.mService.findFirst(AbstractTF.newText("测试控件", true));
82 | if (ces == null) {
83 | Utils.toast("找测试控件失败");
84 | return;
85 | }
86 |
87 | // ces.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);//长按
88 |
89 | //这里为了示范手势的效果
90 | Rect absXY = new Rect();
91 | ces.getBoundsInScreen(absXY);
92 | // HongBaoService.mService.dispatchGestureClick(absXY.left + (absXY.right - absXY.left) / 2, absXY.top + (absXY.bottom - absXY.top) / 2);//手势点击效果
93 | //手势长按效果
94 | //控件正中间
95 | HongBaoService.mService.dispatchGestureLongClick(absXY.left + (absXY.right - absXY.left) / 2, absXY.top + (absXY.bottom - absXY.top) / 2);
96 | }
97 | });
98 |
99 | //用系统的返回效果
100 | findViewById(R.id.bt_main_FanHui).setOnClickListener(new View.OnClickListener() {
101 | @Override
102 | public void onClick(View v) {
103 | HongBaoService.mService.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
104 | }
105 | });
106 |
107 | //测试的控件
108 | View viewCes = findViewById(R.id.bt_main_CeShi);
109 | viewCes.setOnClickListener(new View.OnClickListener() {
110 | @Override
111 | public void onClick(View v) {
112 | Utils.toast("'测试控件'被点击了");
113 | Log.e(TAG, "onClick: '测试控件'被点击了");
114 | }
115 | });
116 | viewCes.setOnLongClickListener(new View.OnLongClickListener() {
117 | @Override
118 | public boolean onLongClick(View v) {
119 | Utils.toast("'测试控件'被长按了");
120 | Log.e(TAG, "onLongClick: '测试控件'被长按了");
121 | return true;
122 | }
123 | });
124 | }
125 |
126 | @Override
127 | protected void onResume() {
128 | super.onResume();
129 | if (!HongBaoService.isStart()) {
130 | try {
131 | this.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
132 | } catch (Exception e) {
133 | this.startActivity(new Intent(Settings.ACTION_SETTINGS));
134 | e.printStackTrace();
135 | }
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wang/hongbaotest/AbstractTF.java:
--------------------------------------------------------------------------------
1 | package com.wang.hongbaotest;
2 |
3 | import android.graphics.Rect;
4 | import android.support.annotation.NonNull;
5 | import android.support.annotation.Nullable;
6 | import android.view.accessibility.AccessibilityNodeInfo;
7 |
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public abstract class AbstractTF {
12 |
13 | /**
14 | * 是包含还必须相等;
15 | */
16 | protected final boolean mIsEquals;
17 | protected final T mCheckData;
18 |
19 | private AbstractTF(@NonNull T checkData, boolean isEquals) {
20 | mCheckData = checkData;
21 | mIsEquals = isEquals;
22 | }
23 |
24 | public abstract boolean checkOk(AccessibilityNodeInfo thisInfo);
25 |
26 | /**
27 | * 找id,就是findAccessibilityNodeInfosByViewId方法
28 | * 和找text一样效率最高,如果能找到,尽量使用这个
29 | */
30 | private static class IdTF extends AbstractTF implements IdTextTF {
31 | private IdTF(@NonNull String idFullName) {
32 | super(idFullName, true);
33 | }
34 |
35 | @Override
36 | public boolean checkOk(AccessibilityNodeInfo thisInfo) {
37 | return true;//此处不需要实现
38 | }
39 |
40 | @Nullable
41 | @Override
42 | public AccessibilityNodeInfo findFirst(AccessibilityNodeInfo root) {
43 | List list = root.findAccessibilityNodeInfosByViewId(mCheckData);
44 | if (Utils.isEmptyArray(list)) {
45 | return null;
46 | }
47 | for (int i = 1; i < list.size(); i++) {//其他的均回收
48 | list.get(i).recycle();
49 | }
50 | return list.get(0);
51 | }
52 |
53 | @Nullable
54 | @Override
55 | public List findAll(AccessibilityNodeInfo root) {
56 | return root.findAccessibilityNodeInfosByViewId(mCheckData);
57 | }
58 | }
59 |
60 | /**
61 | * 普通text,就是findAccessibilityNodeInfosByText方法
62 | * 和找id一样效率最高,如果能找到,尽量使用这个
63 | */
64 | private static class TextTF extends AbstractTF implements IdTextTF {
65 | private TextTF(@NonNull String text, boolean isEquals) {
66 | super(text, isEquals);
67 | }
68 |
69 | @Override
70 | public boolean checkOk(AccessibilityNodeInfo thisInfo) {
71 | return true;//此处不需要实现
72 | }
73 |
74 | @Nullable
75 | @Override
76 | public AccessibilityNodeInfo findFirst(AccessibilityNodeInfo root) {
77 | List list = root.findAccessibilityNodeInfosByText(mCheckData);
78 | if (Utils.isEmptyArray(list)) {
79 | return null;
80 | }
81 | if (mIsEquals) {
82 | AccessibilityNodeInfo returnInfo = null;
83 | for (AccessibilityNodeInfo info : list) {
84 | if (returnInfo == null && info.getText() != null && mCheckData.equals(info.getText().toString())) {
85 | returnInfo = info;
86 | } else {
87 | info.recycle();
88 | }
89 | }
90 | return returnInfo;
91 | } else {
92 | return list.get(0);
93 | }
94 | }
95 |
96 | @Nullable
97 | @Override
98 | public List findAll(AccessibilityNodeInfo root) {
99 | List list = root.findAccessibilityNodeInfosByText(mCheckData);
100 | if (Utils.isEmptyArray(list)) {
101 | return null;
102 | }
103 | if (mIsEquals) {
104 | ArrayList listNew = new ArrayList<>();
105 | for (AccessibilityNodeInfo info : list) {
106 | if (info.getText() != null && mCheckData.equals(info.getText().toString())) {
107 | listNew.add(info);
108 | } else {
109 | info.recycle();
110 | }
111 | }
112 | return listNew;
113 | } else {
114 | return list;
115 | }
116 | }
117 | }
118 |
119 | /**
120 | * 类似uc浏览器,有text值但无法直接根据text来找到
121 | */
122 | private static class WebTextTF extends AbstractTF {
123 | private WebTextTF(@NonNull String checkString, boolean isEquals) {
124 | super(checkString, isEquals);
125 | }
126 |
127 | @Override
128 | public boolean checkOk(AccessibilityNodeInfo thisInfo) {
129 | CharSequence text = thisInfo.getText();
130 | if (mIsEquals) {
131 | return text != null && text.toString().equals(mCheckData);
132 | } else {
133 | return text != null && text.toString().contains(mCheckData);
134 | }
135 | }
136 | }
137 |
138 | /**
139 | * 找ContentDescription字段
140 | */
141 | private static class ContentDescriptionTF extends AbstractTF {
142 | private ContentDescriptionTF(@NonNull String checkString, boolean isEquals) {
143 | super(checkString, isEquals);
144 | }
145 |
146 | @Override
147 | public boolean checkOk(AccessibilityNodeInfo thisInfo) {
148 | CharSequence text = thisInfo.getContentDescription();
149 | if (mIsEquals) {
150 | return text != null && text.toString().equals(mCheckData);
151 | } else {
152 | return text != null && text.toString().contains(mCheckData);
153 | }
154 | }
155 | }
156 |
157 | /**
158 | * 找ClassName匹配
159 | */
160 | private static class ClassNameTF extends AbstractTF {
161 | public ClassNameTF(@NonNull String checkString, boolean isEquals) {
162 | super(checkString, isEquals);
163 | }
164 |
165 | @Override
166 | public boolean checkOk(AccessibilityNodeInfo thisInfo) {
167 | if (mIsEquals) {
168 | return thisInfo.getClassName().toString().equals(mCheckData);
169 | } else {
170 | return thisInfo.getClassName().toString().contains(mCheckData);
171 | }
172 | }
173 | }
174 |
175 | /**
176 | * 在某个区域内的控件
177 | */
178 | private static class RectTF extends AbstractTF {
179 | public RectTF(@NonNull Rect rect) {
180 | super(rect, true);
181 | }
182 |
183 | @Override
184 | public boolean checkOk(AccessibilityNodeInfo thisInfo) {
185 | thisInfo.getBoundsInScreen(mRecycleRect);
186 | return mCheckData.contains(mRecycleRect);
187 | }
188 | }
189 |
190 | public interface IdTextTF {
191 | @Nullable
192 | AccessibilityNodeInfo findFirst(AccessibilityNodeInfo root);
193 |
194 | @Nullable
195 | List findAll(AccessibilityNodeInfo root);
196 | }
197 |
198 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
199 | // 创建方法
200 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
201 |
202 | public static Rect mRecycleRect = new Rect();
203 |
204 | public static final String ST_VIEW = "android.view.View",
205 | ST_TEXTVIEW = "android.widget.TextView",
206 | ST_IMAGEVIEW = "android.widget.ImageView",
207 | ST_BUTTON = "android.widget.Button",
208 | ST_IMAGEBUTTON = "android.widget.ImageButton",
209 | ST_EDITTEXT = "android.widget.EditText",
210 | ST_LISTVIEW = "android.widget.ListView",
211 | ST_LINEARLAYOUT = "android.widget.LinearLayout",
212 | ST_VIEWGROUP = "android.view.ViewGroup",
213 | ST_SYSTEMUI = "com.android.systemui";
214 |
215 | /**
216 | * 找id,就是findAccessibilityNodeInfosByViewId方法
217 | * 和找text一样效率最高,如果能找到,尽量使用这个
218 | *
219 | * @param pageName 被查找项目的包名:com.android.xxx
220 | * @param idName id值:tv_main
221 | */
222 | public static AbstractTF newId(String pageName, String idName) {
223 | return newId(pageName + ":id/" + idName);
224 | }
225 |
226 | /**
227 | * @param idfullName id全称:com.android.xxx:id/tv_main
228 | */
229 | public static AbstractTF newId(@NonNull String idfullName) {
230 | return new IdTF(idfullName);
231 | }
232 |
233 | /**
234 | * 普通text,就是findAccessibilityNodeInfosByText方法
235 | * 和找id一样效率最高,如果能找到,尽量使用这个
236 | */
237 | public static AbstractTF newText(@NonNull String text, boolean isEquals) {
238 | return new TextTF(text, isEquals);
239 | }
240 |
241 | /**
242 | * 类似uc浏览器,有text值但无法直接根据text来找到
243 | */
244 | public static AbstractTF newWebText(@NonNull String webText, boolean isEquals) {
245 | return new WebTextTF(webText, isEquals);
246 | }
247 |
248 | /**
249 | * 找ContentDescription字段
250 | */
251 | public static AbstractTF newContentDescription(@NonNull String cd, boolean isEquals) {
252 | return new ContentDescriptionTF(cd, isEquals);
253 | }
254 |
255 | /**
256 | * 找ClassName匹配
257 | */
258 | public static AbstractTF newClassName(@NonNull String className) {
259 | return new ClassNameTF(className, true);
260 | }
261 |
262 | public static AbstractTF newClassName(@NonNull String className, boolean isEquals) {
263 | return new ClassNameTF(className, isEquals);
264 | }
265 |
266 | /**
267 | * 在某个区域内的控件
268 | */
269 | public static AbstractTF newRect(@NonNull Rect rect) {
270 | return new RectTF(rect);
271 | }
272 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/wang/hongbaotest/HongBaoService.java:
--------------------------------------------------------------------------------
1 | package com.wang.hongbaotest;
2 |
3 | import android.accessibilityservice.AccessibilityService;
4 | import android.accessibilityservice.GestureDescription;
5 | import android.graphics.Path;
6 | import android.graphics.Rect;
7 | import android.support.annotation.NonNull;
8 | import android.support.annotation.Nullable;
9 | import android.support.annotation.RequiresApi;
10 | import android.util.Log;
11 | import android.view.KeyEvent;
12 | import android.view.accessibility.AccessibilityEvent;
13 | import android.view.accessibility.AccessibilityNodeInfo;
14 |
15 | import java.security.InvalidParameterException;
16 | import java.util.ArrayList;
17 | import java.util.List;
18 |
19 | public class HongBaoService extends AccessibilityService {
20 | private final String TAG = getClass().getName();
21 |
22 | public static HongBaoService mService;
23 |
24 | //初始化
25 | @Override
26 | protected void onServiceConnected() {
27 | super.onServiceConnected();
28 | Utils.toast("O(∩_∩)O~~\r\n红包锁定中...");
29 | mService = this;
30 | }
31 |
32 | //实现辅助功能
33 | @Override
34 | public void onAccessibilityEvent(AccessibilityEvent event) {
35 | AccessibilityNodeInfo biaoQingInfo = findFirst(AbstractTF.newContentDescription("表情", true));
36 | if (biaoQingInfo != null) {
37 | Utils.toast("找到wx的表情图标");//第一次运行可能会吐不出来
38 | Log.e(TAG, "onAccessibilityEvent: 找到wx的表情图标");//可以查看日志
39 | biaoQingInfo.recycle();
40 | }
41 | }
42 |
43 | @Override
44 | public void onInterrupt() {
45 | Utils.toast("(;′⌒`)\r\n红包功能被迫中断");
46 | mService = null;
47 | }
48 |
49 | @Override
50 | public void onDestroy() {
51 | super.onDestroy();
52 | Utils.toast("%>_<%\r\n红包功能已关闭");
53 | mService = null;
54 | }
55 |
56 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
57 | // 公共方法
58 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
59 |
60 | /**
61 | * 辅助功能是否启动
62 | */
63 | public static boolean isStart() {
64 | return mService != null;
65 | }
66 |
67 | /**
68 | * 点击该控件
69 | *
70 | * @return true表示点击成功
71 | */
72 | public static boolean clickView(AccessibilityNodeInfo nodeInfo) {
73 | if (nodeInfo != null) {
74 | if (nodeInfo.isClickable()) {
75 | nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
76 | return true;
77 | } else {
78 | AccessibilityNodeInfo parent = nodeInfo.getParent();
79 | if (parent != null) {
80 | boolean b = clickView(parent);
81 | parent.recycle();
82 | if (b) return true;
83 | }
84 | }
85 | }
86 | return false;
87 | }
88 |
89 | /**
90 | * 查找第一个匹配的控件
91 | *
92 | * @param tfs 匹配条件,多个AbstractTF是&&的关系,如:
93 | * AbstractTF.newContentDescription("表情", true),AbstractTF.newClassName(AbstractTF.ST_IMAGEVIEW)
94 | * 表示描述内容是'表情'并且是imageview的控件
95 | */
96 | @Nullable
97 | public AccessibilityNodeInfo findFirst(@NonNull AbstractTF... tfs) {
98 | if (tfs.length == 0) throw new InvalidParameterException("AbstractTF不允许传空");
99 |
100 | AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
101 | if (rootInfo == null) return null;
102 |
103 | int idTextTFCount = 0, idTextIndex = 0;
104 | for (int i = 0; i < tfs.length; i++) {
105 | if (tfs[i] instanceof AbstractTF.IdTextTF) {
106 | idTextTFCount++;
107 | idTextIndex = i;
108 | }
109 | }
110 | switch (idTextTFCount) {
111 | case 0://id或text数量为0,直接循环查找
112 | AccessibilityNodeInfo returnInfo = findFirstRecursive(rootInfo, tfs);
113 | rootInfo.recycle();
114 | return returnInfo;
115 | case 1://id或text数量为1,先查出对应的id或text,然后再查其他条件
116 | if (tfs.length == 1) {
117 | AccessibilityNodeInfo returnInfo2 = ((AbstractTF.IdTextTF) tfs[idTextIndex]).findFirst(rootInfo);
118 | rootInfo.recycle();
119 | return returnInfo2;
120 | } else {
121 | List listIdText = ((AbstractTF.IdTextTF) tfs[idTextIndex]).findAll(rootInfo);
122 | if (Utils.isEmptyArray(listIdText)) {
123 | break;
124 | }
125 | AccessibilityNodeInfo returnInfo3 = null;
126 | for (AccessibilityNodeInfo info : listIdText) {//遍历找到匹配的
127 | if (returnInfo3 == null) {
128 | boolean isOk = true;
129 | for (AbstractTF tf : tfs) {
130 | if (!tf.checkOk(info)) {
131 | isOk = false;
132 | break;
133 | }
134 | }
135 | if (isOk) {
136 | returnInfo3 = info;
137 | } else {
138 | info.recycle();
139 | }
140 | } else {
141 | info.recycle();
142 | }
143 | }
144 | rootInfo.recycle();
145 | return returnInfo3;
146 | }
147 | default:
148 | throw new RuntimeException("由于时间有限,并且多了也没什么用,所以IdTF和TextTF只能有一个");
149 | }
150 | rootInfo.recycle();
151 | return null;
152 | }
153 |
154 | /**
155 | * @param tfs 由于是递归循环,会忽略IdTF和TextTF
156 | */
157 | public static AccessibilityNodeInfo findFirstRecursive(AccessibilityNodeInfo parent, @NonNull AbstractTF... tfs) {
158 | if (parent == null) return null;
159 | if (tfs.length == 0) throw new InvalidParameterException("AbstractTF不允许传空");
160 |
161 | for (int i = 0; i < parent.getChildCount(); i++) {
162 | AccessibilityNodeInfo child = parent.getChild(i);
163 | if (child == null) continue;
164 | boolean isOk = true;
165 | for (AbstractTF tf : tfs) {
166 | if (!tf.checkOk(child)) {
167 | isOk = false;
168 | break;
169 | }
170 | }
171 | if (isOk) {
172 | return child;
173 | } else {
174 | AccessibilityNodeInfo childChild = findFirstRecursive(child, tfs);
175 | child.recycle();
176 | if (childChild != null) {
177 | return childChild;
178 | }
179 | }
180 | }
181 | return null;
182 | }
183 |
184 | /**
185 | * 查找全部匹配的控件
186 | *
187 | * @param tfs 匹配条件,多个AbstractTF是&&的关系,如:
188 | * AbstractTF.newContentDescription("表情", true),AbstractTF.newClassName(AbstractTF.ST_IMAGEVIEW)
189 | * 表示描述内容是'表情'并且是imageview的控件
190 | */
191 | @NonNull
192 | public List findAll(@NonNull AbstractTF... tfs) {
193 | if (tfs.length == 0) throw new InvalidParameterException("AbstractTF不允许传空");
194 |
195 | ArrayList list = new ArrayList<>();
196 | AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
197 | if (rootInfo == null) return list;
198 |
199 | int idTextTFCount = 0, idTextIndex = 0;
200 | for (int i = 0; i < tfs.length; i++) {
201 | if (tfs[i] instanceof AbstractTF.IdTextTF) {
202 | idTextTFCount++;
203 | idTextIndex = i;
204 | }
205 | }
206 | switch (idTextTFCount) {
207 | case 0://id或text数量为0,直接循环查找
208 | findAllRecursive(list, rootInfo, tfs);
209 | break;
210 | case 1://id或text数量为1,先查出对应的id或text,然后再循环
211 | List listIdText = ((AbstractTF.IdTextTF) tfs[idTextIndex]).findAll(rootInfo);
212 | if (Utils.isEmptyArray(listIdText)) {
213 | break;
214 | }
215 | if (tfs.length == 1) {
216 | list.addAll(listIdText);
217 | } else {
218 | for (AccessibilityNodeInfo info : listIdText) {
219 | boolean isOk = true;
220 | for (AbstractTF tf : tfs) {
221 | if (!tf.checkOk(info)) {
222 | isOk = false;
223 | break;
224 | }
225 | }
226 | if (isOk) {
227 | list.add(info);
228 | } else {
229 | info.recycle();
230 | }
231 | }
232 | }
233 | break;
234 | default:
235 | throw new RuntimeException("由于时间有限,并且多了也没什么用,所以IdTF和TextTF只能有一个");
236 | }
237 | rootInfo.recycle();
238 | return list;
239 | }
240 |
241 | /**
242 | * 目前好像只有外部输入设备才会调用(虚拟键盘没用)
243 | */
244 | @Override
245 | protected boolean onKeyEvent(KeyEvent event) {
246 | System.out.println("哈哈哈哈" + event);
247 | return super.onKeyEvent(event);
248 | }
249 |
250 | /**
251 | * @param tfs 由于是递归循环,会忽略IdTF和TextTF
252 | */
253 | public static void findAllRecursive(List list, AccessibilityNodeInfo parent, @NonNull AbstractTF... tfs) {
254 | if (parent == null || list == null) return;
255 | if (tfs.length == 0) throw new InvalidParameterException("AbstractTF不允许传空");
256 |
257 | for (int i = 0; i < parent.getChildCount(); i++) {
258 | AccessibilityNodeInfo child = parent.getChild(i);
259 | if (child == null) continue;
260 | boolean isOk = true;
261 | for (AbstractTF tf : tfs) {
262 | if (!tf.checkOk(child)) {
263 | isOk = false;
264 | break;
265 | }
266 | }
267 | if (isOk) {
268 | list.add(child);
269 | } else {
270 | findAllRecursive(list, child, tfs);
271 | child.recycle();
272 | }
273 | }
274 | }
275 |
276 | /**
277 | * 立即发送移动的手势
278 | * 注意7.0以上的手机才有此方法,请确保运行在7.0手机上
279 | *
280 | * @param path 移动路径
281 | * @param mills 持续总时间
282 | */
283 | @RequiresApi(24)
284 | public void dispatchGestureMove(Path path, long mills) {
285 | dispatchGesture(new GestureDescription.Builder().addStroke(new GestureDescription.StrokeDescription
286 | (path, 0, mills)).build(), null, null);
287 | }
288 |
289 | /**
290 | * 点击指定位置
291 | * 注意7.0以上的手机才有此方法,请确保运行在7.0手机上
292 | */
293 | @RequiresApi(24)
294 | public void dispatchGestureClick(int x, int y) {
295 | Path path = new Path();
296 | path.moveTo(x - 1, y - 1);
297 | path.lineTo(x + 1, y + 1);
298 | dispatchGesture(new GestureDescription.Builder().addStroke(new GestureDescription.StrokeDescription
299 | (path, 0, 100)).build(), null, null);
300 | }
301 |
302 | /**
303 | * 有些应用和谐了{@link #clickView}方法
304 | */
305 | @RequiresApi(24)
306 | public void dispatchGestureClick(AccessibilityNodeInfo info) {
307 | Rect rect = AbstractTF.mRecycleRect;
308 | info.getBoundsInScreen(rect);
309 | dispatchGestureClick(rect.centerX(), rect.centerY());
310 | }
311 |
312 | /**
313 | * 长按指定位置
314 | * 注意7.0以上的手机才有此方法,请确保运行在7.0手机上
315 | */
316 | @RequiresApi(24)
317 | public void dispatchGestureLongClick(int x, int y) {
318 | Path path = new Path();
319 | path.moveTo(x - 1, y - 1);
320 | path.lineTo(x, y - 1);
321 | path.lineTo(x, y);
322 | path.lineTo(x - 1, y);
323 | dispatchGesture(new GestureDescription.Builder().addStroke(new GestureDescription.StrokeDescription
324 | (path, 0, 1000)).build(), null, null);
325 | }
326 |
327 | /**
328 | * 由于太多,最好回收这些AccessibilityNodeInfo
329 | */
330 | public static void recycleAccessibilityNodeInfo(List listInfo) {
331 | if (Utils.isEmptyArray(listInfo)) return;
332 |
333 | for (AccessibilityNodeInfo info : listInfo) {
334 | info.recycle();
335 | }
336 | }
337 | }
--------------------------------------------------------------------------------