rabbit-android/app/src/main/java/com/img/rabbit/utils/UniAppUtils.kt

425 lines
17 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.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
}