美文网首页李某人觉得值得回味的爱上AndroidAndroid学习
Android APK 省心安装 —— 眼睁睁地看着它完成一切

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

作者: GinkWang | 来源:发表于2018-03-25 00:23 被阅读1678次

    项目的一个新需求。因为我们现在做的是一个服务于公共的产品,实施人员也不一定及时地去维护。所以为了方便,我们想要 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 地址

    相关文章

      网友评论

      • 杨晓是大V:谢谢分享
      • 有点健忘:我试着监听游戏,结果发现游戏布局就是3个帧布局,无语了,啥也监听不到。本来想着弄个自动做任务的。。。看来没戏了。游戏应该都是画出来的,按钮都是画出来的,这要执行个点击事件也不成,谁有思路啊,有做过游戏的吗
      • 有点健忘:嗯嗯,逻辑和以前的红包助手一样,没仔细研究,都是看个大概:smile:
        GinkWang:@有点健忘 是这么回事😁
      • 白小先生1:可是应用每次重启进程,都需要重新设置辅助功能?除了进程保活,有其他方法使辅助功保持开启不
        GinkWang:@深漂臊年 这个好像是,有空我研究一下。有方案时共同探讨

      本文标题:Android APK 省心安装 —— 眼睁睁地看着它完成一切

      本文链接:https://www.haomeiwen.com/subject/pkoccftx.html