371 lines
13 KiB
Kotlin
371 lines
13 KiB
Kotlin
package com.gh.download.server
|
||
|
||
import android.content.Context
|
||
import android.os.Build
|
||
import android.util.Base64
|
||
import com.gh.common.constant.Config
|
||
import com.gh.common.util.DirectUtils
|
||
import com.gh.common.util.LogUtils
|
||
import com.gh.common.util.PackageUtils
|
||
import com.gh.gamecenter.ShellActivity
|
||
import com.gh.gamecenter.common.constant.Constants
|
||
import com.gh.gamecenter.common.entity.ExposureEntity
|
||
import com.gh.gamecenter.common.loghub.LoghubUtils
|
||
import com.gh.gamecenter.common.utils.DialogHelper
|
||
import com.gh.gamecenter.common.utils.SensorsBridge
|
||
import com.gh.gamecenter.common.utils.tryCatchInRelease
|
||
import com.gh.gamecenter.core.utils.EmptyCallback
|
||
import com.gh.gamecenter.core.utils.GsonUtils
|
||
import com.gh.gamecenter.core.utils.SPUtils
|
||
import com.gh.gamecenter.entity.NewSettingsEntity
|
||
import com.gh.gamecenter.feature.entity.GameEntity
|
||
import com.gh.gamecenter.feature.exposure.ExposureEvent
|
||
import com.google.gson.JsonObject
|
||
import com.halo.assistant.HaloApp
|
||
import com.lightgame.utils.Utils
|
||
import org.json.JSONArray
|
||
import org.json.JSONObject
|
||
import java.io.File
|
||
import java.net.URLEncoder
|
||
import java.util.*
|
||
import kotlin.collections.ArrayList
|
||
|
||
object BrowserInstallHelper {
|
||
|
||
private const val PORT = 40705
|
||
private const val RESERVE_PORT = 40706
|
||
|
||
private lateinit var mServer: DownloadServer
|
||
private val mFileNameSet by lazy { hashSetOf<String>() }
|
||
private val mContext by lazy { HaloApp.getInstance().application }
|
||
private var mUseReservePort = false
|
||
|
||
private var mValidInstalledPackageList: ArrayList<String> = arrayListOf()
|
||
private var mValidConditionMatchedCache: Boolean? = null
|
||
|
||
private fun getServer(port: Int): DownloadServer {
|
||
val server = DownloadServer(port)
|
||
for (packageName in getAllInstalledPackageList()) {
|
||
if (packageName.contains("com.freeme") || packageName.contains("com.zhuoyi")) {
|
||
server.isBuggyDevice = true
|
||
break
|
||
}
|
||
}
|
||
return server
|
||
}
|
||
|
||
private fun getAllInstalledPackageList(): ArrayList<String> {
|
||
when {
|
||
mValidInstalledPackageList.isNotEmpty() -> {
|
||
return mValidInstalledPackageList
|
||
}
|
||
|
||
else -> {
|
||
val allInstalledPackageList = PackageUtils.getAllPackageNameIncludeSystemApps(mContext)
|
||
|
||
if (allInstalledPackageList.isNotEmpty()) {
|
||
mValidInstalledPackageList = allInstalledPackageList
|
||
}
|
||
return mValidInstalledPackageList
|
||
}
|
||
}
|
||
}
|
||
|
||
fun downloadFile(filePath: String) {
|
||
if (!::mServer.isInitialized) mServer = if (mUseReservePort) getServer(RESERVE_PORT) else getServer(PORT)
|
||
if (!mServer.isAlive && !startServer()) {
|
||
// 端口占用时使用备用端口重试
|
||
mUseReservePort = true
|
||
mServer = getServer(RESERVE_PORT)
|
||
if (!mServer.isAlive) retryStartServer()
|
||
}
|
||
|
||
var fileName = filePath.substringAfterLast(File.separator)
|
||
// 山寨机没有 .apk 后缀但有 Content-Type 管用,还是给它补上 .apk 后缀
|
||
if (!mServer.isBuggyDevice) {
|
||
fileName = fileName.removeSuffix(DownloadServer.APK_SUFFIX)
|
||
}
|
||
val downloadUrl =
|
||
if (mUseReservePort) "http://127.0.0.1:$RESERVE_PORT/$fileName" else "http://127.0.0.1:$PORT/$fileName"
|
||
|
||
mFileNameSet.add(fileName)
|
||
if (mServer.isBuggyDevice) {
|
||
val encodedString =
|
||
Base64.encodeToString(URLEncoder.encode(downloadUrl).trim().toByteArray(), Base64.NO_WRAP)
|
||
DirectUtils.directToExternalBrowser(
|
||
mContext,
|
||
"https://down-and.ghzs6.com/redirect?location=base64($encodedString)"
|
||
)
|
||
} else {
|
||
DirectUtils.directToExternalBrowser(mContext, downloadUrl)
|
||
}
|
||
}
|
||
|
||
@JvmStatic
|
||
fun shouldUseBrowserToInstall(): Boolean {
|
||
if (SPUtils.getBoolean(Constants.SP_USE_BROWSER_TO_INSTALL)) {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
@JvmStatic
|
||
fun shouldAutoSwitchAssistantInstall(gameEntity: GameEntity): Boolean =
|
||
isUseBrowserToInstallEnabled() && shouldUseBrowserToInstall() && gameEntity.isSplitXApk()
|
||
|
||
/**
|
||
* 是否显示使用浏览器来安装弹窗
|
||
*/
|
||
private fun shouldShowUseBrowserToInstallHint(): Boolean {
|
||
return if (isUseBrowserToInstallEnabled()) {
|
||
if (SPUtils.getBoolean(Constants.SP_USE_BROWSER_TO_INSTALL)) {
|
||
false
|
||
} else {
|
||
SPUtils.getBoolean(Constants.SP_SHOULD_SHOW_USE_BROWSER_TO_INSTALL_HINT, true)
|
||
}
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 是否显示游戏详情页使用浏览器安装提示
|
||
*/
|
||
fun shouldShowGameDetailUseBrowserToInstallHint(): Boolean {
|
||
return if (isUseBrowserToInstallEnabled()) {
|
||
// if (SPUtils.getBoolean(Constants.SP_USE_BROWSER_TO_INSTALL)) {
|
||
// false
|
||
// } else {
|
||
SPUtils.getBoolean(Constants.SP_SHOULD_SHOW_GAMEDETAIL_USE_BROWSER_TO_INSTALL_HINT, true)
|
||
// }
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
fun hideGameDetailUseBrowserToInstallHint() {
|
||
SPUtils.setBoolean(Constants.SP_SHOULD_SHOW_GAMEDETAIL_USE_BROWSER_TO_INSTALL_HINT, false)
|
||
}
|
||
|
||
@JvmStatic
|
||
fun showBrowserInstallHintDialog(
|
||
context: Context,
|
||
gameEntity: GameEntity,
|
||
skipBrowserInstallDialog: Boolean = false,
|
||
callback: EmptyCallback
|
||
) {
|
||
if (skipBrowserInstallDialog || !shouldShowUseBrowserToInstallHint()) {
|
||
callback.onCallback()
|
||
return
|
||
}
|
||
|
||
logOrdinaryBrowserEvent(Type.SWITCH_INSTALL_DIALOG)
|
||
SPUtils.setBoolean(Constants.SP_SHOULD_SHOW_USE_BROWSER_TO_INSTALL_HINT, false)
|
||
|
||
val manufacturer = Build.MANUFACTURER.toUpperCase(Locale.CHINA)
|
||
var contentText = "当前安装方式为助手安装,如果出现游戏无法安装的问题,可选择切换安装方式为“浏览器安装”"
|
||
if (manufacturer == "OPPO" || manufacturer == "VIVO") {
|
||
contentText =
|
||
"当前安装方式为助手安装,下载安装游戏需要验证账户密码或指纹。如需免密码安装,可选择切换安装方式为“浏览器安装”"
|
||
}
|
||
|
||
SensorsBridge.trackSwitchInstallDialogShow(
|
||
gameId = gameEntity.id,
|
||
gameName = gameEntity.name ?: "",
|
||
gameType = gameEntity.categoryChinese
|
||
)
|
||
|
||
DialogHelper.showDialog(
|
||
context,
|
||
title = "温馨提示",
|
||
content = contentText,
|
||
confirmText = "切换安装方式",
|
||
cancelText = "继续下载",
|
||
confirmClickCallback = {
|
||
val intent = ShellActivity.getIntent(context, ShellActivity.Type.SWITCH_INSTALL_METHOD, null)
|
||
context.startActivity(intent)
|
||
logOrdinaryBrowserEvent(Type.SWITCH_INSTALL_DIALOG_ACCESS)
|
||
SensorsBridge.trackSwitchInstallDialogClick(
|
||
buttonName = "切换安装方式",
|
||
gameId = gameEntity.id,
|
||
gameName = gameEntity.name ?: "",
|
||
gameType = gameEntity.categoryChinese
|
||
)
|
||
},
|
||
cancelClickCallback = {
|
||
callback.onCallback()
|
||
logOrdinaryBrowserEvent(Type.SWITCH_INSTALL_DIALOG_QUIT)
|
||
SensorsBridge.trackSwitchInstallDialogClick(
|
||
buttonName = "继续下载",
|
||
gameId = gameEntity.id,
|
||
gameName = gameEntity.name ?: "",
|
||
gameType = gameEntity.categoryChinese
|
||
)
|
||
},
|
||
touchOutsideCallback = {
|
||
SensorsBridge.trackSwitchInstallDialogClick(
|
||
buttonName = "关闭弹窗",
|
||
gameId = gameEntity.id,
|
||
gameName = gameEntity.name ?: "",
|
||
gameType = gameEntity.categoryChinese
|
||
)
|
||
},
|
||
extraConfig = DialogHelper.Config(hint = "修改路径:我的光环-设置-切换安装方式")
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 浏览器安装的后台开关
|
||
*/
|
||
@JvmStatic
|
||
fun isUseBrowserToInstallEnabled(): Boolean {
|
||
val settingsEntity = Config.getNewSettingsEntity()
|
||
|
||
if (settingsEntity == null) {
|
||
return false
|
||
} else {
|
||
if (settingsEntity.installModel?.status == "matched") {
|
||
return true
|
||
}
|
||
|
||
return isConditionMatched(settingsEntity)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 浏览器安装的后台开关 (返回仅包名匹配时的状态)
|
||
*/
|
||
@JvmStatic
|
||
fun isUseBrowserToInstallEnabledWithPackageMatched(): Boolean {
|
||
val settingsEntity = Config.getNewSettingsEntity()
|
||
|
||
return if (settingsEntity == null) {
|
||
false
|
||
} else {
|
||
isConditionMatched(settingsEntity)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 是否满足开启浏览器安装的条件
|
||
*/
|
||
private fun isConditionMatched(settingsEntity: NewSettingsEntity): Boolean {
|
||
if (mValidConditionMatchedCache != null) {
|
||
return mValidConditionMatchedCache!!
|
||
}
|
||
|
||
val packageList = getAllInstalledPackageList()
|
||
|
||
if (packageList.isEmpty()) return false
|
||
|
||
settingsEntity.installModel?.whiteList?.let {
|
||
for (packageName in it) {
|
||
if (packageList.contains(packageName)) {
|
||
mValidConditionMatchedCache = false
|
||
break
|
||
}
|
||
}
|
||
}
|
||
settingsEntity.installModel?.packages?.let {
|
||
for (packageName in it) {
|
||
if (packageList.contains(packageName)) {
|
||
mValidConditionMatchedCache = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
// 匹配部分字符串即可
|
||
settingsEntity.installModel?.regexPackages?.let {
|
||
for (packageNamePieces in it) {
|
||
for (installedPackageName in packageList) {
|
||
if (installedPackageName.contains(packageNamePieces)) {
|
||
mValidConditionMatchedCache = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return mValidConditionMatchedCache ?: false
|
||
}
|
||
|
||
fun onApkInstalled(path: String?) {
|
||
path?.let {
|
||
val fileName = path.substringAfterLast(File.separator).removeSuffix(DownloadServer.APK_SUFFIX)
|
||
// 更新下载名称 set,确定是否需要关闭下载服务
|
||
mFileNameSet.remove(fileName)
|
||
stopServerIfNeeded()
|
||
}
|
||
}
|
||
|
||
private fun startServer(): Boolean {
|
||
try {
|
||
mServer.start()
|
||
} catch (e: Exception) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
private fun retryStartServer() {
|
||
try {
|
||
mServer.start()
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
Utils.toast(mContext, "浏览器下载服务开启失败")
|
||
}
|
||
}
|
||
|
||
private fun stopServerIfNeeded() {
|
||
// 下载的文件都下载完了
|
||
if (::mServer.isInitialized && mFileNameSet.size == 0) {
|
||
mServer.stop()
|
||
}
|
||
}
|
||
|
||
/**以下为日志相关方法**/
|
||
fun logWebDownloadStarted(exposureEvent: ExposureEvent, downloadId: String, isDownloadComplete: Boolean) {
|
||
val jsonObject = JSONObject()
|
||
tryCatchInRelease {
|
||
jsonObject.put("event", if (isDownloadComplete) "web_download_complete" else "web_download")
|
||
jsonObject.put("payload", exposureEvent2JSONObject(exposureEvent.payload, downloadId))
|
||
jsonObject.put("meta", LogUtils.getMetaObject())
|
||
jsonObject.put("source", JSONArray(GsonUtils.toJson(exposureEvent.source)))
|
||
jsonObject.put("timestamp", System.currentTimeMillis() / 1000)
|
||
}
|
||
Utils.log(jsonObject.toString())
|
||
LoghubUtils.log(jsonObject, "event", false)
|
||
}
|
||
|
||
private fun exposureEvent2JSONObject(payload: ExposureEntity, downloadId: String): JSONObject {
|
||
val string = (GsonUtils.gson.toJsonTree(payload) as JsonObject).apply {
|
||
addProperty("uni_filename", "$downloadId${DownloadServer.APK_SUFFIX}")
|
||
}.toString()
|
||
|
||
return JSONObject(string)
|
||
}
|
||
|
||
fun logOrdinaryBrowserEvent(type: Type) {
|
||
val jsonObject = JSONObject()
|
||
tryCatchInRelease {
|
||
jsonObject.put("event", type.toString().toLowerCase(Locale.CHINA))
|
||
jsonObject.put("meta", LogUtils.getMetaObject())
|
||
jsonObject.put("timestamp", System.currentTimeMillis() / 1000)
|
||
}
|
||
Utils.log(jsonObject.toString())
|
||
LoghubUtils.log(jsonObject, "event", false)
|
||
}
|
||
|
||
enum class Type {
|
||
SWITCH_INSTALL_DIALOG,
|
||
SWITCH_INSTALL_DIALOG_QUIT,
|
||
SWITCH_INSTALL_DIALOG_ACCESS,
|
||
SWITCH_INSTALL_GUIDE_ACCESS,
|
||
SWITCH_INSTALL_GUIDE_QUIT,
|
||
SWITCH_INSTALL_SETTING,
|
||
SWITCH_INSTALL_SETTING_APP,
|
||
SWITCH_INSTALL_SETTING_WEB
|
||
}
|
||
|
||
} |