Android 源码系列之从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)

        转载请注明出处:http://blog.csdn.net/llew2011/article/details/52822148

        说起外挂特别是玩游戏的小伙伴估计对它很熟悉,肯定有部分小伙伴使用过,至于为什么使用它,你懂得(*^__^*) ……我最早接触外挂是在大二的时候,那时候盛行玩QQ农场,早上一睁眼就是打开电脑先把自己的菜收了,收完之后再去偷别人的;后来童靴说非凡软件上有一个偷菜外挂,于是赶紧整了一个,有了外挂之后就告别了体力时代,省时又省力……既然在PC上有外挂,那在智能手机上可以做外挂呢?答案是OK的,今天这篇文章就是讲解一下如何在Android设备上制作自己的小外挂,需要说明的是本文仅仅做技术交流……

        产生做外挂的念头是在去年春节时支付宝推的咻一咻咻大奖活动,那时候每到咻一咻的时间点就赶紧打开支付宝进入咻一咻页面然后不停的点击咻一咻按钮,后来我就想与其这样一直重复点击按钮不如花点时间整个咻一咻小外挂,于是花了小半天时间写了一个,经过实践发现效果还挺理想的……其实在Android设备上制作小外挂并不是多么高深的技术,核心就是利用AccessibilityService,如果你对该类已经很熟悉,请跳过本文(*^__^*) ……

        AccessibilityService是Google为了方便那些身体不便的用户来使用Android设备而提供的一种无障碍服务,该服务可以帮助那些身体不便的用户更加简单的使用和操作Android设备,这些操作包括文字转语音,触觉反馈,收拾操作,轨迹球和手柄操作等。AccessibilityService提供的这种服务就是用来监听指定的应用的,例如监听指定应用页面内容的边界,页面的跳转,焦点的变化等等。因此我们可以利用该服务做我们想做的小外挂,比如自动安装APP,抢红包外挂还有我之前写的咻一咻外挂,今天我们就讲解一下如何利用AccessibilityService来实现自动安装APP的小外挂。

        AccessibilityService是Service的子类,但是它的声明周期是由系统来管理的,那也就是说我们要想启动该服务就不能够像平时那样直接startService()了而是需要在Android设备的辅助功能列表中手动开启该服务,当开启该服务后其生命周期就交由系统来管理和维护了。需要注意的是虽然不需要通过startService()等方式来启动AccessibilityService服务,但是AccessibilityService依然是需要在配置文件AndroidManifest.xml中配置。由于AccessibilityService是抽象类不能直接使用,所以需要先自定义一个类来继承AccessibilityService,自定义AutoInstallApkService服务类代码如下:

public class AutoInstallApkService extends AccessibilityService {

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // 事件入口处
    }

    @Override
    public void onInterrupt() {
    }
}
        AutoInstallApkService重写了AccessibilityServiced的俩抽象方法,onAccessibilityEvent()方法表示该服务接收系统传递进来的辅助事件(该事件可能是当前窗口内容发生变化触发的,也可能是当前窗口焦点发生变化触发的,还有可能是系统弹出Notification触发的等等),该方法为事件入口,每当监听的指定应用触发了指定事件的时候都会回调此方法。而onInterrupt()方法表示服务中断发生的回调,服务中断意味着不能接收回调了,但是可以在方法中做些相关业务等操作。

        定义完了我们的AutoInstallApkService服务后,接下就是在AndroidManifest.xml文件中配置该服务了,根据官方文档,配置文件如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.llew.wb.project.service.accessibility.installapk">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ui.activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".ui.service.AutoInstallApkService"
            android:label="@string/app_name"
            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/accessibility_service_config" />
        </service>
    </application>

</manifest>

        在配置文件中配置了我们的AutoInstallApkService服务,配置服务的时候需要注意以下几点:

  1. 添加label标签
            AutoInstallApkService需要添加label标签,标签表示服务的名字,应用安装后会在手机辅助功能的列表中显示,若没定义标签则不显示
  2. 添加系统权限
            系统权限android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"是一定要添加的,否则该服务会被系统忽略
  3. 添加过滤器
            一定要添加intent为的android:name="android.accessibilityservice.AccessibilityService"过滤器,否则该服务会被系统忽略
  4. meta-data配置文件
            meta-dataandroid:name表示配置的服务名称,值是固定写法不能修改,android:resource表示引用的具体配置文件,本例引用的是accessibility_service_config.xml文件【注意:此配置是在4.0版本之后的写法,在低版本中可使用另一种写法,稍后会有讲解】

        看完了manifest配置文件后,我们看一下在res目录下的xml文件夹中的accessibility_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="feedbackAllMask"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    android:description="@string/app_name"
    android:notificationTimeout="100"
    android:packageNames="com.android.packageinstaller" />
        在accessibility_service_config.xml中根元素为 accessibility-service为,这是固定写法不能更改;各属性值的说明如下:
  • android:accessibilityEventTypes
            该属性表示当前AutoInstallApkService服务接收的事件类型,typeAllMask表示接收所有的事件类型,常见的事件有:typeWindowContentChanged(窗口内容发生变化的事件),typeWindowStateChanged(窗口焦点发生变化的事件),typeNotificationStateChanged(弹出Notification的事件)等等,如果想了解更多的辅助事件可参阅官方文档
  • android:accessibilityFeedbackType
            该属性表示设置反馈给用户的方式,常见的有语音播报,手机振动等
  • android:canRetrieveWindowContent
            该属性表示是否可以获取当前窗口内容,true表示可以获取否则不可以获取
  • android:description
            表示对当前辅助功能的描述,该值会在Android设置的辅助列表中显示
  • android:notificationTimeout
            表示响应时间,目前设置为100毫秒
  • android:packageNames
            表示当前辅助服务需要监听的应用包名,目前我们是实现自动安装外挂,而安装应用是调用系统的Installer应用,该应用的包名为com.android.packageinstaller,如果想要监听多个应用,中间加逗号。

        好了,配置完了我们的AutoInstallApkService服务后,接下来是实现具体的安装APK的逻辑了。我们知道在Android设备上安装应用的时候会弹出一个安装确认页面,只有确认后安装才会继续执行……这其实是调用系统默认的PackageInstaller安装器(需要指出的是这个APK是第三方的应用,系统应用的安装可以不通过PackageInstaller来安装)。在PackageInstaller安装界面的操作流程一般是:是否允许安装 → 正在进行安装 → 安装完成确认;这三个安装流程的界面是不一样的,拿中国移动的APK来举个栗子,如下图所示:

      

        上图分别展示了在Android手机上安装APP应用时调用系统安装器PackageInstaller不同状态时的样式,我们的自动安装应用小外挂就是当出现了这以上页面中的任一个时我们该外观都能自动来执行安装流程,既然是自动安装也就是说当出现了这些按钮时我们的外挂能主动的执行按钮的点击操作,这样就省去了人为的手动点击操作,这也是外挂的核心作用。由于PackageInstaller的页面发生了变化都会回调AutoInstallApkService的onAccessibilityEvent()方法,因此我们可以在该方法中来模拟用户的操作,要模拟点击操作就要得到对应的按钮,然后执行按钮的点击事件;那怎么样才能得到目标按钮这个对象呢?在AccessibilityService中提供了一个getRootInActiveWindow()方法,该方法返回一个代表当前活动窗口的根节点AccessibilityNodeInfo实例对象,该对象保存了当前窗口界面的相关信息,比如控件在窗口的位置信息,id信息,文本信息,类型信息,文本信息等等,它和ViewGroup类似,对外提供了诸如findAccessibilityNodeInfosByViewId(),findAccessibilityNodeInfosByText(),performAction()等方法。其中findAccessibilityNodeInfosByViewId()是4.3版本之后的新增方法,表示根据给定控件的ID来获取到对应控件,获取到对应控件后就可以通过performAction()方法来执行点击事件了,那怎么获取到指定控件的ID呢?

        在Android的sdk目录中有个tools目录,在该目录下有个uiautomatorviewer工具,该工具很有用,特别是分析apk的页面布局信息,它可以获取到当前手机屏幕上的界面信息,如下图所示:

        在上图中我们通过uiautomatorviewer工具展示了安装APP的界面信息。左侧表示截屏信息,当点击界面上的相关控件的时候,右侧就会出现该控件的相关信息,比如id,text,package,class,clickable等,而这个ID就是我们想要的id,又因为在一个页面上id是唯一的,所以只要我们获取到了所有的符合条件的ID后就可以通过该id获取到对应的AccessibilityNodeInfo对象了,然后通过调用该对象的performAction()方法就可以实现自动点击效果了。通过uiautomatorviewer工具找到所有操作按钮的id后,就可以在我们的AutoInstallApkService的onAccessibilityEvent()方法中做操作了,代码如下:

public class AutoInstallApkService extends AccessibilityService {

    private static final String DEFAULT_PACKAGE_NAME = "com.android.packageinstaller";
    
    private static final String[] IDS = {
            "com.android.packageinstaller:id/ok_button",        // 下一步按钮的ID,注意ID的格式,必须这样写
            "com.android.packageinstaller:id/done_button",      // 完成按钮的ID,注意ID的格式,必须这样写
            "com.android.packageinstaller:id/confirm_button"    // 确认按钮的ID,注意ID的格式,必须这样写
    };

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (null == event) return;
        installApkIfNecessary(event);
        recycleAccessibilityEvent(event);
    }

    private void installApkIfNecessary(AccessibilityEvent event) {
        AccessibilityNodeInfo rootInfo = getRootInActiveWindow();
        if (null == rootInfo) return;
        String packageName = rootInfo.getPackageName().toString();
        if (DEFAULT_PACKAGE_NAME.equals(packageName)) {
            int length = IDS.length;
            AccessibilityNodeInfo availableNode = null;
            for (int i = 0; i < length; i++) {
                availableNode = findAvailableNodeInfoByViewId(rootInfo, IDS[i]);
                if (null != availableNode) {
                    break;
                }
            }
            if (null != availableNode) {
                performClickWithAccessibilityNode(availableNode);
            }
        }
    }

    private AccessibilityNodeInfo findAvailableNodeInfoByViewId(AccessibilityNodeInfo root, String id) {
        List<AccessibilityNodeInfo> availableNodes = root.findAccessibilityNodeInfosByViewId(id);
        if (null == availableNodes || availableNodes.isEmpty()) {
            return null;
        }
        return availableNodes.get(0);
    }

    private void performClickWithAccessibilityNode(AccessibilityNodeInfo nodeInfo) {
        if (null != nodeInfo) {
            if (nodeInfo.isClickable()) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            } else {
                performClickWithAccessibilityNode(nodeInfo.getParent());
            }
        }
    }

    @Override
    public void onInterrupt() {
    }

    private void recycleAccessibilityEvent(AccessibilityEvent event) {
        if (null != event) {
            event.recycle();
            event = null;
        }
    }
}

        以上就是我们AutoInstallApkService的全部代码,相信小伙伴们都看的懂,DEFAULT_PACKAGE_NAME表示监听应用的包名,IDS表示需要点击的所有的ID控件集合,在onAccessibilityEvent()方法中我们首先判断传递进来的event是否非空,如果非空就执行installApkIfNecessary()方法,在该方法中先获取根节点rootInfo,然后循环IDS通过调用rootInfo的findAccessibilityNodeInfosByViewId()方法找到对应的控件节点,如果找到了对应节点就调用performClickWithAccessibilityNode()方法来执行点击动作,需要注意的是在performClickWithAccessibilityNode()方法中如果当前控件不可点击我们就递归调用找其父控件来执行点击事件,只所以这么做是为了避免为了扩大点击面积我们往往在当前控件外嵌套一个父布局然后使父布局来响应点击事件的情况存在(因为之前在做抢红包外挂时碰见这种情况)。

        好了,现在我们的自动安装APP的外挂已经完成,接下来运行在手机上后要在手机的辅助功能列表中开启该服务,否则该服务不起作用,操作截图如下:

        开启了AutoInstallApkService服务后,就可以在手机上尝试安装一个APP看看效果了。因为我们的AutoInstallApkService是监听的系统的PackageInstaller应用,所以只要我们点击了已经下载过的APP后都是可以调用此应用的,直接点击一个应用,效果如下:


        根据截图效果来看,安装APP时的操作流程都可以自动完成,运行结果也达到了我们的预期,这就是在手机上做的一个小外挂,是不是很简单?(*^__^*) ……其实外挂听起来很高大上,但只要了解了它的核心思想,做一个小外挂出来还是很容易的(就像当时我写了一个咻一咻外挂),现在想想当年的偷菜外挂猜测应该也是借助了系统的辅助功能来实现的吧。

        好了,到这里有关自动安装APP的小外挂实验已经结果了,在下篇文章Android 源码系列之<十一>从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(下)中,我将带领小伙伴们们从源码出发深入理解一下AccessibilityService的执行原理,敬请期待!!!最后感谢观看(*^__^*) ……






发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/52822148