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>(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 }