1、app更新弹窗、下载、安装
2、客服
3、部分状态处理
This commit is contained in:
shenzuqiang 2026-02-28 17:21:45 +08:00
parent 1168061f72
commit d558fde6f3
20 changed files with 1203 additions and 222 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=JRBI89BIE6AI5TG6" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=Y5DELZR46DZTCI9D" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -23,8 +23,8 @@ android {
applicationId = "com.img.rabbit" applicationId = "com.img.rabbit"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 2 versionCode = 1
versionName = "1.1" versionName = "1.0.0"
setManifestPlaceholders(mapOf( setManifestPlaceholders(mapOf(

View File

@ -9,7 +9,7 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:ignore="SelectedPhotoAccess" /> tools:ignore="ScopedStorage,SelectedPhotoAccess" />
<uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE" <uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:ignore="SelectedPhotoAccess" /> tools:ignore="SelectedPhotoAccess" />
@ -27,6 +27,13 @@
<!--开关wifi状态解决国内机型移动网络权限问题需要--> <!--开关wifi状态解决国内机型移动网络权限问题需要-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <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> <queries>
<package android:name="com.eg.android.AlipayGphone" /> <!-- 支付宝 --> <package android:name="com.eg.android.AlipayGphone" /> <!-- 支付宝 -->

View File

@ -1,8 +1,11 @@
package com.img.rabbit package com.img.rabbit
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@ -32,8 +35,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -52,16 +57,19 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.img.rabbit.config.Constants import com.img.rabbit.config.Constants
import com.img.rabbit.config.Constants.agreementUrl import com.img.rabbit.config.Constants.agreementUrl
import com.img.rabbit.config.Constants.privacyUrl import com.img.rabbit.config.Constants.privacyUrl
import com.img.rabbit.pages.LoginScreen import com.img.rabbit.pages.LoginScreen
import com.img.rabbit.pages.LoginScreenType import com.img.rabbit.pages.LoginScreenType
import com.img.rabbit.pages.MainScreen 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.GlobalStateManager
import com.img.rabbit.provider.storage.PreferenceUtil import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.provider.storage.PreferenceUtil.saveBDVID import com.img.rabbit.provider.storage.PreferenceUtil.saveBDVID
import com.img.rabbit.utils.ChannelUtils import com.img.rabbit.utils.ChannelUtils
import com.img.rabbit.utils.UpdateUtils
import com.img.rabbit.utils.UrlLinkUtils.openAgreement import com.img.rabbit.utils.UrlLinkUtils.openAgreement
import com.img.rabbit.viewmodel.GeneralViewModel import com.img.rabbit.viewmodel.GeneralViewModel
import com.img.rabbit.viewmodel.LoginViewModel import com.img.rabbit.viewmodel.LoginViewModel
@ -75,7 +83,7 @@ import kotlinx.coroutines.launch
import kotlin.system.exitProcess import kotlin.system.exitProcess
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class, ExperimentalPermissionsApi::class)
@SuppressLint("UnrememberedMutableState", "CoroutineCreationDuringComposition") @SuppressLint("UnrememberedMutableState", "CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// 必须在 super.onCreate 之前调用 // 必须在 super.onCreate 之前调用
@ -86,7 +94,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
var showUpdateDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope()
val splashViewModel: SplashViewModel = viewModel() val splashViewModel: SplashViewModel = viewModel()
val generalViewModel: GeneralViewModel = viewModel() val generalViewModel: GeneralViewModel = viewModel()
@ -95,7 +103,9 @@ class MainActivity : ComponentActivity() {
var showSplash by remember { mutableStateOf(false) } var showSplash by remember { mutableStateOf(false) }
var globalLogin by mutableStateOf(GlobalStateManager(context).globalLoginNotifyFlow().collectAsState(initial = false)) var globalLogin by mutableStateOf(GlobalStateManager(context).globalLoginNotifyFlow().collectAsState(initial = false))
var globalLogout by mutableStateOf(GlobalStateManager(context).globalLogoutNotifyFlow().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 globalUnBind by mutableStateOf(GlobalStateManager(context).globalUnBindNotifyFlow().collectAsState(initial = false))
var globalUpdate by mutableStateOf(GlobalStateManager(context).globalUpdateNotifyFlow().collectAsState(initial = false))
LaunchedEffect(generalViewModel.agreementStatus.value) { LaunchedEffect(generalViewModel.agreementStatus.value) {
if (generalViewModel.agreementStatus.value == true){ if (generalViewModel.agreementStatus.value == true){
@ -117,6 +127,13 @@ class MainActivity : ComponentActivity() {
splashScreen.setKeepOnScreenCondition { splashScreen.setKeepOnScreenCondition {
splashViewModel.isLoading.value // 当为 true 时,启动页不消失 splashViewModel.isLoading.value // 当为 true 时,启动页不消失
} }
//更新用户配置与用户信息
if(globalLogin.value == true || globalBind.value == true || globalUnBind.value == true || globalLogout.value == true){
loginViewModel.requestUserConfig()
loginViewModel.requestUserInfo()
}
// 登录成功后2秒后自动更新状态 // 登录成功后2秒后自动更新状态
if(globalLogin.value == true){ if(globalLogin.value == true){
GlobalScope.launch { 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秒后自动更新状态 // 解绑成功后2秒后自动更新状态
if(globalUnBind.value == true){ if(globalUnBind.value == true){
GlobalScope.launch { GlobalScope.launch {
@ -195,15 +221,43 @@ class MainActivity : ComponentActivity() {
} }
} }
val isStartDownload = mutableStateOf(false)
if(showUpdateDialog){ val progressState = mutableFloatStateOf(0f)
if(globalUpdate.value == true){
UpdateDialog( UpdateDialog(
title = "新版本,更新提示", title = PreferenceUtil.getUserConfig()?.config?.versionEntity?.title?:"新版本,更新提示",
newVersion = "1.0.0", newVersion = "V${PreferenceUtil.getUserConfig()?.config?.versionEntity?.version}",
desc = "修复了一些问题,新增了一些功能", desc = PreferenceUtil.getUserConfig()?.config?.versionEntity?.description?:"",
url = "https://www.baidu.com", url = PreferenceUtil.getUserConfig()?.config?.versionEntity?.url?:"",
){ state -> scope = scope,
showUpdateDialog = state 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) @Preview(showBackground = true)
@Composable @Composable
fun MainScreenPreview() { fun MainScreenPreview() {

View File

@ -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 = ""
}

View File

@ -106,10 +106,6 @@ fun LoginScreen(navController: NavHostController? = null, generalViewModel: Gene
if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) { if (loginViewModel.loginState.value !=null && loginViewModel.loginState.value?.data?.token != null) {
//登录成功 //登录成功
PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token) PreferenceUtil.saveAccessToken(loginViewModel.loginState.value?.data?.token)
// 获取用户配置
loginViewModel.requestUserConfig()
// 获取用户信息
loginViewModel.requestUserInfo()
loginViewModel.setLogin(true) loginViewModel.setLogin(true)
//更新登录状态 //更新登录状态

View File

@ -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)
)
}
}
}
}
}
}
}

View File

@ -53,8 +53,7 @@ import com.img.rabbit.provider.storage.PreferenceUtil
import com.img.rabbit.utils.AppUpdate import com.img.rabbit.utils.AppUpdate
import com.img.rabbit.viewmodel.GeneralViewModel import com.img.rabbit.viewmodel.GeneralViewModel
import com.img.rabbit.viewmodel.LoginViewModel import com.img.rabbit.viewmodel.LoginViewModel
import kotlinx.coroutines.GlobalScope import com.img.rabbit.viewmodel.MineViewModel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -62,12 +61,14 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun MineScreen( fun MineScreen(
navController: NavHostController, navController: NavHostController,
viewModel: MineViewModel = viewModel(),
generalViewModel: GeneralViewModel, generalViewModel: GeneralViewModel,
) { ) {
val TAG = "Rabbit_Mine" val TAG = "Rabbit_Mine"
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var globalLogin by mutableStateOf(GlobalStateManager(context).globalLoginNotifyFlow().collectAsState(initial = false)) 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)) var globalUnBind by mutableStateOf(GlobalStateManager(context).globalUnBindNotifyFlow().collectAsState(initial = false))
val vipMember by remember { mutableStateOf(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 { scope.launch {
delay(300) delay(300)
userInfo = PreferenceUtil.getUserInfo() userInfo = PreferenceUtil.getUserInfo()
@ -161,7 +162,7 @@ fun MineScreen(
// 跳转登录页面 // 跳转登录页面
navController.navigate("login?type=${LoginViewModel.JumpLoginType.NORMAL.type}") navController.navigate("login?type=${LoginViewModel.JumpLoginType.NORMAL.type}")
} else { } else {
//TODO 已登录,跳转个人信息页面 // 已登录,跳转个人信息页面
//navController.navigate("userInfo") //navController.navigate("userInfo")
} }
} }
@ -219,10 +220,11 @@ fun MineScreen(
} }
} }
// VIP bar
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
/*
// VIP bar
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -300,6 +302,7 @@ fun MineScreen(
) )
} }
} }
*/
//功能项 //功能项
Column( Column(
@ -381,10 +384,11 @@ fun MineScreen(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
// 隐藏TabBar if (!generalViewModel.api.isWXAppInstalled) {
generalViewModel.setNavigationBarVisible(false) Toast.makeText(context, "未安装微信客户端", Toast.LENGTH_SHORT).show()
// 跳转在线客服页面 }else if(userInfo != null){
navController.navigate("onlineService") viewModel.requestServiceLink(context, generalViewModel.api)
}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -444,7 +448,9 @@ fun MineScreen(
if (isUpdate) { if (isUpdate) {
//提示执行更新 //提示执行更新
//startUpdate(result, fragment) //startUpdate(result, fragment)
Log.i(TAG, "checkUpdate: 有新版本, tips = $tips") scope.launch {
GlobalStateManager(context).storeGlobalUpdateNotify(true)
}
} else { } else {
Toast.makeText(context, tips, Toast.LENGTH_SHORT).show() Toast.makeText(context, tips, Toast.LENGTH_SHORT).show()
} }

View File

@ -65,10 +65,6 @@ fun AccountBindScreen(navController: NavHostController, viewModel: AccountBindVi
LaunchedEffect(viewModel.unBindState.value) { LaunchedEffect(viewModel.unBindState.value) {
if(viewModel.unBindState.value != null){ if(viewModel.unBindState.value != null){
// 获取用户配置
loginViewModel.requestUserConfig()
// 获取用户信息
loginViewModel.requestUserInfo()
Toast.makeText(context, "解绑成功!", Toast.LENGTH_SHORT).show() Toast.makeText(context, "解绑成功!", Toast.LENGTH_SHORT).show()
viewModel.unBindState.value = null viewModel.unBindState.value = null

View File

@ -77,10 +77,6 @@ fun AccountManagerScreen(navController: NavHostController, viewModel: AccountMan
if (viewModel.switchState.value != null && viewModel.switchState.value?.data?.token != null) { if (viewModel.switchState.value != null && viewModel.switchState.value?.data?.token != null) {
// 切换账号成功 // 切换账号成功
PreferenceUtil.saveAccessToken(viewModel.switchState.value?.data?.token) PreferenceUtil.saveAccessToken(viewModel.switchState.value?.data?.token)
// 获取用户配置
loginViewModel.requestUserConfig()
// 获取用户信息
loginViewModel.requestUserInfo()
loginViewModel.setLogin(true) loginViewModel.setLogin(true)
//更新登录状态 //更新登录状态

View File

@ -109,6 +109,9 @@ fun BindScreen(navController: NavHostController, viewModel: BindViewModel = view
LaunchedEffect(viewModel.bindState.value) { LaunchedEffect(viewModel.bindState.value) {
if (viewModel.bindState.value != null && viewModel.bindState.value?.data?.token != null) { if (viewModel.bindState.value != null && viewModel.bindState.value?.data?.token != null) {
Toast.makeText(context, "绑定成功!", Toast.LENGTH_SHORT).show() Toast.makeText(context, "绑定成功!", Toast.LENGTH_SHORT).show()
GlobalStateManager(context).storeGlobalBindNotify(true)
navController.popBackStack()
}else if (viewModel.bindState.value != null){ }else if (viewModel.bindState.value != null){
Toast.makeText(context, "绑定失败!", Toast.LENGTH_SHORT).show() Toast.makeText(context, "绑定失败!", Toast.LENGTH_SHORT).show()
} }

View File

@ -21,7 +21,9 @@ class GlobalStateManager(
private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization") private val GLOBAL_WX_AUTHORIZATION = stringPreferencesKey("global_wx_authorization")
private val GLOBAL_LOGIN_NOTIFY = booleanPreferencesKey("global_login_notify") private val GLOBAL_LOGIN_NOTIFY = booleanPreferencesKey("global_login_notify")
private val GLOBAL_LOGOUT_NOTIFY = booleanPreferencesKey("global_logout_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_UNBIND_NOTIFY = booleanPreferencesKey("global_unbind_notify")
private val GLOBAL_UPDATE_NOTIFY = booleanPreferencesKey("global_update_notify")
} }
suspend fun storeGlobalLoading(value: Boolean) { 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) { suspend fun storeGlobalUnBindNotify(value: Boolean) {
context.storeData.edit { preferences -> context.storeData.edit { preferences ->
preferences[GLOBAL_UNBIND_NOTIFY] = value 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]
}
}
} }

View File

@ -48,4 +48,9 @@ object AppUpdate {
} }
return false return false
} }
fun getFileNameFromUrl(url: String): String {
return url.substring(url.lastIndexOf('/') + 1)
}
} }

View File

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

View File

@ -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();
}
}
}

View File

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

View File

@ -93,7 +93,8 @@ class LoginViewModel : BaseViewModel() {
fun requestUserConfig(){ fun requestUserConfig(){
mLaunch { mLaunch {
val oaid = MMKVUtils.getString("oaid") ?: "" 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) { if (response.status) {
PreferenceUtil.saveXToken(response.data.token) PreferenceUtil.saveXToken(response.data.token)
PreferenceUtil.setTimeDiff(response.data.nowtime.toLong() - System.currentTimeMillis() / 1000) PreferenceUtil.setTimeDiff(response.data.nowtime.toLong() - System.currentTimeMillis() / 1000)

View File

@ -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)
}
}
}

View File

@ -4,6 +4,7 @@ import com.img.rabbit.bean.response.AlipayParamEntity
import com.img.rabbit.bean.response.CaptchaCodeEntity import com.img.rabbit.bean.response.CaptchaCodeEntity
import com.img.rabbit.bean.response.UploadFileEntity import com.img.rabbit.bean.response.UploadFileEntity
import com.img.rabbit.bean.response.LoginInfoEntity 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.UserConfigEntity
import com.img.rabbit.bean.response.UserInfoEntity import com.img.rabbit.bean.response.UserInfoEntity
import com.img.rabbit.provider.api.ResultVo import com.img.rabbit.provider.api.ResultVo
@ -38,11 +39,11 @@ interface ServiceVo {
*/ */
@GET("/api/user/config") @GET("/api/user/config")
suspend fun getUserConfig( suspend fun getUserConfig(
@Query("oaid") oaid: String, // @Query("oaid") oaid: String,
@Query("os_version") osVersion: Int, // @Query("os_version") osVersion: Int,
@Query("ua") ua: String, // @Query("ua") ua: String,
@Query("imei") imei: String, // @Query("imei") imei: String,
@Query("cid") cid: String, // @Query("cid") cid: String,
): ResultVo<UserConfigEntity> ): ResultVo<UserConfigEntity>
@GET("api/alipay/app_param") @GET("api/alipay/app_param")
@ -50,6 +51,7 @@ interface ServiceVo {
/** /**
* 发送验证码 * 发送验证码
*
*/ */
@POST("api/user/code") @POST("api/user/code")
suspend fun sendCode(@Body requestBody: RequestBody): ResultVo<CaptchaCodeEntity> suspend fun sendCode(@Body requestBody: RequestBody): ResultVo<CaptchaCodeEntity>
@ -103,4 +105,10 @@ interface ServiceVo {
*/ */
@POST("/api/user/feedback") @POST("/api/user/feedback")
suspend fun feedback(@Body requestBody: RequestBody): ResultVo<Any> suspend fun feedback(@Body requestBody: RequestBody): ResultVo<Any>
/**
* 联系客服获取客服连接
*/
@GET("/api/weixin/service")
suspend fun wxService(): ResultVo<ServiceWxLinkEntity>
} }

View File

@ -47,4 +47,5 @@
<cache-path <cache-path
name="cache" name="cache"
path="." /> path="." />
</paths> </paths>