1521 lines
57 KiB
Kotlin
1521 lines
57 KiB
Kotlin
package com.gh.vspace
|
||
|
||
import android.annotation.SuppressLint
|
||
import android.content.ContentValues
|
||
import android.content.Context
|
||
import android.content.Intent
|
||
import android.content.pm.PackageManager
|
||
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 androidx.lifecycle.Observer
|
||
import com.gh.common.constant.Config
|
||
import com.gh.common.exposure.ExposureUtils
|
||
import com.gh.common.history.HistoryHelper
|
||
import com.gh.common.util.DataCollectionUtils
|
||
import com.gh.common.util.DirectUtils
|
||
import com.gh.common.util.NewFlatLogUtils
|
||
import com.gh.common.util.PackageUtils
|
||
import com.gh.download.DownloadManager
|
||
import com.gh.download.PackageObserver
|
||
import com.gh.gamecenter.R
|
||
import com.gh.gamecenter.SplashScreenActivity
|
||
import com.gh.gamecenter.common.BuildConfig
|
||
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.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.vspace.VirtualAppManager
|
||
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.VGameInstallerResult
|
||
import com.lightgame.download.DownloadEntity
|
||
import com.lightgame.utils.AppManager
|
||
import com.lightgame.utils.Utils
|
||
import io.reactivex.schedulers.Schedulers
|
||
import org.greenrobot.eventbus.EventBus
|
||
import java.io.File
|
||
import java.util.*
|
||
import java.util.concurrent.CopyOnWriteArrayList
|
||
|
||
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_DOWNLOAD_DEV_URL =
|
||
"https://dev-and-static.ghzs.com/game/apk/smooth_google/google_play_services.zip"
|
||
const val G_APPS_DOWNLOAD_URL = "https://and-static.ghzs.com/game/apk/smooth_google/google_play_services.zip"
|
||
|
||
// 畅玩游戏位数
|
||
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 PLATFORM_V = "畅玩版"
|
||
|
||
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
|
||
|
||
private val mVGameDao 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 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 mBatchInstallListener: ((isSuccess: 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>()
|
||
|
||
val vGameLiveData by lazy { mVGameDao.getAllLiveData() }
|
||
|
||
private val mVGameObserver by lazy {
|
||
Observer<List<VGameEntity>> {
|
||
mVGameSnapshotList = ArrayList(it)
|
||
}
|
||
}
|
||
|
||
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) {
|
||
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)
|
||
SensorsBridge.trackEvent("HaloFunInstallDone")
|
||
} else if (it.type == EBPackage.TYPE_UNINSTALLED) {
|
||
// 执行卸载逻辑
|
||
mIsServiceConnected = false
|
||
mInstalledInfoList.clear()
|
||
} else if (it.type == EBPackage.TYPE_REPLACED) {
|
||
connectService()
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化
|
||
*/
|
||
@SuppressLint("CheckResult")
|
||
@JvmStatic
|
||
fun init(context: Context) {
|
||
if (isVGameOn()) {
|
||
if (!mIsInitialized) {
|
||
mIsInitialized = true
|
||
|
||
VArchiveHelper.init()
|
||
|
||
val config = Config.getVSettingEntity()?.va
|
||
if (config?.arch64 != null
|
||
&& PackageUtils.isInstalledFromAllPackage(context, config.arch64.packageName)
|
||
) {
|
||
if (isVIsUsed()) {
|
||
connectService(shouldCheckUpdate = true)
|
||
}
|
||
|
||
// 检查畅玩助手64位组件更新
|
||
getVSpaceUpdate(config.arch64, true)
|
||
}
|
||
|
||
if (config?.arch32 != null
|
||
&& PackageUtils.isInstalledFromAllPackage(context, config.arch32.packageName)
|
||
) {
|
||
// 检查畅玩助手32位组件更新
|
||
getVSpaceUpdate(config.arch32, false)
|
||
}
|
||
PackageObserver.registerPackageChangeChangeListener(mPackageObserver)
|
||
}
|
||
|
||
vGameLiveData.observeForever(mVGameObserver)
|
||
|
||
// 注册应用可见事件监听
|
||
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
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 手动刷新畅玩游戏快照列表
|
||
*/
|
||
@WorkerThread
|
||
fun refreshVGameSnapshot() {
|
||
mVGameSnapshotList = ArrayList(mVGameDao.getAll())
|
||
}
|
||
|
||
/**
|
||
* 如果断连了,尝试静静重连
|
||
*/
|
||
fun reconnectServiceIfNeeded() {
|
||
if (!mIsServiceConnected) {
|
||
connectService(shouldConnectSilently = true)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 连接服务
|
||
*/
|
||
fun connectService(
|
||
shouldCheckUpdate: Boolean = false,
|
||
shouldConnectSilently: Boolean = false,
|
||
callbackClosure: (() -> Unit)? = null
|
||
) {
|
||
Utils.log(LOG_TAG, "尝试连接 V 服务")
|
||
|
||
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
|
||
|
||
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 {
|
||
PackageUtils.launchApplicationByPackageName(HaloApp.getInstance(), 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 {
|
||
val config = Config.getVSettingEntity()?.va
|
||
return config?.arch64 != null && PackageUtils.isInstalledFromAllPackage(context, config.arch64.packageName)
|
||
}
|
||
|
||
/**
|
||
* 显示下载畅玩空间弹窗
|
||
*/
|
||
fun showVSpaceDialog(context: Context, gameEntity: GameEntity?) {
|
||
VSpaceDialogFragment.showDownloadDialog(
|
||
context,
|
||
appEntity64 = getVSpaceDownloadEntity(true),
|
||
appEntity32 = getVSpaceDownloadEntity(false),
|
||
gameEntity = gameEntity
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 获取数据库里所有的畅玩游戏
|
||
*
|
||
* (查询数据库,请在工作线程调用)
|
||
*/
|
||
@WorkerThread
|
||
fun getAllVGame(): List<VGameEntity> = mVGameDao.getAll()
|
||
|
||
/**
|
||
* 获取库里全部畅玩游戏列表的内存快照
|
||
*/
|
||
fun getAllVGameSnapshots(): ArrayList<VGameEntity> = ArrayList(mVGameSnapshotList)
|
||
|
||
/**
|
||
* 获取内存里的畅玩游戏快照
|
||
*
|
||
* @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()
|
||
}
|
||
|
||
/**
|
||
* 是否已安装此包名的畅玩游戏
|
||
*/
|
||
@JvmStatic
|
||
fun isInstalled(packageName: String?): Boolean {
|
||
val isInstalled = mInstalledInfoList.any { it.packageName == packageName }
|
||
|
||
Utils.log(LOG_TAG, "$packageName 已安装列表里 -> $isInstalled")
|
||
return isInstalled
|
||
}
|
||
|
||
/**
|
||
* 启动成功,五秒内退出才显示反馈弹框
|
||
*/
|
||
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 try {
|
||
mDelegateManager.getAppOccupiedSpace(packageName)
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
0
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在执行 callback 前先检查组件是否已安装,是否可下载
|
||
*/
|
||
fun validateVSpaceBeforeAction(context: Context, gameEntity: GameEntity?, callback: () -> Unit) {
|
||
// 仅下载类型为畅玩的类型才执行判断
|
||
if (gameEntity == null || gameEntity.isVGame()) {
|
||
if (showDialogIfVSpaceIsNeeded(
|
||
context,
|
||
gameEntity?.id ?: "",
|
||
gameEntity?.name ?: "",
|
||
gameEntity?.getApk()?.get(0)?.bit ?: ""
|
||
)
|
||
) {
|
||
mBit = gameEntity?.getApk()?.get(0)?.bit ?: ""
|
||
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?.getApk()?.get(0)?.bit == "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?.getApk()?.get(0)?.bit
|
||
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.theme_font.toColor(context))
|
||
}
|
||
|
||
it.confirmTv.setTextColor(R.color.text_subtitle.toColor(context))
|
||
}
|
||
)
|
||
return
|
||
}
|
||
|
||
callback.invoke()
|
||
} else {
|
||
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}")
|
||
|
||
if (showDialogIfVSpaceIsNeeded(
|
||
context,
|
||
downloadEntity.gameId,
|
||
downloadEntity.name,
|
||
downloadEntity.getMetaExtra(KEY_BIT)
|
||
)
|
||
) return
|
||
|
||
// 更新此包名对应的 gameId Map
|
||
mTempPackageNameAndGameIdMap[downloadEntity.packageName] = downloadEntity.gameId
|
||
|
||
// 当且仅当
|
||
// 1. 全局安装完成启动游戏开关打开
|
||
// 2. 服务连接不成功或是手动触发的安装
|
||
// 3. 不需要谷歌框架或者已经安装好谷歌框架
|
||
// 才启用安装完成后自动启动游戏
|
||
val shouldLaunchGameAfterInstallation =
|
||
mShouldLaunchGameAfterInstallation
|
||
&& (!mIsServiceConnected || isManualInstall)
|
||
&& (!isGAppsRequired(downloadEntity) || isGAppsInstalled())
|
||
|
||
// 恢复安装完启动游戏开关
|
||
mShouldLaunchGameAfterInstallation = true
|
||
|
||
connectService {
|
||
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) || isManualInstall) {
|
||
try {
|
||
mInstallingVaPathSet.add(path)
|
||
|
||
runOnUiThread {
|
||
val intent = VirtualAppManager.getInstallIntent(context, path, downloadEntity.packageName)
|
||
|
||
Utils.log(LOG_TAG, "正在安装 ${downloadEntity.packageName}")
|
||
context.startActivity(intent)
|
||
}
|
||
} catch (e: Exception) {
|
||
ToastUtils.toast(e.localizedMessage ?: "")
|
||
}
|
||
}
|
||
}
|
||
|
||
if (mDelegateManager.isConnectAidlInterface) {
|
||
installClosure.invoke()
|
||
} else {
|
||
connectService(
|
||
shouldCheckUpdate = false,
|
||
shouldConnectSilently = false,
|
||
callbackClosure = installClosure
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量安装
|
||
* 安装完成后不插入数据库,目前应用于谷歌框架安装
|
||
*/
|
||
fun batchInstall(context: Context, installMap: HashMap<String, File>) {
|
||
Utils.log(LOG_TAG, "尝试批量安装 ${installMap.size} 个新应用")
|
||
|
||
mBatchInstallMap = installMap
|
||
mBatchInstallFailedCount = 0
|
||
install(context, installMap.values.first())
|
||
}
|
||
|
||
/**
|
||
* 注册批量安装结果回调,值为空时为取消注册
|
||
*/
|
||
fun registerBatchInstallListener(listener: ((isSuccess: Boolean) -> Unit)? = null) {
|
||
mBatchInstallListener = listener
|
||
}
|
||
|
||
/**
|
||
* 安装新应用
|
||
* 不会作为游戏存储,目前应用于谷歌框架
|
||
* 因为是静默安装,所以不检查其它限制条件
|
||
*/
|
||
private fun install(context: Context, file: File) {
|
||
Utils.log(LOG_TAG, "尝试安装新应用 ${file.name}")
|
||
|
||
val packageName = file.nameWithoutExtension
|
||
|
||
val installClosure: () -> Unit = {
|
||
val path = file.path
|
||
// 正在安装中,忽略重复调用 (手动安装时强行再安装,避免安装结果没有回调时永远卡住,无法安装)
|
||
if (!mInstallingVaPathSet.contains(path)) {
|
||
try {
|
||
mInstallingVaPathSet.add(path)
|
||
|
||
runOnUiThread {
|
||
val intent = VirtualAppManager.getInstallIntent(context, path, packageName)
|
||
|
||
Utils.log(LOG_TAG, "正在安装 $packageName")
|
||
context.startActivity(intent)
|
||
}
|
||
} catch (e: Exception) {
|
||
Utils.log(LOG_TAG, "安装异常,${e.localizedMessage}")
|
||
}
|
||
}
|
||
}
|
||
|
||
if (mDelegateManager.isConnectAidlInterface) {
|
||
installClosure.invoke()
|
||
} else {
|
||
connectService(shouldCheckUpdate = false, shouldConnectSilently = false, callbackClosure = installClosure)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 禁用安装完直接启动游戏 (仅此次安装)
|
||
*/
|
||
fun disableLaunchGameAfterInstallation() {
|
||
mShouldLaunchGameAfterInstallation = false
|
||
}
|
||
|
||
/**
|
||
* 安装完是否直接启动游戏
|
||
*/
|
||
fun shouldLaunchGameAfterInstallation() = mShouldLaunchGameAfterInstallation
|
||
|
||
/**
|
||
* 安装完成回调
|
||
*/
|
||
fun onInstallFinished(packageName: String, result: VGameInstallerResult) {
|
||
runOnIoThread {
|
||
val gameId = mTempPackageNameAndGameIdMap[packageName] ?: ""
|
||
|
||
val downloadEntity = getVDownloadEntity(gameId = gameId, packageName = packageName)
|
||
|
||
if (downloadEntity != null) {
|
||
// 去掉更新标记
|
||
downloadEntity.isUpdate = false
|
||
mVGameDao.insert(VGameEntity.from(downloadEntity))
|
||
|
||
if (result.status == 0) {
|
||
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}")
|
||
}
|
||
|
||
mInstallingVaPathSet.remove(downloadEntity.path)
|
||
|
||
mPackageInstalledLiveData.postValue(result.packageName)
|
||
|
||
VBackupHelper.backupDBToExternalStorage(HaloApp.getInstance())
|
||
} else {
|
||
// downloadEntity 为空,可能是框架类的安装 (走另外一个下载管理)
|
||
if (result.status == 0) {
|
||
updateInstalledList()
|
||
} else {
|
||
Utils.log(LOG_TAG, "安装出现异常, ${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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 游戏是否正在安装中
|
||
*/
|
||
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 获取)
|
||
* 下载管理不存在时从畅玩游戏数据库的里获取
|
||
*/
|
||
private fun getVDownloadEntity(gameId: String, packageName: String): DownloadEntity? {
|
||
return DownloadManager.getInstance().getDownloadEntitySnapshot("", gameId, true)
|
||
?: getVDownloadEntitySnapshot(gameId = gameId, packageName = packageName)
|
||
}
|
||
|
||
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.core.provider/download_game")
|
||
HaloApp.getInstance().contentResolver.insert(uri, values)
|
||
} catch (e: Throwable) {
|
||
if (!fromRetry) {
|
||
insertInstalledGameToProvider(downloadEntity, true)
|
||
} else {
|
||
SentryHelper.onEvent("INSERT_GAME_TO_PROVIDER_ERROR", "packName", downloadEntity.packageName)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun installOrLaunch(context: Context, downloadEntity: DownloadEntity) {
|
||
Utils.log(LOG_TAG, "检测是需要安装还是启动 ${downloadEntity.gameId}")
|
||
|
||
installOrLaunch(
|
||
context,
|
||
downloadEntity.packageName,
|
||
downloadEntity.gameId,
|
||
downloadEntity.name,
|
||
downloadEntity.getMetaExtra(KEY_BIT)
|
||
)
|
||
}
|
||
|
||
@JvmStatic
|
||
fun installOrLaunch(context: Context, gameEntity: GameEntity) {
|
||
Utils.log(LOG_TAG, "检测是需要安装还是启动 ${gameEntity.id}")
|
||
|
||
installOrLaunch(
|
||
context,
|
||
gameEntity.getUniquePackageName() ?: "",
|
||
gameEntity.id,
|
||
gameEntity.name ?: "",
|
||
gameEntity.getApk()[0].bit
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 安装或启动应用
|
||
*/
|
||
private fun installOrLaunch(context: Context, packageName: String, gameId: String, gameName: String, bit: String) {
|
||
Utils.log(LOG_TAG, "检测是需要安装还是启动 $packageName")
|
||
|
||
validateVSpaceBeforeAction(context, GameEntity().apply {
|
||
id = gameId
|
||
name = gameName
|
||
downloadStatus = "smooth"
|
||
setApk(arrayListOf(ApkEntity(packageName = packageName, bit = bit)))
|
||
}) {
|
||
if (isInstalled(packageName)) {
|
||
launch(context, packageName)
|
||
} else {
|
||
// 这里重新检查一遍是否已安装,因为有可能因为上一次更新列表失败而出现重新下载的情况
|
||
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")
|
||
|
||
// 置空下载挂起回调(能 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) && !isGAppsInstalled()) {
|
||
// show dialog
|
||
val currentActivity = CurrentActivityHolder.getCurrentActivity() as? AppCompatActivity ?: return
|
||
GAppsDownloadDialogFragment
|
||
.getInstance(packageName, gameId, gameName)
|
||
.show(currentActivity.supportFragmentManager, "G_APPS_FRAGMENT")
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (showLoading) {
|
||
val intent = mDelegateManager.getStartGameIntent(
|
||
packageName,
|
||
gameId,
|
||
gameName,
|
||
gameIcon,
|
||
MetaUtil.getBase64EncodedAndroidId(),
|
||
HaloApp.getInstance().gid,
|
||
com.gh.gamecenter.BuildConfig.VERSION_NAME,
|
||
HaloApp.getInstance().channel
|
||
)
|
||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||
context.startActivity(intent)
|
||
} else {
|
||
VirtualAppManager.get().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) {
|
||
mVGameDao.insert(VGameEntity.from(downloadEntity))
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
ToastUtils.toast(e.localizedMessage ?: "")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 卸载应用
|
||
*/
|
||
fun uninstall(packageName: String?) {
|
||
Utils.log(LOG_TAG, "卸载游戏 $packageName")
|
||
|
||
if (packageName.isNullOrBlank()) return
|
||
|
||
mVGameDao.delete(packageName)
|
||
|
||
if (mVGameDao.getAll().isEmpty()) {
|
||
VBackupHelper.removeAllDatabase()
|
||
} else {
|
||
VBackupHelper.backupDBToExternalStorage(HaloApp.getInstance())
|
||
}
|
||
|
||
val uninstallClosure: () -> Unit = {
|
||
try {
|
||
val result = VirtualAppManager.get().uninstallGame(packageName)
|
||
if (result) {
|
||
updateInstalledList()
|
||
// 卸载的时候移除已安装的包名历史
|
||
mPackageInstalledLiveData.postValue("")
|
||
}
|
||
|
||
Utils.log(LOG_TAG, "卸载应用结果 -> $result")
|
||
} catch (e: Exception) {
|
||
ToastUtils.toast(e.localizedMessage ?: "")
|
||
}
|
||
}
|
||
|
||
if (mDelegateManager.isConnectAidlInterface) {
|
||
uninstallClosure.invoke()
|
||
} else {
|
||
connectService(shouldCheckUpdate = false, shouldConnectSilently = false, callbackClosure = uninstallClosure)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取畅玩助手版本号
|
||
*/
|
||
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 {
|
||
mVGameDao.insert(it)
|
||
} 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, 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),
|
||
gameId = gameId,
|
||
gameName = gameName,
|
||
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 = getVUrl(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.removeUpdate(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)
|
||
|
||
HistoryHelper.insertGameEntity(gameEntity)
|
||
DownloadManager.getInstance().add(originDownloadEntity)
|
||
|
||
// 收集下载数据
|
||
DataCollectionUtils.uploadDownload(HaloApp.getInstance(), originDownloadEntity, "开始")
|
||
|
||
SensorsBridge.trackEvent(
|
||
"HaloFunGameDownloadClick",
|
||
"game_name", originDownloadEntity.name,
|
||
"game_id", originDownloadEntity.gameId
|
||
)
|
||
}
|
||
|
||
/***
|
||
* 实体转换为普通游戏实体
|
||
*/
|
||
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
|
||
)
|
||
)
|
||
)
|
||
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 = "smooth"
|
||
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())
|
||
}
|
||
|
||
/**
|
||
* 获取畅玩游戏的下载地址(为了区分测试时与普通游戏一样的下载地址)
|
||
*/
|
||
fun getVUrl(originUrl: String?): String {
|
||
if (originUrl?.endsWith("type=v") == false) {
|
||
return if (originUrl.contains("?")) {
|
||
"$originUrl&type=v"
|
||
} else {
|
||
"$originUrl?type=v"
|
||
}
|
||
}
|
||
return originUrl ?: ""
|
||
}
|
||
|
||
/**
|
||
* 获取未经畅玩处理的原始下载地址
|
||
*/
|
||
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()
|
||
|
||
/**
|
||
* 畅玩功能是否启用
|
||
*/
|
||
@JvmStatic
|
||
fun isVGameOn() = BuildConfig.DEBUG
|
||
|| (BuildConfig.IS_VGAME_ON && Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1 && Config.isVGameEnabled())
|
||
|
||
/**
|
||
* 检查畅玩游戏是否满足生成存档要求
|
||
*/
|
||
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
|
||
*/
|
||
fun isGAppsRequired(gameEntity: GameEntity?): Boolean {
|
||
return gameEntity?.gAppsSwitch == "on"
|
||
}
|
||
|
||
/**
|
||
* 是否已经成功安装了 GApps
|
||
*/
|
||
fun isGAppsInstalled(): Boolean {
|
||
return isInstalled(G_GMS_PACKAGE_NAME)
|
||
&& isInstalled(G_GSF_PACKAGE_NAME)
|
||
&& isInstalled(G_VENDING_PACKAGE_NAME)
|
||
}
|
||
|
||
} |