如何使用 AccessibilityService 实现蚂蚁森林自动收取能量,无需Root,无需连接电脑
AccessibilityService 设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。
前提准备
1. 继承 AccessbilityService
继承AccessbilityService,在 onServiceConnected 的时候做一些初始化,在 onAccessibilityEvent 里面监听页面事件,实现具体的辅助功能
public class AccessibilityServiceDemo extends AccessibilityService {
//初始化
@Override
protected void onServiceConnected() {
super.onServiceConnected();
}
//实现辅助功能
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
@Override
public void onInterrupt() {
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
2. 在AndroidManifest中注册该服务
因为 AccessbilityService 本质上还是一个 Service ,所以必须要在 AndroidManifest.xml 中注册该服务
<service
android:name=".auto.AccessibilityServiceDemo"
android:enabled="true"
android:exported="true"
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/accessibitydemo" />
</service>
android:permission=“android.permission.BIND_ACCESSIBILITY_SERVICE” 是为了确保只有系统可以绑定该服务。
3. 在 meta-data 中配置监听属性
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackAllMask"
// 必须加上 flagRequestEnhancedWebAccessibility ,不然无法检查到 WebView 里面的元素,支付宝大部分页面都是 WebView
android:accessibilityFlags="flagIncludeNotImportantViews|flagReportViewIds|flagRetrieveInteractiveWindows|flagRequestEnhancedWebAccessibility|flagRequestFilterKeyEvents"
android:canPerformGestures="true"
android:canRequestEnhancedWebAccessibility="true"
android:canRequestFilterKeyEvents="true"
android:canRetrieveWindowContent="true"
// 此处必须用@string方式,不能直接写,会报错
android:description="@string/accessibility_des"
android:notificationTimeout="100"
// 监听的包名如果多个可以用 "," 隔开,例如:"com.eg.android.AlipayGphone,com.eg.android.AlipayGphone"
android:packageNames="com.eg.android.AlipayGphone" />
4. 指引用户去手动打开该服务
因为无障碍服务无法自动开启,只能引导用户手动开启
public static boolean isAccessibilitySettingsOn(Context mContext, Class<? extends AccessibilityService> clazz) {
int accessibilityEnabled = 0;
final String service = mContext.getPackageName() + "/" + clazz.getCanonicalName();
try {
accessibilityEnabled = Settings.Secure.getInt(mContext.getApplicationContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_ENABLED);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
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;
}
没有开启的话,就跳到服务的开启页面,让用户手动开启
startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
5. 封装元素查找的方法
下面举个例子,想看完整版的,自己看demo吧
public AccessibilityNodeInfo findFirst(@NonNull AbstractTF... tfs) {
if (tfs.length == 0) throw new InvalidParameterException("AbstractTF不允许传空");
AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
if (rootInfo == null) return null;
int idTextTFCount = 0, idTextIndex = 0;
for (int i = 0; i < tfs.length; i++) {
if (tfs[i] instanceof AbstractTF.IdTextTF) {
idTextTFCount++;
idTextIndex = i;
}
}
switch (idTextTFCount) {
case 0://id或text数量为0,直接循环查找
AccessibilityNodeInfo returnInfo = findFirstRecursive(rootInfo, tfs);
rootInfo.recycle();
return returnInfo;
case 1://id或text数量为1,先查出对应的id或text,然后再查其他条件
if (tfs.length == 1) {
AccessibilityNodeInfo returnInfo2 = ((AbstractTF.IdTextTF) tfs[idTextIndex]).findFirst(rootInfo);
rootInfo.recycle();
return returnInfo2;
} else {
...
}
default:
throw new RuntimeException("由于时间有限,并且多了也没什么用,所以IdTF和TextTF只能有一个");
}
rootInfo.recycle();
return null;
}
开始自动领取能量
自己的 AccessbilityService 无障碍服务已经创建了,接下来就要做自动领取能量流程了
1. 打开支付宝
Intent intent = activity.getPackageManager().getLaunchIntentForPackage("com.eg.android.AlipayGphone");
if (intent != null) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}
直接打开支付宝的 Launch 页面
2. 创建子线程执行任务
protected ExecutorService mExecutor;
public void onAccessibilityEvent(AutoAccessibilityService autoAccessibilityService, AccessibilityEvent event) {
this.autoAccessibilityService = autoAccessibilityService;
if (needStop) {
LogUtils.d(TAG, "needStop = true, stop");
return;
}
if (!needStart) return;
if (mExecutor != null && !mExecutor.isShutdown()) {
mExecutor.shutdownNow();
}
mExecutor = Executors.newSingleThreadExecutor();
mExecutor.execute(new Runnable() {
@Override
public void run() {
doTask();
}
});
needStart = false;
}
创建一个线程来执行任务,虽然在 Service 中可以做的操作是在后台的,但是在主线程执行的操作过长还是会卡主线程,造成anr的
3. 查找“蚂蚁森林”入口
不知道查找元素的,先看下这篇:如何使用 AccessibilityService 查找元素
找到“蚂蚁森林”这个元素,然后点击进入
AccessibilityNodeInfo homeAntTreesNode = loopFindFirst(AbstractTF.newText("蚂蚁森林", true));
if (homeAntTreesNode == null) {
LogUtils.d(TAG, "collectEnergy, homeAntTreesNode == null");
needStart = true;
return;
}
// 进入蚂蚁森林页面
autoAccessibilityService.clickView(homeAntTreesNode);
这里我用的是一个叫 Auto.js 的工具,自己也一直想实现一个,实在懒,还没动工
首先进行布局范围分析
然后点击蚂蚁森林,可以看到 txt 是“蚂蚁森林”,所以这个可以用 newText 来查找
4.领取自己的能量
首先判断是否到了领取能量页面,这里用了“种树”元素,找到说明到了,没找到说明还没到,就怕网不好,所以循环等待
AccessibilityNodeInfo plantTreeNode = loopFindFirst(10, AbstractTF.newWebText("种树", true));
if (plantTreeNode == null) {
LogUtils.d(TAG, "collectEnergy, plantTreeNode == null");
needStart = true;
return;
} else {
LogUtils.d(TAG, "collectEnergy, plantTreeNode != null");
}
到了自己页面之后领取能量,首先“收集能量”的元素,也是同样通过 Auto.js 来获取的,可以点击能量球,看到"收集能量4克",所以就查找搜索“收集能量”四个字了,而且看了下是WebView上的,所以用 newWebText 来找
要是有好友的昵称这么奇葩,带了“收集能量”,那就自认倒霉吧
找到可以收取的能量后疯狂点击(好歹隔了0.5s,防止手机被你点坏了),由于这些元素都是没有点击事件的,所以只能点击这个元素的中心点了(必须API 24 以上才有用)
List<AccessibilityNodeInfo> energyNode = autoAccessibilityService.findAll(AbstractTF.newWebText("收集能量", false));
for (int i = 0; i < energyNode.size(); i++) {
autoAccessibilityService.dispatchGestureClick(energyNode.get(i));
SystemClock.sleep(500);
}
5.收取好友能量
由于支付宝小手手是一张图片,而且那个位置固定有这个元素(估计是看多了太多自动收能量脚本,迫不得已让你找不到),没办法看谁有谁没有能量,只能暴力所有人点一遍了
-
点击“查看更多好友”按钮
-
搜索带有"g"的元素,第一个肯定是自己舍去,看第二个元素,然后通过层级关系,找到 ListView 相应的层级
-
找到当前昵称所在的元素(记录下来,避免重复点击)
-
找到“邀请”所在的元素(需要邀请的人不需要点,反正没能量,也不能点击)
-
点击 ListView 里面的一个个Item,开始收好友能量吧
-
一页点完,滚动下,继续循环找
-
通过找元素“没有更多了”在屏幕内,就说明点完了,收工
// 点击查看更多好友
AccessibilityNodeInfo checkAllFriendNode = loopFindFirst(10, AbstractTF.newWebText("查看更多好友", true));
autoAccessibilityService.clickView(checkAllFriendNode);
SystemClock.sleep(2000);
collectedNames.clear();
AccessibilityNodeInfo noMoreNode = null;
Rect noMoreNodeOutBounds = new Rect();
while (noMoreNode == null || (noMoreNodeOutBounds.bottom - noMoreNodeOutBounds.top) < Utils.dp2px(AutoApplication.context, 10)) {
if (!isInPackage()) {
LogUtils.d(TAG, "collectEnergy, not in package return");
return;
}
List<AccessibilityNodeInfo> specialNodes = loopFindAll(AbstractTF.newWebText("g", false));
AccessibilityNodeInfo listItems = null;
try {
listItems = specialNodes.get(1).getParent().getParent().getParent();
} catch (Exception e) {
LogUtils.d(TAG, "collectEnergy, can not find lists, can not collect friend energy");
}
LogUtils.d(TAG, "collectEnergy, collect friend energy: " + (listItems == null ? 0 : listItems.getChildCount()));
for (int i = 0; listItems != null && i < listItems.getChildCount(); i++) {
if (!isInPackage()) {
LogUtils.d(TAG, "collectEnergy, not in package return");
return;
}
AccessibilityNodeInfo item = listItems.getChild(i);
Rect outBounds = new Rect();
item.getBoundsInScreen(outBounds);
String friendName = "";
try {
friendName = item.getChild(2).getChild(0).getChild(0).getText().toString();
String friendNameContent = item.getChild(2).getChild(0).getChild(0).getContentDescription().toString();
LogUtils.d(TAG, "collectEnergy, get friend name text: " + friendName + ", ContentDescription: " + friendNameContent);
} catch (Exception e) {
LogUtils.d(TAG, "collectEnergy, get friend name error");
}
String hasNoEnergy = "";
try {
hasNoEnergy = item.getChild(3).getChild(0).getChild(0).getText().toString();
} catch (Exception e) {
LogUtils.d(TAG, "collectEnergy, get has no energy error");
}
if (!TextUtils.isEmpty(friendName) && !"邀请".equals(hasNoEnergy) && !collectedNames.contains(friendName)
&& outBounds.top > 0 && outBounds.bottom < AutoApplication.context.getResources().getDisplayMetrics().heightPixels && outBounds.top < outBounds.bottom) {
LogUtils.d(TAG, "collectEnergy, try collect friend energy name: " + friendName);
collectFriendEnergy(item);
collectedNames.add(friendName);
SystemClock.sleep(1000);
} else {
LogUtils.d(TAG, "collectEnergy, can not collect friend energy name: " + friendName);
}
}
noMoreNode = autoAccessibilityService.findFirst(AbstractTF.newWebText("没有更多了", false));
if (noMoreNode != null) {
noMoreNode.getBoundsInScreen(noMoreNodeOutBounds);
LogUtils.d(TAG, "collectEnergy, has no more node left: " + noMoreNodeOutBounds.left + ", right: " + noMoreNodeOutBounds.right
+ ", top: " + noMoreNodeOutBounds.top + ", bottom: " + noMoreNodeOutBounds.bottom);
}
LogUtils.d(TAG, "collectEnergy, do scroll to collect other friends");
doLargeScroll(true);
}
总结
这里用的还是比较暴力的方法,直接每一个好友都点一遍,实在是太耗时了。(主要是支付宝太鸡贼了,估计这种脚本太多,蚂蚁森林过段时间就更新,改结构,让你脚本失效)
后面考虑优化下,采用截图的方式,判断收取能量小手手存不存在,再点进去收取能量,期待下次的分析吧。。。。。。
最后附上完整demo的地址:蚂蚁森林自动领取能量