425 lines
17 KiB
Kotlin
425 lines
17 KiB
Kotlin
package com.img.rabbit.utils
|
||
|
||
import android.content.Context
|
||
import android.util.Log
|
||
import android.webkit.MimeTypeMap
|
||
import com.github.gzuliyujiang.oaid.DeviceIdentifier
|
||
import com.img.rabbit.BuildConfig
|
||
import com.img.rabbit.bean.request.ReportKey
|
||
import com.img.rabbit.bean.request.ReportRequest
|
||
import com.img.rabbit.bean.request.ReportType
|
||
import com.img.rabbit.bean.response.UniVersionEntity
|
||
import com.img.rabbit.components.CenterToast
|
||
import com.img.rabbit.config.Constants
|
||
import com.img.rabbit.provider.storage.PreferenceUtil
|
||
import com.img.rabbit.provider.utils.HeadParamUtils.applicationContext
|
||
import com.img.rabbit.provider.utils.HeadParamUtils.getAppVersionName
|
||
import com.img.rabbit.uni.UniMPAlipaySplashView
|
||
import com.img.rabbit.uni.UniMPWxSplashView
|
||
import com.img.rabbit.viewmodel.ReportViewModel
|
||
import com.tencent.mm.opensdk.modelbiz.WXLaunchMiniProgram
|
||
import com.tencent.mm.opensdk.openapi.IWXAPI
|
||
import io.dcloud.feature.sdk.DCUniMPSDK
|
||
import io.dcloud.feature.sdk.Interface.IUniMP
|
||
import io.dcloud.feature.unimp.config.UniMPOpenConfiguration
|
||
import io.dcloud.feature.unimp.config.UniMPReleaseConfiguration
|
||
import kotlinx.coroutines.CoroutineScope
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.flow.MutableStateFlow
|
||
import kotlinx.coroutines.flow.asStateFlow
|
||
import kotlinx.coroutines.launch
|
||
import kotlinx.coroutines.withContext
|
||
import okhttp3.OkHttpClient
|
||
import okhttp3.Request
|
||
import java.io.File
|
||
import kotlin.jvm.java
|
||
|
||
/**
|
||
* UniApp工具类
|
||
* 小程序运行路径:/data/user/0/com.img.rabbit/files/apps/(一般/data/data/com.img.rabbit/files/apps/)
|
||
*/
|
||
object UniAppUtils {
|
||
private const val TAG = "UniAppUtils"
|
||
/**
|
||
* 所有运行的UniMp小程序实体
|
||
*/
|
||
private val _uniMpFlow = MutableStateFlow<Map<String?, IUniMP?>>(emptyMap())
|
||
val uniMpFlow = _uniMpFlow.asStateFlow()
|
||
fun updateUniMp(id: String?, mp: IUniMP?) {
|
||
val currentMap = _uniMpFlow.value.toMutableMap()
|
||
currentMap[id] = mp
|
||
_uniMpFlow.value = currentMap
|
||
}
|
||
|
||
//当前正在更新的小程序
|
||
var currentUpdateUniMp: UniVersionEntity? = null
|
||
//仅当跳转指定小程序时,才需要跳转指定位置
|
||
var currentUniMpJumpPatch: String? = null
|
||
|
||
private fun getWgtName(uniMpId: String): String{
|
||
return String.format("%s.wgt", uniMpId)
|
||
//return String.format("%s.zip", uniMpId)
|
||
}
|
||
|
||
private fun getWgtFile(uniMpId: String): File{
|
||
val wgtName = getWgtName(uniMpId)
|
||
return File(FileUtils.instance?.cacheUniAppDir?.absolutePath, wgtName)
|
||
}
|
||
/**
|
||
*获取当前前台运行的UniMp小程序实体
|
||
*/
|
||
fun getCurrentUniMp(): IUniMP?{
|
||
return uniMpFlow.value.filter { it.value?.isRunning == true }.values.firstOrNull()
|
||
//return uniMpPair.filter { it.value?.isRunning == true }.values.firstOrNull()
|
||
}
|
||
/**
|
||
* 是否存在更新
|
||
*/
|
||
fun isUpdate(uniVersion: UniVersionEntity): Boolean{
|
||
return checkUpdate(uniVersion, PreferenceUtil.getWgtVersion(uniVersion.unimp_id)?:"0.0.0")
|
||
}
|
||
|
||
/**
|
||
* 是否强制更新
|
||
*/
|
||
private fun isUpdateForce(uniVersion: UniVersionEntity): Boolean{
|
||
return checkUpdate(uniVersion, PreferenceUtil.getWgtVersion(uniVersion.unimp_id)?:"0.0.0") && uniVersion.force
|
||
}
|
||
|
||
/**
|
||
* 是否需要下载(强制更新或者不存在wgt文件)
|
||
*/
|
||
fun isDownloadUniMp(uniVersion: UniVersionEntity): Boolean{
|
||
val wgtFile = getWgtFile(uniVersion.unimp_id)
|
||
return isUpdateForce(uniVersion) || !(FileUtils.instance?.fileIsExists(wgtFile) == true && FileUtils.getFileSize(wgtFile) > 0)
|
||
}
|
||
|
||
fun wgtIsExists(uniMpId: String): Boolean{
|
||
val wgtFile = getWgtFile(uniMpId)
|
||
//判断wgt是否下载
|
||
return FileUtils.instance?.fileIsExists(wgtFile) == true && FileUtils.getFileSize(wgtFile) > 0
|
||
}
|
||
|
||
fun isRelease(uniMpId: String): Boolean{
|
||
return DCUniMPSDK.getInstance().isExistsApp(uniMpId)
|
||
}
|
||
|
||
/**
|
||
* 检查更新
|
||
*/
|
||
private fun checkUpdate(uniVersion: UniVersionEntity, currentVersion: String): Boolean {
|
||
val version = uniVersion.version
|
||
if(version.isEmpty())return false
|
||
|
||
val newVersions = version.split(".")
|
||
val originVersions = currentVersion.split(".")
|
||
|
||
if(newVersions.size != 3 || originVersions.size != 3)return false
|
||
|
||
for (i in 0..2) {
|
||
val newVal = newVersions[i].toIntOrNull() ?: 0
|
||
val oldVal = originVersions[i].toIntOrNull() ?: 0
|
||
|
||
if (newVal > oldVal) {
|
||
return true
|
||
} else if (newVal < oldVal) {
|
||
return false
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 分发uniMP(下载、更新与启动)
|
||
*/
|
||
fun distributeUniMp(context: Context, uniVersion: UniVersionEntity,reportViewModel: ReportViewModel, onResult:(loading: Boolean) -> Unit){
|
||
val isExists = DCUniMPSDK.getInstance().isExistsApp(uniVersion.unimp_id)
|
||
if(isExists){
|
||
//资源已释放,直接启动
|
||
startUniMp(context, uniVersion, onResult)
|
||
}else{
|
||
//资源未释放,先释放后启动
|
||
releaseWgt(uniVersion,reportViewModel){ isSuccess, versionEntity ->
|
||
if(isSuccess){
|
||
startUniMp(context, versionEntity, onResult)
|
||
}else{
|
||
onResult(false)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun startUniMp(context: Context, uniVersion: UniVersionEntity, onResult:(loading: Boolean) -> Unit){
|
||
val uniMp = _uniMpFlow.value[uniVersion.unimp_id]//uniMpPair[uniVersion.unimp_id]
|
||
if(uniMp?.isRuning == true){
|
||
uniMp.showUniMP()
|
||
}else{
|
||
val configuration = getUniMPOpenConfiguration()
|
||
if(uniVersion.unimp_type == "wx"){
|
||
configuration.splashClass = UniMPWxSplashView::class.java
|
||
}else if("alipay" == uniVersion.unimp_type){
|
||
configuration.splashClass = UniMPAlipaySplashView::class.java
|
||
}
|
||
updateUniMp(uniVersion.unimp_id, DCUniMPSDK.getInstance().openUniMP(context, uniVersion.unimp_id, configuration))
|
||
}
|
||
onResult(false)
|
||
}
|
||
|
||
/**
|
||
* 启动小程序并直达指定页面
|
||
*/
|
||
fun startUniMpPage(context: Context, uniMpId: String, uniMpType: String, pagePath: String, reportViewModel: ReportViewModel){
|
||
if(!isRelease(uniMpId)){
|
||
releaseWgt(uniMpId,reportViewModel){
|
||
if(it){
|
||
// 启动直达页面
|
||
startUniMpToPage(context, uniMpId, uniMpType, pagePath)
|
||
}
|
||
}
|
||
}else {
|
||
val uniMp = _uniMpFlow.value[uniMpId]
|
||
if(uniMp?.isRuning == true){
|
||
uniMp.showUniMP()
|
||
}
|
||
// 启动直达页面
|
||
startUniMpToPage(context, uniMpId, uniMpType, pagePath)
|
||
}
|
||
}
|
||
private fun startUniMpToPage(context: Context, uniMpId: String, uniMpType: String, pagePath: String){
|
||
// 启动直达页面
|
||
val configuration = getUniMPOpenConfiguration()
|
||
if(uniMpType == "wx"){
|
||
configuration.splashClass = UniMPWxSplashView::class.java
|
||
}else if("alipay" == uniMpType){
|
||
configuration.splashClass = UniMPAlipaySplashView::class.java
|
||
}
|
||
configuration.path = pagePath
|
||
updateUniMp(uniMpId, DCUniMPSDK.getInstance().openUniMP(context, uniMpId, configuration))
|
||
}
|
||
|
||
private fun releaseWgt(versionEntity: UniVersionEntity, reportViewModel: ReportViewModel, onReleaseWgt: (isSuccess: Boolean, versionEntity: UniVersionEntity) -> Unit) {
|
||
releaseWgt(versionEntity.unimp_id,reportViewModel){ isSuccess ->
|
||
if(isSuccess){
|
||
onReleaseWgt(true, versionEntity)
|
||
}else{
|
||
onReleaseWgt(false, versionEntity)
|
||
}
|
||
}
|
||
}
|
||
|
||
//释放资源
|
||
private fun releaseWgt(uniMpId: String, reportViewModel: ReportViewModel, onReleaseWgt: (isSuccess: Boolean) -> Unit) {
|
||
val appBasePath = DCUniMPSDK.getInstance().getAppBasePath(applicationContext)
|
||
val deleteSuccess = File(appBasePath, uniMpId).deleteRecursively()
|
||
if(deleteSuccess){
|
||
val wgtFile = getWgtFile(uniMpId)
|
||
val uniMPReleaseConfiguration = UniMPReleaseConfiguration().apply {
|
||
wgtPath = wgtFile.path
|
||
password = PreferenceUtil.getUserConfig()?.config?.wgtPassword//"6462"////没有密码可以不写
|
||
}
|
||
DCUniMPSDK.getInstance().releaseWgtToRunPath(uniMpId, uniMPReleaseConfiguration) { code, _ ->
|
||
if (code == 1) {
|
||
//释放wgt完成
|
||
onReleaseWgt(true)
|
||
} else {
|
||
//释放wgt失败
|
||
CenterToast.show("加载失败,请重试或联系客服!")
|
||
onReleaseWgt(false)
|
||
File(appBasePath, uniMpId).deleteRecursively()
|
||
//事件提交
|
||
reportViewModel.requestReport(
|
||
ReportRequest(
|
||
ReportType.ERROR,
|
||
ReportKey.EVENT_CLIENT_UNI_RELEASE_WGT,
|
||
uniMpId,
|
||
"释放资源失败"
|
||
)
|
||
)
|
||
}
|
||
}
|
||
}else{
|
||
CenterToast.show("加载失败,请重试或联系客服!")
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 下载wgt文件
|
||
*/
|
||
fun downloadWGT(context: Context,scope: CoroutineScope, uniVersion: UniVersionEntity, reportViewModel: ReportViewModel = ReportViewModel(), onProgress:(state: UniMpUpdate, filePath: String?, progress: Float?) -> Unit) {
|
||
val wgtFile = getWgtFile(uniVersion.unimp_id)
|
||
onProgress(UniMpUpdate.DOWNLOAD_START, wgtFile.path, 0f)
|
||
|
||
downloadUniMp(scope, uniVersion){uniState, filePath, progress ->
|
||
onProgress(uniState, filePath, progress)
|
||
if(uniState == UniMpUpdate.DOWNLOAD_FINISH){
|
||
distributeUniMp(context, uniVersion,reportViewModel) { _ ->}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载并释放资源(但不会启动)
|
||
*/
|
||
fun downloadReleaseWgt(scope: CoroutineScope, uniVersion: UniVersionEntity,reportViewModel: ReportViewModel, onProgress:(state: UniMpUpdate, progress: Float?) -> Unit,onRelease:(isSuccess: Boolean) -> Unit){
|
||
val uniMpId = uniVersion.unimp_id
|
||
onProgress(UniMpUpdate.DOWNLOAD_START, 0f)
|
||
downloadUniMp(scope, uniVersion){uniState, _, progress ->
|
||
onProgress(UniMpUpdate.DOWNLOAD_LOADING, progress)
|
||
if(uniState == UniMpUpdate.DOWNLOAD_FINISH){
|
||
onProgress(UniMpUpdate.DOWNLOAD_FINISH, 1f)
|
||
releaseWgt(uniMpId,reportViewModel){ isSuccess ->
|
||
if(isSuccess){
|
||
onRelease(true)
|
||
}else{
|
||
onRelease(false)
|
||
}
|
||
}
|
||
}else if(uniState == UniMpUpdate.DOWNLOAD_FAIL){
|
||
onProgress(UniMpUpdate.DOWNLOAD_FAIL, -1f)
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
//下载资源
|
||
private fun downloadUniMp(
|
||
scope: CoroutineScope,
|
||
uniVersion: UniVersionEntity,
|
||
onProgress:(state:UniMpUpdate,filePath: String?, progress: Float) -> Unit
|
||
) {
|
||
val extension = MimeTypeMap.getFileExtensionFromUrl(uniVersion.url)
|
||
if(extension != "zip" && extension != "wgt"){
|
||
CenterToast.show("资源文件格式不被支持...")
|
||
onProgress(UniMpUpdate.DOWNLOAD_FAIL, null, -1f)
|
||
return
|
||
}
|
||
|
||
val uniMpId = uniVersion.unimp_id
|
||
val wgtName = getWgtName(uniMpId)
|
||
val path = FileUtils.instance?.cacheUniAppDir?.absolutePath?:""
|
||
//先删除旧文件
|
||
val oldFile = File(path, wgtName)
|
||
if(oldFile.exists()){
|
||
|
||
oldFile.delete()
|
||
}
|
||
onProgress(UniMpUpdate.DOWNLOAD_LOADING, null, 0.0f)
|
||
|
||
scope.launch {
|
||
val isAvailable = isFileDownloadable(uniVersion.url)
|
||
if(!isAvailable){
|
||
onProgress(UniMpUpdate.DOWNLOAD_FAIL, null, -1f)
|
||
}else{
|
||
UpdateUtils.download(
|
||
scope = scope,
|
||
url = uniVersion.url,
|
||
filePath = path,
|
||
fileName = wgtName,
|
||
onProgress = {progress->
|
||
if(progress.toFloat()/100f>0.1f) {
|
||
onProgress(
|
||
UniMpUpdate.DOWNLOAD_LOADING,
|
||
null,
|
||
progress.toFloat() / 100f
|
||
)
|
||
}
|
||
},
|
||
onFinish = {isSuccess, filePath ->
|
||
if(isSuccess){
|
||
PreferenceUtil.saveWgtVersion(uniMpId, uniVersion.version)
|
||
onProgress(UniMpUpdate.DOWNLOAD_FINISH, filePath, 1f)
|
||
}else{
|
||
onProgress(UniMpUpdate.DOWNLOAD_FAIL, filePath, -1f)
|
||
|
||
CenterToast.show("下载失败...")
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun getUniMPOpenConfiguration(): UniMPOpenConfiguration{
|
||
val stringBuilder = StringBuilder()
|
||
stringBuilder.append("-------------> 📤 header start<-------------\n")
|
||
stringBuilder.append("│ x-token = ${PreferenceUtil.getXToken()}\n")
|
||
stringBuilder.append("│ x-version = ${getAppVersionName()}\n")
|
||
stringBuilder.append("│ x-platform = android\n")
|
||
stringBuilder.append("│ x-device-id = ${DeviceIdentifier.getAndroidID(applicationContext)}\n")
|
||
stringBuilder.append("│ x-mobile-brand = ${android.os.Build.BRAND}\n")
|
||
stringBuilder.append("│ x-mobile-model = ${android.os.Build.MODEL}\n")
|
||
stringBuilder.append("│ x-channel = ${ChannelUtils.getChannel(applicationContext)}\n")
|
||
stringBuilder.append("│ x-package = ${BuildConfig.APPLICATION_ID}\n")
|
||
stringBuilder.append("│ x-click-id = ${ChannelUtils.getBdVid(applicationContext)}\n")
|
||
stringBuilder.append("│ host = ${Constants.RELEASE_BASE_URL}\n")
|
||
stringBuilder.append("│ decrypt = ${Constants.AESDecrypt}\n")
|
||
stringBuilder.append("│ decrypt = ${Constants.Signature}\n")
|
||
stringBuilder.append("│ isCombo = ok\n")
|
||
stringBuilder.append("-------------> header end <-------------")
|
||
Log.i("UniAppUtils", stringBuilder.toString())
|
||
|
||
return UniMPOpenConfiguration().apply {
|
||
extraData.put("x-token", PreferenceUtil.getXToken()?:"")
|
||
extraData.put("x-version", getAppVersionName()?:"")
|
||
extraData.put("x-platform", "android")
|
||
extraData.put("x-device-id", DeviceIdentifier.getAndroidID(applicationContext))
|
||
extraData.put("x-mobile-brand", android.os.Build.BRAND)
|
||
extraData.put("x-mobile-model", android.os.Build.MODEL)
|
||
extraData.put("x-channel", ChannelUtils.getChannel(applicationContext))
|
||
extraData.put("x-package", BuildConfig.APPLICATION_ID)
|
||
extraData.put("x-click-id",ChannelUtils.getBdVid(applicationContext))
|
||
extraData.put("host", "${Constants.RELEASE_BASE_URL}/")
|
||
extraData.put("decrypt", Constants.AESDecrypt)
|
||
extraData.put("encrypt", Constants.Signature)
|
||
extraData.put("isCombo", "ok")
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 拉起微信小程序来支付
|
||
*/
|
||
fun startUniPay(api: IWXAPI, weixinMpOriId: String, outTradeNo: String) {
|
||
//isStartComboPay = true
|
||
val req = WXLaunchMiniProgram.Req()
|
||
req.userName = weixinMpOriId // 填小程序原始id
|
||
req.path = "pages/index/index?outTradeNo=$outTradeNo" //拉起小程序页面的可带参路径,不填默认拉起小程序首页,对于小游戏,可以只传入 query 部分,来实现传参效果,如:传入 "?foo=bar"。
|
||
req.miniprogramType = WXLaunchMiniProgram.Req.MINIPTOGRAM_TYPE_RELEASE// 可选打开 开发版,体验版和正式版
|
||
api.sendReq(req)
|
||
}
|
||
|
||
private val okHttpClient: OkHttpClient by lazy {
|
||
OkHttpClient.Builder()
|
||
// 如果需要,可以在这里配置超时、缓存等
|
||
// .connectTimeout(15, TimeUnit.SECONDS)
|
||
.build()
|
||
}
|
||
suspend fun isFileDownloadable(url: String): Boolean = withContext(Dispatchers.IO) {
|
||
val request = Request.Builder().url(url).build()
|
||
try {
|
||
okHttpClient.newCall(request).execute().use { response ->
|
||
val body = response.body
|
||
val fileSize = body.contentLength()
|
||
|
||
// 下载文件小于1KB,需要判断Url是否可靠
|
||
if (fileSize < 1024) {
|
||
return@withContext response.isSuccessful
|
||
} else {
|
||
return@withContext true
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
}
|
||
return@withContext false
|
||
}
|
||
|
||
|
||
}
|
||
|
||
enum class UniMpUpdate{
|
||
DOWNLOAD_START,
|
||
DOWNLOAD_LOADING,
|
||
DOWNLOAD_FINISH,
|
||
DOWNLOAD_FAIL
|
||
} |