优雅的解决安装Apk时的未知来源安装权限问题

934 阅读2分钟

问题

在Android 8.0 后增加了未知来源安装权限,需要跳到App设置页面去打开权限,需要进行兼容。

否则可能会出现打开未知来源安装权限后,App又检测更新,重新下载的问题。

这里我们以一般的启动App登录页为例:

  1. 启动登录页时onCreate()进行升级检测,检测到升级弹窗提醒用户
  2. 用户点击确认升级,进行下载
  3. 下载完成后,如果没有打开未知来源安装权限,需要先打开权限后调起安装程序进行安装

这里问题主要出现在步骤3,跳到设置页面修改权限的时候,有可能会导致App的重建(不同机型的策略不同),导致后续的调起安装程序进行安装没有执行,而是又从onCrate() 开始执行,从而又提示一次下载。

解决方式

主要为2个部分

1. 设置App修改未知来源安装权限后的回调

通过startActivityForResult() 去调起设置页,然后在onActivityResult 处理回调内容。

这里需要注意,虽然是同一个App,但是重建前后并不是同一个进程,如果什么都不处理,重建前的变量内容都会丢失。 于是就有了第2个部分

2. 获取重建前获取到的App升级信息

就像我们平时处理低内存的重建一样,需要保存的状态,可以通过onSaveInstanceState 去保存,然后再onRestoreInstanceState取出

一般是第步骤1得到的是否要强制升级等升级信息和步骤2得到的apk安装包

实现

这里我们通过Activity Result API 替代startActivityForResult,通过SavedStateHandle 替代onSaveInstanceState 从而减小对项目的侵入。


import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel

/**
 * @param activity
 * @param notPermit 用户没有打开权限时的回调
 */
class InstallApkHelper(
    private val activity: ComponentActivity,
    notPermit: (helper: InstallApkHelper, apk: Uri, info: Bundle?) -> Unit
) {
    init {
        check(activity.lifecycle.currentState < Lifecycle.State.STARTED) { "Must be instantiated before onStart" }
    }

    private val storeModel by activity.viewModels<StoreModel>()
    private val launch =
        activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            val apk = storeModel.getApk() ?: return@registerForActivityResult
            if (canRequestPackageInstalls(activity)) {
                installApk(activity, apk)
            } else {
                notPermit(this, apk, storeModel.getInfo())
            }
        }

    /**
     * 安装apk
     * @param apk 安装包uri
     * @param
     */
    fun upgrade(apk: Uri, info: Bundle? = null) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !canRequestPackageInstalls(activity)) {
            storeModel.set(apk, info)
            launch.launch(
                Intent(
                    Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
                    Uri.parse("package:${activity.packageName}")
                )
            )
            return
        }
        installApk(activity, apk)
    }


    class StoreModel(private val handle: SavedStateHandle) : ViewModel() {
        fun getApk(): Uri? = handle.get<Uri>(STORE_KEY_APK)
        fun getInfo(): Bundle? = handle.get<Bundle>(STORE_KEY_INFO)
        fun set(apk: Uri, info: Bundle?) {
            handle.set(STORE_KEY_APK, apk)
            if (info != null) {
                handle.set(STORE_KEY_INFO, info)
            }
        }
    }

    companion object {
        private const val STORE_KEY_APK = "apkUri"
        private const val STORE_KEY_INFO = "info"
        private fun canRequestPackageInstalls(context: Context) =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.packageManager.canRequestPackageInstalls() else true

        private fun installApk(context: Context, apk: Uri): Boolean {

            return kotlin.runCatching {
                val intent = Intent(Intent.ACTION_VIEW)
                intent.setDataAndType(apk, "application/vnd.android.package-archive")

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                }
                context.startActivity(intent)
            }.isSuccess

        }
    }
}

使用

class MyLoginActivity : FragmentActivity() {
    private val helper = InstallApkHelper(this) { helper, apk, info ->
        AlertDialog.Builder(this)
            .setMessage("您没有打开未知来源安装权限")
            .setPositiveButton("去打开") { d, _ ->
                d.dismiss()
                helper.upgrade(apk, info)
            }
            .setNegativeButton("取消安装") { d, _ ->
                d.dismiss()
            }
            .show()
    }

    fun doUpgrade(file: File, info: UpgradeInfo) {

        val uri = getUriFromFile(file)
        val info = Bundle().apply {
            putSerializable("info", info)
        }
        helper.upgrade(uri, info)
    }
}


总结

本文对App安装时的申请未知来源安装权限导致的重建提供了一种解决方案

利用Activity Result API和SavedStateHandle 实现了一个帮助类,简化App安装。