通用技术 尝试使用 AccessibilityService 解决应用安装提示

Yorn · 2015年12月16日 · 最后由 testerhaha 回复于 2018年12月10日 · 5118 次阅读
本帖已被设为精华帖!

0. 干货区

如果你不 Care 这是怎么实现的,只想要个解决方案,那么请戳这里下载 AutoInstall 的 apk来安装并开启服务即可。
如果你顺便还想拿源码来自己定制一下,可从这里找到AndroidStudio 工程源码,仅一个 Service 而已。
如果你想知道一下什么是 AccessibilityService,可自行搜索学习或看官方介绍 http://developer.android.com/reference/android/accessibilityservice/AccessibilityService.html

开启方法:
普通手机: 设置 -> 无障碍/辅助功能 -> 服务 -> AutoInstall -> 开启 -> 确定
某些手机:设置 -> 其它高级设置 -> 辅助功能 -> 服务 -> AutoInstall -> 开启 -> 确定

注意:
开启自动安装不仅适用于 adb install,也适用于主动点击 apk 来启动安装。所以有安全风险,建议仅在测试机器上安装

1. 代码区

不需要 Activity,仅需要一个继承 AccessibilityService 的服务,在服务里兼听 onAccessibilityEvent,当出现安装界面的时候,自动去点击。在安装完成后,到辅助功能里开启即可。

AutoInstallService.java
package {你的包名};

import android.accessibilityservice.AccessibilityService;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;

import java.util.List;

public class AutoInstallService extends AccessibilityService {

    private static final String TAG = "AutoInstallService";
    private static String PACKAGE_INSTALLER = "com.android.packageinstaller";

    public AutoInstallService() {
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        /*
         * 回调方法,当事件发生时会从这里进入,在这里判断需要捕获的内容,
         * 可通过下面这句log将所有事件详情打印出来,分析决定怎么过滤。
         */
        //log(event.toString());
        if (event.getSource() == null) {
            log("<null> event source");
            return;
        }
        int eventType = event.getEventType();
        /*
         * 在弹出安装界面时会发生 TYPE_WINDOW_STATE_CHANGED 事件,其属主
         * 是系统安装器com.android.packageinstaller
         */
        if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                && event.getPackageName().equals(PACKAGE_INSTALLER)) {
            boolean r = performInstallation(event);
            log("Action Perform: " + r);
        }

    }

    @Override
    public void onInterrupt() {
        log("AutoInstallServiceInterrupted");
    }

    private void log(String s) {
        Log.d(TAG, s);
    }

    private boolean performInstallation(AccessibilityEvent event) {
        List<AccessibilityNodeInfo> nodeInfoList;
        /*
         * 有的手机会弹2次,有的只弹一次,在替换安装时会出现确定按钮,
         * 为了大而全,下面定义了比较多的内容,可按需增减。
         */
        String[] labels = new String[]{"确定", "安装", "下一步", "完成"};
        for (String label : labels) {
            nodeInfoList = event.getSource().findAccessibilityNodeInfosByText(label);
            if (nodeInfoList != null && !nodeInfoList.isEmpty()) {
                boolean performed = performClick(nodeInfoList);
                if (performed) return true;
            }
        }
        return false;
    }

    private boolean performClick(List<AccessibilityNodeInfo> nodeInfoList) {
        for (AccessibilityNodeInfo node : nodeInfoList) {
            /*
             * 这里还可以根据node的类名来过滤,大多数是button类,这里也是为了大而全,
             * 判断只要是可点击的是可用的就点。
             */
            if (node.isClickable() && node.isEnabled()) {
                return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
        }
        return false;
    }

}

AndroidManifest 里面要声明权限,除了上面从代码里面可以过滤,通过 meta-data 的 xml 里也可直接配置过滤

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mrqyoung.autoinstall" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@android:style/Theme.Black" >
        <service
            android:name=".AutoInstallService"
            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/accessibilityservice" />
        </service>
    </application>

</manifest>

在 AndroidManifest 里面引用的 meta-data 文件,样例

@xml/accessibilityservice.xml
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
    android:accessibilityEventTypes="typeWindowStateChanged"    
    android:packageNames="com.android.packageinstaller"              
    android:description="@string/description"
    android:accessibilityFeedbackType="feedbackVisual"
    android:notificationTimeout="100"
    android:accessibilityFlags="flagDefault"
    android:canRetrieveWindowContent="true"
    xmlns:android="http://schemas.android.com/apk/res/android" />

<!--  第3行等同于过滤 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -->
<!--  第4行只监听 com.android.packageinstaller  -->

2. 展示区

进行了 3 种方式的安装测试:

  • 直接在手机上点击 apk 文件弹出安装,它能自动安装完成。写代码时没有那些特殊机器只好用这种方式来调试。
    ins0.gif

  • 通过 adb 安装,新安装和覆盖安装,在特殊手机上弹出安装后能自动安装完成。
    [无图]

  • 在运行 appium 时,appium 会安装 2 个 apk,新安装和覆盖安装,弹出安装后能自动完成。
    apum.gif

3. 其它区

利用 AccessibilityService 可以自动识别界面上的内容,并进行操作,也能达到自动化操作的目的。绿色守护的自动停止应用,豌豆荚的自动安装,一些抢红包的工具是用它来实现的。在最开始遇到这个问题的时候,我给了一个简单直接暴力的解决方法,在批处理中:

...
start adb shell "sleep 3 && input tap 1200 300"
adb install -r xxx.apk
...

后来为了兼容性强,做成了一个自动安装的工具,也许能勉强解决在自动化过程中出现安装提示的问题吧,期待大家共同验证和完善。

共收到 19 条回复 时间 点赞

不错呢 赞一个 战略 make

这篇文章通过辅助工具进行安卓 Toast 文本检查的方法里面说

Appium 启用 Uiautomator 建立 session 之后,所有的 AccessibilityService 服务全都挂死了

上面的例子能够运行成功是因为那个时候 uiautomator 还没起来是吗?

Yorn #3 · 2015年12月17日 Author

@zsx10110 真是不幸呢!我要再验证一下这个问题。但是,昨天我在 Appium 1.4.8 和 1.4.16 两个版本上面都试过,都可以自动安装成功并且把我的用例跑了一遍。无论如何,在我们这里已经满足要求了因为我们的自动化框架无关与 UiAutomator 是通过 cmd 安装启动应用的。

非常不错。

Yorn #5 · 2015年12月17日 Author

@zsx10110 刚刚验证了一下,果然一个uiautomator dump就不工作了。但是手动安装了一次到完成的时候它又复活了。这个方式看来还是不合适,该改标题。

UiAutomator can't be used along with an AccessibilityService. When you turn on the service the uiAutomator will crash.

However, as UiAutomator 2.0 is based on instrumentation you will probably be able to access the information you need without the service.

ref

不过,我又发现,在uiautomator dump使之不工作之后,锁屏一次再解锁一次,它就活了,所以呢,曲线救国也许能行吧

#5 楼 @mrqyoung 我写的 AccessibilityService 装进手机里启动了 成功调用了 onServiceConnected ,但是 onAccessibilityEvent 没有收到任何事件是什么情况?你的 DEMO 能发我 让我看下吗

Yorn #17 · 2015年12月18日 Author

@dongdong 什么 demo?如果是 AccessibilityService 的话,源码和 APK 在顶部都有链接可下载。如果是 Appium 在代码的话,我也可以贴上来。估计是你没有去系统设置的辅助设置里面打开?

#!/usr/bin/env python
#  -*- coding:utf-8 -*-


import os
from time import sleep

import unittest

from appium import webdriver


class MyTests(unittest.TestCase):

    def setUp(self):
        conf = {}
        conf['platformName'] = 'Android'
        conf['platformVersion'] = '4.4.4'
        conf['deviceName'] = '<your-device-id>'
        conf['app'] = 'http://www.mrqyoung.tk/apps/HiAPK_2.01.apk'
        #conf['appPackage'] = 'com.mrqyoung.hiapk'
        #conf['appActivity'] = '.HiAPKActivity'
        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', conf)

    def tearDown(self):
        self.driver.quit()

    def _home(self):
        print('Press HOME')
        self.driver.press_keycode(3)

    def _back(self):
        print('Press BACK')
        self.driver.press_keycode(4)

    def _is_main(self):
        e = self.driver.find_elements_by_id('android:id/title')
        #print(len(e))
        return len(e) == 4

    #@unittest.skip("debug")
    def test_installed(self):
        print('test_installed...')
        e = self.driver.find_element_by_name('Installed')
        #e = self.driver.find_element_by_android_uiautomator('new UiSelector().text("Installed")')
        e.click()  # change to TAB_INSTALLED
        self.driver.find_element_by_name('com.android.contacts').click()  # open Contacts' APP_INFO
        self.driver.find_element_by_id('com.android.settings:id/left_button').click()  # tap FORCE_STOP
        self._back()  # back to MAIN
        self._back()
        assert self._is_main()

    #@unittest.skip("debug")
    def test_saved(self):
        print('skip test_saved.')
        pass

    def test_remote(self):
        print('test_remote...')
        e = self.driver.find_element_by_name('Remote').click()  # change to TAB_REMOTE
        sleep(5)  # wait for webview loading
        e = self.driver.find_element_by_class_name('android.webkit.WebView')
        print(self.driver.contexts)
        self.driver.switch_to.context(self.driver.contexts[-1])
        e = self.driver.find_element_by_id('footer')
        print(e.text)
        e = self.driver.find_element_by_link_text('DeviceInfo.apk')
        print(e.text)
        e.click()
        self.driver.switch_to.context('NATIVE_APP')
        assert self._is_main()

    #@unittest.skip("debug")
    def test_apks(self):
        print('skip test_apks.')
        self.driver.find_element_by_name('Apk files').click()
        pass


if __name__ == '__main__':
    unittest.main(verbosity=2)

叫受你好

nice,回头研究看看

我锤子手机 安装了 不顶用啊,没点掉

我的确认提示框,是 “同意”,怎么改,那个 java 文件里面的么,改了之后怎么打包成 apk 啊

请问,AccessibilityServer 可以获取到系统权限弹框的 event 吗?

#12 楼 @miki 亲试系统弹框是返回的 event 是 null

Yorn 回复

我测试的可以用 uiautomator 跑自动化,貌似没有影响

用的三星手机 ,三星 S5,6 的系统,从 sdcard 点了一个 apk 试了下,界面上有安装文字,但没有下一步,没有给自动点,是不是不兼容?

我在小米 4 android6.0 的手机运行楼主提供的 apk 功能不起作用啊 打开 logcat 也看不到任何 AutoInstallService 相关的日志 而且我也从 “无障碍” 里把 autoinstall 打开了, 是不是小米手机不支持 AccessibilitySerivce?

我在你的工程代码里修改了
String[] labels = new String[]{"允许", "继续安装","确定", "安装", "下一步", "完成" };
重新构建打包安装,在小米手机上没有起到作用,这种解决方案对国产魔改的手机是不是不起作用?

仅楼主可见

我在 vivo 手机上面使用了,但是在使用 adb,弹窗安装后,没有被点击掉,辅助功能都已经开启了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册