AccessibilityService 设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知、Toast等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。
AccessibilityService官网
继承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();
}
}
因为 AccessbilityService 本质上还是一个 Service ,所以必须要在 AndroidManifest.xml 中注册该服务
android:permission=“android.permission.BIND_ACCESSIBILITY_SERVICE” 是为了确保只有系统可以绑定该服务。
因为无障碍服务无法自动开启,只能引导用户手动开启
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));
下面举个例子,想看完整版的,自己看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 无障碍服务已经创建了,接下来就要做自动领取能量流程了
Intent intent = activity.getPackageManager().getLaunchIntentForPackage("com.eg.android.AlipayGphone");
if (intent != null) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
}
直接打开支付宝的 Launch 页面
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的
不知道查找元素的,先看下这篇:如何使用 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 来查找
首先判断是否到了领取能量页面,这里用了“种树”元素,找到说明到了,没找到说明还没到,就怕网不好,所以循环等待
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 energyNode = autoAccessibilityService.findAll(AbstractTF.newWebText("收集能量", false));
for (int i = 0; i < energyNode.size(); i++) {
autoAccessibilityService.dispatchGestureClick(energyNode.get(i));
SystemClock.sleep(500);
}
由于支付宝小手手是一张图片,而且那个位置固定有这个元素(估计是看多了太多自动收能量脚本,迫不得已让你找不到),没办法看谁有谁没有能量,只能暴力所有人点一遍了
点击“查看更多好友”按钮
搜索带有"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 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的地址:蚂蚁森林自动领取能量