问题
在Android 8.0 后增加了未知来源安装权限,需要跳到App设置页面去打开权限,需要进行兼容。
否则可能会出现打开未知来源安装权限后,App又检测更新,重新下载的问题。
这里我们以一般的启动App登录页为例:
- 启动登录页时onCreate()进行升级检测,检测到升级弹窗提醒用户
- 用户点击确认升级,进行下载
- 下载完成后,如果没有打开未知来源安装权限,需要先打开权限后调起安装程序进行安装
这里问题主要出现在步骤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安装。