Android APK 省心安装 —— 眼睁睁地看着它完成一切

96
GinkWang
0.3 2018.03.25 00:23* 字数 1862

项目的一个新需求。因为我们现在做的是一个服务于公共的产品,实施人员也不一定及时地去维护。所以为了方便,我们想要 APK 更新时静默更新,就是不需要人去主动触发或者同意,就能够实现软件更新。上网查资料,最终实现方法如下:

网上关于静默更新的文章有很多,包括郭霖大神都介绍过相关技术。所以这块就不多卖关子了,静默安装 APK 有两种实现方式:

  • 静默安装

  • 省心安装

效果更好的是静默安装,因为他真的是静默——无声无息。就类似于你使用的是小米手机,然后在小米软件商店下载软件,你会发现你点击下载后不用你做任何同意安装的操作,软件就已经下载到你的手机了。这样自然是最好的效果,也正是我们想要的。但是得到好的东西就要付出一些代价,实现静默安装的一个大前提是——Root 权限。这个条件太苛刻了,你不能要求谁的手机都已经 Root。那你可能会问,为啥小米安装软件就能静默?

没为啥,因为 MIUI 就是人家小米自己定制的 Android 系统,自然是想改什么就改什么。而我们普通开发者肯定是没有这么高的权限了。

网上也有一些教程,就是可以通过 Java 的反射机制获取到 IPackageManager 这个对象,然后使用它的 installPackage 方法进行安装。但是这种方法需要配置很多文件,而且在不同机型,需要配置得文件也不一样。适配难度很大。所以只能忍痛挥别这种最好的方案。

那现在只好选择省心安装的方案了,什么是省心安装?

还拿上面的例子说,你使用小米手机,但是你在应用宝中下载软件,这时你就会遇到这种情况,安装软件时会需要你点击是否同意安装,如下:

应用宝下载.gif

按照前文的逻辑来说,这是正常的情况。因为应用宝在 MIUI 中也只是个普通应用啊,没有 Root 就没法使用静默安装。下图是应用宝的设置界面:


应用宝设置界面.png

但是可以在上图中看到,应用宝有一个备用解决方案——省心装,关于该功能的介绍是 安装时无须频繁点击下一步与完成 。看来还行,我们开启一下这个功能看看。

点击开启之后,会提示要授予应用宝辅助功能权限。于是我们就去辅助功能界面去授权:

辅助界面.png

这个界面详细介绍了关于省心装的功能,我们点击右上方开启。然后回到应用宝安装一个应用试一下:

应用宝省心装.gif

可以看到,就如同应用宝对于该功能介绍的那样,我在选择安装软件之后,就没用我再点击确认安装什么的了。一切步骤全是应用宝替我搞定的。这样的实现虽然还是显示安装界面,不如静默安装安静,但是也可以满足我的需求,实现我想要的效果。

所以最终选择省心安装这个方案。另外,这个方案在不同软件上有不同的叫法,有叫他自动装的,也有叫智能装的。反正追根溯源,说的都是一个东西。

下面看一下此方案的具体实现。

先新建一个工程,然后在 res 下新建一个 xml 目录,在xml目录内新建一个accessibility_service_config.xml文件,内容如下:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_desc"
android:packageNames="com.android.packageinstaller"/>

其中,packageNames指定我们要监听哪个应用程序下的窗口活动,这里写com.android.packageinstaller表示监听Android系统的安装界面。accessibility_desc指定在无障碍服务当中显示给用户看的说明信息,上图中应用宝的一大段内容就是在这里指定的。accessibilityEventTypes指定我们在监听窗口中可以模拟哪些事件,这里写typeAllMask表示所有的事件都能模拟。accessibilityFlags可以指定无障碍服务的一些附加参数,这里我们传默认值flagDefault就行。accessibilityFeedbackType指定无障碍服务的反馈方式,实际上辅助功能(无障碍服务)这个功能是 Android 提供给一些残疾人士使用的,比如说盲人不方便使用手机,就可以借助无障碍服务配合语音反馈来操作手机,而我们其实是不需要反馈的,因此随便传一个值就可以,这里传入feedbackGeneric。最后canRetrieveWindowContent指定是否允许我们的程序读取窗口中的节点和内容,必须写true

然后我们还要在代码中新建一个监听安装程序的 Service,继承自 AccessibilityService

​public class SmartInstallAPKAccessibilityService extends AccessibilityService {

    private static final String TAG = "[SmartInstallAPKAccessibilityService]";
    private Map<Integer, Boolean> handleMap = new HashMap<>();

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo nodeInfo = event.getSource();
        if (nodeInfo != null) {
            int eventType = event.getEventType();
            if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ||
                    eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                if (handleMap.get(event.getWindowId()) == null) {
                    boolean handled = iterateNodesAndHandle(nodeInfo);
                    if (handled) {
                        handleMap.put(event.getWindowId(), true);
                    }
                }
            }

        }
    }

    @Override
    public void onInterrupt() {

    }

    //遍历节点,模拟点击安装按钮
    private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            int childCount = nodeInfo.getChildCount();
            if ("android.widget.Button".equals(nodeInfo.getClassName())) {
                String nodeCotent = nodeInfo.getText().toString();
                Log.d(TAG, "content is: " + nodeCotent);
                if ("安装".equals(nodeCotent)
//                        || "完成".equals(nodeCotent)
                        || "确定".equals(nodeCotent)
                        || "打开".equals(nodeCotent)) {
                    nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return true;
                }
            }
            //遇到ScrollView的时候模拟滑动一下
            else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
            for (int i = 0; i < childCount; i++) {
                AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
                if (iterateNodesAndHandle(childNodeInfo)) {
                    return true;
                }
            }
        }
        return false;
    }
}

这里的代码含义是,每当有窗口活动时,就会触发 onAccessibilityEvent() 方法,我们根据传入的 AccessibilityEvent 参数来判断当前事件的类型。我们只需要监听TYPE_WINDOW_CONTENT_CHANGEDTYPE_WINDOW_STATE_CHANGED这两种事件就可以了,因为在整个安装过程中,这两个事件必定有一个会被触发。当然也有两个同时都被触发的可能,那么为了防止二次处理的情况,这里我们使用了一个Map集合来过滤掉重复事件。

然后通过iterateNodesAndHandle方法来进行当前界面节点的判断,如果是按钮节点,我们就判断按钮上的文字是不是安装完成确定打开这几项,如果是,就进行模拟点击。(完成打开不能同时在代码中进行判断,如果你安装完软件不想打开软件,就保留完成。反之则保留打开) 。

之后还要在清单文件中注册该服务,

<service
    android:name=".SmartInstallAPKAccessibilityService"
    android:label="智能安装 APK"
    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>

最后在 Activity 中写使用逻辑,

private Button mBtnOpenAssist;
private Button mBtnSmartInstall;

private String mAPKPath;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    initView();
}

private void initView() {
    mBtnOpenAssist = (Button) findViewById(R.id.btn_open_assist);
    mBtnOpenAssist.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View pView) {
            Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
            startActivity(intent);
        }
    });

    mBtnSmartInstall = (Button) findViewById(R.id.btn_smart_install);
    mBtnSmartInstall.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View pView) {
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
            //intent.setType(“image/*”);//选择图片
            //intent.setType(“audio/*”); //选择音频
            //intent.setType(“video/*”); //选择视频 (mp4 3gp 是android支持的视频格式)
            //intent.setType(“video/*;image/*”);//同时选择视频和图片
            intent.setType("*/*");//无类型限制
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            startActivityForResult(intent, 1);
        }
    });
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == Activity.RESULT_OK) {
        Uri uri = data.getData();
        if ("file".equalsIgnoreCase(uri.getScheme())) {//使用第三方应用打开
            mAPKPath = uri.getPath();
            Toast.makeText(this, mAPKPath, Toast.LENGTH_SHORT).show();
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {//4.4以后
            mAPKPath = getPath(this, uri);
            Toast.makeText(this, mAPKPath, Toast.LENGTH_SHORT).show();
        } else {//4.4以下下系统调用方法
            mAPKPath = getRealPathFromURI(uri);
            Toast.makeText(MainActivity.this, mAPKPath, Toast.LENGTH_SHORT).show();
        }
        doInstallAPK(new File(mAPKPath));
    }
}

//安装程序
private void doInstallAPK(File pFile) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setDataAndType(Uri.fromFile(pFile), "application/vnd.android.package-archive");
    startActivity(intent);
}

逻辑很简单,就是定义两个按钮,一个是去开启辅助功能,一个是选择 APK 文件进行安装(注意,这里只是核心代码,全部代码见文末链接)。

最后,看一下实际效果:


实际效果.gif

符合我们的预期。

然后这里还有个地方要注意,因为 Android 系统被各家手机厂商定制的花样百出,所以不一定所有手机的安装程序都叫com.android.packageinstaller(好像魅族就不是),所以这个还要自己排一下坑。

参考:
Android静默安装实现方案,仿360手机助手秒装和智能安装功能
Android 静默安装和智能安装的实现方法

代码:
Github 地址

Android 笔记