Android后台模拟点击探索(附源码)

工作中我们需要自制一套工具,其中遇到需要模拟点击事件的需求,类似按键精灵的功能,支持后台持续运行,满足触发条件时完成点击。

经过一番探索,一共整理出两种不同的方案:AccessibilityService 和 adb shell命令,读者可自行选择合适的场景。

AccessibilityService

无障碍模式是我首先想到的方案,对于不知道Android无障碍模式的,可自行百度。这里简单说明一下,AccessibilityService是Android为残障人士提供的贴心功能,比如可以报出当前页面有哪些按钮balabala。使用官方提供的一些列API,我们还可以完成一些自动运行的“黑科技”操作,比如早些年的红包插件、微信自动回复插件、自动点赞插件等。

本方案原理比较简单:扫描当前页面的View树,找到目标控件,模拟点击操作,下面详细阐述。

添加配置文件

首先需要在res目录下建立配置文件:accessible_service_config.xml ,名字随意取。

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:accessibilityEventTypes="typeAllMask"
 android:accessibilityFeedbackType="feedbackGeneric"
 android:accessibilityFlags="flagReportViewIds"
 android:canRetrieveWindowContent="true"
 android:notificationTimeout="100"
 android:description="@string/description"
 android:packageNames="目标包名"/>

accessibilityEventTypes:设置响应事件的类型,这里设置typeAllMask,就是响应全部类型的事件。

accessibilityFeedbackType:设置回馈给用户的方式,有语音播出和振动,这里使用通用类型。

notificationTimeout:设置响应时间。

packageNames:目标包名,比如红包插件就要设置微信包名,关于包名如何获取,下文会提到。

继承AccessibilityService编码

接着我们继承AccessibilityService新建AutoClickAccessibilityService,重写onAccessibilityEvent(AccessibilityEvent event)。

public class AutoClickAccessibilityService extends AccessibilityService {
 private static final String TAG = "GK";

 @Override
 public void onAccessibilityEvent(AccessibilityEvent event) {
  ztLog("===start===");
  try {
   //拿到根节点
   AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
   if (rootInfo == null) {
    return;
   //开始遍历,这里拎出来细讲,直接往下看正文
   if (rootInfo.getChildCount() != 0) {
    ……
   }
  } catch (Exception e) {
  ztLog("Exception:" + e.getMessage(), true);
 }
}

拿到根节点以后,我们有两种方式开始寻找目标节点:

  1. 根据View id:findAccessibilityNodeInfosByViewId
  2. 根据控件文案:findAccessibilityNodeInfosByText

这里我们拿魅族手机自带的音乐App做例子,假如我们需要自动点击下图的 专栏 :

使用findAccessibilityNodeInfosByViewId寻找目标

我们可以使用findAccessibilityNodeInfosByViewId(),通过id找到目标节点,关于View id,可以使用DDMS中的Dump View Hierarchy for UI Automator,就是点击下图按钮(不知道如何打开eclipse或者AS的DDMS的同学可以自行百度):

稍等片刻,生成屏幕快照,并解析出View树,从右下的属性框就可以找到id,同时仔细看,包名也可以获取到啦~

这里很有可能因为目标apk混淆严重而读不到id,比如是个?,那么可以尝试第二个方法。

使用findAccessibilityNodeInfosByText寻找目标

使用findAccessibilityNodeInfosByText("最热MV"),顾名思义,就是根据文案找控件。

找到控件以后,就可以执行点击操作了,但是且慢,这里有个坑。

因为注意看这里的view树:

无论我们根据id还是文案,找到的可能只是一个TextView或者Button,但是根据我们日常经验,我们肯定是给其父布局设置的点击事件,也就是这里的LinearLayout或者FrameLayout。

所以我的方案是根据View树的结构,自行遍历。比如这里的View树结构如下:

我先做深度优先遍历找到GridView,然后遍历它所有孩子直至找到专栏这个TextView,为什么我不直接DFS找到专栏呢?因为我要记录它的父节点甚至爷爷节点,方便接下来执行点击操作。

如果有同学使用这种方案,建议根据实际View树的结构,自行遍历寻找,我的代码如下:

/**
 * 深度优先遍历寻找目标节点
 */
private void DFS(AccessibilityNodeInfo rootInfo) {
  if (rootInfo == null || TextUtils.isEmpty(rootInfo.getClassName())) {
    return;
  }
  if (!"android.widget.GridView".equals(rootInfo.getClassName())) {
    ztLog(rootInfo.getClassName().toString());
    for (int i = 0; i < rootInfo.getChildCount(); i++) {
      DFS(rootInfo.getChild(i));
    }
  } else {
    ztLog("==find gridView==");
    final AccessibilityNodeInfo GridViewInfo = rootInfo;
    for (int i = 0; i < GridViewInfo.getChildCount(); i++) {
      final AccessibilityNodeInfo frameLayoutInfo = GridViewInfo.getChild(i);
      //细心的同学会发现,我代码里的遍历的逻辑跟View树里显示的结构不一样,
      //快照显示的FrameLayout下明明该是LinearLayout,我这里却是TextView,
      //这个我也不知道,实际调试出来的就是这样……所以大家实操过程中也要注意了
      final AccessibilityNodeInfo childInfo = frameLayoutInfo.getChild(0);
      String text = childInfo.getText().toString();
      if (text.equals("专栏")) {
        performClick(frameLayoutInfo);
      } else {
        ztLog(text);
      }
    }
  }
}

private void performClick(AccessibilityNodeInfo targetInfo) {
  targetInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}

AndroidManifest文件添加Service配置

AccessibilityService也是一个Servcie,所以要在AndroidManifest配置一下。

<service
  android:name=".AutoClickService"
  android:exported="false"
  <!-- label就是在手机设置中的无障碍里,显示的标签 -->
  android:label="自动点击Demo"
  <!-- 注意这里的android:permission是在service结构里面的!! -->
  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/accessible_service_config" />
</service>

至此无障碍模式方案就讲完了,运行之后,需要在手机设置中的无障碍里打开对应的开关:

打开以后,自动点击功能可以自动后台运行了,不想用时可以在上图开关那里关闭即可。

以后需要先运行App,再打开开关,开启功能。

无障碍模式虽然用着挺舒服,但是在很多厂商的系统里,已经打开的无障碍模式隔一段时间经常会被自动关闭,比如MIUI系统里就要给App加开机运行的权限。

而厂商自带的无障碍就没事,猜测系统里内置了处理,这也是无障碍模式的一个坑吧。

小结

最后总结一下,AccessibilityService是一个很有趣的功能,发挥想象力可以做很多事,但是要小心踩坑:

  1. 通过findAccessibilityNodeInfosByViewId或者findAccessibilityNodeInfosByText找到的目标控件不一定是你想要的点击控件
  2. 各家厂商系统可能对无障碍模式内置了屏蔽处理

adb shell命令

adb可以方便我们直接高效的操作真机,比如安装apk,批量安装apk,复制文件等,而模拟点击事件也是可以通过adb命令完成的。

我是突然想到,前阵子看过网上流传的一个“微信跳一跳”的辅助,使用python + adb完成。

原理就是adb负责截图,python负责图像识别像素计算距离,最后再由adb模拟点击。

如果我们需要点击的目标,坐标相对确定,那我们直接在代码里执行adb命令模拟点击即可。

真机实验

我们先用USB连接真机,在cmd命令行工具里:

adb shell
shell@PRO6:/ $ input tap 125 521
shell@PRO6:/ $ 

这里的意思就是点击屏幕上 (x, y) = (125, 521)的地方。果然手机响应了,缺点就是响应时间略长,感觉有1秒左右。

同理其他手势操作也可以完成,这里不作详解,感兴趣的可以自行搜索。

下面我们需要做的就是在代码里完成上述操作,并且可以持续在后台运行。这里我也是踩坑无数,听我慢慢吐槽。

寻找后台执行adb命令的方案

ProcessBuilder — OUT

没什么好说的,直接看代码:

  int x = 0, y = 0;
  String[] order = { "input", "tap", " ", x + "", y + "" };
  try {
    new ProcessBuilder(order).start();
  } catch (IOException e) {
    Log.i("GK", e.getMessage());
    e.printStackTrace();
  }

这种版本,在Activity中可行,但是切后台不行……这肯定无法满足需求,再找!

Instrumentation — OUT

try {
  Instrumentation inst = new Instrumentation();
  inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0));
  inst.sendPointerSync(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0));
  Log.i("GK", "模拟点击" + x + ", " + y);
} catch (Exception e) {
  Log.e("Exception when sendPointerSync", e.toString());
}

这种版本和上一个一模一样,不能后台,差评!!

救世主Runtime登场

private OutputStream os;

/**
 * 执行ADB命令: input tap 125 340
 */
private final void exec(String cmd) {
  try {
    if (os == null) {
      os = Runtime.getRuntime().exec("su").getOutputStream();
    }
    os.write(cmd.getBytes());
    os.flush();
  } catch (Exception e) {
    e.printStackTrace();
    Log.e("GK", e.getMessage());
  }
}

后台问题迎刃而解!

添加合适的时机

目前我们把核心功能做完了,最后需要做的就是找到合适的时机,执行操作。

首先我们的容器肯定是一个Service,然后后台不断的判断当前app是否是目标app,如果是的话,再执行自动点击操作。

所以我们需要判断当前前台app的包名或者Activity的名字是否是我们的目标。

/**
 * 如果前台APP是目标apk
 */
private boolean isCurrentAppIsTarget() {
  String name = getForegroundAppPackageName();
  if (!TextUtils.isEmpty(name) && PACKAGE_NAME.equalsIgnoreCase(name)) {
    return true;
  }
  return false;
}

/**
 * 获取前台程序包名
 */
public String getForegroundAppPackageName() {
  ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
  List<RunningAppProcessInfo> lr = am.getRunningAppProcesses();
  if (lr == null) {
    return null;
  }

  for (RunningAppProcessInfo ra : lr) {
    if (ra.importance == RunningAppProcessInfo.IMPORTANCE_VISIBLE || ra.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
      Log.i("GK", ra.processName);
      return ra.processName;
    }
  }
  return "";
}

以上就是adb shell方案,这种方案缺陷也比较明显,就是要求 自动点击的位置不能改变。

对于如何获取点击位置的坐标,可以打开开发者选项中的指针位置:

直接查看坐标。

总结

模拟点击这种需求,我们一般都不会用到,也有点歪门邪道的意思。但是无论什么需求,中间的探索过程才最珍贵。技术也是人,不是每次都会有说干就干的决心和勇气,保持一颗好奇心,珍惜每次探索的机会,学有所得,小有收获,也未尝不是一种自我认可。

最后附上源码:AutoClickService

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

您可能感兴趣的文章:

  • Android模拟开关按钮点击打开动画(属性动画之平移动画)
  • Android模拟用户点击的实现方法
(0)

相关推荐

  • Android模拟开关按钮点击打开动画(属性动画之平移动画)

    在Android里面,一些炫酷的动画确实是很吸引人的地方,让然看了就赏心悦目,一个好看的动画可能会提高用户对软件的使用率.另外说到动画,在Android里面支持两种动画:补间动画和属性动画,至于这两种动画的区别这里不再介绍,希望开发者都能在使用的过程中体会两者的不同. 本文使用属性动画完成,说到属性动画,肯定要提到 JakeWharton大神写的NineOldAndroids动画库,如果你的app需要在android3.0以下使用属性动画,那么这个库就很有作用了,如果只需要在高版本使用,那么直接

  • Android模拟用户点击的实现方法

    前言 Android模拟用户点击.在自动化测试中可使用的工具. 可以利用adb命令,也可以使用Android SDK中的monkeyrunner工具. win7-64 gitbash 使用adb命令 主要使用input命令 usage: input ... input text <string> input keyevent <key code number or name> input tap <x> <y> input swipe <x1>

  • Android后台模拟点击探索(附源码)

    工作中我们需要自制一套工具,其中遇到需要模拟点击事件的需求,类似按键精灵的功能,支持后台持续运行,满足触发条件时完成点击. 经过一番探索,一共整理出两种不同的方案:AccessibilityService 和 adb shell命令,读者可自行选择合适的场景. AccessibilityService 无障碍模式是我首先想到的方案,对于不知道Android无障碍模式的,可自行百度.这里简单说明一下,AccessibilityService是Android为残障人士提供的贴心功能,比如可以报出当前

  • Android编程单元测试实例详解(附源码)

    本文实例讲述了Android编程单元测试.分享给大家供大家参考,具体如下: 完整实例代码代码点击此处本站下载. 本文是在上一篇文章<java编程之单元测试(Junit)实例分析>的基础上继续讲解android的单元测试,android源码中引入了java单元测试的框架(android源码目录:libcore\junit\src\main\java\junit\framework中可见),然后在java单元测试框架的基础上扩展属于android自己的测试框架.android具体框架类的关系图如下

  • 详解Android TabHost的多种实现方法 附源码下载

    最近仔细研究了下TabHost,主要是为了实现微信底部导航栏的功能,最后也给出一个文章链接,大家不要着急 正文: TabHost的实现分为两种,一个是不继承TabActivity,一个是继承自TabActivity:当然了选用继承自TabActivity的话就相对容易一些,下面来看看分别是怎样来实现的吧. 方法一.定义tabhost:不用继承TabActivity  1.布局文件:activity_main.xml <LinearLayout xmlns:android="http://s

  • 比较完整的android MP3 LRC歌词滚动高亮显示(附源码)

    1.以前的滚动只是安行来刷新,现在不是按行来滚动了,其实就是在一定时间内整体往上移动,比如说在1S内刷新10次,由于认得肉眼看起来像是滚动. 关键代码如下: 复制代码 代码如下: float plus = currentDunringTime == 0 ? 30                : 30                        + (((float) currentTime - (float) sentenctTime) / (float) currentDunringTim

  • Android辅助功能实现自动抢红包(附源码)

    一.描述 最近看到同事有用抢红包的软件,就想看看抢红包的具体实现是如何的,所以了解了一下,有用辅助功能实现的,所以在下面的示例中会展示一个抢红包的小Demo,附带源码抢红包源码. 二.效果图 在桌面收到红包进行抢 在聊天页面收到口令红包 三.AccessibilityService使用 创建辅助服务类,继承AccessibilityService,实现两个接口,接收系统的事件 public class MyService extends AccessibilityService { @Overr

  • Android编程实现画板功能的方法总结【附源码下载】

    本文实例讲述了Android编程实现画板功能的方法.分享给大家供大家参考,具体如下: Android实现画板主要有2种方式,一种是用自定义View实现,另一种是通过Canvas类实现.当然自定义View内部也是用的Canvas.第一种方式的思路是,创建一个自定义View(推荐SurfaceView),在自定义View里通过Path对象记录手指滑动的路径调用lineTo()绘制:第二种方式的思路是,先用Canvas绘制一张空的Bitmap,通过ImageView的setImageBitmap()方

  • 自定义Android六边形进度条(附源码)

    本文实例讲述了Android自定义圆形进度条,分享给大家供大家参考.具体如下: 大家也可以参考这两篇文章进行学习: <自定义Android圆形进度条(附源码)>   <Android带进度的圆形进度条> 运行效果截图如下: 主要代码: package com.sxc.hexagonprogress; import java.util.Random; import android.content.Context; import android.content.res.ColorSta

  • 微信小程序国际化探索实现(附源码地址)

    随着小程序应用越来越广泛,国际化支持逐渐成了刚需. 官方文档给出了一个 国际化方案 ,但觉得配置起来稍微有点复杂,对项目结构还有一定的要求.如果是旧项目改动成本太大,遂决定自己实现一个小程序国际化方案. 源码地址:https://github.com/cachecats/miniprogram-i18n 一.项目结构 整体目录结构如下图: assets 存放资源文件,如图片 constants 存放项目中用到的常量 i18n 存放语言文件,中文是 zh-CN.js 英文是 en-US.js ,如

  • Android实现一个比相册更高大上的左右滑动特效(附源码)

    目录 实现思路 源码如下: 在Android里面,想要实现一个类似相册的左右滑动效果,我们除了可以用Gallery.HorizontalScrollView.ViewPager等控件,还可以用一个叫做 ViewFlipper 的类来代替实现,它继承于 ViewAnimator.如见其名,这个类是跟动画有关,会将添加到它里面的两个或者多个View做一个动画,然后每次只显示一个子View,通过在 View 之间切换时执行动画,最终达到一个类似相册能左右滑动的效果. 本次功能要实现的两个基本效果 最基

  • 用React-Native+Mobx做一个迷你水果商城APP(附源码)

    前言 最近一直在学习微信小程序,在学习过程中,看到了 wxapp-mall 这个微信小程序的项目,觉得很不错,UI挺小清新的,便clone下来研究研究,在看源码过程中,发现并不复杂,用不多的代码来实现丰富的功能确实令我十分惊喜,于是,我就想,如果用react-native来做一个类似这种小项目难不难呢,何况,写一套代码还能同时跑android和ios(小程序也是...),要不写一个来玩玩?有了这个想法,我便直接 react-native init 一个project来写一下吧(๑•̀ㅂ•́)و✧

随机推荐