Qt for Android(十五) —— Android Q 适配之静默安装APP

291 阅读2分钟

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

背景

  因为项目的特殊性,客户有静默升级的需求。比如说有新版本了,要在不干扰的情况下进行新版本的自安装,完成后自动打开。前提是我们的板卡是有root权限的。在Android Q之前,我们用的方式是通过adb命令:pm install -r 实现APK的静默安装。但是在Android Q之后,哪怕拥有了root权限,此方案也行不通了。

新方案

注:新方案要求APP具有系统签名。

首先添加权限:

<!--静默安装权限-->
<uses-permission
    android:name="android.permission.INSTALL_PACKAGES"
    tools:ignore="ProtectedPermissions" />
<!--应用卸载权限-->
<uses-permission android:name="permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
    android:name="android.permission.DELETE_PACKAGES"
    tools:ignore="ProtectedPermissions" />
    <!--读写外部存储权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!--允许装载和卸载文件系统权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--注意android Q之后记得在Application标签下添加requestLegacyExternalStorage标识-->
<application ... android:requestLegacyExternalStorage="true"> </application>
 public void silenceInstall(String packagePath) {
        execSuCmd("pm install -r " + packagePath);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
        {
            install2Q(packagePath);
        }else{
            execSuCmd("pm install -r " + packagePath);
        }
    }
    
 // 适配android9的安装方法。
    private void install2Q(String apkFilePath) {
        File apkFile = new File(apkFilePath);
        PackageInstaller packageInstaller = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            packageInstaller = appContext.getPackageManager().getPackageInstaller();
        }
        PackageInstaller.SessionParams sessionParams
                = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sessionParams = new PackageInstaller.SessionParams(PackageInstaller
                    .SessionParams.MODE_FULL_INSTALL);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sessionParams.setSize(apkFile.length());
        }
         int sessionId = createSession(packageInstaller, sessionParams);
        if (sessionId != -1) {
            boolean copySuccess = copyInstallFile(packageInstaller, sessionId, apkFilePath);
            if (copySuccess) {
                execInstallCommand(packageInstaller, sessionId);
            }
        }
    }
      private int createSession(PackageInstaller packageInstaller,
                              PackageInstaller.SessionParams sessionParams) {
        int sessionId = -1;
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                sessionId = packageInstaller.createSession(sessionParams);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sessionId;
    }
    //通过文件流传输apk
      @SuppressLint("RestrictedApi")
    private boolean copyInstallFile(PackageInstaller packageInstaller,
                                    int sessionId, String apkFilePath) {
        InputStream in = null;
        OutputStream out = null;
        PackageInstaller.Session session = null;
        boolean success = false;
        try {
            File apkFile = new File(apkFilePath);
            session = packageInstaller.openSession(sessionId);
 out = session.openWrite("base.apk", 0, apkFile.length());
            in = new FileInputStream(apkFile);
            int total = 0, c;
             byte[] buffer = new byte[65536];
            while ((c = in.read(buffer)) != -1) {
                total += c;
                out.write(buffer, 0, c);
            }
            session.fsync(out);
             Log.i("AmainActivity", "streamed " + total + " bytes");
            success = true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeQuietly(out);
            closeQuietly(in);
            closeQuietly(session);
        }
        return success;
    }
    
    //执行安装并通知安装结果
        @SuppressLint("RestrictedApi")
    private void execInstallCommand(PackageInstaller packageInstaller, int sessionId) {
        PackageInstaller.Session session = null;
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                session = packageInstaller.openSession(sessionId);
            }
            Intent intent = new Intent("");
             PendingIntent pendingIntent = PendingIntent.getBroadcast(appContext,
                    1, intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                session.commit(pendingIntent.getIntentSender());
            }
        } catch (IOException e) {
         e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                closeQuietly(session);
            }
        }
    }

此方案的原理是通过文件流传输apk,发送给android 9后支持的包安装器,最后通过广播触发安装动作。当然支持安装我们也支持静默卸载。

public void uninstall(String packageName) {
    Intent broadcastIntent = new Intent(this, InstallResultReceiver.class);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1,
            broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
    packageInstaller.uninstall(packageName, pendingIntent.getIntentSender());
}