Files
assistant-android/app/src/main/java/com/gh/vspace/VHelper.kt
2024-03-12 09:59:48 +08:00

2093 lines
80 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.vspace
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteDiskIOException
import android.database.sqlite.SQLiteFullException
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import android.view.View
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.*
import com.g00fy2.versioncompare.Version
import com.gh.ad.AdDelegateHelper
import com.gh.common.constant.Config
import com.gh.common.exposure.ExposureUtils
import com.gh.common.history.HistoryHelper
import com.gh.common.util.*
import com.gh.common.util.NewFlatLogUtils
import com.gh.download.DownloadManager
import com.gh.download.PackageObserver
import com.gh.download.simple.DownloadListener
import com.gh.download.simple.DownloadMessageHandler
import com.gh.download.simple.SimpleDownloadManager
import com.gh.gamecenter.R
import com.gh.gamecenter.SplashScreenActivity
import com.gh.gamecenter.common.base.GlobalActivityManager
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.eventbus.EBReuse
import com.gh.gamecenter.common.exposure.meta.MetaUtil
import com.gh.gamecenter.common.retrofit.BiResponse
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.AppExecutor
import com.gh.gamecenter.core.runOnIoThread
import com.gh.gamecenter.core.runOnUiThread
import com.gh.gamecenter.core.utils.*
import com.gh.gamecenter.entity.*
import com.gh.gamecenter.eventbus.EBPackage
import com.gh.gamecenter.feature.entity.ApkEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.InstallGameEntity
import com.gh.gamecenter.manager.PackagesManager
import com.gh.gamecenter.packagehelper.PackageRepository
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.vspace.db.VGameDatabase
import com.gh.vspace.db.VGameEntity
import com.gh.vspace.gapps.GAppsDownloadDialogFragment
import com.halo.assistant.HaloApp
import com.lg.download.DownloadError
import com.lg.download.DownloadStatus
import com.lg.download.httpclient.DefaultHttpClient
import com.lg.ndownload.DownloadConfigBuilder
import com.lg.vspace.VaApp
import com.lg.vspace.VirtualAppManager
import com.lg.vspace.bridge.BuildConfig
import com.lg.vspace.plugin.host.PluginFileUtils
import com.lg.vspace.plugin.host.PluginHelper
import com.lg.vspace.remote.BinderPool
import com.lg.vspace.remote.listener.RemoteConnectListener
import com.lg.vspace.remote.model.AppInstallerInfo
import com.lg.vspace.remote.model.VGameInstallerParams
import com.lg.vspace.remote.model.VGameInstallerResult
import com.lightgame.download.DownloadEntity
import com.lightgame.utils.AppManager
import com.lightgame.utils.Utils
import com.va.host.HostUtils
import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
object VHelper {
// 畅玩游戏是否已被使用过
private const val KEY_V_IS_USED = "v_is_used"
private const val KEY_LAST_PLAYED_TIME = "last_played_time"
private const val KEY_LAST_ALERT_64_UPDATE_URL = "last_alert_64_update_url"
private const val KEY_LAST_ALERT_32_UPDATE_URL = "last_alert_32_update_url"
private const val KEY_TOTAL_PLAYED_TIME = "total_played_time"
private const val G_GMS_PACKAGE_NAME = "com.google.android.gms"
private const val G_VENDING_PACKAGE_NAME = "com.android.vending"
private const val G_GSF_PACKAGE_NAME = "com.google.android.gsf"
const val KEY_REQUIRED_G_APPS = "required_g_apps"
const val G_APPS_DOWNLOAD_ID = "d673761a1dc031d40afc90d0a6efd25a"
const val G_APPS_INSTALLED_MD5 = "g_apps_installed_md5"
// 畅玩游戏位数
const val KEY_BIT = "bit"
const val LOG_TAG = "VSPACE"
const val DEFAULT_VSPACE_PACKAGENAME = "com.lg.vspace"
const val VSPACE_32BIT_PACKAGENAME = "$DEFAULT_VSPACE_PACKAGENAME.addon"
const val VSPACE_32BIT_NEW_PACKAGENAME = com.lg.vspace.BuildConfig.EXT_PACKAGE_NAME
private val mDelegateManager by lazy { VirtualAppManager.get() }
private val mPackageInstalledLiveData by lazy { MutableLiveData<String>() }
private var mInstalledInfoList: ArrayList<AppInstallerInfo> = arrayListOf() // 已安装的列表,由畅玩服务返回
private var mLastSuccessfullyLaunchedGame: Pair<Long, String>? = null
val vGameDao by lazy { VGameDatabase.instance.vGameDao() }
private var mVGameSnapshotList = arrayListOf<VGameEntity>()
private var m64UpdateEntity: AppEntity? = null
private var m32UpdateEntity: AppEntity? = null
private var mIsInitialized = false // 是否已初始化
private var mMustInitialized = false // 是否已必须初始化
private var mIsServiceConnected = false // AIDL 服务是否成功连接,不可作为 AIDL 仍然可用的依据
private var mShouldLaunchGameAfterInstallation = true // 是否需要在安装完成后启动游戏的开关
// 当前正卡在安装中的 VA 游戏,避免重复调用安装
private val mInstallingVaPathSet by lazy { Collections.synchronizedSet(hashSetOf<String>()) }
// 畅玩服务连接成功前挂起的动作,连接成功后执行
private var mPendingActionList = CopyOnWriteArrayList<() -> Unit>()
// 启动畅玩失败次数统计
private var mStartServiceFailureCount = 0
// 因为显示下载畅玩弹窗而处于挂起状态的游戏回调,供安装完畅玩组件后自动下载用
private var mPendingDownloadCallback: (() -> Unit)? = null
// 供安装完畅玩组件判断畅玩游戏位数使用
private var mBit = ""
// 批量安装的 map k: 包名, v: 文件
private var mBatchInstallMap = hashMapOf<String, File>()
private var mTriggerPackageName: String = ""
// 批量安装的监听
private var mBatchInstallListener: ((isSuccess: Boolean, interrupted: Boolean) -> Unit)? = null
// 批量安装失败的统计
private var mBatchInstallFailedCount = 0
// 应用是否可见
private var mIsAppVisible = false
// 下次应用可见时是否需要尝试重连
private var mShouldReConnectOnVisible = false
// 是否已经尝试过重连
private var mHasAlreadyTriedReConnect = false
// 临时的用来临时匹配安装完成时包名对应的游戏 ID 的 Map
private var mTempPackageNameAndGameIdMap = hashMapOf<String, String>()
const val callSiteOnInstallComplet = 1
const val callSiteUninstall = 2
val callSite = PublishSubject.create<Int>()
private val mPackageObserver by lazy {
PackageObserver.PackageChangeListener {
val vaConfig = Config.getVSettingEntity()?.va ?: return@PackageChangeListener
val isVSpace32 = it.packageName == vaConfig.arch32?.packageName
val isVSpace64 = it.packageName == vaConfig.arch64?.packageName
if (!isVSpace64 && !isVSpace32) return@PackageChangeListener
// 需要安装32位畅玩时跳过64位畅玩安装成功的回调
val skip64VSpaceInstalled = mBit == "32" && isVSpace64 && !PackageUtils.isInstalledFromAllPackage(
HaloApp.getInstance(),
vaConfig.arch32?.packageName
)
if (it.type == EBPackage.TYPE_INSTALLED) {
SensorsBridge.trackEvent("HaloFunInstallDone", "space_schema_type", if (isVSpace32) "32位" else "64位")
if (isVSpace32) {
SensorsBridge.trackEvent("HaloFunExpandInstallDone")
}
if (skip64VSpaceInstalled) return@PackageChangeListener
// 帮用户启动因为没有安装畅玩组件而没法进行的下载任务
// 因为有可能回调时还在后台,但还要弹窗什么的,所以可能会闪退?先 try catch 一下看看
try {
mPendingDownloadCallback?.invoke()
} catch (e: Exception) {
SentryHelper.onEvent("AUTO_DOWNLOAD_VGAME_ERROR", "error_digest", e.localizedMessage)
}
mPendingDownloadCallback = null
mBit = ""
}
if (!isVIsUsed()) return@PackageChangeListener
if (it.type == EBPackage.TYPE_INSTALLED) {
if (skip64VSpaceInstalled) return@PackageChangeListener
// 即时调用大概率调不起来,我也不知道为什么,只能延迟大法了
AppExecutor.uiExecutor.executeWithDelay({
connectService()
}, 500)
} else if (it.type == EBPackage.TYPE_UNINSTALLED) {
if (isVSpace64) {
// 执行卸载逻辑
mIsServiceConnected = false
mInstalledInfoList.clear()
}
} else if (it.type == EBPackage.TYPE_REPLACED) {
connectService()
}
}
}
/**
* 初始化
*/
@SuppressLint("CheckResult")
@JvmStatic
fun init(context: Context) {
Completable.fromAction {
refreshVGameSnapshot()
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
onInit(context)
}
}
private fun onInit(context: Context) {
if (!mMustInitialized) {
mMustInitialized = true
VArchiveHelper.init()
val allGames = mVGameSnapshotList.map { it.packageName }.toArrayList()
if (allGames.isNotEmpty()) {
PackageRepository.addInstalledGames(allGames, true)
}
}
if (isVGameOn()) {
if (!mIsInitialized) {
mIsInitialized = true
if (isVIsUsed()) {
connectService(shouldCheckUpdate = true)
}
PackageObserver.registerPackageChangeChangeListener(mPackageObserver)
// 注册应用可见事件监听
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
mIsAppVisible = true
if (mShouldReConnectOnVisible) {
mShouldReConnectOnVisible = false
connectService(shouldConnectSilently = true)
}
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
mIsAppVisible = false
}
})
}
checkVspaceUpdate(context)
}
}
private fun checkVspaceUpdate(context: Context) {
val config = Config.getVSettingEntity()?.va
if (config?.arch64 != null
&& m64UpdateEntity == null
&& PackageUtils.isInstalledFromAllPackage(context, config.arch64.packageName)
) {
// 检查畅玩助手 64 位组件更新
getVSpaceUpdate(config.arch64, true)
}
if (config?.arch32 != null
&& m32UpdateEntity == null
&& PackageUtils.isInstalledFromAllPackage(context, config.arch32.packageName)
) {
// 检查畅玩助手 32 位组件更新
getVSpaceUpdate(config.arch32, false)
}
}
/**
* 手动刷新畅玩游戏快照列表
*/
@WorkerThread
@Synchronized
fun refreshVGameSnapshot() {
try {
mVGameSnapshotList = ArrayList(vGameDao.getAll())
} catch (e: SQLiteDiskIOException) {
ToastUtils.toast("磁盘出现异常,请稍后再试")
e.printStackTrace()
}
}
/**
* 如果断连了,尝试静静重连
*/
fun reconnectServiceIfNeeded() {
if (!mIsServiceConnected) {
connectService(shouldConnectSilently = true)
}
}
/**
* 连接服务
*/
fun connectService(
shouldCheckUpdate: Boolean = false,
shouldConnectSilently: Boolean = false,
callbackClosure: (() -> Unit)? = null
) {
Utils.log(LOG_TAG, "尝试连接 V 服务")
val isOldCwInstalled = PackageUtils.isInstalledFromAllPackage(
HaloApp.getInstance().applicationContext,
BuildConfig.AIDL_SERVER_PACKAGE_NAME
)
if (!isOldCwInstalled) {
Utils.log(LOG_TAG, "分体式畅玩没有安装")
return
}
if (!shouldConnectSilently) {
toastIfServiceConnectTimeout()
}
if (callbackClosure != null) {
mPendingActionList.add(callbackClosure)
}
mDelegateManager.connectService(object : RemoteConnectListener {
override fun onServiceConnectionSuccessed() {
Utils.log(LOG_TAG, "V 服务连接成功")
mStartServiceFailureCount = 0
mShouldReConnectOnVisible = false
mHasAlreadyTriedReConnect = false
onConnectionCompleted(shouldCheckUpdate = shouldCheckUpdate)
}
override fun onServiceConnectionFailed(failCode: Int) {
Utils.log(LOG_TAG, "V 服务连接失败")
// 已断开
mIsServiceConnected = false
// 断开时清空正在安装列表
mInstallingVaPathSet.clear()
if (failCode == BinderPool.CONNECT_STATE_NOT_INSTALLED) {
Utils.log(LOG_TAG, "未安装畅玩助手")
}
VirtualAppManager.get().resetService()
if (!mHasAlreadyTriedReConnect) {
Utils.log(LOG_TAG, "执行断线重连")
mHasAlreadyTriedReConnect = true
if (mIsAppVisible) {
connectService(shouldConnectSilently = true)
} else {
mShouldReConnectOnVisible = true
}
}
}
override fun onStartServiceFailed(failCode: Int) {
Utils.log(LOG_TAG, "V 服务启动失败")
if (++mStartServiceFailureCount % 3 == 0) {
val config = Config.getVSettingEntity()?.va
runOnUiThread {
PackageLauncher.launchApp(HaloApp.getInstance(), packageName = config?.arch64?.packageName)
}
}
}
})
}
/**
* 连接成功回调
* @param shouldCheckUpdate 是否需要检查畅玩游戏更新
*/
fun onConnectionCompleted(shouldCheckUpdate: Boolean = false) {
mIsServiceConnected = true
SPUtils.setBoolean(KEY_V_IS_USED, true)
runOnIoThread {
// 更新已安装列表
updateInstalledList()
//执行等待连接前的事件,注意是在子线程
for (action in mPendingActionList) {
action.invoke()
mPendingActionList.remove(action)
}
if (shouldCheckUpdate) {
checkUpdateViaPackageRepository()
}
}
}
@JvmStatic
fun postOnInitialized(callback: () -> Unit) {
if (mIsServiceConnected) {
callback()
} else {
connectService(shouldCheckUpdate = false, shouldConnectSilently = false, callbackClosure = callback)
}
}
private fun toastIfServiceConnectTimeout(timeout: Long = 3000L) {
if (!mIsServiceConnected) {
AppExecutor.uiExecutor.executeWithDelay({
if (!mIsServiceConnected) {
ToastUtils.toast("允许启动畅玩助手才能继续游戏哦~ ")
}
}, timeout)
}
}
/**
* 畅玩助手是否已安装
*/
fun isVSpaceInstalled(context: Context): Boolean {
return PackageUtils.isInstalledFromAllPackage(context, VirtualAppManager.AIDL_SERVER_PACKAGE_NAME)
}
/**
* 显示下载畅玩空间弹窗
*/
fun showVSpaceDialog(context: Context, gameEntity: GameEntity?) {
VSpaceDialogFragment.showDownloadDialog(
context,
appEntity64 = getVSpaceDownloadEntity(true),
appEntity32 = getVSpaceDownloadEntity(false),
gameEntity = gameEntity
)
}
/**
* 获取数据库里所有的畅玩游戏
*
* (查询数据库,请在工作线程调用)
*/
@WorkerThread
fun getAllVGame(): List<VGameEntity> = vGameDao.getAll()
/**
* 获取库里全部畅玩游戏列表的内存快照
*/
fun getAllVGameSnapshots(): ArrayList<VGameEntity> = ArrayList(mVGameSnapshotList)
fun getAllInstalledVGameEntity(): ArrayList<InstallGameEntity> = getAllVGameSnapshots().map {
InstallGameEntity().apply {
gameIcon = it.downloadEntity.icon
gameName = it.downloadEntity.getMetaExtra(Constants.GAME_NAME)
gameVersion = it.downloadEntity.versionName
packageName = it.packageName
}
}.toArrayList()
/**
* 获取内存里的畅玩游戏快照
*
* @param gameId 游戏 ID
* @param packageName 需要获取的畅玩游戏的包名
*
*/
@JvmStatic
fun getVGameSnapshot(gameId: String? = null, packageName: String? = null): VGameEntity? {
return mVGameSnapshotList.find {
it.packageName == packageName && (gameId.isNullOrEmpty() || it.downloadEntity.gameId == gameId)
}
}
/**
* 更新已安装的畅玩列表
*/
private fun updateInstalledList() {
mInstalledInfoList = getInstalledList()
}
/**
* 此语义是判断是否是安装的畅玩游戏
* 畅玩组件是否已安装此包名的畅玩游戏, 包含 1. 分体式畅玩, 2. 一体式畅玩 两个地方
* 否则应该单独针对分体式和一体式判断
*/
@JvmStatic
fun isInstalled(packageName: String?) = isInnerInstalled(packageName) || isLegacyInstalled(packageName)
/**
* 分体式畅玩是否安装了游戏
*/
@JvmStatic
fun isLegacyInstalled(packageName: String?): Boolean {
var isInstalled = mInstalledInfoList.any { it.packageName == packageName }
Utils.log(LOG_TAG, "$packageName 已安装列表里 -> $isInstalled")
// 有可能 mInstalledInfoList 获取异常,导致判断为未安装,手动调用 AIDL 再查一次
if (!isInstalled
&& mIsServiceConnected
&& mDelegateManager.isConnectAidlInterface
) {
try {
isInstalled = mDelegateManager.checkGameInstalled(packageName)
Utils.log(LOG_TAG, "手动调用 AIDL 获取安装情况 -> $isInstalled")
} catch (e: RuntimeException) {
Utils.log(LOG_TAG, "手动调用 AIDL 获取安装情况异常 ${e.localizedMessage}")
}
}
return isInstalled
}
@JvmStatic
fun isInnerInstalled(packageName: String?) =
VaApp.get().appManager.installedGamesInfo.any { it.packageName == packageName }
/**
* 启动成功,五秒内退出才显示反馈弹框
*/
fun showFeedbackDialogIfLastSuccessfulLaunchedGameExitUnexpectedly(activity: AppCompatActivity) {
if (isVGameOn()) {
val time = mLastSuccessfullyLaunchedGame?.first
val packageName = mLastSuccessfullyLaunchedGame?.second
if (activity !is SplashScreenActivity
&& time != null
&& packageName != null
&& System.currentTimeMillis() - time < 5000
&& !VFeedbackSuppressedSimpleDao().contains(packageName)
) {
getVGameSnapshot(null, packageName)?.let {
VFeedbackDialogFragment.show(activity, toGameEntity(it.downloadEntity))
mLastSuccessfullyLaunchedGame = null
}
}
}
}
/**
* 获取游戏占用的空间
*/
fun getAppOccupiedSpace(packageName: String): Long {
return if (isInnerInstalled(packageName)) {
VaApp.get().appManager.getAppOccupiedSpace(packageName)
} else {
try {
mDelegateManager.getAppOccupiedSpace(packageName)
} catch (e: Exception) {
e.printStackTrace()
0
}
}
}
/**
* 在执行 callback 前先检查组件是否已安装,是否可下载
*/
fun validateVSpaceBeforeAction(
context: Context,
packageName: String?,
gameEntity: GameEntity?,
callback: () -> Unit
) {
if (isLegacyInstalled(packageName)) {
oldCwValidateVSpaceBeforeAction(context, gameEntity, callback)
} else {
newCwValidateVspaceBeforeAction(context, gameEntity, callback)
}
}
private fun oldCwValidateVSpaceBeforeAction(context: Context, gameEntity: GameEntity?, callback: () -> Unit) {
if (showDialogIfVSpaceIsNeeded(
context,
gameEntity?.id ?: "",
gameEntity?.name ?: "",
gameEntity?.categoryChinese ?: "",
gameEntity?.gameBit ?: ""
)
) {
mBit = gameEntity?.gameBit ?: ""
mPendingDownloadCallback = callback
return
}
val vaConfig64 = Config.getVSettingEntity()?.va?.arch64
val vaConfig32 = Config.getVSettingEntity()?.va?.arch32
val installed64SpaceVersionCode = PackageUtils.getVersionCodeByPackageName(vaConfig64?.packageName)
val installed32SpaceVersionCode = PackageUtils.getVersionCodeByPackageName(vaConfig32?.packageName)
// 检查更新
val contains64Update = shouldShowVSpaceUpdate(m64UpdateEntity, installed64SpaceVersionCode)
val contains32Update = if (gameEntity?.gameBit == "32") shouldShowVSpaceUpdate(
m32UpdateEntity,
installed32SpaceVersionCode
) else false
var containsRelated64Update = false
if (contains32Update) {
// 检查关联64位是否需要更新
if (m32UpdateEntity?.relation != null) {
containsRelated64Update =
shouldShowVSpaceUpdate(m32UpdateEntity?.relation, installed64SpaceVersionCode, true)
}
}
if (contains32Update || contains64Update) {
val gameBit = gameEntity?.gameBit
val updateEntity = if (gameBit == "32" && contains32Update) m32UpdateEntity else m64UpdateEntity
val dialogType = if (updateEntity!!.isAlertEveryTime()) "强制更新" else "提示更新"
NewFlatLogUtils.logHaloFunUpdateDialogShow(
gameEntity?.id ?: "",
gameEntity?.name ?: "",
if (gameBit == "32") "32位" else "64位"
)
SPUtils.setString(
if (updateEntity.category == "32-bit") KEY_LAST_ALERT_32_UPDATE_URL else KEY_LAST_ALERT_64_UPDATE_URL,
updateEntity.url + updateEntity.alert
)
DialogHelper.showDialog(
context = context,
title = "服务工具更新提示",
content = updateEntity.content.toString(),
cancelText = "立即更新",
confirmText = "继续游戏",
cancelClickCallback = {
val appEntity64 =
if (gameBit == "32" && contains32Update && m32UpdateEntity?.relation != null) {
// 32位游戏且包含32位更新和32位关联64位更新
// 存在64位单独更新时对比32位关联的64位更新与64位单独更新推送的版本号
if (contains64Update && (m64UpdateEntity?.versionCode
?: 0) > (m32UpdateEntity?.relation?.versionCode ?: 0)
) {
// 64位单独更新版本号更高时使用64位单独更新
m64UpdateEntity
} else if (containsRelated64Update) {
// 不存在64位单独更新或32位关联64位更新版本号更高
// 对比已安装版本号和32位关联的64位更新版本号32位关联的64位更新版本号更高时使用32位关联的64位更新
m32UpdateEntity?.relation
} else {
null
}
} else if ((gameBit != "32" && contains64Update) || (gameBit == "32" && contains64Update && !contains32Update)) {
// 64位游戏且包含64位单独更新 / 32位游戏无32位更新且包含64位更新
m64UpdateEntity
} else {
null
}
val appEntity32 = if (contains32Update) updateEntity else null
NewFlatLogUtils.logHaloFunUpdateDialogClick(
dialogType,
"立即更新",
if (appEntity64 != null && appEntity32 != null) "32位&64位" else if (appEntity64 != null) "64位" else "32位"
)
VSpaceDialogFragment.showDownloadDialog(
context,
appEntity64,
appEntity32,
gameEntity,
autoDownload = true,
isUpdate = true
)
},
confirmClickCallback = {
NewFlatLogUtils.logHaloFunUpdateDialogClick(dialogType, "继续游戏", "")
callback.invoke()
},
extraConfig = DialogHelper.Config(centerTitle = true),
uiModificationCallback = {
if (updateEntity.isAlertEveryTime()) {
it.confirmTv.visibility = View.GONE
it.cancelTv.setTextColor(R.color.text_theme.toColor(context))
}
it.confirmTv.setTextColor(R.color.text_secondary.toColor(context))
}
)
return
}
callback.invoke()
}
/**
* 判断是否需要安装32位组件64位直接callback
*/
private fun newCwValidateVspaceBeforeAction(context: Context, gameEntity: GameEntity?, callback: () -> Unit) {
val bit = gameEntity?.gameBit ?: ""
if (bit == "32") {
val vaArch32 = Config.getVNewSettingEntity()?.va?.arch32
if (vaArch32 == null) {
ToastUtils.toast("畅玩助手空间暂未上线")
return
}
val new32UpdateEntity = Config.getNew32UpdateEntity()
val is32VSpaceInstalled = PackageUtils.isInstalledFromAllPackage(context, vaArch32.packageName)
if (!is32VSpaceInstalled) {
val appEntity = AppEntity()
if (new32UpdateEntity != null && (new32UpdateEntity.versionCode > vaArch32.versionCode)) {
appEntity.versionCode = new32UpdateEntity.versionCode
appEntity.version = new32UpdateEntity.version
appEntity.url = new32UpdateEntity.url
} else {
appEntity.versionCode = vaArch32.versionCode
appEntity.version = vaArch32.versionName
appEntity.url = vaArch32.url
}
VSpace32NewDialogFragment.showDownloadDialog(
context,
appEntity,
gameId = gameEntity?.id ?: "",
gameName = gameEntity?.name ?: "",
)
return
}
if (new32UpdateEntity != null && (PackageUtils.getVersionCodeByPackageName(vaArch32.packageName) < new32UpdateEntity.versionCode)) {
val dialogType = if (new32UpdateEntity.isForce) "强制更新" else "提示更新"
NewFlatLogUtils.logHaloFunUpdateDialogShow(
gameEntity?.id ?: "",
gameEntity?.name ?: "",
"32位"
)
DialogHelper.showDialog(
context = context,
title = "服务工具更新提示",
content = new32UpdateEntity.content.toString(),
cancelText = "立即更新",
confirmText = if (new32UpdateEntity.isForce) "取消" else "继续游戏",
cancelClickCallback = {
NewFlatLogUtils.logHaloFunUpdateDialogClick(
dialogType,
"立即更新",
"32位"
)
VSpaceUpdate32NewDialogFragment.showDownloadDialog(
context,
new32UpdateEntity,
gameEntity,
autoDownload = true,
isUpdate = true
)
},
confirmClickCallback = {
NewFlatLogUtils.logHaloFunUpdateDialogClick(dialogType, "继续游戏", "")
if (!new32UpdateEntity.isForce) {
callback.invoke()
}
},
extraConfig = DialogHelper.Config(centerTitle = true),
uiModificationCallback = {
if (new32UpdateEntity.isAlertEveryTime()) {
it.confirmTv.visibility = View.GONE
it.cancelTv.setTextColor(R.color.theme_font.toColor(context))
}
it.confirmTv.setTextColor(R.color.text_subtitle.toColor(context))
}
)
return
}
}
callback.invoke()
}
/**
* 获取已安装的包名列表
*/
private fun getInstalledPackageList(): ArrayList<String> {
val installedList = mInstalledInfoList
val installedPackageList = arrayListOf<String>()
for (info in installedList) {
info.packageName?.let {
installedPackageList.add(info.packageName)
}
}
Utils.log(LOG_TAG, "获取已安装包名列表 $installedPackageList")
return installedPackageList
}
/**
* 获取已安装列表信息 (每次调用都会生成一份新的副本,放心使用)
*/
@WorkerThread
fun getInstalledList(): ArrayList<AppInstallerInfo> {
var list: List<AppInstallerInfo>
try {
list = mDelegateManager.installedGamesInfo
Utils.log(LOG_TAG, "获取已安装应用数量,数量为 ${list.size}")
} catch (e: Exception) {
// 异常时使用缓存数据
list = mInstalledInfoList
Utils.log(LOG_TAG, "获取已安装应用数量异常 ${e.localizedMessage}")
}
return ArrayList(list)
}
/**
* 安装新游戏
* 正常的游戏使用这个方法来安装,内部有重试处理
*/
fun install(
context: Context,
downloadEntity: DownloadEntity,
isManualInstall: Boolean = false
) {
Utils.log(LOG_TAG, "尝试安装新游戏 ${downloadEntity.path}")
// 如果一个游戏存在旧版畅玩助手内,此次安装就是游戏更新
val isLegacyGame = isLegacyInstalled(downloadEntity.packageName)
Utils.log(LOG_TAG, "${downloadEntity.packageName} ${if (isLegacyGame) "是旧畅玩助手的游戏" else "是新畅玩组件的游戏"}")
// 更新此包名对应的 gameId Map
mTempPackageNameAndGameIdMap[downloadEntity.packageName] = downloadEntity.gameId
// 当且仅当
// 1. 全局安装完成启动游戏开关打开
// 2. 手动触发的安装
// 3. 不需要谷歌框架或者已经安装好谷歌框架
// 才启用安装完成后自动启动游戏
val shouldLaunchGameAfterInstallation: Boolean
if (isLegacyGame) {
shouldLaunchGameAfterInstallation =
mShouldLaunchGameAfterInstallation
&& (!mIsServiceConnected || isManualInstall)
&& (!isGAppsRequired(downloadEntity) || isGAppsInstalledInCwLegacy())
} else {
shouldLaunchGameAfterInstallation =
mShouldLaunchGameAfterInstallation
&& isManualInstall
&& (!isGAppsRequired(downloadEntity) || isGAppsInstalled())
}
// 恢复安装完启动游戏开关
mShouldLaunchGameAfterInstallation = true
val installClosure: () -> Unit = {
if (shouldLaunchGameAfterInstallation) {
runOnUiThread {
val gameEntity = toGameEntity(downloadEntity)
try {
context.startActivity(VSpaceLoadingActivity.getIntent(context, gameEntity, true))
} catch (e: Exception) {
ToastUtils.toast(e.localizedMessage ?: "启动游戏异常,请稍候再试")
}
}
}
val path = downloadEntity.path
// 正在安装中,忽略重复调用 (手动安装时强行再安装,避免安装结果没有回调时永远卡住,无法安装)
if (!mInstallingVaPathSet.contains(path)) {
try {
mInstallingVaPathSet.add(path)
if (isLegacyGame) {
runOnUiThread {
val intent = VirtualAppManager.getInstallIntent(context, path, downloadEntity.packageName)
Utils.log(LOG_TAG, "正在安装 ${downloadEntity.packageName}")
context.startActivity(intent)
}
} else {
runOnIoThread {
onInstallFinished(
downloadEntity.packageName,
VaApp.get().appManager.installGame(
path,
VGameInstallerParams(VGameInstallerParams.FLAG_INSTALL_OVERRIDE_NO_CHECK)
)
)
}
}
} catch (e: Exception) {
ToastUtils.toast(e.localizedMessage ?: "")
}
} else {
Utils.log(LOG_TAG, "$path 正在安装中,此次安装调用无效")
}
}
if (isLegacyGame) {
connectService {
if (mDelegateManager.isConnectAidlInterface) {
installClosure.invoke()
} else {
connectService(
shouldCheckUpdate = false,
shouldConnectSilently = false,
callbackClosure = installClosure
)
}
}
} else {
installClosure.invoke()
}
}
/**
* 批量安装
* 安装完成后不插入数据库,目前应用于谷歌框架安装
*/
fun batchInstall(context: Context, installMap: HashMap<String, File>, triggerPackageName: String) {
Utils.log(LOG_TAG, "尝试批量安装 ${installMap.size} 个新应用")
// 记录触发批量安装的游戏包名
mTriggerPackageName = triggerPackageName
mBatchInstallMap = installMap
mBatchInstallFailedCount = 0
install(context, installMap.values.first())
}
/**
* 注册批量安装结果回调,值为空时为取消注册
*/
fun registerBatchInstallListener(listener: ((isSuccess: Boolean, isInterrupted: Boolean) -> Unit)? = null) {
mBatchInstallListener = listener
}
/**
* 安装新应用
* 不会作为游戏存储,目前仅应用于谷歌框架
* 因为是静默安装,所以不检查其它限制条件
*/
private fun install(context: Context, file: File) {
Utils.log(LOG_TAG, "尝试安装新应用 ${file.name}")
val packageName = file.nameWithoutExtension
val path = file.path
if (!mInstallingVaPathSet.contains(path)) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& !PackageUtils.isAppOnForeground(context)
) {
mBatchInstallListener?.invoke(false, true)
Utils.log(LOG_TAG, "应用切换至后台,批量安装被打断")
return
}
mInstallingVaPathSet.add(path)
if(isLegacyInstalled(mTriggerPackageName)) {
val intent = VirtualAppManager.getInstallIntent(context, path, packageName)
Utils.log(LOG_TAG, "正在安装 $packageName")
context.startActivity(intent)
} else {
runOnIoThread {
val installParams = VGameInstallerParams(VGameInstallerParams.FLAG_INSTALL_OVERRIDE_NO_CHECK)
val installGameResult = VaApp.get().appManager.installGame(path, installParams)
onInstallFinished(packageName, installGameResult)
}
}
} catch (e: Exception) {
Utils.log(LOG_TAG, "安装异常,${e.localizedMessage}")
}
}
}
/**
* 禁用安装完直接启动游戏 (仅此次安装)
*/
fun disableLaunchGameAfterInstallation() {
mShouldLaunchGameAfterInstallation = false
}
/**
* 安装完是否直接启动游戏
*/
fun shouldLaunchGameAfterInstallation() = mShouldLaunchGameAfterInstallation
/**
* 安装完成回调
*/
fun onInstallFinished(packageName: String, result: VGameInstallerResult) {
if (PackageFlavorHelper.IS_TEST_FLAVOR) {
callSite.onNext(callSiteOnInstallComplet)
}
runOnIoThread {
val gameId = mTempPackageNameAndGameIdMap[packageName] ?: ""
val downloadEntity = getVDownloadEntity(gameId = gameId, packageName = packageName)
if (downloadEntity != null) {
// 去掉更新标记
downloadEntity.isUpdate = false
mInstallingVaPathSet.remove(downloadEntity.path)
try {
vGameDao.insert(VGameEntity.from(downloadEntity))
refreshVGameSnapshot()
} catch (e: SQLiteFullException) {
ToastUtils.toast("存储空间不足,安装失败")
return@runOnIoThread
}
if (result.status == 0) {
if (isLegacyInstalled(packageName)) {
// 存在旧版畅玩组件的游戏更新时才更新安装列表了,不再会有新的包安装到旧版的畅玩空间了
updateInstalledList()
}
PackageObserver.onPackageChanged(
EBPackage(EBPackage.TYPE_INSTALLED, result.packageName, "unknown").also {
it.gameId = downloadEntity.gameId
it.isVGame = true
}
)
insertInstalledGameToProvider(downloadEntity)
runOnUiThread {
showFloatingWindow(downloadEntity)
}
} else {
ToastUtils.toast("安装出现异常, ${result.status}")
}
mPackageInstalledLiveData.postValue(result.packageName)
VBackupHelper.backupDBToExternalStorage(HaloApp.getInstance())
} else {
// downloadEntity 为空,可能是框架类的安装 (走另外一个下载管理)
if (result.status == 0) {
if (mIsServiceConnected) {
updateInstalledList()
}
} else {
ToastUtils.toast("安装出现异常, ${result.status}")
}
}
// 更新已安装的任务,执行其它等待安装的任务
if (mBatchInstallMap.size != 0) {
val iterator = mBatchInstallMap.iterator()
var nextFileToInstall: File? = null
while (iterator.hasNext()) {
val keySet = iterator.next()
if (keySet.key == packageName) {
// 从待安装列表里移除已安装完成的任务
iterator.remove()
mInstallingVaPathSet.remove(keySet.value.path)
// 安装失败时更新失败计数
if (result.status != 0) {
mBatchInstallFailedCount++
}
} else {
// 包名不一样,将下一个需要安装的包设为它
nextFileToInstall = keySet.value
}
}
if (nextFileToInstall != null) {
install(HaloApp.getInstance(), nextFileToInstall)
}
if (mBatchInstallMap.size == 0) {
runOnUiThread {
// 批量安装完成后回调成功/失败事件
mBatchInstallListener?.invoke(mBatchInstallFailedCount == 0, false)
}
}
}
}
}
/**
* 游戏是否正在安装中
*/
fun isInstalling(packageName: String): Boolean {
val gameId = mTempPackageNameAndGameIdMap[packageName] ?: ""
val downloadEntity = getVDownloadEntity(gameId = gameId, packageName = packageName) ?: return false
return (mInstallingVaPathSet.contains(downloadEntity.path))
}
/**
* 获取实体 (虽然实体是 DownloadEntity 其实就是游戏实体)
* 优先从下载管理里获取(根据 gameId 获取)
* 下载管理不存在时从畅玩游戏数据库的里获取
* 根据 gameId 找不到的时候,回落到用 packageName 在 DownloadManager 里再找匹配的且是畅玩的
*/
private fun getVDownloadEntity(gameId: String, packageName: String): DownloadEntity? {
var matchedEntity = DownloadManager.getInstance().getDownloadEntitySnapshot("", gameId)
?: getVDownloadEntitySnapshot(gameId = gameId, packageName = packageName)
if (matchedEntity == null) {
val packageNameMatchedEntity =
DownloadManager.getInstance().getDownloadEntitySnapshotByPackageName(packageName)
if (packageNameMatchedEntity?.asVGame() == true) {
matchedEntity = packageNameMatchedEntity
}
}
return matchedEntity
}
private fun insertInstalledGameToProvider(downloadEntity: DownloadEntity, fromRetry: Boolean = false) {
// 因为 downloadEntity 的 meta 会在下载线程被修改,所以 GsonUtils.toJson(downloadEntity.meta) 会出现ConcurrentModificationException
// 这里切换线程再做一次尝试
runOnIoThread(!fromRetry) {
try {
val values = ContentValues()
val packageName = downloadEntity.packageName
if (packageName.isEmpty()) return@runOnIoThread
//主键
values.put("package_name", packageName)
values.put("url", downloadEntity.url)
values.put("name", downloadEntity.name)
values.put("size", downloadEntity.size)
values.put("meta", GsonUtils.toJson(downloadEntity.meta))
val type = if (PackageFlavorHelper.IS_TEST_FLAVOR) "test_flavor" else ""
values.put("type", type)
val uri =
Uri.parse("content://${com.lg.vspace.BuildConfig.VA_AUTHORITY_PREFIX}.provider.download_game/download_game")
HaloApp.getInstance().contentResolver.insert(uri, values)
} catch (e: Throwable) {
Utils.log("TEST", "::insertInstalledGameToProvider, e: ${e.toString()}")
if (!fromRetry) {
insertInstalledGameToProvider(downloadEntity, true)
} else {
SentryHelper.onEvent("INSERT_GAME_TO_PROVIDER_ERROR", "packName", downloadEntity.packageName)
}
}
}
}
fun installOrLaunch(
context: Context,
downloadEntity: DownloadEntity,
location: String? = null
) {
Utils.log(LOG_TAG, "检测是需要安装还是启动(DownloadEntity) ${downloadEntity.gameId}")
if (downloadEntity.name.isNullOrEmpty()) {
SentryHelper.onEvent(
"V_GAME_DOWNLOAD_ENTITY_NAME_EMPTY",
"game_id",
downloadEntity.gameId,
"location",
location
)
}
installOrLaunch(
context,
downloadEntity.packageName,
downloadEntity.gameId,
downloadEntity.name ?: "",
downloadEntity.getGameCategory(),
downloadEntity.getMetaExtra(KEY_BIT),
location
)
}
@JvmStatic
fun installOrLaunch(
context: Context,
gameEntity: GameEntity,
location: String? = null
) {
Utils.log(LOG_TAG, "检测是需要安装还是启动(GameEntity) ${gameEntity.id}")
installOrLaunch(
context,
gameEntity.getUniquePackageName() ?: "",
gameEntity.id,
gameEntity.name ?: "",
gameEntity.category ?: "",
gameEntity.gameBit,
location
)
}
/**
* 安装或启动应用
*/
private fun installOrLaunch(
context: Context,
packageName: String,
gameId: String,
gameName: String,
gameType: String,
bit: String,
location: String?
) {
Utils.log(LOG_TAG, "检测是需要安装还是启动 $packageName")
if (location != null) {
logLaunchButtonClicked(packageName, gameId, gameName, gameType, location)
}
validateVSpaceBeforeAction(context, packageName, GameEntity().apply {
id = gameId
name = gameName
downloadStatus = Constants.V_GAME
category = gameType
setApk(arrayListOf(ApkEntity(packageName = packageName, bit = bit)))
}) {
if (isInstalled(packageName)) {
launch(context, packageName)
return@validateVSpaceBeforeAction
}
// 检查下载管理是否有下载实体,有实体表明未安装成功
val downloadEntity = getVDownloadEntity(gameId = gameId, packageName = packageName)
if (downloadEntity != null) {
val downloadedFile = File(downloadEntity.path)
if (downloadedFile.exists() && downloadedFile.length() == downloadEntity.size) {
install(context, downloadEntity, true)
} else {
// 重新下载
updateOrReDownload(downloadEntity, null)
}
} else {
ToastUtils.toast("该游戏已损坏,请重新下载")
}
}
}
/**
* 启动应用
* @param ignoreGApps 忽略谷歌框架
*/
@JvmStatic
fun launch(
context: Context,
packageName: String,
ignoreGApps: Boolean = false,
showLoading: Boolean = true
) {
Utils.log(LOG_TAG, "尝试打开应用 $packageName")
val isLegacyGame = isLegacyInstalled(packageName)
// 置空下载挂起回调(能 launch 游戏说明畅玩组件已安装)
mPendingDownloadCallback = null
val downloadEntity = getVDownloadEntity(gameId = "", packageName = packageName)
val gameId = downloadEntity?.gameId ?: "unknown"
val gameName = downloadEntity?.name ?: "unknown"
val gameIcon = downloadEntity?.icon ?: "unknown"
// 若需要安装谷歌框架,弹谷歌框架安装弹窗
if (!ignoreGApps && isGAppsRequired(downloadEntity) && if (isLegacyGame) {
!isGAppsInstalledInCwLegacy()
} else {
!isGAppsInstalled()
}
) {
// show dialog
val currentActivity = CurrentActivityHolder.getCurrentActivity() as? AppCompatActivity
?: if (context is AppCompatActivity) context else return
GAppsDownloadDialogFragment
.getInstance(packageName, gameId, gameName)
.show(currentActivity.supportFragmentManager, "G_APPS_FRAGMENT")
return
}
try {
logLaunch(gameId, gameName, downloadEntity?.getGameCategory() ?: "", packageName)
if (showLoading) {
val thirdPartyAd = AdDelegateHelper.vGameLaunchAd?.thirdPartyAd
val intent = mDelegateManager.getStartGameIntent(
packageName,
gameId,
gameName,
gameIcon,
MetaUtil.getBase64EncodedAndroidId(),
HaloApp.getInstance().gid,
com.gh.gamecenter.BuildConfig.VERSION_NAME,
HaloApp.getInstance().channel,
thirdPartyAd?.slotId,
thirdPartyAd?.displaySize
)
// 覆盖字段
if (!isLegacyGame) {
intent.setComponent(
ComponentName(
com.gh.gamecenter.BuildConfig.APPLICATION_ID,
VirtualAppManager.AIDL_SERVER_REMOTE_GUIDE_ACTIVITY
)
)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
} else {
if (isLegacyGame) {
VirtualAppManager.get().launchGame(packageName)
} else {
VaApp.get().appManager.launchGame(packageName)
}
}
mLastSuccessfullyLaunchedGame = Pair(System.currentTimeMillis(), packageName)
runOnIoThread {
updateVGamePlayedTime(packageName, System.currentTimeMillis(), getPlayedTime(packageName))
VBackupHelper.backupDBToExternalStorage(HaloApp.getInstance())
val isThisVGameExistInDatabase = getAllVGame().any { it.packageName == packageName }
// 存在安装完成任务,但数据没有正常插入到畅玩数据库的情况(磁盘满了?),遇到这种情况,手动再插一遍
if (!isThisVGameExistInDatabase && downloadEntity != null) {
vGameDao.insert(VGameEntity.from(downloadEntity))
refreshVGameSnapshot()
}
}
} catch (e: Exception) {
ToastUtils.toast(e.localizedMessage ?: "")
}
}
/**
* 记录启动事件
*/
private fun logLaunch(
gameId: String,
gameName: String,
gameCategory: String,
packageName: String
) {
val downloadEntity = getVDownloadEntity(gameId = "", packageName = packageName)
// boundedObject 里找不到游戏类型时,尝试从已安装列表中获取
val notNullGameCategory = if (gameCategory.isEmpty() && packageName.isNotEmpty()) {
PackageRepository.gameInstalled.find { it.packageName == packageName }?.category ?: ""
} else {
downloadEntity?.getGameCategory() ?: ""
}
NewFlatLogUtils.logGameLaunch(
gameId = gameId,
gameName = gameName,
gameCategory = notNullGameCategory,
downloadStatus = "畅玩"
)
}
/**
* 记录启动按钮被点击 ()
*/
fun logLaunchButtonClicked(
packageName: String,
gameId: String? = null,
gameName: String? = null,
gameCategory: String? = null,
location: String
) {
val downloadEntity = getVDownloadEntity(gameId = "", packageName = packageName)
// boundedObject 里找不到游戏类型时,尝试从已安装列表中获取
val notNullGameCategory = if (gameCategory.isNullOrEmpty() && packageName.isNotEmpty()) {
PackageRepository.gameInstalled.find { it.packageName == packageName }?.category ?: ""
} else {
downloadEntity?.getGameCategory() ?: ""
}
NewFlatLogUtils.logGameLaunchButtonClicked(
gameId = gameId ?: downloadEntity?.gameId ?: "",
gameName = gameName ?: downloadEntity?.name ?: "",
location = location,
gameCategory = notNullGameCategory,
downloadStatus = "畅玩"
)
}
/**
* 卸载应用
*/
fun uninstall(packageName: String?) {
Utils.log(LOG_TAG, "卸载游戏 $packageName")
if (packageName.isNullOrBlank()) return
try {
vGameDao.delete(packageName)
refreshVGameSnapshot()
if (vGameDao.getAll().isEmpty()) {
VBackupHelper.removeAllDatabase()
} else {
VBackupHelper.backupDBToExternalStorage(HaloApp.getInstance())
}
} catch (e: SQLiteFullException) {
e.printStackTrace()
}
val isLegacyGame = isLegacyInstalled(packageName)
val uninstallClosure: () -> Unit = {
try {
val result = if (isLegacyGame) VirtualAppManager.get()
.uninstallGame(packageName) else VaApp.get().appManager.uninstallGame(packageName)
if (result) {
if (isLegacyGame) {
updateInstalledList()
}
// 卸载的时候移除已安装的包名历史
mPackageInstalledLiveData.postValue("")
}
if (PackageFlavorHelper.IS_TEST_FLAVOR) {
callSite.onNext(callSiteUninstall)
}
Utils.log(LOG_TAG, "卸载应用结果 -> $result")
} catch (e: Exception) {
ToastUtils.toast(e.localizedMessage ?: "")
}
}
if (isLegacyGame) {
if (mDelegateManager.isConnectAidlInterface) {
uninstallClosure.invoke()
} else {
connectService(
shouldCheckUpdate = false,
shouldConnectSilently = false,
callbackClosure = uninstallClosure
)
}
} else {
uninstallClosure.invoke()
}
}
/**
* 获取畅玩助手版本号
*/
fun getVersionName(context: Context): String? = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.getPackageInfo(
VirtualAppManager.AIDL_SERVER_PACKAGE_NAME,
PackageManager.PackageInfoFlags.of(0)
)
} else {
context.packageManager.getPackageInfo(
VirtualAppManager.AIDL_SERVER_PACKAGE_NAME,
0
)
}.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
null
}
/**
* 清除游戏数据
*/
@JvmStatic
fun cleanGameData(packageName: String?) {
Utils.log(LOG_TAG, "清除游戏数据 $packageName")
if (packageName.isNullOrBlank()) return
val cleanGameDataClosure: () -> Unit = {
try {
val result = VirtualAppManager.get().cleanGameData(packageName)
if (result) {
updateInstalledList()
}
Utils.log(LOG_TAG, "清除游戏数据结果 -> $result")
} catch (e: Exception) {
ToastUtils.toast(e.localizedMessage ?: "")
}
}
if (mDelegateManager.isConnectAidlInterface) {
cleanGameDataClosure.invoke()
} else {
connectService(
shouldCheckUpdate = false,
shouldConnectSilently = false,
callbackClosure = cleanGameDataClosure
)
}
}
/**
* 获取游戏最后打开的时间
* @return 最后打开的时间戳
*/
fun getLastPlayedTime(downloadEntity: DownloadEntity): Long {
val lastPlayedTimeString = downloadEntity.getMetaExtra(KEY_LAST_PLAYED_TIME)
return if (TextUtils.isEmpty(lastPlayedTimeString)) 0 else lastPlayedTimeString.toLong()
}
/**
* 获取游戏的游玩时长
*/
fun getTotalPlayedTime(packageName: String): Long {
return mInstalledInfoList.firstOrNull { it.packageName == packageName }?.appTotalPlayTime ?: 0L
}
/**
* 根据游戏 ID 和包名获取畅玩游戏的实体快照
*/
@JvmStatic
fun getVDownloadEntitySnapshot(gameId: String?, packageName: String?): DownloadEntity? {
return getVGameSnapshot(gameId, packageName)?.downloadEntity
}
/**
* 更新数据库重中的畅玩游戏
*/
private fun updateVGamePlayedTime(packageName: String, lastPlayedTime: Long, totalPlayedTime: Long) {
getVGameSnapshot(null, packageName)?.let {
it.downloadEntity.addMetaExtra(KEY_LAST_PLAYED_TIME, lastPlayedTime.toString())
it.downloadEntity.addMetaExtra(KEY_TOTAL_PLAYED_TIME, totalPlayedTime.toString())
try {
vGameDao.insert(it)
refreshVGameSnapshot()
} catch (e: SQLiteFullException) {
ToastUtils.toast("设备存储空间不足,请清理后重试")
}
}
// 更新首页排序
EventBus.getDefault().post(EBReuse("vgame"))
}
/**
* 获取畅玩安装完成的 LiveData内容是包名
*/
fun getPackageInstalledLiveData(): LiveData<String> = mPackageInstalledLiveData
fun showFloatingWindow(downloadEntity: DownloadEntity) {
val topActivity = AppManager.getInstance().currentActivity() ?: return
if (topActivity.isFinishing || topActivity is VSpaceLoadingActivity) return
VLoadCompleteWindowHelper.showFloatingWindow(topActivity, toGameEntity(downloadEntity))
}
/**
* 畅玩空间是否已安装
* 如果已安装或配置为空返回 true
* 未安装的情况下会弹弹窗
*/
fun showDialogIfVSpaceIsNeeded(
context: Context,
gameId: String,
gameName: String,
gameType: String,
bit: String
): Boolean {
Utils.log(LOG_TAG, "检测是否已安装畅玩空间")
val vaConfig = Config.getVSettingEntity()?.va
if (vaConfig == null) {
ToastUtils.toast("畅玩空间暂未上线")
Utils.log(LOG_TAG, "畅玩空间暂未上线")
return true
}
val is64VSpaceInstalled = PackageUtils.isInstalledFromAllPackage(context, vaConfig.arch64?.packageName)
val is32VSpaceInstalled = PackageUtils.isInstalledFromAllPackage(context, vaConfig.arch32?.packageName)
when {
!is64VSpaceInstalled -> {
VSpaceDialogFragment.showDownloadDialog(
context,
appEntity64 = getVSpaceDownloadEntity(true),
appEntity32 = getVSpaceDownloadEntity(false),
autoDownload = true,
gameId = gameId,
gameName = gameName,
gameType = gameType,
bit = bit
)
Utils.log(LOG_TAG, "显示下载64位畅玩空间弹窗")
return true
}
bit == "32" && !is32VSpaceInstalled -> {
VSpace32DialogFragment.showDownloadDialog(
context,
getVSpaceDownloadEntity(false),
gameId = gameId,
gameName = gameName
)
Utils.log(LOG_TAG, "显示下载32位畅玩空间弹窗")
return true
}
}
Utils.log(LOG_TAG, "畅玩空间已安装")
return false
}
private fun getVSpaceDownloadEntity(is64Bit: Boolean): AppEntity {
val appEntity = AppEntity()
val vaConfig =
if (is64Bit) Config.getVSettingEntity()?.va?.arch64 else Config.getVSettingEntity()?.va?.arch32
appEntity.versionCode = vaConfig?.versionCode ?: 0
appEntity.version = vaConfig?.versionName
appEntity.url = vaConfig?.url
return appEntity
}
/**
* 更新或重下载
*/
@JvmStatic
fun updateOrReDownload(gameEntity: GameEntity) {
PackagesManager.getUpdateList().firstOrNull { it.id == gameEntity.id }
?.let { updateEntity ->
getVGameSnapshot(gameEntity.id, updateEntity.packageName)
?.let { vGame ->
updateOrReDownload(vGame.downloadEntity, updateEntity)
}
}
}
/**
* 更新或重下载
*
* @param originDownloadEntity 旧的下载实体
* @param updateEntity 更新内容,当 updateEntity 为空时重新下载
*/
fun updateOrReDownload(originDownloadEntity: DownloadEntity, updateEntity: GameUpdateEntity? = null) {
Utils.log(LOG_TAG, "更新应用${originDownloadEntity.packageName}")
if (updateEntity != null) {
originDownloadEntity.url = updateEntity.url
originDownloadEntity.gameId = updateEntity.id
originDownloadEntity.name = updateEntity.name
originDownloadEntity.eTag = updateEntity.etag
originDownloadEntity.icon = updateEntity.icon
originDownloadEntity.size = 0
originDownloadEntity.platform = updateEntity.platform
originDownloadEntity.packageName = updateEntity.packageName
originDownloadEntity.versionName = updateEntity.version
originDownloadEntity.isUpdate = true
originDownloadEntity.addMetaExtra(Constants.RAW_GAME_ICON, updateEntity.rawIcon)
originDownloadEntity.addMetaExtra(
Constants.GAME_ICON_SUBSCRIPT,
updateEntity.iconSubscript
)
originDownloadEntity.addMetaExtra(Constants.APK_MD5, updateEntity.md5)
if (updateEntity.iconFloat != null) {
originDownloadEntity.addMetaExtra(
Constants.GAME_ICON_FLOAT_TOP_TEXT,
updateEntity.iconFloat?.upperLeftText
)
originDownloadEntity.addMetaExtra(
Constants.GAME_ICON_FLOAT_TOP_COLOR,
updateEntity.iconFloat?.upperLeftColor
)
originDownloadEntity.addMetaExtra(
Constants.GAME_ICON_FLOAT_BOTTOM_TEXT,
updateEntity.iconFloat?.bottomText
)
}
PackageRepository.removeVGameUpdate(updateEntity.id, true)
}
originDownloadEntity.progress = 0
originDownloadEntity.finalRedirectedUrl = ""
originDownloadEntity.percent = 0.0
// 确定下载类型
val downloadType =
if (updateEntity == null) ExposureUtils.DownloadType.FUN_DOWNLOAD else ExposureUtils.DownloadType.FUN_UPDATE
val gameEntity = GameEntity(originDownloadEntity.gameId, originDownloadEntity.name)
gameEntity.gameVersion = originDownloadEntity.versionName ?: ""
val event = ExposureUtils.logADownloadExposureEvent(
gameEntity,
originDownloadEntity.platform,
null,
downloadType
)
originDownloadEntity.exposureTrace = GsonUtils.toJson(event)
originDownloadEntity.setVGameDownloadModeInDualDownloadMode()
HistoryHelper.insertGameEntity(gameEntity)
DownloadManager.getInstance().add(originDownloadEntity, true)
// 收集下载数据
DataCollectionUtils.uploadDownload(HaloApp.getInstance(), originDownloadEntity, "开始")
SensorsBridge.trackEvent(
"HaloFunGameDownloadClick",
"game_name", originDownloadEntity.name,
"game_id", originDownloadEntity.gameId,
"game_schema_type", if (originDownloadEntity.getMetaExtra(KEY_BIT) == "32") "32位" else "64位"
)
SensorsBridge.trackEventWithExposureSource(
"DownloadProcessBegin",
event.source,
"game_id", originDownloadEntity.gameId,
"game_name", originDownloadEntity.name ?: "",
"game_type", originDownloadEntity.categoryChinese,
"page_name", GlobalActivityManager.getCurrentPageEntity().pageName,
"page_id", GlobalActivityManager.getCurrentPageEntity().pageId,
"page_business_id", GlobalActivityManager.getCurrentPageEntity().pageBusinessId,
"last_page_name", GlobalActivityManager.getLastPageEntity().pageName,
"last_page_id", GlobalActivityManager.getLastPageEntity().pageId,
"last_page_business_id", GlobalActivityManager.getLastPageEntity().pageBusinessId,
"download_type", "畅玩下载",
)
}
/***
* 实体转换为普通游戏实体
*/
fun toGameEntity(downloadEntity: DownloadEntity): GameEntity {
val lastPlayedTimeString = downloadEntity.getMetaExtra(KEY_LAST_PLAYED_TIME)
return GameEntity(id = downloadEntity.gameId, name = downloadEntity.name).apply {
setApk(
arrayListOf(
ApkEntity(
packageName = downloadEntity.packageName,
url = downloadEntity.url,
platform = downloadEntity.platform,
version = downloadEntity.versionName,
size = downloadEntity.getMetaExtra(Constants.APK_SIZE)
)
)
)
icon = downloadEntity.icon
rawIcon = downloadEntity.getMetaExtra(Constants.RAW_GAME_ICON)
iconSubscript = downloadEntity.getMetaExtra(Constants.GAME_ICON_SUBSCRIPT)
lastPlayedTime = if (lastPlayedTimeString == "") 0L else lastPlayedTimeString.toLong()
playedTime = getPlayedTime(downloadEntity.packageName, downloadEntity)
downloadStatus = Constants.V_GAME
setEntryMap(DownloadManager.getInstance().getEntryMap(name))
}
}
/**
* 获取畅玩游戏游玩时长
*/
private fun getPlayedTime(packageName: String, downloadEntity: DownloadEntity? = null): Long {
var playedTime = mInstalledInfoList.firstOrNull { it.packageName == packageName }?.appTotalPlayTime ?: 0L
val cachedPlayedTimeString = downloadEntity?.getMetaExtra(KEY_TOTAL_PLAYED_TIME)
if (playedTime == 0L && !cachedPlayedTimeString.isNullOrEmpty()) {
playedTime = cachedPlayedTimeString.toLong()
}
return playedTime
}
/**
* 获取畅玩空间更新
*/
@SuppressLint("CheckResult")
private fun getVSpaceUpdate(config: VSetting.VaArch, is64Bit: Boolean) {
val installedVersionName = PackageUtils.getVersionNameByPackageName(config.packageName)
val installedVersionCode = PackageUtils.getVersionCodeByPackageName(config.packageName)
RetrofitManager.getInstance()
.vApi
.getPackageUpdate(installedVersionName, installedVersionCode, config.packageName)
.subscribeOn(Schedulers.io())
.subscribe(object : BiResponse<AppEntity>() {
override fun onSuccess(data: AppEntity) {
if (is64Bit) m64UpdateEntity = data else m32UpdateEntity = data
}
})
}
/**
* 通过 PackageRepository 检查更新
*/
private fun checkUpdateViaPackageRepository() {
val rawInstalledPackageList = getInstalledPackageList()
val validInstalledPackageList = arrayListOf<String>()
for (packageName in rawInstalledPackageList) {
if (getVGameSnapshot(null, packageName) != null) {
validInstalledPackageList.add(packageName)
}
}
PackageRepository.addInstalledGames(validInstalledPackageList, true)
}
/**
* 是否需要显示畅玩助手更新
*/
private fun shouldShowVSpaceUpdate(
updateEntity: AppEntity?,
installedSpaceVersionCode: Int,
isRelatedUpdate: Boolean = false
): Boolean {
if (updateEntity == null) return false
val hasNewerVersion = installedSpaceVersionCode < updateEntity.versionCode
if (!hasNewerVersion) return false
// 关联更新忽略检查上次提示更新
if (isRelatedUpdate) return true
if (updateEntity.isAlertEveryTime()) return true
val lastAlertUpdateUrl = updateEntity.url + updateEntity.alert
if (updateEntity.isAlertOnceADay()
&& SPUtils.getString(if (updateEntity.category == "32-bit") KEY_LAST_ALERT_32_UPDATE_URL else KEY_LAST_ALERT_64_UPDATE_URL) != lastAlertUpdateUrl
) {
return true
}
return false
}
/**
* 打开畅玩广场(版块)
*/
fun startVSpaceSquare(context: Context) {
val entity =
SubjectRecommendEntity(link = "62fc8047b07e0c0bb63058c2", nameNormal = "畅玩广场", text = "畅玩广场")
DirectUtils.directToBlock(context, entity, "")
}
/**
* 恢复畅玩数据
*/
@JvmStatic
fun recoverVDataIfPossible() {
VBackupHelper.recoverValidData(HaloApp.getInstance())
}
/**
* 获取未经畅玩处理的原始下载地址
*/
@Deprecated("不再使用这个后缀来区分游戏,保留只是为了兼容旧数据")
fun getOriginalUrl(vUrl: String?): String {
if (vUrl?.endsWith("type=v") == true) {
return vUrl.removeSuffix("&type=v").removeSuffix("?type=v")
}
return vUrl ?: ""
}
/**
* 畅玩功能是否用过
*/
fun isVIsUsed(): Boolean = SPUtils.getBoolean(KEY_V_IS_USED) || getAllVGameSnapshots().isNotEmpty()
/**
* 畅玩功能是否启用
* 设备系统版本大于 7.1 且后台接口开启时才启用畅玩功能
*
* 2024-01-16新增当处于兼职包特殊渠道GH_jzcs强行开启畅玩服务
* https://jira.shanqu.cc/browse/GHZS-4520
*/
@JvmStatic
fun isVGameOn() = PackageFlavorHelper.IS_TEST_FLAVOR
|| HaloApp.getInstance().channel == "GH_jzcs" // 兼职包特殊渠道强行开启畅玩服务
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1 && isVGameOnByConfigSettings())
/**
* 后台接口是否启用畅玩功能(若获取不到开关情况时,判断用户是否曾用过畅玩功能)
*/
private fun isVGameOnByConfigSettings(): Boolean {
val configSettings = Config.getSettings()
// 当 settings 为空时,若曾经用过畅玩,也允许用户继续使用畅玩
return if (configSettings == null) {
isVIsUsed()
} else {
"on" == configSettings.gameSmooth
}
}
/**
* 检查畅玩游戏是否满足生成存档要求
*/
fun checkArchiveExist(packageName: String, archiveConfigStr: String): Boolean {
if (!mIsServiceConnected) {
connectService(shouldConnectSilently = true)
return false
}
return try {
mDelegateManager.checkArchiveExist(packageName, archiveConfigStr)
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* 是否需要安装 GApps
*/
private fun isGAppsRequired(downloadEntity: DownloadEntity?): Boolean {
return downloadEntity?.getMetaExtra(KEY_REQUIRED_G_APPS) == "on"
}
/**
* 畅玩是否已经成功安装了 GApps
*/
private fun isGAppsInstalled() = isInnerInstalled(G_GMS_PACKAGE_NAME) && isInnerInstalled(G_GSF_PACKAGE_NAME)
&& isInnerInstalled(G_VENDING_PACKAGE_NAME)
/**
* 旧版畅玩是否已经成功安装了 GApps
*/
private fun isGAppsInstalledInCwLegacy(): Boolean {
return isLegacyInstalled(G_GMS_PACKAGE_NAME)
&& isLegacyInstalled(G_GSF_PACKAGE_NAME)
&& isLegacyInstalled(G_VENDING_PACKAGE_NAME)
}
/**
* 获取排好序的最近在玩列表
*
* 首位:固定展示最近一次点击过下载\启动的游戏
* 后续位置-优先级:完成下载但未启动过的游戏 > 完成下载且已启动过的游戏
* 完成下载但未启动过的游戏:按照点击下载的时间倒序排列
* 完成下载且已启动过的游戏:按照游戏的畅玩时长从左(长)向右(短)排列
*/
fun getSortedRecentlyPlayedList(): List<VGameItemData> {
val rawDownloadEntityList = DownloadManager.getInstance().allVDownloadTaskSnapshots
val rawInstalledEntityList = getAllVGameSnapshots()
val rawEntityList = arrayListOf<DownloadEntity>()
var fixedTopEntity: VGameItemData? = null
val unPlayedEntityList = arrayListOf<VGameItemData>()
val playedEntityList = arrayListOf<VGameItemData>()
val sortedEntityList = arrayListOf<VGameItemData>()
val distinctIdSet = hashSetOf<String>()
// 将已安装任务的实体放进待排序列表里
for (rawInstalledEntity in rawInstalledEntityList) {
distinctIdSet.add(rawInstalledEntity.downloadEntity.gameId)
rawEntityList.add(rawInstalledEntity.downloadEntity)
}
// 将下载任务的实体放进待排序列表里
for (rawDownloadEntity in rawDownloadEntityList) {
if (distinctIdSet.contains(rawDownloadEntity.gameId)) {
continue
}
rawEntityList.add(rawDownloadEntity)
}
distinctIdSet.clear()
for (rawEntity in rawEntityList) {
val lastPlayedTime = getLastPlayedTime(rawEntity)
val latestModifiedTime = maxOf(rawEntity.start, getLastPlayedTime(rawEntity))
if (fixedTopEntity == null) {
fixedTopEntity = VGameItemData.from(rawEntity)
} else {
val fixedTopLatestModifiedTime =
maxOf(fixedTopEntity.downloadEntity.start, getLastPlayedTime(fixedTopEntity.downloadEntity))
if (latestModifiedTime > fixedTopLatestModifiedTime) {
fixedTopEntity = VGameItemData.from(rawEntity)
}
}
if (lastPlayedTime == 0L) {
unPlayedEntityList.add(VGameItemData.from(rawEntity))
} else {
playedEntityList.add(VGameItemData.from(rawEntity))
}
}
fixedTopEntity?.let {
sortedEntityList.add(it)
unPlayedEntityList.removeAll { entity ->
entity.downloadEntity.packageName == fixedTopEntity.downloadEntity.packageName
}
playedEntityList.removeAll { entity ->
entity.downloadEntity.packageName == fixedTopEntity.downloadEntity.packageName
}
}
sortedEntityList.addAll(unPlayedEntityList.sortedByDescending { it.downloadEntity.start })
sortedEntityList.addAll(playedEntityList.sortedByDescending { getTotalPlayedTime(it.downloadEntity.packageName) })
return sortedEntityList.take(8)
}
@SuppressLint("CheckResult")
fun preparePluginUpdate() {
Config.vNewSettingSubject.debounce(2, TimeUnit.SECONDS).doOnNext {
it?.vaPlugin?.let { vaPlugin ->
if (!vaPlugin.id.isNullOrEmpty()) {
Utils.log(LOG_TAG, "开发者需要插件更新功能请push文件到手机或者【修改插件打包的配置为debug版本然后上传推送】")
if (!vaPlugin.url64.isNullOrEmpty()) {
val installedPluginVersion = HostUtils.getPluginVersion()
// 因为服务端下发的插件都是release版本的所以开发者要调试插件更新暂时先push文件到手机更新吧
if (true) {
val file = File("/data/local/tmp/gh-plugins/artifacts.zip")
if (file.exists()) {
Utils.log(LOG_TAG, "有本地更新文件: 64位插件")
PluginHelper.getInstance().updatePlugin(file)
}
} else {
if (installedPluginVersion?.isNotEmpty() == true
&& !Version(vaPlugin.versionName).isEqual(installedPluginVersion)
) {
downloadPlugin(id = "${vaPlugin.id}64", url = vaPlugin.url64) {
PluginHelper.getInstance().updatePlugin(it)
}
}
}
}
if (!vaPlugin.url32.isNullOrEmpty()) {
if (com.gh.gamecenter.BuildConfig.DEBUG) {
val file = File("/data/local/tmp/gh-plugins/artifacts32.zip")
if (file.exists()) {
Utils.log(LOG_TAG, "有本地更新文件: 32位插件")
VaApp.get().appManager.updatePlugin(file.absolutePath, "${vaPlugin.id}32.zip")
}
} else {
val installedPluginVersion = VaApp.get().appManager.extPluginVersion
if (installedPluginVersion?.isNotEmpty() == true
&& !Version(vaPlugin.versionName).isEqual(installedPluginVersion)
) {
downloadPlugin(id = "${vaPlugin.id}32", url = vaPlugin.url32) {
VaApp.get().appManager.updatePlugin(it.absolutePath, "${vaPlugin.id}32.zip")
}
}
}
}
}
}
}.subscribeOn(Schedulers.io()).subscribe({}, {})
}
private fun downloadPlugin(id: String, url: String, fn: (File) -> Unit) {
val pluginFileName = "${id}.zip"
val currentPluginFile = File(HaloApp.getInstance().cacheDir, pluginFileName)
if (currentPluginFile.exists() && PluginFileUtils.isZipFile(currentPluginFile)) {
// 已经下载好了
fn(currentPluginFile)
} else {
val currentDownloadEntity = DownloadMessageHandler.findEntity(id)
if (currentDownloadEntity != null) {
SimpleDownloadManager.cancel(id)
}
SimpleDownloadManager.download(
DownloadConfigBuilder()
.setUniqueId(id)
.setFileName(pluginFileName)
.setUrl(url)
.setPathToStore(HaloApp.getInstance().cacheDir.absolutePath + File.separator)
.setHttpClient(DefaultHttpClient())
.setDownloadThreadSize(2)
.setDownloadListener(DownloadMessageHandler)
.setDownloadExecutor(AppExecutor.ioExecutor)
.build()
)
DownloadMessageHandler.registerListener(id, object : DownloadListener {
override fun onError(error: DownloadError) {
}
override fun onProgress(progress: Float) {
}
override fun onSizeReceived(fileSize: Long) {
}
override fun onStatusChanged(status: DownloadStatus) {
if (status == DownloadStatus.COMPLETED) {
fn(currentPluginFile)
DownloadMessageHandler.unregisterListener(id, this)
}
}
override fun onSpeedChanged(speed: Float) {
}
})
}
}
}