parent
1168061f72
commit
d558fde6f3
|
|
@ -4,10 +4,10 @@
|
|||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-02-27T07:54:46.218482200Z">
|
||||
<DropdownSelection timestamp="2026-02-28T08:59:56.877010800Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=JRBI89BIE6AI5TG6" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=Y5DELZR46DZTCI9D" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ android {
|
|||
applicationId = "com.img.rabbit"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 2
|
||||
versionName = "1.1"
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
|
||||
|
||||
setManifestPlaceholders(mapOf(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="SelectedPhotoAccess" />
|
||||
tools:ignore="ScopedStorage,SelectedPhotoAccess" />
|
||||
<uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
tools:ignore="SelectedPhotoAccess" />
|
||||
|
|
@ -27,6 +27,13 @@
|
|||
<!--开关wifi状态,解决国内机型移动网络权限问题需要-->
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
|
||||
<!-- 安装应用权限 -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||
tools:ignore="RequestInstallPackagesPolicy" />
|
||||
<uses-permission
|
||||
android:name="android.permission.INSTALL_PACKAGES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
|
||||
<queries>
|
||||
<package android:name="com.eg.android.AlipayGphone" /> <!-- 支付宝 -->
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.img.rabbit
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
|
@ -32,8 +35,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -52,16 +57,19 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.img.rabbit.config.Constants
|
||||
import com.img.rabbit.config.Constants.agreementUrl
|
||||
import com.img.rabbit.config.Constants.privacyUrl
|
||||
import com.img.rabbit.pages.LoginScreen
|
||||
import com.img.rabbit.pages.LoginScreenType
|
||||
import com.img.rabbit.pages.MainScreen
|
||||
import com.img.rabbit.pages.dialog.UpdateDialog
|
||||
import com.img.rabbit.provider.storage.GlobalStateManager
|
||||
import com.img.rabbit.provider.storage.PreferenceUtil
|
||||
import com.img.rabbit.provider.storage.PreferenceUtil.saveBDVID
|
||||
import com.img.rabbit.utils.ChannelUtils
|
||||
import com.img.rabbit.utils.UpdateUtils
|
||||
import com.img.rabbit.utils.UrlLinkUtils.openAgreement
|
||||
import com.img.rabbit.viewmodel.GeneralViewModel
|
||||
import com.img.rabbit.viewmodel.LoginViewModel
|
||||
|
|
@ -75,7 +83,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlin.system.exitProcess
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalPermissionsApi::class)
|
||||
@SuppressLint("UnrememberedMutableState", "CoroutineCreationDuringComposition")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// 必须在 super.onCreate 之前调用
|
||||
|
|
@ -86,7 +94,7 @@ class MainActivity : ComponentActivity() {
|
|||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val splashViewModel: SplashViewModel = viewModel()
|
||||
val generalViewModel: GeneralViewModel = viewModel()
|
||||
|
|
@ -95,7 +103,9 @@ class MainActivity : ComponentActivity() {
|
|||
var showSplash by remember { mutableStateOf(false) }
|
||||
var globalLogin by mutableStateOf(GlobalStateManager(context).globalLoginNotifyFlow().collectAsState(initial = false))
|
||||
var globalLogout by mutableStateOf(GlobalStateManager(context).globalLogoutNotifyFlow().collectAsState(initial = false))
|
||||
var globalBind by mutableStateOf(GlobalStateManager(context).globalBindNotifyFlow().collectAsState(initial = false))
|
||||
var globalUnBind by mutableStateOf(GlobalStateManager(context).globalUnBindNotifyFlow().collectAsState(initial = false))
|
||||
var globalUpdate by mutableStateOf(GlobalStateManager(context).globalUpdateNotifyFlow().collectAsState(initial = false))
|
||||
|
||||
LaunchedEffect(generalViewModel.agreementStatus.value) {
|
||||
if (generalViewModel.agreementStatus.value == true){
|
||||
|
|
@ -117,6 +127,13 @@ class MainActivity : ComponentActivity() {
|
|||
splashScreen.setKeepOnScreenCondition {
|
||||
splashViewModel.isLoading.value // 当为 true 时,启动页不消失
|
||||
}
|
||||
|
||||
//更新用户配置与用户信息
|
||||
if(globalLogin.value == true || globalBind.value == true || globalUnBind.value == true || globalLogout.value == true){
|
||||
loginViewModel.requestUserConfig()
|
||||
loginViewModel.requestUserInfo()
|
||||
}
|
||||
|
||||
// 登录成功后,2秒后自动更新状态
|
||||
if(globalLogin.value == true){
|
||||
GlobalScope.launch {
|
||||
|
|
@ -135,6 +152,15 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// 绑定成功后,2秒后自动更新状态
|
||||
if(globalBind.value == true){
|
||||
GlobalScope.launch {
|
||||
//延迟2秒,方便处理多有事件都收到通知
|
||||
delay(2*1000)
|
||||
GlobalStateManager(context).storeGlobalBindNotify(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 解绑成功后,2秒后自动更新状态
|
||||
if(globalUnBind.value == true){
|
||||
GlobalScope.launch {
|
||||
|
|
@ -195,15 +221,43 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if(showUpdateDialog){
|
||||
val isStartDownload = mutableStateOf(false)
|
||||
val progressState = mutableFloatStateOf(0f)
|
||||
if(globalUpdate.value == true){
|
||||
UpdateDialog(
|
||||
title = "新版本,更新提示",
|
||||
newVersion = "1.0.0",
|
||||
desc = "修复了一些问题,新增了一些功能",
|
||||
url = "https://www.baidu.com",
|
||||
){ state ->
|
||||
showUpdateDialog = state
|
||||
title = PreferenceUtil.getUserConfig()?.config?.versionEntity?.title?:"新版本,更新提示",
|
||||
newVersion = "V${PreferenceUtil.getUserConfig()?.config?.versionEntity?.version}",
|
||||
desc = PreferenceUtil.getUserConfig()?.config?.versionEntity?.description?:"",
|
||||
url = PreferenceUtil.getUserConfig()?.config?.versionEntity?.url?:"",
|
||||
scope = scope,
|
||||
isStartDown = isStartDownload,
|
||||
downProgress = progressState
|
||||
){ state, isCancel, url ->
|
||||
if(isCancel) {
|
||||
scope.launch {
|
||||
GlobalStateManager(context).storeGlobalUpdateNotify(state)
|
||||
}
|
||||
}
|
||||
if(!isCancel){
|
||||
isStartDownload.value = true
|
||||
UpdateUtils.download(
|
||||
scope = scope,
|
||||
url = url,
|
||||
onProgress = {progress->
|
||||
progressState.floatValue = progress.toFloat()/100f
|
||||
},
|
||||
onFinish = {isSuccess, filePath ->
|
||||
if(isSuccess){
|
||||
filePath?.let {
|
||||
UpdateUtils.install(context,it)
|
||||
}
|
||||
scope.launch {
|
||||
GlobalStateManager(context).storeGlobalUpdateNotify(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -490,185 +544,6 @@ private fun PrivacyPolicyScreen(viewModel: LoginViewModel, onAgreementChange: (B
|
|||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun UpdateDialog(
|
||||
title: String = "新版本,更新提示",
|
||||
newVersion: String = "1.0.0",
|
||||
desc: String = "修复了一些问题,新增了一些功能",
|
||||
url: String,
|
||||
onStatusChange: (state: Boolean) -> Unit
|
||||
){
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x80000000))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onStatusChange(false)
|
||||
},
|
||||
verticalArrangement = Arrangement.Center
|
||||
){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 36.dp)
|
||||
.background(Color.White, shape = RoundedCornerShape(26.dp))
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
//什么都不用做,只是解决点击穿透问题
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.ic_dialog_top_mask1),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 46.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF1A1A1A),
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(14.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = newVersion,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF767676),
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = desc,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF767676),
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(16.dp)
|
||||
)
|
||||
|
||||
//切换/退出账号
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 18.dp, end = 18.dp, bottom = 20.dp)
|
||||
) {
|
||||
//取消解绑手机号
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.weight(1f)
|
||||
.background(
|
||||
Color(0x00000000),
|
||||
shape = RoundedCornerShape(359.dp),
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color(0xFF000000),
|
||||
shape = RoundedCornerShape(359.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
// 取消解绑手机号
|
||||
onStatusChange(false)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"取消",
|
||||
color = Color(0xFF1A1A1A),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(11.dp)
|
||||
)
|
||||
|
||||
|
||||
|
||||
//退出登录
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.weight(1f)
|
||||
.background(
|
||||
Color(0xFF252525),
|
||||
shape = RoundedCornerShape(359.dp),
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
//TODO 确定解绑手机号
|
||||
onStatusChange(false)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"确定",
|
||||
color = Color(0xFFC2FF43),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MainScreenPreview() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.img.rabbit.bean.response
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class ServiceWxLinkEntity {
|
||||
var corpid = ""
|
||||
|
||||
@SerializedName("kf.address")
|
||||
var address = ""
|
||||
}
|
||||
|
|
@ -106,10 +106,6 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
|
|||
if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) {
|
||||
//登录成功
|
||||
PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token)
|
||||
// 获取用户配置
|
||||
loginViewModel.requestUserConfig()
|
||||
// 获取用户信息
|
||||
loginViewModel.requestUserInfo()
|
||||
|
||||
loginViewModel.setLogin(true)
|
||||
//更新登录状态
|
||||
|
|
|
|||
|
|
@ -0,0 +1,263 @@
|
|||
package com.img.rabbit.pages.dialog
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import com.img.rabbit.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@SuppressLint("UnrememberedMutableState")
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun UpdateDialog(
|
||||
title: String = "发现新版本~",
|
||||
newVersion: String = "V1.0.0",
|
||||
desc: String = "修复了一些问题,新增了一些功能",
|
||||
url: String,
|
||||
scope: CoroutineScope,
|
||||
isStartDown: MutableState<Boolean>,
|
||||
downProgress: MutableState<Float>,
|
||||
onStatusChange: (state: Boolean, isCancel: Boolean, url:String) -> Unit
|
||||
){
|
||||
val storagePermissionState = rememberPermissionState(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.READ_MEDIA_IMAGES // Android 13+ 使用具体媒体权限
|
||||
} else {
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE // 旧版本使用常规存储权限
|
||||
}
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x80000000))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
//onStatusChange(false)
|
||||
},
|
||||
verticalArrangement = Arrangement.Center
|
||||
){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 36.dp)
|
||||
.background(Color.White, shape = RoundedCornerShape(26.dp))
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
//什么都不用做,只是解决点击穿透问题
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.mipmap.ic_dialog_top_mask1),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(top = 26.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF1A1A1A),
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(14.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = newVersion,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF767676),
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(horizontal = 12.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = desc,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color(0xFF767676),
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.padding(12.dp)
|
||||
)
|
||||
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(16.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 18.dp, end = 18.dp, bottom = 20.dp)
|
||||
) {
|
||||
if(isStartDown.value){
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
|
||||
) {
|
||||
// 使用 LinearProgressIndicator 显示确定性进度
|
||||
LinearProgressIndicator(
|
||||
progress = { downProgress.value }, // 使用 Lambda 更新进度0~1
|
||||
modifier = Modifier.fillMaxWidth().height(8.dp)
|
||||
)
|
||||
Text(text = "下载进度: ${(downProgress.value * 100).toInt()}%")
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.weight(1f)
|
||||
.background(
|
||||
Color(0x00000000),
|
||||
shape = RoundedCornerShape(359.dp),
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = Color(0xFF000000),
|
||||
shape = RoundedCornerShape(359.dp)
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onStatusChange(false, true, url)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"暂不",
|
||||
color = Color(0xFF1A1A1A),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(11.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.weight(1f)
|
||||
.background(
|
||||
Color(0xFF252525),
|
||||
shape = RoundedCornerShape(359.dp),
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
// 执行下载更新
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// isStartDown.value = true
|
||||
onStatusChange(false, false, url)
|
||||
}else{
|
||||
when {
|
||||
// 情况 A:已获得权限
|
||||
storagePermissionState.status.isGranted -> {
|
||||
// isStartDown.value = true
|
||||
onStatusChange(false, false, url)
|
||||
}
|
||||
// 情况 B:需要向用户解释(之前拒绝过,但未勾选“不再询问”)
|
||||
storagePermissionState.status.shouldShowRationale -> {
|
||||
storagePermissionState.launchPermissionRequest()
|
||||
}
|
||||
// 情况 C:从未请求过权限或已被彻底拒绝
|
||||
else -> {
|
||||
storagePermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
"立即更新",
|
||||
color = Color(0xFFC2FF43),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,8 +53,7 @@ import com.img.rabbit.provider.storage.PreferenceUtil
|
|||
import com.img.rabbit.utils.AppUpdate
|
||||
import com.img.rabbit.viewmodel.GeneralViewModel
|
||||
import com.img.rabbit.viewmodel.LoginViewModel
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import com.img.rabbit.viewmodel.MineViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -62,12 +61,14 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun MineScreen(
|
||||
navController: NavHostController,
|
||||
viewModel: MineViewModel = viewModel(),
|
||||
generalViewModel: GeneralViewModel,
|
||||
) {
|
||||
val TAG = "Rabbit_Mine"
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var globalLogin by mutableStateOf(GlobalStateManager(context).globalLoginNotifyFlow().collectAsState(initial = false))
|
||||
var globalBind by mutableStateOf(GlobalStateManager(context).globalBindNotifyFlow().collectAsState(initial = false))
|
||||
var globalUnBind by mutableStateOf(GlobalStateManager(context).globalUnBindNotifyFlow().collectAsState(initial = false))
|
||||
|
||||
val vipMember by remember { mutableStateOf(false) }
|
||||
|
|
@ -85,7 +86,7 @@ fun MineScreen(
|
|||
}
|
||||
|
||||
//刷新用户信息
|
||||
if(globalLogin.value == true || globalUnBind.value == true){
|
||||
if(globalLogin.value == true || globalBind.value == true || globalUnBind.value == true){
|
||||
scope.launch {
|
||||
delay(300)
|
||||
userInfo = PreferenceUtil.getUserInfo()
|
||||
|
|
@ -161,7 +162,7 @@ fun MineScreen(
|
|||
// 跳转登录页面
|
||||
navController.navigate("login?type=${LoginViewModel.JumpLoginType.NORMAL.type}")
|
||||
} else {
|
||||
//TODO 已登录,跳转个人信息页面
|
||||
// 已登录,跳转个人信息页面
|
||||
//navController.navigate("userInfo")
|
||||
}
|
||||
}
|
||||
|
|
@ -219,10 +220,11 @@ fun MineScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// VIP bar
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
/*
|
||||
// VIP bar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -300,6 +302,7 @@ fun MineScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//功能项
|
||||
Column(
|
||||
|
|
@ -381,10 +384,11 @@ fun MineScreen(
|
|||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
// 隐藏TabBar
|
||||
generalViewModel.setNavigationBarVisible(false)
|
||||
// 跳转在线客服页面
|
||||
navController.navigate("onlineService")
|
||||
if (!generalViewModel.api.isWXAppInstalled) {
|
||||
Toast.makeText(context, "未安装微信客户端", Toast.LENGTH_SHORT).show()
|
||||
}else if(userInfo != null){
|
||||
viewModel.requestServiceLink(context, generalViewModel.api)
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
|
@ -444,7 +448,9 @@ fun MineScreen(
|
|||
if (isUpdate) {
|
||||
//提示执行更新
|
||||
//startUpdate(result, fragment)
|
||||
Log.i(TAG, "checkUpdate: 有新版本, tips = $tips")
|
||||
scope.launch {
|
||||
GlobalStateManager(context).storeGlobalUpdateNotify(true)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, tips, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,10 +65,6 @@ fun AccountBindScreen(navController: NavHostController, viewModel: AccountBindVi
|
|||
|
||||
LaunchedEffect(viewModel.unBindState.value) {
|
||||
if(viewModel.unBindState.value != null){
|
||||
// 获取用户配置
|
||||
loginViewModel.requestUserConfig()
|
||||
// 获取用户信息
|
||||
loginViewModel.requestUserInfo()
|
||||
|
||||
Toast.makeText(context, "解绑成功!", Toast.LENGTH_SHORT).show()
|
||||
viewModel.unBindState.value = null
|
||||
|
|
|
|||
|
|
@ -77,10 +77,6 @@ fun AccountManagerScreen(navController: NavHostController, viewModel: AccountMan
|
|||
if (viewModel.switchState.value != null && viewModel.switchState.value?.data?.token != null) {
|
||||
// 切换账号成功
|
||||
PreferenceUtil.saveAccessToken(viewModel.switchState.value?.data?.token)
|
||||
// 获取用户配置
|
||||
loginViewModel.requestUserConfig()
|
||||
// 获取用户信息
|
||||
loginViewModel.requestUserInfo()
|
||||
|
||||
loginViewModel.setLogin(true)
|
||||
//更新登录状态
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ fun BindScreen(navController: NavHostController, viewModel: BindViewModel = view
|
|||
LaunchedEffect(viewModel.bindState.value) {
|
||||
if (viewModel.bindState.value != null && viewModel.bindState.value?.data?.token != null) {
|
||||
Toast.makeText(context, "绑定成功!", Toast.LENGTH_SHORT).show()
|
||||
|
||||
GlobalStateManager(context).storeGlobalBindNotify(true)
|
||||
navController.popBackStack()
|
||||
}else if (viewModel.bindState.value != null){
|
||||
Toast.makeText(context, "绑定失败!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ class GlobalStateManager(
|
|||
private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization")
|
||||
private val GLOBAL_LOGIN_NOTIFY = booleanPreferencesKey("global_login_notify")
|
||||
private val GLOBAL_LOGOUT_NOTIFY = booleanPreferencesKey("global_logout_notify")
|
||||
private val GLOBAL_BIND_NOTIFY = booleanPreferencesKey("global_bind_notify")
|
||||
private val GLOBAL_UNBIND_NOTIFY = booleanPreferencesKey("global_unbind_notify")
|
||||
private val GLOBAL_UPDATE_NOTIFY = booleanPreferencesKey("global_update_notify")
|
||||
}
|
||||
|
||||
suspend fun storeGlobalLoading(value: Boolean) {
|
||||
|
|
@ -63,6 +65,18 @@ class GlobalStateManager(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun storeGlobalBindNotify(value: Boolean) {
|
||||
context.storeData.edit { preferences ->
|
||||
preferences[GLOBAL_BIND_NOTIFY] = value
|
||||
}
|
||||
}
|
||||
fun globalBindNotifyFlow(): Flow<Boolean?> {
|
||||
return context.storeData.data.map {
|
||||
preferences ->
|
||||
preferences[GLOBAL_BIND_NOTIFY]
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun storeGlobalUnBindNotify(value: Boolean) {
|
||||
context.storeData.edit { preferences ->
|
||||
preferences[GLOBAL_UNBIND_NOTIFY] = value
|
||||
|
|
@ -88,4 +102,18 @@ class GlobalStateManager(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun storeGlobalUpdateNotify(value: Boolean) {
|
||||
context.storeData.edit { preferences ->
|
||||
preferences[GLOBAL_UPDATE_NOTIFY] = value
|
||||
}
|
||||
}
|
||||
fun globalUpdateNotifyFlow(): Flow<Boolean?> {
|
||||
return context.storeData.data.map {
|
||||
preferences ->
|
||||
preferences[GLOBAL_UPDATE_NOTIFY]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -48,4 +48,9 @@ object AppUpdate {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
fun getFileNameFromUrl(url: String): String {
|
||||
return url.substring(url.lastIndexOf('/') + 1)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
package com.img.rabbit.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import okhttp3.*
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import java.io.*
|
||||
import java.net.URL
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class DownLoadUtils private constructor() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DownLoadUtils"
|
||||
private val downLoadHttpUtils: DownLoadUtils by lazy {
|
||||
DownLoadUtils()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getInstance(): DownLoadUtils {
|
||||
return downLoadHttpUtils
|
||||
}
|
||||
}
|
||||
|
||||
private var downloadSizeInfo = mutableMapOf<String, Long>()
|
||||
private var cancelledList = mutableListOf<String>()
|
||||
|
||||
private var buffSize = 4096//建议设置为2048
|
||||
fun setBuffSize(size: Int): DownLoadUtils {
|
||||
this.buffSize = size
|
||||
return this
|
||||
}
|
||||
|
||||
private var interceptor: Interceptor? = null
|
||||
fun setInterceptor(interceptor: Interceptor?): DownLoadUtils {
|
||||
this.interceptor = interceptor
|
||||
return this
|
||||
}
|
||||
|
||||
private var readTimeOut = 30L
|
||||
fun setReadTImeOut(read: Long): DownLoadUtils {
|
||||
this.readTimeOut = read
|
||||
return this
|
||||
}
|
||||
|
||||
private var writeTimeout = 30L
|
||||
fun setWriteTimeOut(write: Long): DownLoadUtils {
|
||||
this.writeTimeout = write
|
||||
return this
|
||||
}
|
||||
|
||||
private var connectTimeout = 30L
|
||||
fun setConnectTimeOut(connect: Long): DownLoadUtils {
|
||||
this.connectTimeout = connect
|
||||
return this
|
||||
}
|
||||
|
||||
var filePath = ""
|
||||
fun setFilePath(path: String): DownLoadUtils {
|
||||
this.filePath = path
|
||||
return this
|
||||
}
|
||||
|
||||
private var fileName = ""
|
||||
fun setFileName(name: String): DownLoadUtils {
|
||||
this.fileName = name
|
||||
return this
|
||||
}
|
||||
|
||||
private var deleteWhenException = true
|
||||
fun setDeleteWhenException(dele: Boolean): DownLoadUtils {
|
||||
this.deleteWhenException = dele
|
||||
return this
|
||||
}
|
||||
|
||||
private val requestBuilder: Request.Builder = Request.Builder()
|
||||
private var urlBuilder: HttpUrl.Builder? = null
|
||||
|
||||
private val okHttpClient = lazy {
|
||||
OkHttpClient.Builder()
|
||||
.readTimeout(readTimeOut, TimeUnit.SECONDS)
|
||||
.writeTimeout(writeTimeout, TimeUnit.SECONDS)
|
||||
.connectTimeout(connectTimeout, TimeUnit.SECONDS)
|
||||
.addInterceptor(interceptor ?: LoggingInterceptor())
|
||||
.build()
|
||||
}
|
||||
private var actionGetTotal: (total: Long) -> Unit? = { _ -> }
|
||||
private var actionProgress: (position: Long) -> Unit? = { _ -> }
|
||||
private var actionSuccess: (file: File) -> Unit? = { _ -> }
|
||||
private var actionCancel: () -> Unit? = { }
|
||||
private var actionFail: (msg: String) -> Unit? = { _ -> }
|
||||
|
||||
fun setActionCallBack(
|
||||
actionGetTotal: (total: Long) -> Unit,
|
||||
actionProgress: (position: Long) -> Unit,
|
||||
actionSuccess: (file: File) -> Unit,
|
||||
actionFail: (msg: String) -> Unit,
|
||||
): DownLoadUtils {
|
||||
this.actionGetTotal = actionGetTotal
|
||||
this.actionProgress = actionProgress
|
||||
this.actionSuccess = actionSuccess
|
||||
this.actionFail = actionFail
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private var downCallBack: DownCallBack? = null
|
||||
fun setDownCallBack(callBack: DownCallBack): DownLoadUtils {
|
||||
this.downCallBack = callBack
|
||||
return this
|
||||
}
|
||||
|
||||
fun initUrl(url: String, params: Map<String, String>?): DownLoadUtils {
|
||||
urlBuilder = url.toHttpUrlOrNull()?.newBuilder()
|
||||
if (params.isNullOrEmpty()) {
|
||||
return this
|
||||
} else {
|
||||
for ((k, v) in params) {
|
||||
checkName(k)
|
||||
urlBuilder?.setQueryParameter(k, v)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun addHeader(map: Map<String, String>): DownLoadUtils {
|
||||
requestBuilder.headers(map.toHeaders())
|
||||
return this
|
||||
}
|
||||
|
||||
private fun checkName(name: String) {
|
||||
require(name.isNotEmpty()) { "name is empty" }
|
||||
}
|
||||
|
||||
fun down() {
|
||||
if (urlBuilder == null) {
|
||||
throw IllegalStateException("url not init")
|
||||
} else {
|
||||
doDown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doDown() {
|
||||
val startTime = System.currentTimeMillis()
|
||||
Log.i(TAG, "startTime=$startTime")
|
||||
val url = urlBuilder?.build()
|
||||
if (url == null) {
|
||||
doException("url is null")
|
||||
return
|
||||
}
|
||||
if (isDowning(filePath + fileName)) {
|
||||
return
|
||||
}
|
||||
cancelledList.remove(fileName)
|
||||
|
||||
val currentLen = downloadSizeInfo[fileName] ?: 0L
|
||||
if (isCanContinueDownload(url.toUrl(), currentLen)) {
|
||||
requestBuilder.removeHeader("RANGE")
|
||||
requestBuilder.addHeader("RANGE", "bytes=${currentLen}-")
|
||||
}
|
||||
val request = requestBuilder.url(url).tag(filePath + fileName).build()
|
||||
|
||||
var `is`: InputStream? = null
|
||||
var raf: RandomAccessFile? = null
|
||||
var file: File? = null
|
||||
try {
|
||||
val response = okHttpClient.value.newCall(request).execute()
|
||||
val total = response.body?.contentLength() ?: 0
|
||||
doGetTotal(currentLen + total)
|
||||
|
||||
val buf = ByteArray(buffSize)
|
||||
var len: Int
|
||||
|
||||
file = if (fileName.isEmpty()) {
|
||||
File(filePath)
|
||||
} else {
|
||||
val fileDir = File(filePath)
|
||||
if (!fileDir.exists() || !fileDir.isDirectory) {
|
||||
fileDir.mkdirs()
|
||||
}
|
||||
File(filePath, fileName)
|
||||
}
|
||||
|
||||
`is` = response.body?.byteStream() ?: FileInputStream("")
|
||||
raf = RandomAccessFile(file, "rw")
|
||||
raf.seek(currentLen)
|
||||
|
||||
var sum: Long = currentLen
|
||||
while (`is`.read(buf).also { len = it } != -1) {
|
||||
if (isCancelled()) throw CancellationException()
|
||||
raf.write(buf, 0, len)
|
||||
sum += len.toLong()
|
||||
downloadSizeInfo[fileName] = sum
|
||||
Log.e(TAG, "download progress : $sum")
|
||||
doProgress(sum)
|
||||
}
|
||||
Log.e(TAG, "download success")
|
||||
|
||||
if (!file.exists()) {
|
||||
throw FileNotFoundException("file create err,not exists")
|
||||
} else {
|
||||
doSuccess(file)
|
||||
}
|
||||
Log.e(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
|
||||
} catch (e: Exception) {
|
||||
if (deleteWhenException && file?.exists() == true) file.delete()
|
||||
Log.e(TAG, "download failed : " + e.message)
|
||||
doException(e.message.toString())
|
||||
} finally {
|
||||
try {
|
||||
`is`?.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
try {
|
||||
raf?.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCanContinueDownload(url: URL, start: Long): Boolean { //是否支持断点下载
|
||||
val requestBuilder = Request.Builder()
|
||||
requestBuilder.addHeader("RANGE", "bytes=$start-")
|
||||
requestBuilder.addHeader("Connection", "close")
|
||||
val request: Request = requestBuilder.url(url).head().build()
|
||||
val response: Response = okHttpClient.value.newCall(request).execute()
|
||||
return if (response.isSuccessful) {
|
||||
if (response.code == 206) { //支持
|
||||
response.close()
|
||||
true
|
||||
} else { //不支持
|
||||
response.close()
|
||||
false
|
||||
}
|
||||
} else {
|
||||
response.close()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDowning(tag: String): Boolean {
|
||||
for (call in okHttpClient.value.dispatcher.runningCalls()) {
|
||||
if (call.request().tag() == tag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isCancelled(): Boolean {
|
||||
return cancelledList.contains(fileName)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
cancel(filePath + fileName)
|
||||
}
|
||||
|
||||
fun cancel(tag: String) {
|
||||
cancelledList.add(fileName)
|
||||
if (okHttpClient.value.dispatcher.runningCalls().isNotEmpty()) {
|
||||
for (call in okHttpClient.value.dispatcher.runningCalls()) {
|
||||
if (call.request().tag() == tag) {
|
||||
call.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
doCancel()
|
||||
}
|
||||
|
||||
private fun doException(err: String) {
|
||||
runOnUiThread {
|
||||
if (downCallBack == null) {
|
||||
actionFail.invoke(err)
|
||||
} else {
|
||||
downCallBack?.fail(err)
|
||||
}
|
||||
mainThread = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun doSuccess(file: File?) {
|
||||
runOnUiThread {
|
||||
if (file == null) {
|
||||
doException("file not exit")
|
||||
} else {
|
||||
if (downCallBack == null) {
|
||||
actionSuccess.invoke(file)
|
||||
} else {
|
||||
downCallBack?.success(file)
|
||||
}
|
||||
}
|
||||
mainThread = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun doGetTotal(total: Long) {
|
||||
runOnUiThread {
|
||||
if (downCallBack == null) {
|
||||
actionGetTotal.invoke(total)
|
||||
} else {
|
||||
downCallBack?.total(total)
|
||||
}
|
||||
}
|
||||
mainThread = null
|
||||
}
|
||||
|
||||
private fun doProgress(progress: Long) {
|
||||
runOnUiThread {
|
||||
if (downCallBack == null) {
|
||||
actionProgress.invoke(progress)
|
||||
} else {
|
||||
downCallBack?.progress(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doCancel() {
|
||||
runOnUiThread {
|
||||
if (downCallBack == null) {
|
||||
actionCancel.invoke()
|
||||
} else {
|
||||
downCallBack?.cancel()
|
||||
}
|
||||
}
|
||||
mainThread = null
|
||||
}
|
||||
|
||||
private var mainThread: Handler? = null
|
||||
private fun runOnUiThread(action: () -> Unit) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) { // If we finish marking off of the main thread, we need to
|
||||
// actually do it on the main thread to ensure correct ordering.
|
||||
if (mainThread == null) {
|
||||
mainThread = Handler(Looper.getMainLooper())
|
||||
}
|
||||
mainThread?.post {
|
||||
action.invoke()
|
||||
}
|
||||
return
|
||||
}
|
||||
action.invoke()
|
||||
}
|
||||
|
||||
|
||||
class LoggingInterceptor : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request: Request = chain.request()
|
||||
val startTime = System.nanoTime()
|
||||
Log.d(
|
||||
TAG, String.format(
|
||||
"Sending request %s on %s%n%s",
|
||||
request.url, chain.connection(), request.headers
|
||||
)
|
||||
)
|
||||
val response: Response = chain.proceed(request)
|
||||
val endTime = System.nanoTime()
|
||||
Log.d(
|
||||
TAG, String.format(
|
||||
"Received response for %s in %.1fms%n%s",
|
||||
response.request.url, (endTime - startTime) / 1e6, response.headers
|
||||
)
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LoggingInterceptor"
|
||||
}
|
||||
}
|
||||
|
||||
interface DownCallBack {
|
||||
fun success(file: File)
|
||||
fun fail(str: String)
|
||||
fun progress(position: Long)
|
||||
fun total(total: Long)
|
||||
fun cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
package com.img.rabbit.utils;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Environment;
|
||||
import android.os.StatFs;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.img.rabbit.provider.utils.HeadParamUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings({"ResultOfMethodCallIgnored", "CallToPrintStackTrace"})
|
||||
public class FileUtils {
|
||||
|
||||
private static FileUtils instance;
|
||||
private static String packageName = "devcon";
|
||||
|
||||
// 文件缓存路径
|
||||
private String CACHE_DIR;
|
||||
|
||||
// 下载目录
|
||||
private File downloadDir;
|
||||
// 缓存目录
|
||||
private File cacheDir;
|
||||
// 图片缓存目录
|
||||
private File cacheImageDir;
|
||||
|
||||
private File cacheOriginalImageDir;
|
||||
|
||||
private File cacheEditImageDir;
|
||||
|
||||
private File cachePuzzleImageDir;
|
||||
|
||||
public static FileUtils getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (FileUtils.class) {
|
||||
if (instance == null) {
|
||||
instance = new FileUtils(Objects.requireNonNull(HeadParamUtils.INSTANCE.getApplicationContext()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private FileUtils(Context context) {
|
||||
CACHE_DIR = Objects.requireNonNull(context.getExternalFilesDir(null)).getAbsolutePath() + File.separator + packageName + File.separator;
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
cacheDir = new File(CACHE_DIR, "/cache");
|
||||
} else {
|
||||
cacheDir = context.getCacheDir();
|
||||
}
|
||||
if (!cacheDir.exists())
|
||||
cacheDir.mkdirs();
|
||||
cacheImageDir = new File(cacheDir, "/image/");
|
||||
if (!cacheImageDir.exists())
|
||||
cacheImageDir.mkdirs();
|
||||
|
||||
cacheOriginalImageDir = new File(cacheDir, "/originalImage/");
|
||||
if (!cacheOriginalImageDir.exists())
|
||||
cacheOriginalImageDir.mkdirs();
|
||||
|
||||
cacheEditImageDir = new File(cacheDir, "/cacheEditImage/");
|
||||
if (!cacheEditImageDir.exists())
|
||||
cacheEditImageDir.mkdirs();
|
||||
|
||||
cachePuzzleImageDir = new File(cacheDir, "/cachePuzzleImage/");
|
||||
if (!cachePuzzleImageDir.exists())
|
||||
cachePuzzleImageDir.mkdirs();
|
||||
|
||||
downloadDir = new File(cacheDir, "/download/");
|
||||
if (!downloadDir.exists())
|
||||
downloadDir.mkdirs();
|
||||
}
|
||||
|
||||
public String getCACHE_DIR() {
|
||||
return CACHE_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存目录
|
||||
*/
|
||||
public File getCacheDir() {
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载目录
|
||||
*/
|
||||
public File getCacheDownLoderDir() {
|
||||
return downloadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存图片目录
|
||||
*/
|
||||
public File getCacheImageDir() {
|
||||
return cacheImageDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 水印照片编辑后的路径
|
||||
*/
|
||||
public File getCacheEditImageDir() {
|
||||
return cacheEditImageDir;
|
||||
}
|
||||
|
||||
public File getCacheOriginalImageDir() {
|
||||
return cacheOriginalImageDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼图路径
|
||||
*/
|
||||
public File getCachePuzzleImageDir() {
|
||||
return cachePuzzleImageDir;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建一个临时图片文件
|
||||
*/
|
||||
public File newTempImageFile() {
|
||||
return new File(cacheImageDir, System.currentTimeMillis() + ".jpg");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否安装SD卡
|
||||
*/
|
||||
public static boolean checkSaveLocationExists() {
|
||||
String sDCardStatus = Environment.getExternalStorageState();
|
||||
boolean status;
|
||||
status = sDCardStatus.equals(Environment.MEDIA_MOUNTED);
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定目录下文件及目录
|
||||
*/
|
||||
public static void deleteFolderFile(String filePath) {
|
||||
if (!TextUtils.isEmpty(filePath)) {
|
||||
try {
|
||||
File file = new File(filePath);
|
||||
if (file.isDirectory()) {// 处理目录
|
||||
File[] files = file.listFiles();
|
||||
for (int i = 0; i < Objects.requireNonNull(files).length; i++) {
|
||||
deleteFolderFile(files[i].getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (!file.isDirectory()) {// 如果是文件,删除
|
||||
file.delete();
|
||||
} else {// 目录
|
||||
if (Objects.requireNonNull(file.listFiles()).length == 0) {// 目录下没有文件或者目录,删除
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("FileUtils", Objects.requireNonNull(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
public boolean deleteFile(String path) {
|
||||
boolean status;
|
||||
SecurityManager checker = new SecurityManager();
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
File newPath = new File(path);
|
||||
checker.checkDelete(newPath.toString());
|
||||
if (newPath.isFile()) {
|
||||
try {
|
||||
|
||||
newPath.delete();
|
||||
status = true;
|
||||
} catch (SecurityException se) {
|
||||
Log.e("FileUtils", Objects.requireNonNull(se.getMessage()));
|
||||
status = false;
|
||||
}
|
||||
} else
|
||||
status = false;
|
||||
} else
|
||||
status = false;
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录文件大小
|
||||
*/
|
||||
public static long getDirSize(File dir) {
|
||||
if (dir == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!dir.isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
long dirSize = 0;
|
||||
File[] files = dir.listFiles();
|
||||
for (File file : Objects.requireNonNull(files)) {
|
||||
if (file.isFile()) {
|
||||
dirSize += file.length();
|
||||
} else if (file.isDirectory()) {
|
||||
dirSize += file.length();
|
||||
dirSize += getDirSize(file); // 递归调用继续统计
|
||||
}
|
||||
}
|
||||
return dirSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定文件大小
|
||||
*/
|
||||
@SuppressWarnings("resource")
|
||||
public static long getFileSize(File file) throws Exception {
|
||||
long size = 0;
|
||||
if (file.exists()) {
|
||||
FileInputStream fis = null;
|
||||
fis = new FileInputStream(file);
|
||||
size = fis.available();
|
||||
} else {
|
||||
|
||||
Log.e("获取文件大小", "文件不存在!");
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定文件夹
|
||||
*/
|
||||
public static long getFileSizes(File f) throws Exception {
|
||||
long size = 0;
|
||||
File[] flist = f.listFiles();
|
||||
for (int i = 0; i < Objects.requireNonNull(flist).length; i++) {
|
||||
if (flist[i].isDirectory()) {
|
||||
size = size + getFileSizes(flist[i]);
|
||||
} else {
|
||||
size = size + getFileSize(flist[i]);
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换文件大小
|
||||
*/
|
||||
public static String toFileSize(long fileS) {
|
||||
DecimalFormat df = new DecimalFormat("#.00");
|
||||
String fileSizeString;
|
||||
String wrongSize = "0M";
|
||||
if (fileS == 0) {
|
||||
return wrongSize;
|
||||
}
|
||||
if (fileS < 1024) {
|
||||
fileSizeString = df.format((double) fileS) + "B";
|
||||
} else if (fileS < 1048576) {
|
||||
fileSizeString = df.format((double) fileS / 1024) + "K";
|
||||
} else if (fileS < 1073741824) {
|
||||
fileSizeString = df.format((double) fileS / 1048576) + "M";
|
||||
} else {
|
||||
fileSizeString = df.format((double) fileS / 1073741824) + "G";
|
||||
}
|
||||
return fileSizeString;
|
||||
}
|
||||
|
||||
//判断文件是否存在
|
||||
public boolean fileIsExists(String strFile) {
|
||||
try {
|
||||
File f = new File(strFile);
|
||||
if (!f.exists()) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//外部存储空间
|
||||
public static long getExternalStorageSpace() {
|
||||
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
StatFs externalStatFs = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath());
|
||||
long externalBlockSize = externalStatFs.getBlockSizeLong();
|
||||
long externalTotalSize = externalStatFs.getBlockCountLong() * externalBlockSize;
|
||||
long externalAvailableSize = externalStatFs.getAvailableBlocksLong() * externalBlockSize;
|
||||
Log.d("FileUtils", "当前外部空间总大小----" + externalTotalSize);
|
||||
Log.d("FileUtils", "当前外部空间可用大小----" + externalAvailableSize);
|
||||
return externalAvailableSize;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开图库
|
||||
*/
|
||||
public void openGallery(Context context) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
|
||||
context.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.img.rabbit.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
object UpdateUtils {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun download(scope: CoroutineScope, url: String, onProgress:(progress:Int)-> Unit, onFinish:(isSuccess: Boolean, filePath: String?)-> Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
var totalProgress = 0L
|
||||
DownLoadUtils.getInstance()
|
||||
.setReadTImeOut(10L)
|
||||
.setDeleteWhenException(false)
|
||||
.initUrl(url, null)
|
||||
.setFilePath(FileUtils.getInstance().cacheDownLoderDir.absolutePath)
|
||||
.setFileName(AppUpdate.getFileNameFromUrl(url))
|
||||
.setActionCallBack(
|
||||
{ totalProgress = it },
|
||||
{
|
||||
val percent = it.toDouble() / totalProgress.toDouble() * 100
|
||||
val curProgress = percent.toInt()
|
||||
onProgress(curProgress)
|
||||
},
|
||||
{
|
||||
onFinish(true, it.absolutePath)
|
||||
}, {
|
||||
onFinish(false, null)
|
||||
}).down()
|
||||
}
|
||||
}
|
||||
|
||||
fun install(context: Context, apkFilePath: String) {
|
||||
try {
|
||||
val apkFile = File(FileUtils.getInstance().cacheDownLoderDir, AppUpdate.getFileNameFromUrl(apkFilePath))
|
||||
if (!apkFile.exists()) {
|
||||
return
|
||||
}
|
||||
val apkUri = FileProvider.getUriForFile(context, context.packageName + ".fileProvider", apkFile)
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,8 @@ class LoginViewModel : BaseViewModel() {
|
|||
fun requestUserConfig(){
|
||||
mLaunch {
|
||||
val oaid = MMKVUtils.getString("oaid") ?: ""
|
||||
val response = ApiManager.serviceVo.getUserConfig(oaid, Build.VERSION.SDK_INT, "", DeviceIdentifier.getAndroidID(applicationContext), MMKVUtils.getString("gt_cid") ?: "")
|
||||
//val response = ApiManager.serviceVo.getUserConfig(oaid, Build.VERSION.SDK_INT, "", DeviceIdentifier.getAndroidID(applicationContext), MMKVUtils.getString("gt_cid") ?: "")
|
||||
val response = ApiManager.serviceVo.getUserConfig()
|
||||
if (response.status) {
|
||||
PreferenceUtil.saveXToken(response.data.token)
|
||||
PreferenceUtil.setTimeDiff(response.data.nowtime.toLong() - System.currentTimeMillis() / 1000)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.img.rabbit.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import com.img.rabbit.bean.response.ServiceWxLinkEntity
|
||||
import com.img.rabbit.provider.api.ApiManager
|
||||
import com.tencent.mm.opensdk.constants.Build
|
||||
import com.tencent.mm.opensdk.modelbiz.WXOpenCustomerServiceChat
|
||||
import com.tencent.mm.opensdk.openapi.IWXAPI
|
||||
|
||||
class MineViewModel : BaseViewModel() {
|
||||
private val TAG = "MineViewModel"
|
||||
|
||||
//请求客服连接
|
||||
fun requestServiceLink(context: Context,api: IWXAPI){
|
||||
mLaunch {
|
||||
val response = ApiManager.serviceVo.wxService()
|
||||
if (response.status) {
|
||||
contactClientService(response.data,context,api)
|
||||
} else {
|
||||
}
|
||||
isLoading.value = false // 加载完成
|
||||
}
|
||||
}
|
||||
|
||||
private fun contactClientService(data: ServiceWxLinkEntity,context: Context,api: IWXAPI) {
|
||||
if (api.wxAppSupportAPI >= Build.SUPPORT_OPEN_CUSTOMER_SERVICE_CHAT) {
|
||||
val req = WXOpenCustomerServiceChat.Req()
|
||||
req.corpId = data.corpid
|
||||
req.url = data.address
|
||||
api.sendReq(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.img.rabbit.bean.response.AlipayParamEntity
|
|||
import com.img.rabbit.bean.response.CaptchaCodeEntity
|
||||
import com.img.rabbit.bean.response.UploadFileEntity
|
||||
import com.img.rabbit.bean.response.LoginInfoEntity
|
||||
import com.img.rabbit.bean.response.ServiceWxLinkEntity
|
||||
import com.img.rabbit.bean.response.UserConfigEntity
|
||||
import com.img.rabbit.bean.response.UserInfoEntity
|
||||
import com.img.rabbit.provider.api.ResultVo
|
||||
|
|
@ -38,11 +39,11 @@ interface ServiceVo {
|
|||
*/
|
||||
@GET("/api/user/config")
|
||||
suspend fun getUserConfig(
|
||||
@Query("oaid") oaid: String,
|
||||
@Query("os_version") osVersion: Int,
|
||||
@Query("ua") ua: String,
|
||||
@Query("imei") imei: String,
|
||||
@Query("cid") cid: String,
|
||||
// @Query("oaid") oaid: String,
|
||||
// @Query("os_version") osVersion: Int,
|
||||
// @Query("ua") ua: String,
|
||||
// @Query("imei") imei: String,
|
||||
// @Query("cid") cid: String,
|
||||
): ResultVo<UserConfigEntity>
|
||||
|
||||
@GET("api/alipay/app_param")
|
||||
|
|
@ -50,6 +51,7 @@ interface ServiceVo {
|
|||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
*/
|
||||
@POST("api/user/code")
|
||||
suspend fun sendCode(@Body requestBody: RequestBody): ResultVo<CaptchaCodeEntity>
|
||||
|
|
@ -103,4 +105,10 @@ interface ServiceVo {
|
|||
*/
|
||||
@POST("/api/user/feedback")
|
||||
suspend fun feedback(@Body requestBody: RequestBody): ResultVo<Any>
|
||||
|
||||
/**
|
||||
* 联系客服(获取客服连接)
|
||||
*/
|
||||
@GET("/api/weixin/service")
|
||||
suspend fun wxService(): ResultVo<ServiceWxLinkEntity>
|
||||
}
|
||||
|
|
@ -47,4 +47,5 @@
|
|||
<cache-path
|
||||
name="cache"
|
||||
path="." />
|
||||
|
||||
</paths>
|
||||
Loading…
Reference in New Issue