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

606 lines
29 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

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

package com.gh.common.util
import com.gh.common.constant.Config
import com.gh.common.exposure.ExposureUtils
import com.gh.common.simulator.SimulatorDownloadManager
import com.gh.common.simulator.SimulatorGameManager
import com.gh.common.xapk.XapkInstaller
import com.gh.download.DownloadDataHelper
import com.gh.download.DownloadManager
import com.gh.gamecenter.R
import com.gh.gamecenter.ShellActivity
import com.gh.gamecenter.common.base.GlobalActivityManager.getCurrentPageEntity
import com.gh.gamecenter.common.base.GlobalActivityManager.getLastPageEntity
import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.common.callback.ConfirmListener
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.entity.SimpleGameEntity
import com.gh.gamecenter.common.entity.SuggestType
import com.gh.gamecenter.common.eventbus.EBShowDialog
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.common.utils.NewFlatLogUtils
import com.gh.gamecenter.core.utils.GsonUtils
import com.gh.gamecenter.core.utils.MtaHelper
import com.gh.gamecenter.core.utils.SPUtils
import com.gh.gamecenter.core.utils.ToastUtils
import com.gh.gamecenter.eventbus.EBDownloadStatus
import com.gh.gamecenter.feature.entity.CustomPageTrackData
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.SimulatorEntity
import com.gh.gamecenter.feature.utils.PlatformUtils
import com.gh.gamecenter.help.HelpAndFeedbackBridge
import com.gh.gamecenter.pkg.PkgHelper
import com.gh.ndownload.NDownloadBridge
import com.gh.vspace.VHelper
import com.halo.assistant.HaloApp
import com.lightgame.download.DataWatcher
import com.lightgame.download.DownloadConfig
import com.lightgame.download.DownloadEntity
import com.lightgame.download.DownloadStatus
import com.lightgame.utils.AppManager
import com.lightgame.utils.Utils
import org.greenrobot.eventbus.EventBus
object DownloadObserver {
private val mApplication = HaloApp.getInstance().application
// 简单 debounce 因为内存更新 downloadEntity 对象造成的触发双重下载完成事件
// TODO 修复因为更改内存对象造成的双重下载完成事件问题,具体触发代码见 DownloadDao.updateSnapshotList
private var mDoneDebouncePair: Pair<String, Long>? = null
private const val TAG = "DownloadObserver"
private const val CORE_EVENT_DOWNLOAD_COMPLETE_LOGGED = "CORE_EVENT_DOWNLOAD_COMPLETE_LOGGED"
private val mRetryableHashMap = hashMapOf<String, Boolean>()
/**
* 当下载任务是 预约上线提醒 触发的,则所有弹窗均不显示
*/
private val DownloadEntity.isDownloadByReserveOnlineReminder: Boolean
get() = !meta?.get(Constants.IS_RESERVE_ONLINE_REMINDER).isNullOrBlank()
// 如果在WIFI状态下,下载自动暂停,则再重试一遍
@JvmStatic
fun initObserver() {
val dataWatcher = object : DataWatcher() {
override fun onDataChanged(downloadEntity: DownloadEntity) {
// todo 如何处理xapk安装问题
val xapkStatus = downloadEntity.meta[XapkInstaller.XAPK_UNZIP_STATUS]
if (!xapkStatus.isNullOrEmpty()) return
val gameId = downloadEntity.gameId
val downloadManager = DownloadManager.getInstance()
tryCatchInRelease {
DownloadDataHelper.uploadDownloadEvent(downloadEntity)
}
val status = downloadEntity.status
if (DownloadStatus.hijack == status) {
// 链接被劫持
if (downloadEntity.isDownloadByReserveOnlineReminder) {
return
}
processHijack(downloadEntity)
return
} else if (DownloadStatus.notfound == status) {
// 404 Not Found
// 删除任务
downloadEntity.status = DownloadStatus.cancel
downloadManager.cancel(downloadEntity.url)
Utils.toast(mApplication, "该链接已失效!请联系管理员。")
val currentActivity = AppManager.getInstance().currentActivity() ?: return
SensorsBridge.trackDownloadLinkRotDialogShow(
gameId = downloadEntity.gameId,
gameName = downloadEntity.name,
gameType = downloadEntity.categoryChinese
)
if (downloadEntity.isDownloadByReserveOnlineReminder) {
return
}
DialogHelper.showDialog(
currentActivity,
"下载失败",
"下载链接已失效,建议提交反馈",
"立即反馈",
"取消",
confirmClickCallback = {
HelpAndFeedbackBridge.startSuggestionActivity(
currentActivity,
SuggestType.GAME, "notfound",
"问题反馈:下载链接失效",
SimpleGameEntity(gameId, downloadEntity.name, "")
)
SensorsBridge.trackDownloadLinkRotDialogClick(
buttonName = "立即反馈",
gameId = downloadEntity.gameId,
gameName = downloadEntity.name,
gameType = downloadEntity.categoryChinese
)
},
cancelClickCallback = {
SensorsBridge.trackDownloadLinkRotDialogClick(
buttonName = "取消",
gameId = downloadEntity.gameId,
gameName = downloadEntity.name,
gameType = downloadEntity.categoryChinese
)
},
touchOutsideCallback = {
SensorsBridge.trackDownloadLinkRotDialogClick(
buttonName = "关闭弹窗",
gameId = downloadEntity.gameId,
gameName = downloadEntity.name,
gameType = downloadEntity.categoryChinese
)
},
extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true)
)
return
} else if (DownloadStatus.neterror == status
|| DownloadStatus.diskioerror == status
|| DownloadStatus.timeout == status
) {
if (mRetryableHashMap[downloadEntity.url] == true
&& NetworkUtils.isWifiConnected(HaloApp.getInstance().application)
) {
downloadManager.resumeDownload(downloadEntity.url)
mRetryableHashMap[downloadEntity.url] = false
Utils.log(TAG, "下载重试->" + downloadEntity.toJson())
} else {
if (DownloadStatus.diskioerror == status) {
ToastUtils.toast("磁盘 IO 异常,请稍后重试")
} else {
ToastUtils.toast("网络不稳定,下载任务已暂停")
}
DataLogUtils.uploadNeterrorLog(mApplication, downloadEntity)
Utils.log(TAG, "下载自动暂停->" + downloadEntity.toJson())
}
} else if (DownloadStatus.diskisfull == status) {
ToastUtils.toast("磁盘已满,请清理空间后重试下载")
downloadManager.pause(downloadEntity.url)
} else if (DownloadStatus.redirected == status) {
Utils.log(TAG, "重定向完毕")
DownloadDataHelper.uploadRedirectEvent(downloadEntity)
} else if (DownloadStatus.unqualified == status) {
// 未成年
if (!downloadEntity.isDownloadByReserveOnlineReminder) {
RealNameHelper.showRealNameUnqualifiedDialog(downloadEntity)
}
// 删除任务
downloadEntity.status = DownloadStatus.cancel
downloadManager.cancel(downloadEntity.url)
} else if (DownloadStatus.unavailable == status) {
// 未接入防沉迷系统
val currentActivity = AppManager.getInstance().currentActivity()
if (currentActivity != null && !downloadEntity.isDownloadByReserveOnlineReminder) {
DialogHelper.showDialog(
context = currentActivity,
title = "温馨提示",
content = "该游戏未接入防沉迷系统,暂不支持下载",
confirmText = "知道了",
cancelText = ""
)
} else {
ToastUtils.toast("该游戏未接入防沉迷系统,暂不支持下载")
}
// 删除任务
downloadEntity.status = DownloadStatus.cancel
downloadManager.cancel(downloadEntity.url)
} else if (DownloadStatus.isCertificating == status) {
// 未接入防沉迷系统
val currentActivity = AppManager.getInstance().currentActivity()
if (currentActivity != null && !downloadEntity.isDownloadByReserveOnlineReminder) {
DialogHelper.showDialog(
context = currentActivity,
title = "实名提示",
content = "您当前的身份信息正在认证中,根据相关政策要求,该游戏需通过认证后才能进行下载",
cancelText = "知道了",
confirmText = "查看实名认证",
cancelClickCallback = null,
confirmClickCallback = {
currentActivity.startActivity(
ShellActivity.getIntent(
currentActivity,
ShellActivity.Type.REAL_NAME_INFO,
)
)
}
)
} else {
ToastUtils.toast("您当前的身份信息正在认证中,根据相关政策要求,该游戏需通过认证后才能进行下载")
}
// 删除任务
downloadEntity.status = DownloadStatus.cancel
downloadManager.cancel(downloadEntity.url)
} else if (DownloadStatus.uncertificated == status) {
// 未实名
if (!downloadEntity.isDownloadByReserveOnlineReminder) {
RealNameHelper.showRealNameUncertificatedDialog(downloadEntity)
}
// 删除任务
downloadEntity.status = DownloadStatus.cancel
downloadManager.cancel(downloadEntity.url)
} else if (DownloadStatus.banned == status) {
ToastUtils.showToast("网络异常")
// 删除任务
downloadEntity.status = DownloadStatus.cancel
downloadManager.cancel(downloadEntity.url)
}
if (DownloadStatus.done == status) {
if (mDoneDebouncePair?.first != downloadEntity.url) {
mDoneDebouncePair = Pair(downloadEntity.url, System.currentTimeMillis())
// 清理临时变量
downloadEntity.addMetaExtra(Constants.IS_RESERVE_ONLINE_REMINDER, "")
performDownloadCompleteAction(downloadEntity, downloadManager)
} else {
if (mDoneDebouncePair?.second == 0L
|| System.currentTimeMillis() - (mDoneDebouncePair?.second ?: 0) > 500
) {
// 清理临时变量
downloadEntity.addMetaExtra(Constants.IS_RESERVE_ONLINE_REMINDER, "")
performDownloadCompleteAction(downloadEntity, downloadManager)
}
}
mRetryableHashMap.remove(downloadEntity.url)
EventBus.getDefault().post(EBDownloadStatus("done", "", "", "", downloadEntity.packageName, ""))
}
DownloadNotificationHelper.addOrUpdateDownloadNotification(downloadEntity)
// 如果已下载大小发生变化,表示成功恢复下载,则重置重试标记
if (status == DownloadStatus.downloading) {
mRetryableHashMap[downloadEntity.url] = true
}
}
}
// 添加观察者
DownloadManager.getInstance().addObserver(dataWatcher)
}
private fun performDownloadCompleteAction(
downloadEntity: DownloadEntity,
downloadManager: DownloadManager
) {
if (downloadEntity.name.contains(mApplication.getString(R.string.app_name))) {
statDoneEvent(downloadEntity)
MtaHelper.onEvent("软件更新", "下载完成")
// 会有 ActivityNotFoundException 异常catch 掉不管了
tryWithDefaultCatch {
if (Constants.SILENT_UPDATE != downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE)) {
NewFlatLogUtils.logGameInstall(
gameId = downloadEntity.gameId ?: "",
gameName = downloadEntity.name ?: "",
trigger = "自动安装"
)
SensorsBridge.trackInstallGameClick(
gameName = downloadEntity.name,
gameId = downloadEntity.gameId,
action = "自动安装"
)
// TODO 在 Android 11 上没有授权安装未知应用的权限前第一次调用这个方法系统会杀掉我们的进程...
// 没能找到类似的解释,最接近的是这个 https://issuetracker.google.com/issues/154157387但也只是点授权杀进程而已
PackageInstaller.install(mApplication, downloadEntity);
DataLogUtils.uploadUpgradeLog(mApplication, "install") //上传更新安装数据
}
}
} else {
statDoneEvent(downloadEntity)
logCoreEventIfNeeded(downloadEntity.getGameCategory())
GameActivityDownloadHelper.clear()
val platform = PlatformUtils.getInstance(mApplication)
.getPlatformName(downloadEntity.platform)
if (platform != null) {
when {
// TODO 插件化传 path 从 apk 中获取 packageName 的形式有可能出现拿不到 packageName 的情况
downloadEntity.isPluggable -> // 弹出插件化提示框
EventBus.getDefault().post(
EBShowDialog(
BaseActivity.PLUGGABLE,
downloadEntity.path,
downloadEntity
)
)
downloadEntity.isPlugin -> Utils.toast(
mApplication,
downloadEntity.name + " - " + platform + " - 下载完成"
)
else -> {
if (!downloadEntity.asVGame()) {
Utils.toast(mApplication, downloadEntity.name + " - 下载完成")
}
}
}
} else {
if (!downloadEntity.asVGame()) {
Utils.toast(mApplication, downloadEntity.name + " - 下载完成")
}
}
if (!downloadEntity.isPluggable) {
if (downloadEntity.isSimulatorGame()) {
val simulatorJson = downloadEntity.getMetaExtra(Constants.SIMULATOR)
val gameName = downloadEntity.getMetaExtra(Constants.GAME_NAME)
if (simulatorJson.isEmpty()) return
var simulator = GsonUtils.fromJson(simulatorJson, SimulatorEntity::class.java)
val isInstalledNewSimulator =
SimulatorGameManager.isNewSimulatorInstalled(HaloApp.getInstance().application)
val isInstalledOldSimulator =
SimulatorGameManager.isOldSimulatorInstalled(HaloApp.getInstance().application)
val currentActivity = AppManager.getInstance().currentActivity()
?: return
val newSimulator = Config.getNewSimulatorEntitySetting()
if ((!isInstalledOldSimulator && newSimulator != null && newSimulator.active) || isInstalledNewSimulator) { //如果没有安装任一旧的模拟器 或者下载了新模拟器 则使用新版本模拟器
simulator = newSimulator ?: simulator
}
SimulatorDownloadManager.getInstance().showDownloadDialog(
currentActivity,
simulator,
SimulatorDownloadManager.SimulatorLocation.LAUNCH,
downloadEntity.gameId,
gameName,
downloadEntity.categoryChinese,
null
)
SimulatorGameManager.recordDownloadSimulatorGame(downloadEntity.gameId, simulator.type)
SimulatorGameManager.postPlayedGame(downloadEntity.gameId, downloadEntity.packageName)
} else {
val downloadType = downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE)
// 是否是自动安装
val isAutoInstall = SPUtils.getBoolean(Constants.SP_AUTO_INSTALL, true)
if (downloadType != Constants.VSPACE_32_DOWNLOAD_ONLY && (downloadType == Constants.SIMULATOR_DOWNLOAD || isAutoInstall)) {
if (FileUtils.isEmptyFile(downloadEntity.path)) {
Utils.toast(mApplication, com.gh.gamecenter.common.R.string.install_failure_hint)
downloadManager.cancel(downloadEntity.url)
} else {
if (PackageUtils.isCanLaunchSetup(mApplication, downloadEntity.path)
|| downloadType == Constants.VGAME
|| downloadType == Constants.DUAL_DOWNLOAD_VGAME
) {
downloadEntity.meta[Constants.MARK_ALREADY_TRIGGERED_INSTALLATION] = "YES"
tryWithDefaultCatch {
NewFlatLogUtils.logGameInstall(
gameId = downloadEntity.gameId ?: "",
gameName = downloadEntity.name ?: "",
trigger = "自动安装"
)
SensorsBridge.trackInstallGameClick(
gameId = downloadEntity.gameId,
gameName = downloadEntity.name,
action = "自动安装"
)
PackageInstaller.install(mApplication, downloadEntity, false, ignoreAsVGame = false)
}
} else {
// 弹出卸载提示框
if (downloadEntity.isPlugin) {
EventBus.getDefault().post(
EBShowDialog(
BaseActivity.PLUGGABLE,
downloadEntity.path,
downloadEntity
)
)
} else {
EventBus.getDefault().post(
EBShowDialog(
BaseActivity.SIGNATURE_CONFLICT,
downloadEntity.path,
downloadEntity
)
)
}
}
}
}
}
}
}
// 下载过程分析统计
// 部分设备 (已知 vivo 5.1.1) 在调用 packageManager.getPackageArchiveInfo 获取比较大的 APK 文件时会出现 ANR
// 这里为了让它能用就不判断是否解析包错误了
if (PackageUtils.isDeviceUnableToHandleBigApkFile(downloadEntity.path)) {
return
}
val pm = mApplication.packageManager
val packageInfo = pm.getPackageArchiveInfo(downloadEntity.path, 0)
if (packageInfo == null) {
val downloadType = downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE)
if (downloadType == Constants.SIMULATOR_DOWNLOAD) {
val currentActivity = AppManager.getInstance().currentActivity()
?: return
DialogUtils.showSimulatorParseErrorDialog(
currentActivity,
downloadEntity.gameId,
downloadEntity.name,
object : ConfirmListener {
override fun onConfirm() {
val simulator = HaloApp.get(downloadEntity.name, true) as? SimulatorEntity
?: return
DownloadManager.getInstance().cancel(downloadEntity.url, true, true, false)
SimulatorDownloadManager.getInstance()
.showDownloadDialog(
currentActivity,
simulator,
SimulatorDownloadManager.SimulatorLocation.SIMULATOR_GAME
)
}
})
}
}
}
// 统计下载完成事件
private fun statDoneEvent(downloadEntity: DownloadEntity) {
var type: ExposureUtils.DownloadType
if (downloadEntity.isUpdate) {
if (downloadEntity.asVGame()) {
type = ExposureUtils.DownloadType.FUN_UPDATE
} else {
type = ExposureUtils.DownloadType.UPDATE
if (downloadEntity.isPlugin) {
type = ExposureUtils.DownloadType.PLUGIN_UPDATE
}
}
} else {
type = if (downloadEntity.asVGame()) {
ExposureUtils.DownloadType.FUN_DOWNLOAD
} else {
ExposureUtils.DownloadType.DOWNLOAD
}
}
if (downloadEntity.isPluggable) {
type = ExposureUtils.DownloadType.PLUGIN_DOWNLOAD
}
var downloadSpeed = 0L
val elapsedTimeString = downloadEntity.meta[DownloadConfig.KEY_DOWNLOAD_ELAPSED_TIME]
if (elapsedTimeString != null) {
var elapsedTime = elapsedTimeString.toLong()
if (elapsedTime == 0L) {
elapsedTime = 1L
}
downloadSpeed = downloadEntity.size / elapsedTime
}
val isPlatformRecommend =
java.lang.Boolean.parseBoolean(downloadEntity.getMetaExtra(Constants.IS_PLATFORM_RECOMMEND))
val exposureEvent = ExposureUtils.logADownloadCompleteExposureEvent(
GameEntity(
_id = downloadEntity.gameId,
mName = downloadEntity.name.removeSuffix(Constants.GAME_NAME_DECORATOR),
gameVersion = downloadEntity.versionName ?: "",
isPlatformRecommend = isPlatformRecommend,
adIconActive = downloadEntity.meta[Constants.AD_ICON_ACTIVE].toBoolean(),
isAdData = downloadEntity.meta[Constants.IS_AD_DATA].toBoolean()
),
downloadEntity.platform,
downloadEntity.exposureTrace,
downloadSpeed,
downloadEntity.meta[DownloadEntity.DOWNLOAD_HOST_KEY] ?: "unknown",
downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown",
downloadEntity.meta[NDownloadBridge.REDIRECTED_URL_LIST] ?: "unknown",
type
)
if (downloadEntity.asVGame()) {
val customTrackDataJson = downloadEntity.customPageTrackDataJson
val kvs = if (customTrackDataJson.isNullOrBlank()) {
arrayOf<String>()
} else {
GsonUtils.fromJson(customTrackDataJson, CustomPageTrackData::class.java).toKV()
}
SensorsBridge.trackEventWithExposureSource(
"HaloFunGameDownloadDone",
exposureEvent?.source,
"game_name", downloadEntity.name,
"game_id", downloadEntity.gameId,
"game_type", downloadEntity.categoryChinese,
"game_schema_type", if (downloadEntity.getMetaExtra(Constants.KEY_BIT) == "32") "32位" else "64位",
*kvs
)
} else if (downloadEntity.gameId == Constants.HALO_FUN_GAME_ID) {
SensorsBridge.trackEvent(
"HaloFunDownloadDone",
"space_schema_type",
if (downloadEntity.packageName == VHelper.VSPACE_32BIT_PACKAGENAME) "32位" else "64位"
)
} else if (downloadEntity.gameId == Constants.HALO_FUN_NEW_32_GAME_ID) {
SensorsBridge.trackEvent(
"HaloFunDownloadDone",
"space_schema_type",
"32位(新)"
)
}
if (downloadEntity.gameId != Constants.GHZS_GAME_ID
&& downloadEntity.getMetaExtra(Constants.EXTRA_DOWNLOAD_TYPE) != Constants.SIMULATOR_DOWNLOAD
&& downloadEntity.gameId != Constants.HALO_FUN_GAME_ID
&& downloadEntity.gameId != Constants.HALO_FUN_NEW_32_GAME_ID
) {
val trackJson = downloadEntity.customPageTrackDataJson
val kvs = if (!trackJson.isNullOrBlank()) {
GsonUtils.fromJson(trackJson, CustomPageTrackData::class.java).toKV()
} else {
arrayOf()
}
SensorsBridge.trackEventWithExposureSource(
"DownloadProcessFinish",
exposureEvent?.source,
"game_id", downloadEntity.gameId,
"game_name", downloadEntity.meta[Constants.GAME_NAME] ?: "",
"game_type", downloadEntity.meta[Constants.GAME_CATEGORY_IN_CHINESE] ?: "",
"game_label", downloadEntity.tags.joinToString(","),
"game_schema_type", if (downloadEntity.getMetaExtra(Constants.KEY_BIT) == "32") "32位" else "64位",
"page_name", getCurrentPageEntity().pageName,
"page_id", getCurrentPageEntity().pageId,
"page_business_id", getCurrentPageEntity().pageBusinessId,
"last_page_name", getLastPageEntity().pageName,
"last_page_id", getLastPageEntity().pageId,
"last_page_business_id", getLastPageEntity().pageBusinessId,
"download_status", downloadEntity.meta[Constants.DOWNLOAD_STATUS_IN_CHINESE] ?: "",
"download_type", if (downloadEntity.asVGame()) "畅玩下载" else "本地下载",
*kvs
)
}
DataCollectionUtils.uploadDownload(mApplication, downloadEntity, "完成")
}
private fun processHijack(downloadEntity: DownloadEntity) {
// 删除任务
downloadEntity.status = DownloadStatus.cancel
DownloadManager.getInstance().cancel(downloadEntity.url)
// 弹出提示框
EventBus.getDefault().post(EBShowDialog(BaseActivity.DOWNLOAD_HIJACK))
// 记录链接被劫持
DataCollectionUtils.uploadHijack(mApplication, downloadEntity)
// 上传劫持log
DataLogUtils.uploadHijack(mApplication, downloadEntity)
}
/**
* 根据预设的游戏类型上报关键事件(下载完成事件)
*/
private fun logCoreEventIfNeeded(gameCategory: String) {
val category = PkgHelper.getCoreEventGameCategory()
val categoryMatched = if (category == "standard" || category.isNullOrEmpty()) {
true
} else {
gameCategory == category
}
if (!SPUtils.getBoolean(CORE_EVENT_DOWNLOAD_COMPLETE_LOGGED) && categoryMatched) {
HaloApp.getInstance().flavorProvider.logCoreEvent()
SPUtils.setBoolean(CORE_EVENT_DOWNLOAD_COMPLETE_LOGGED, true)
}
}
}