Files
assistant-android/app/src/main/java/com/gh/common/util/PackageInstaller.kt

361 lines
13 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.gh.common.util
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.gh.common.dialog.InstallPermissionDialogFragment
import com.gh.common.xapk.XapkInstallReceiver
import com.gh.common.xapk.XapkInstaller
import com.gh.download.server.BrowserInstallHelper
import com.gh.gamecenter.BuildConfig
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.utils.MD5Utils
import com.gh.gamecenter.core.utils.ToastUtils
import com.gh.vspace.VHelper
import com.halo.assistant.HaloApp
import com.lightgame.download.DownloadEntity
import com.lightgame.utils.AppManager
import com.lightgame.utils.Utils
import java.io.File
// TODO 将弹窗改成以责任链模式来处理
object PackageInstaller {
private val listeners = mutableListOf<OnInstallListener>()
private val packageInstallerPackageName = arrayOf(
"com.android.packageinstaller",
"com.miui.packageinstaller",
"com.google.android.packageinstaller",
"com.samsung.android.packageinstaller",
"com.lenovo",
"com.zhuoyi"
)
/**
* 为了兼容java代码
*/
@JvmStatic
fun install(context: Context, downloadEntity: DownloadEntity?) {
downloadEntity?.let { install(context, downloadEntity, showUnzipToast = true, ignoreAsVGame = false) }
}
/**
* 启动安装应用程序并取消通知栏安装消息
*
* 兼容所有包安装(apk,xapk),后续可加上apks
*
* 主动点击安装如果是xapk则需要toast提示
*
* @param ignoreAsVGame 是否不需要以畅玩形式安装
*
*
*/
@JvmStatic
fun install(context: Context,
downloadEntity: DownloadEntity,
showUnzipToast: Boolean,
ignoreAsVGame: Boolean,
) {
val pkgPath = downloadEntity.path
val isXapk = XapkInstaller.XAPK_EXTENSION_NAME == pkgPath.getExtension()
val isDownloadAsVGame = downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE) == Constants.VGAME
|| downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE) == Constants.DUAL_DOWNLOAD_VGAME
val currentActivity = AppManager.getInstance().currentActivity() ?: return
if (!ignoreAsVGame && isDownloadAsVGame) {
VHelper.install(currentActivity, downloadEntity)
return
}
// 已知问题
// 1. 此处可能遇到 activity 是 WXEntryActivity因为 WXEntryActivity 不是 AppCompatActivity 调不起弹窗
// 2. 当 activity 全部出栈,但是应用还在下载游戏,下载完会唤不起安装
if (currentActivity is AppCompatActivity && !currentActivity.isFinishing) {
InstallPermissionDialogFragment.show(currentActivity, downloadEntity) { isFromPermissionGrantedCallback ->
// 取消状态栏下载完成的通知,若存在
downloadEntity.meta[Constants.MARK_ALREADY_TRIGGERED_INSTALLATION] = "YES"
DownloadNotificationHelper.addOrUpdateDownloadNotification(downloadEntity)
// 安卓12后的设备需要重启应用来继续解压 XAPK 文件
if (isFromPermissionGrantedCallback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val pm = context.packageManager
val intent = pm.getLaunchIntentForPackage(context.packageName)
val mainIntent = Intent.makeRestartActivityTask(intent!!.component)
context.startActivity(mainIntent)
Runtime.getRuntime().exit(0)
} else {
if (isXapk) {
XapkInstaller.install(context, downloadEntity, showUnzipToast)
} else {
install(context, downloadEntity.isPlugin, downloadEntity.path, downloadEntity)
}
}
}
}
}
/**
* 启动安装应用程序
*
* 除非特殊情况,请勿使用此方法进行应用安装操作
* 除非你已经确定该文件一定是Apk
*
* @param downloadEntity 仅用来记录日志用,非必须可为空
*/
@JvmStatic
fun install(context: Context, isPluggin: Boolean = false, pkgPath: String?, downloadEntity: DownloadEntity? = null) {
if (pkgPath.isNullOrEmpty()) {
ToastUtils.toast("下载文件异常")
return
}
val packageName = downloadEntity?.packageName ?: PackageUtils.getPackageNameByPath(context, pkgPath)
packageName?.let {
PackageChangeHelper.addInstallPendingPackage(packageName)
}
try {
// 判断是否需要使用浏览器来进行安装
if (BrowserInstallHelper.isUseBrowserToInstallEnabled()
&& BrowserInstallHelper.shouldUseBrowserToInstall()
) {
BrowserInstallHelper.downloadFile(pkgPath)
return
}
if (PackageUtils.isCanLaunchSetup(context, pkgPath)) {
installWithPureModeHandled(
context,
pkgPath,
downloadEntity?.packageName,
downloadEntity?.gameId ?: "unknown",
downloadEntity?.name ?: "unknown",
downloadEntity?.categoryChinese ?: "unknown"
)
} else {
if (isPluggin) {
DialogHelper.showPluginDialog(
context,
pluginDesc = downloadEntity?.pluginDesc,
gameId = downloadEntity?.gameId ?: "",
gameName = downloadEntity?.name ?: "",
gameType = downloadEntity?.categoryChinese ?: "",
platform = downloadEntity?.platform ?: ""
) {
uninstall(context, pkgPath)
}
} else {
// 非插件化的同包名不同签名冲突
DialogHelper.showSignatureConflictDialog(
context,
gameId = downloadEntity?.gameId ?: "",
gameName = downloadEntity?.name ?: "",
gameType = downloadEntity?.categoryChinese ?: ""
) {
uninstall(context, pkgPath)
}
}
}
} catch (e: Exception) {
Utils.toast(context, e.message)
}
}
/**
* 处理纯净模式后的安装
*/
private fun installWithPureModeHandled(
context: Context,
pkgPath: String,
pkgName: String?,
gameId: String,
gameName: String,
gameType: String,
) {
PureModeHelper.handlePureModeIfNeeded(context, gameId, gameName, gameType) {
install(context, pkgPath, pkgName)
}
}
/**
* 最终执行安装的方法
*/
private fun install(context: Context, pkgPath: String, pkgName: String?) {
HaloApp.put(Constants.LAST_INSTALL_GAME, pkgPath)
val installIntent = getInstallIntent(context, pkgPath)
context.startActivity(installIntent)
dispatchOnInstallListener(pkgPath, pkgName)
}
fun installMultiple(
context: Context,
pkgName: String,
pkgPath: String,
sessionId: Int = -1
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return
}
val installer = context.packageManager.packageInstaller
val session = installer.openSession(sessionId)
// 监听安装回调的组件可以是Activity、Service或者是BroadcastReceiver
val intent = Intent(context, XapkInstallReceiver::class.java)
.also {
it.putExtra(XapkInstallReceiver.KEY_PACKAGE_PATH, pkgPath)
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
val pendingIntent = PendingIntent.getActivity(context, sessionId, intent, flags)
// 提交数据流并执行安装
session.commit(pendingIntent.intentSender)
dispatchOnInstallListener(pkgName, pkgPath)
}
fun registerOnInstallListener(listener: OnInstallListener) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
}
fun unregisterOnInstallListener(listener: OnInstallListener) {
listeners.remove(listener)
}
private fun dispatchOnInstallListener(pkgPath: String, pkgName: String?) {
for (listener in listeners) {
listener.onInstalling(pkgName, pkgPath)
}
}
/**
* 获取启动安装意图
*
* 除非特殊情况,请勿使用此方法进行应用安装操作
* 除非你已经确定该文件一定是Apk且该次安装无需检查签名情况
*/
@JvmStatic
fun getInstallIntent(context: Context, path: String): Intent {
var uri = Uri.fromFile(File(path))
val installIntent =
if (BrowserInstallHelper.isUseBrowserToInstallEnabledWithPackageMatched()) {
Intent(Intent.ACTION_INSTALL_PACKAGE)
} else {
Intent(Intent.ACTION_VIEW)
}
if ("smartisan" == Build.MANUFACTURER) {
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, File(path))
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
installIntent.setDataAndType(uri, "application/vnd.android.package-archive")
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
// 应用内更新不加 FLAG_ACTIVITY_NEW_TASK 在模拟器上会出现安装完成后安装界面也一并消失的类似闪退的表现
// Application 上下文就更不用说了
val pkgName = PackageUtils.getPackageNameByPath(context, path)
if (pkgName == context.packageName || context !is Activity) {
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
installIntent.setDataAndType(uri, "application/vnd.android.package-archive")
}
// 优选系统的安装器(遇到 Exception 就回落到正常的安装器)
try {
updateSystemInstallerIfAvailable(context, installIntent)
} catch (ignored: Exception) {
// ignored
}
InstallUtils.getInstance()
.addInstall(PackageUtils.getPackageNameByPath(context, path))
return installIntent
}
/**
* 查找并设置系统安装器作为安装 intent 的包名
*/
private fun updateSystemInstallerIfAvailable(context: Context, intent: Intent) {
for (rf in context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)) {
val targetPackageName = packageInstallerPackageName.firstOrNull { rf.activityInfo.packageName.contains(it) }
if (!targetPackageName.isNullOrEmpty()) {
intent.setPackage(targetPackageName)
break
}
}
}
/**
* 卸载应用
*/
@JvmStatic
fun uninstall(context: Context, path: String) {
// TODO 根据 path 获取包名可能出现获取不到的情况,看能不能改成直接用包名!
val packageName = PackageUtils.getPackageNameByPath(context, path)
uninstallForPackageName(context, packageName)
}
/**
* 根据包名卸载应用
*/
@JvmStatic
fun uninstallForPackageName(context: Context, pkn: String?) {
if (pkn.isNullOrEmpty()) return
PackageChangeHelper.addUninstallPendingPackage(pkn)
val uninstallIntent = Intent()
uninstallIntent.action = Intent.ACTION_DELETE
uninstallIntent.addCategory(Intent.CATEGORY_DEFAULT)
if (context !is Activity) {
uninstallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
uninstallIntent.data = Uri.parse("package:$pkn")
InstallUtils.getInstance().addUninstall(pkn)
context.startActivity(uninstallIntent)
}
@JvmStatic
fun getDownloadPathWithId(id: String, format: String?): String {
return FileUtils.getDownloadPath(
HaloApp.getInstance().application,
id + "." + getFileSuffixByFormat(format)
)
}
private fun getFileSuffixByFormat(format: String?): String {
return if (format == Constants.XAPK_FORMAT
|| format == Constants.XAPK_APKS_FORMAT) {
XapkInstaller.XAPK_EXTENSION_NAME
} else {
"apk"
}
}
@JvmStatic
fun createDownloadId(gameName: String?): String {
return MD5Utils.getContentMD5(gameName + "_" + System.currentTimeMillis())
}
interface OnInstallListener {
fun onInstalling(packageName: String?, packagePath: String)
}
}