如何使用 AccessibilityService 实现蚂蚁森林自动收取能量,无需Root,无需连接电脑

如何使用 AccessibilityService 实现蚂蚁森林自动收取能量,无需Root,无需连接电脑

AccessibilityService 设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。

AccessibilityService官网

前提准备

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 的工具,自己也一直想实现一个,实在懒,还没动工

Auto.js git地址

伸手党这里直接打包了一个debug包

首先进行布局范围分析

然后点击蚂蚁森林,可以看到 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.收取好友能量

由于支付宝小手手是一张图片,而且那个位置固定有这个元素(估计是看多了太多自动收能量脚本,迫不得已让你找不到),没办法看谁有谁没有能量,只能暴力所有人点一遍了

  1. 点击“查看更多好友”按钮

  2. 搜索带有"g"的元素,第一个肯定是自己舍去,看第二个元素,然后通过层级关系,找到 ListView 相应的层级

  3. 找到当前昵称所在的元素(记录下来,避免重复点击)

  4. 找到“邀请”所在的元素(需要邀请的人不需要点,反正没能量,也不能点击)

  5. 点击 ListView 里面的一个个Item,开始收好友能量吧

  6. 一页点完,滚动下,继续循环找

  7. 通过找元素“没有更多了”在屏幕内,就说明点完了,收工

// 点击查看更多好友
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的地址:蚂蚁森林自动领取能量

发布了29 篇原创文章 · 获赞 3 · 访问量 1121

猜你喜欢

转载自blog.csdn.net/qq_16927853/article/details/103356801