361 lines
13 KiB
Kotlin
361 lines
13 KiB
Kotlin
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)
|
||
}
|
||
|
||
} |