辅助功能(AccessibilityService)其实是一个Android系统提供给的一种服务,本身是继承Service类的。这个服务提供了增强的用户界面,旨在帮助残障人士使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。
AccessibilityService具有很多强大的功能。但是从开发者的角度看,其实最主要的就是提供两种功能:查找界面元素,实现模拟点击。这也是我们实现自动抢红包软件的关键。当View、ViewGroup、TextView等控件这些状态变化时控件会回调系统API,API系统然后对这些对象的数据进行组装,为了数据的安全性,系统会重新创建一些对象(AccessibilityEvent、AccessibilityNodeInfo)来间接保存这些数据,然后通过跨进程将这些数据返回给对应的Service中。
辅助功能不可能直接操作外部对象,辅助功能只能在本进程调用指定系统方法,由系统再分发给指定外部对象,辅助功能做的事基本和用户能做的差不多。
AccessibilityEvent、AccessibilityNodeInfo里面的所有set方法均无用(这些方法是系统调用把数据塞进去用的),我们能做的只有:get、is、find等获取数据的方法,以及极少的操作performAction,dispatchGesture等。
对于辅助功能类的配置有两种方式:
(1)在onServiceConnected() 中配置(不推荐,部分属性可能无法设置)
/**
* 当系统连接上你的服务时被调用
*/
@Override
protected void onServiceConnected() {
//设置监听的应用包名(微信和qq)
AccessibilityServiceInfo info = getServiceInfo();
info.packageNames = new String[]{WX_PKG, QQ_PKG};
setServiceInfo(info);
super.onServiceConnected();
}
(2)在XML中配置(推荐)
新建配置 service_config.xml
这里来简单介绍一下这些配置:
accessibilityEventTypes 过滤事件类型
accessibilityFeedbackType 反馈类型
canRetrieveWindowContent 请求访问权限
packageNames 需要监听的包名列表
notificationTimeout 响应时间设置
编写自己的辅助服务类,需要继承AccessibilityService类。
生命周期
新建PacketService.java类
/**
* 必须重写的方法:此方法用了接受系统发来的event。在你注册的event发生是被调用。在整个生命周期会被调用多次。
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
//得到对应的事件类型,这里有很多很多种的事件类型,具体可以自行翻阅AccessibilityEvent类中的定义。
int eventType = event.getEventType();
LogUtil.d("eventType ===> " + eventType);
switch (eventType) {
// 通知栏事件
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
//获取通知栏消息的文字
List texts = event.getText();
if (!texts.isEmpty()) {
for (CharSequence text : texts) {
String content = text.toString();
LogUtil.d("notification content ===> " + content);
// 监听到微信红包的notification,打开通知
if (content.contains("[微信红包]")) {
//点击打开通知栏消息
openNotification(event);
}
}
}
break;
//窗口状态改变
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
//获取到窗口的类名
String className = event.getClassName().toString();
LogUtil.i("window state = " + className);
if (className.equals(WX_GET)) {
// 微信聊天界面,领取微信红包
getWXPacket(event);
} else if (className.equals(WX_OPEN_OLD)) {
//微信红包弹框界面,打开微信红包
openPacket(event);
} else if (className.equals(WX_OPEN_NEW_YEAR)) {
//拜年红包
openNewYearPacket(event);
} else if (className.equals(WX_RESULT)) {
//进入红包结果页,获取抢到的红包金额
getNumber(event);
}
break;
}
模拟点击打开通知消息
/**
* 模拟点击打开通知消息
*/
private void openNotification(AccessibilityEvent event) {
if (event.getParcelableData() != null
&& event.getParcelableData() instanceof Notification) {
Notification notification = (Notification) event.getParcelableData();
//获取通知栏的intent
PendingIntent pendingIntent = notification.contentIntent;
try {
//根据intent跳转
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
}
如何在界面中找到相应的控件节点进行模拟点击呢?
/**
* 找到打开的按钮,模拟点击打开微信红包
*/
private void openPacket(AccessibilityEvent event) {
//根据打开按钮的id找出来,模拟点击
AccessibilityNodeInfo info = findFirstViewById("com.tencent.mm:id/cyf");
if (info != null) {
boolean isSuccess = clickView(info);
info.recycle();
}
}
/**
* 点击该控件
*
* @return true表示点击成功
*/
public static boolean clickView(AccessibilityNodeInfo nodeInfo) {
if (nodeInfo != null) {
if (nodeInfo.isClickable()) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
return true;
} else {
AccessibilityNodeInfo parent = nodeInfo.getParent();
if (parent != null) {
boolean b = clickView(parent);
parent.recycle();
if (b) return true;
}
}
}
return false;
}
几个实用的API:
getRootInActiveWindow() 获取当前屏幕的布局
findAccessibilityNodeInfosByViewId() 根据id找到相应的view
findAccessibilityNodeInfosByViewId() 根据id找到相应的view
findAccessibilityNodeInfosByViewId() 根据id找到相应的view
如何找到相应的组件的id?
这里就需要用到Android studio自带的一个神器:uiautomatorviewer.bat
打开uiautomatorviewer,连接手机,即可查看当前手机界面的布局。
完整的服务类代码:
/**
* 通过利用AccessibilityService辅助服务,监测屏幕内容,如监听状态栏的信息,屏幕跳转等,以此来实现自动拆红包的功能
*/
public class PacketService extends AccessibilityService {
//微信包名
private final static String WX_PKG = "com.tencent.mm";
//微信聊天界面
private final static String WX_GET = "com.tencent.mm.ui.LauncherUI";
//微信弹出红包界面
private final static String WX_OPEN_OLD = "com.tencent.mm.plugin.luckymoney.ui" +
".LuckyMoneyNotHookReceiveUI";
//新年红包打开弹框
private final static String WX_OPEN_NEW_YEAR = "com.tencent.mm.plugin.luckymoney.ui" +
".LuckyMoneyNewYearReceiveUI";
//红包结果页
private final static String WX_RESULT = "com.tencent.mm.plugin.luckymoney.ui" +
".LuckyMoneyDetailUI";
private List list;
/**
* 辅助功能是否启动
*/
public static boolean isStart = false;
private boolean isAny;
/**
* 必须重写的方法:此方法用了接受系统发来的event。在你注册的event发生是被调用。在整个生命周期会被调用多次。
*/
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
//得到对应的事件类型,这里有很多很多种的事件类型,具体可以自行翻阅AccessibilityEvent类中的定义。
int eventType = event.getEventType();
LogUtil.d("eventType ===> " + eventType);
switch (eventType) {
// 通知栏事件
case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED:
//获取通知栏消息的文字
List texts = event.getText();
if (!texts.isEmpty()) {
for (CharSequence text : texts) {
String content = text.toString();
LogUtil.d("notification content ===> " + content);
// 监听到微信QQ红包的notification,打开通知
if (content.contains("[微信红包]")) {
//解开屏幕锁
if (isScreenLocked()) {
unlockScreen();
}
//点击打开通知栏消息
openNotification(event);
}
}
}
break;
//窗口状态改变
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
//获取到窗口的类名
String className = event.getClassName().toString();
LogUtil.i("window state = " + className);
if (className.equals(WX_GET)) {
// 微信聊天界面,领取微信红包
getWXPacket(event);
} else if (className.equals(WX_OPEN_OLD)) {
//微信红包弹框界面,打开微信红包
openPacket(event);
} else if (className.equals(WX_OPEN_NEW_YEAR)) {
openNewYearPacket(event);
} else if (className.equals(WX_RESULT)) {
//进入红包结果页
getNumber(event);
}
break;
//监听界面文字改变(太耗资源,列表页会出现卡顿)
// case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
// LogUtil.i("content = " + event);
// if (event.getPackageName().equals(WX_PKG)) {
// mWeiXinClicked = false;
// // 领取微信红包
// getWXPacket();
// }
// break;
default:
break;
}
}
/**
* 必须重写的方法:系统要中断此service返回的响应时会调用。在整个生命周期会被调用多次。
*/
@Override
public void onInterrupt() {
LogUtil.d("红包功能被迫中断");
isStart = false;
}
/**
* 当系统连接上你的服务时被调用
*/
@Override
protected void onServiceConnected() {
super.onServiceConnected();
LogUtil.d("红包功能检测中....");
isStart = true;
if (isScreenLocked()) {
unlockScreen();
}
}
/**
* 在系统要关闭此service时调用。
*/
@Override
public boolean onUnbind(Intent intent) {
LogUtil.d("红包功能已断开");
isStart = false;
return super.onUnbind(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
LogUtil.d("红包功能已关闭");
isStart = false;
}
/**
* 模拟点击打开通知消息
*/
private void openNotification(AccessibilityEvent event) {
if (event.getParcelableData() != null
&& event.getParcelableData() instanceof Notification) {
Notification notification = (Notification) event.getParcelableData();
//获取通知栏的intent
PendingIntent pendingIntent = notification.contentIntent;
try {
//根据intent跳转
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
}
/**
* 拿到可领取的微信红包的节点,点击打开弹框
*/
private void getWXPacket(AccessibilityEvent event) {
if (mWeiXinClicked) {
return;
}
LogUtil.d("-->进入聊天界面:");
//找到所有红包对应的id列表
List list = findViewById("com.tencent.mm:id/aou");
if (!isEmptyArray(list)) {
LogUtil.d("-->有红包个数:" + list.size());
//过滤有多少个待领取的红包
List listNum = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
AccessibilityNodeInfo info = list.get(i);
List listOld = info.findAccessibilityNodeInfosByViewId
("com.tencent.mm:id/aq6");
List listNew = info.findAccessibilityNodeInfosByViewId
("com.tencent.mm:id/aqb");
if (isEmptyArray(listOld) && isEmptyArray(listNew)) {
//如果有,证明是普通红包
List list3 = info.findAccessibilityNodeInfosByViewId
("com.tencent.mm:id/aq3");
if (!isEmptyArray(list3)) {
//如果是普通红包,并且有这个的话才是别人发过来的
List list5 = info.findAccessibilityNodeInfosByViewId
("com.tencent.mm:id/aq5");
if (!isEmptyArray(list5)) {
listNum.add(i);
list5.get(0).recycle();
}
list3.get(0).recycle();
} else {
listNum.add(i);
}
}
if (!isEmptyArray(listOld)) {
listOld.get(0).recycle();
}
if (!isEmptyArray(listNew)) {
listNew.get(0).recycle();
}
//如果不是第一个可领取的,都回收
if (listNum.size() == 0 || i != listNum.get(0)) {
info.recycle();
}
}
if (listNum.size() > 0) {
LogUtil.d("-->待领取红包数:" + listNum.size());
AccessibilityNodeInfo info = list.get(listNum.get(0));
if (info.isClickable()) {
isOpen = false;
LogUtil.d("-->点击打开普通红包弹框:");
clickView(info);
} else {
List listClick = info.getParent()
.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/aqc");
if (!isEmptyArray(listClick)) {
LogUtil.d("-->点击打开拜年红包弹框:");
clickView(listClick.get(0));
}
}
isAny = listNum.size() > 1;
info.recycle();
}
}
}
/**
* 找到打开的按钮,模拟点击打开微信红包
*/
private void openPacket(AccessibilityEvent event) {
//根据打开按钮的id找出来,模拟点击
AccessibilityNodeInfo info = findFirstViewById("com.tencent.mm:id/cyf");
if (info != null) {
boolean isSuccess = clickView(info);
info.recycle();
}
}
/**
* 点击拜年红包,弹框自动领取
*/
private void openNewYearPacket(AccessibilityEvent event) {
if (isAny) {
isAny = false;
performBackClick();
}
}
/**
* 获取抢到的红包价格
*/
private void getNumber(AccessibilityEvent event) {
AccessibilityNodeInfo info = findFirstViewById("com.tencent.mm:id/cqv");
if (info != null) {
String number = info.getText().toString();
//获取到抢到的金额
}
if (isAny) {
isAny = false;
performBackClick();
}
}
//////////////////////工具方法/////////////////////////
/**
* 系统是否在锁屏状态
*/
private boolean isScreenLocked() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
boolean isScreenOn = pm.isScreenOn();//如果为true,则表示屏幕“亮”了,否则屏幕“暗”了。
return !isScreenOn;
}
private void unlockScreen() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP
| PowerManager.ON_AFTER_RELEASE, "MyWakeLock");
wakeLock.acquire();
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context
.KEYGUARD_SERVICE);
final KeyguardManager.KeyguardLock keyguardLock = keyguardManager.newKeyguardLock
("MyKeyguardLock");
keyguardLock.disableKeyguard();
}
/**
* 判断列表是否为空
*/
private boolean isEmptyArray(List list) {
return list == null || list.size() == 0;
}
/**
* 根据getRootInActiveWindow查找当前id的控件集合(类似listview这种一个页面重复的id很多)
*
* @param idfullName id全称:com.android.xxx:id/tv_main
*/
@Nullable
public List findViewById(String idfullName, AccessibilityNodeInfo
rootInfo) {
try {
if (rootInfo == null) {
rootInfo = getRootInActiveWindow();
}
if (rootInfo == null) return null;
List list = rootInfo.findAccessibilityNodeInfosByViewId
(idfullName);
rootInfo.recycle();
return list;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public List findViewById(String idfullName) {
return findViewById(idfullName, null);
}
/**
* 根据getRootInActiveWindow查找当前id的控件集合(类似listview这种一个页面重复的id很多,只取第一个)
*
* @param idfullName id全称:com.android.xxx:id/tv_main
*/
@Nullable
public AccessibilityNodeInfo findFirstViewById(String idfullName, AccessibilityNodeInfo
rootInfo) {
List list = findViewById(idfullName, rootInfo);
return isEmptyArray(list) ? null : list.get(0);
}
public AccessibilityNodeInfo findFirstViewById(String idfullName) {
return findFirstViewById(idfullName, null);
}
/**
* 点击该控件
*
* @return true表示点击成功
*/
public static boolean clickView(AccessibilityNodeInfo nodeInfo) {
if (nodeInfo != null) {
if (nodeInfo.isClickable()) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
return true;
} else {
AccessibilityNodeInfo parent = nodeInfo.getParent();
if (parent != null) {
boolean b = clickView(parent);
parent.recycle();
if (b) return true;
}
}
}
return false;
}
/**
* 根据getRootInActiveWindow查找包含当前text的控件
*
* @param containsText 只要内容包含就会找到(应该是根据drawText找的)
*/
@Nullable
public List findViewByContainsText(@NonNull String containsText) {
AccessibilityNodeInfo info = getRootInActiveWindow();
if (info == null) return null;
List list = info.findAccessibilityNodeInfosByText(containsText);
info.recycle();
return list;
}
/**
* 根据getRootInActiveWindow查找和当前text相等的控件
*
* @param equalsText 需要找的text
*/
@Nullable
public List findViewByEqualsText(@NonNull String equalsText) {
List listOld = findViewByContainsText(equalsText);
LogUtil.d("-->获取包含领取红包的TextView:" + listOld.size());
if (isEmptyArray(listOld)) {
return null;
}
LogUtil.d("-->获取包含领取红包的TextView:" + listOld.size());
ArrayList listNew = new ArrayList<>();
for (AccessibilityNodeInfo ani : listOld) {
LogUtil.d("-->包含领取红包字样的view的文字:" + ani.getText().toString());
if (ani.getText() != null && equalsText.equals(ani.getText().toString())) {
listNew.add(ani);
} else {
ani.recycle();
}
}
return listNew;
}
/**
* Check当前辅助服务是否启用
*
* @param serviceName serviceName
* @return 是否启用
*/
private boolean checkAccessibilityEnabled(Context context, String serviceName) {
AccessibilityManager mAccessibilityManager = (AccessibilityManager) context
.getSystemService(Context.ACCESSIBILITY_SERVICE);
List accessibilityServices =
mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo
.FEEDBACK_GENERIC);
for (AccessibilityServiceInfo info : accessibilityServices) {
if (info.getId().equals(serviceName)) {
return true;
}
}
return false;
}
/**
* 前往开启辅助服务界面
*/
public void goAccess(Context context) {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* 模拟点击事件
*
* @param nodeInfo nodeInfo
*/
public void performViewClick(AccessibilityNodeInfo nodeInfo) {
if (nodeInfo == null) {
return;
}
while (nodeInfo != null) {
if (nodeInfo.isClickable()) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
break;
}
nodeInfo = nodeInfo.getParent();
}
}
/**
* 模拟返回操作
*/
public void performBackClick() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
performGlobalAction(GLOBAL_ACTION_BACK);
}
}