feat: 添加双 token 机制,进行无感更新token

This commit is contained in:
糕小菜 2025-01-22 22:03:38 +08:00
parent 4e0acd5335
commit 632cd5c4e6
22 changed files with 431 additions and 73 deletions

View File

@ -154,7 +154,7 @@
},
{
"id": "5:2885532406154205395",
"lastPropertyId": "7:7996607163318458427",
"lastPropertyId": "9:1671273767790122969",
"name": "UserInfo",
"properties": [
{
@ -194,6 +194,16 @@
"id": "7:7996607163318458427",
"name": "status",
"type": 9
},
{
"id": "8:6299664727899448104",
"name": "accessToken",
"type": 9
},
{
"id": "9:1671273767790122969",
"name": "refreshToken",
"type": 9
}
],
"relations": []

View File

@ -74,7 +74,7 @@
},
{
"id": "4:6179749773128044271",
"lastPropertyId": "14:5371512009949707960",
"lastPropertyId": "15:191614052410347083",
"name": "Contact",
"properties": [
{
@ -145,8 +145,8 @@
"type": 9
},
{
"id": "14:5371512009949707960",
"name": "disturb",
"id": "15:191614052410347083",
"name": "doNotDisturb",
"type": 1
}
],
@ -298,7 +298,8 @@
2020630799900991467,
385998119105891942,
8166842332862045141,
605708604168234493
605708604168234493,
5371512009949707960
],
"retiredRelationUids": [],
"version": 1

View File

@ -0,0 +1 @@
{"ddd":0,"fr":100,"h":200,"ip":0,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"loader_21","ln":"WlKberZN","sr":1,"ks":{"a":{"ix":2,"a":0,"k":[0,0]},"p":{"ix":2,"a":0,"k":[0,0]},"s":{"ix":2,"a":0,"k":[100,100]},"r":{"ix":2,"a":0,"k":0},"sk":{"ix":2,"a":0,"k":0},"sa":{"ix":2,"a":0,"k":0},"o":{"ix":2,"a":0,"k":100}},"ao":0,"ip":0,"op":101,"st":0,"bm":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Shape 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,-19.75],[0,20]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[49],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[49],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":83,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[50],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[50],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":83,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[0.45098039215686275,0.4235294117647059,0.9294117647058824],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":16,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[40,40.125],"ix":2},"a":{"a":0,"k":[0,0.125],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[90],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[90],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":103,"s":[180],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Shape 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"v":[[0,-19.75],[0,20]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}}},{"ty":"tm","s":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[49],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[49],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":83,"s":[0],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[50],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[50],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":83,"s":[100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":0,"ix":2},"m":1},{"ty":"st","c":{"a":0,"k":[0.9607843137254902,0.3607843137254902,0.47843137254901963],"ix":2},"o":{"a":0,"k":100,"ix":2},"w":{"a":0,"k":16,"ix":2},"lc":2,"lj":2,"ml":4},{"ty":"tr","p":{"a":0,"k":[40,40.125],"ix":2},"a":{"a":0,"k":[0,0.125],"ix":2},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":33,"s":[94,94],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[94,94],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":83,"s":[100,100],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"r":{"a":1,"k":[{"t":0,"s":[90],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":43,"s":[180],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":50,"s":[180],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}},{"t":103,"s":[270],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[100.0000038146973,99.99999618530273],"ix":2},"a":{"a":0,"k":[39.99999904632568,40.12500286102295],"ix":2},"s":{"a":0,"k":[150,150],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}]}],"markers":[],"meta":{"g":"jsDesign"},"nm":"Comp 1","op":100,"v":"5.7.5","w":200}

View File

@ -21,5 +21,7 @@ data class UserInfo(
var avatarUrl: String,
var signature: String,
var telephone: String,
var status: String? = null
var status: String? = null,
var accessToken: String? = null,
var refreshToken: String? = null
)

View File

@ -17,7 +17,7 @@ import io.objectbox.Box
*/
class UserAuthRepository {
private val userApiService = RetrofitClient.userApiService
private val authApiService = RetrofitClient.authApiService
private val userInfoBox: Box<UserInfo> by lazy { getBoxStore().boxFor(UserInfo::class.java) }
@ -26,7 +26,7 @@ class UserAuthRepository {
// 用户注册
suspend fun register(registerRequest: RegisterRequest): Result<Register?> {
return apiCall(
apiCall = { userApiService.register(registerRequest) },
apiCall = { authApiService.register(registerRequest) },
errorMessage = "注册成功,但未返回用户数据"
).onSuccess { register ->
register?.let {
@ -38,7 +38,7 @@ class UserAuthRepository {
// 登录方法
suspend fun loginByUsername(username: String, password: String): Result<UserInfo?> {
return apiCall(
apiCall = { userApiService.loginByUsername(username, password) },
apiCall = { authApiService.loginByUsername(username, password) },
errorMessage = "登录成功,但未返回用户数据"
).onSuccess { userInfo ->
userInfo?.let {
@ -50,7 +50,7 @@ class UserAuthRepository {
suspend fun loginByTelephone(telephone: String, password: String): Result<UserInfo?> {
return apiCall(
apiCall = { userApiService.loginByTelephone(telephone, password) },
apiCall = { authApiService.loginByTelephone(telephone, password) },
errorMessage = "登录成功,但未返回用户数据"
).onSuccess { userInfo ->
userInfo?.let {

View File

@ -0,0 +1,96 @@
package com.kaixed.kchat.manager
/**
* @Author: kaixed
* @Date: 2025/1/15 21:29
*/
import android.app.Activity
import android.content.Context
import java.util.Stack
class AppManager private constructor() {
private val activityStack: Stack<Activity> = Stack()
companion object {
val instance: AppManager by lazy { AppManager() }
}
/**
* 添加 Activity 到堆栈
*/
fun addActivity(activity: Activity) {
activityStack.add(activity)
}
/**
* 获取当前 Activity堆栈中最后一个压入的
*/
fun currentActivity(): Activity? {
return if (activityStack.isNotEmpty()) {
activityStack.lastElement()
} else {
null
}
}
/**
* 结束当前 Activity堆栈中最后一个压入的
*/
fun finishActivity() {
currentActivity()?.let { finishActivity(it) }
}
/**
* 结束指定的 Activity
*/
fun finishActivity(activity: Activity) {
activityStack.remove(activity)
activity.finish()
}
/**
* 结束指定类名的 Activity
*/
fun finishActivity(cls: Class<*>) {
val iterator = activityStack.iterator()
while (iterator.hasNext()) {
val activity = iterator.next()
if (activity.javaClass == cls) {
iterator.remove()
activity.finish()
}
}
}
/**
* 结束所有 Activity
*/
fun finishAllActivities() {
while (activityStack.isNotEmpty()) {
val activity = activityStack.pop()
activity.finish()
}
}
/**
* 结束所有 Activity
*/
fun finishAllActivities(activity: Activity) {
activity.finishAffinity()
}
/**
* 退出应用程序
*/
fun appExit(context: Context) {
try {
finishAllActivities()
val activityMgr =
context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
activityMgr.restartPackage(context.packageName) // 注意:此方法已被废弃,实际应用中可能需要调整退出逻辑
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@ -7,9 +7,10 @@ package com.kaixed.kchat.network
object NetworkInterface {
// private const val URL = "app.kaixed.com/kchat"
// private const val URL = "192.168.45.209:6196/kchat"
private const val URL = "49.233.105.103:6000"
const val SERVER_URL = "https://$URL"
private const val URL = "192.168.31.18:6196"
// private const val URL = "49.233.105.103:6000"
// const val SERVER_URL = "https://$URL"
const val SERVER_URL = "http://$URL"
const val WEBSOCKET_SERVER_URL = "wss://$URL"
const val WEBSOCKET = "/websocket/single/"
const val USER_INFO = "/users/info/"

View File

@ -1,5 +1,7 @@
package com.kaixed.kchat.network
import com.kaixed.kchat.network.interceptor.TokenRefreshInterceptor
import com.kaixed.kchat.network.service.AuthApiService
import com.kaixed.kchat.network.service.ContactService
import com.kaixed.kchat.network.service.FileApiService
import com.kaixed.kchat.network.service.UserApiService
@ -7,6 +9,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
/**
* @Author: kaixed
@ -14,28 +17,50 @@ import retrofit2.converter.gson.GsonConverterFactory
*/
object RetrofitClient {
// private const val BASE_URL = "https://app.kaixed.com/kchat/"
// private const val BASE_URL = "http://192.168.45.209:6196/"
private const val BASE_URL = "http://49.233.105.103:6000/"
private const val BASE_URL = "${NetworkInterface.SERVER_URL}/"
// 添加日志拦截器
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val client = OkHttpClient.Builder()
// 创建一个不包含 Token 拦截器的 OkHttpClient专门用于授权 API
private val authClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
// .addInterceptor(SignInterceptor()) // 添加签名拦截器
.pingInterval(15, TimeUnit.SECONDS)
.build()
// 创建单独的 Retrofit 实例(专门用于授权 API
private val authRetrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(authClient) // 使用不包含 Token 拦截器的 OkHttpClient
.build()
}
// 授权 API 服务实例,使用不包含 Token 拦截器的 Retrofit 实例
val authApiService: AuthApiService by lazy {
authRetrofit.create(AuthApiService::class.java)
}
// 创建一个 OkHttpClient包含 Token 拦截器
private val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.pingInterval(15, TimeUnit.SECONDS)
.addInterceptor(TokenRefreshInterceptor()) // Token 拦截器
.build()
// 创建 Retrofit 实例(用于普通 API 调用)
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.client(client) // 使用包含 Token 拦截器的 OkHttpClient
.build()
}
// API 服务实例
val userApiService: UserApiService by lazy {
retrofit.create(UserApiService::class.java)
}

View File

@ -0,0 +1,127 @@
package com.kaixed.kchat.network.interceptor
import android.util.Base64
import android.util.Log
import com.kaixed.kchat.network.ApiCall
import com.kaixed.kchat.network.RetrofitClient
import com.kaixed.kchat.utils.CacheUtils
import com.kaixed.kchat.utils.ConstantsUtils.getUsername
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.Interceptor
import okhttp3.Response
import org.json.JSONObject
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class TokenRefreshInterceptor() : Interceptor {
private val lock = ReentrantLock()
private val authApiService by lazy { RetrofitClient.authApiService }
override fun intercept(chain: Interceptor.Chain): Response {
// 获取当前的 accessToken
var accessToken = CacheUtils.getAccessToken()
var refreshToken = CacheUtils.getRefreshToken()
val expiration = parseTokenManually(accessToken)
val refreshTokenExpiration = parseTokenManually(refreshToken)
val now = System.currentTimeMillis() / 1000
Log.d("haha", now.toString())
// 如果 Token 距离过期时间小于 5 分钟,刷新 Token
if (expiration - now < 300) {
lock.withLock {
// Double-check 防止并发刷新
val latestAccessToken = CacheUtils.getAccessToken()
if (latestAccessToken == accessToken) {
// 调用刷新 Token 方法
runBlocking {
refreshAccessToken() // 刷新 Refresh Token
}
} else {
accessToken = latestAccessToken
}
}
}
// 判断 Refresh Token 是否即将过期(提前一天刷新)
if (refreshTokenExpiration - now < 24 * 60 * 60) { // 剩余小于24小时
lock.withLock {
val latestRefreshToken = CacheUtils.getRefreshToken()
if (latestRefreshToken == refreshToken) {
runBlocking {
refreshRefreshToken() // 刷新 Refresh Token
}
}
}
}
// 将最新的 Token 添加到请求头
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $accessToken")
.build()
return chain.proceed(newRequest)
}
private suspend fun refreshAccessToken() {
return withContext(Dispatchers.IO) {
try {
ApiCall.apiCall(
apiCall = { authApiService.auth("Bearer ${CacheUtils.getRefreshToken()}", getUsername()) },
errorMessage = "刷新Token失败"
).onSuccess {
it?.let {
CacheUtils.setAccessToken(it)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private suspend fun refreshRefreshToken() {
return withContext(Dispatchers.IO) {
try {
ApiCall.apiCall(
apiCall = {
authApiService.refresh(
CacheUtils.getRefreshToken(),
getUsername()
)
},
errorMessage = "刷新Token失败"
).onSuccess {
it?.let {
CacheUtils.setRefreshToken(it)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun parseTokenManually(token: String): Long {
var expiration = 0L
try {
// JWT 的有效载荷部分是第二部分
val payload = token.split(".")[1]
// 解码Base64编码的字符串
val decodedBytes = Base64.decode(payload, Base64.URL_SAFE or Base64.NO_WRAP)
val decodedString = String(decodedBytes)
// 将解码后的字符串转为JSONObject
val jsonObject = JSONObject(decodedString)
expiration = jsonObject.optLong("exp") // 获取过期时间
} catch (e: Exception) {
e.printStackTrace()
}
return expiration
}
}

View File

@ -0,0 +1,53 @@
package com.kaixed.kchat.network.service
import com.kaixed.kchat.data.local.entity.UserInfo
import com.kaixed.kchat.data.model.request.RegisterRequest
import com.kaixed.kchat.data.model.response.register.Register
import com.kaixed.kchat.network.ApiResponse
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
/**
* @Author: kaixed
* @Date: 2025/1/22 18:35
*/
interface AuthApiService {
@POST("users/access-token")
suspend fun auth(
@Header("Authorization") token: String,
@Query("username") username: String
): ApiResponse<String>
@POST("users/refresh-token")
suspend fun refresh(
@Header("Authorization") token: String,
@Query("username") username: String
): ApiResponse<String>
// 登录接口(根据用户名登录)
@FormUrlEncoded
@POST("users/login/username")
suspend fun loginByUsername(
@Field("username") username: String,
@Field("password") password: String
): ApiResponse<UserInfo?>
// 登录接口(根据电话登录)
@FormUrlEncoded
@POST("users/login/telephone")
suspend fun loginByTelephone(
@Field("telephone") telephone: String,
@Field("password") password: String
): ApiResponse<UserInfo>
// 注册接口
@POST("users/register")
suspend fun register(
@Body registerRequest: RegisterRequest
): ApiResponse<Register>
}

View File

@ -24,28 +24,6 @@ import retrofit2.http.Path
*/
interface UserApiService {
// 注册接口
@POST("users/register")
suspend fun register(
@Body registerRequest: RegisterRequest
): ApiResponse<Register>
// 登录接口(根据用户名登录)
@FormUrlEncoded
@POST("users/login/username")
suspend fun loginByUsername(
@Field("username") username: String,
@Field("password") password: String
): ApiResponse<UserInfo?>
// 登录接口(根据电话登录)
@FormUrlEncoded
@POST("users/login/telephone")
suspend fun loginByTelephone(
@Field("telephone") telephone: String,
@Field("password") password: String
): ApiResponse<UserInfo>
// 获取用户列表
@GET("userList/{username}")
suspend fun getUserListByNickname(

View File

@ -15,9 +15,10 @@ import com.kaixed.kchat.R
import com.kaixed.kchat.data.LocalDatabase
import com.kaixed.kchat.databinding.ActivityLoginBinding
import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.Constants.ACCESS_TOKEN
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
import com.kaixed.kchat.utils.Constants.REFRESH_TOKEN
import com.kaixed.kchat.utils.DrawableUtil.createDrawable
import com.kaixed.kchat.utils.ScreenUtils.dp2px
import com.kaixed.kchat.viewmodel.UserViewModel
@ -80,6 +81,8 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
loginResult.onSuccess {
it?.let {
LocalDatabase.saveUserInfo(it)
MMKV.defaultMMKV().putString(ACCESS_TOKEN, it.accessToken)
MMKV.defaultMMKV().putString(REFRESH_TOKEN, it.refreshToken)
}
navigateToMain()
}

View File

@ -90,13 +90,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
initViewPager()
setListener()
preLoad()
}
private fun preLoad() {
Glide.with(this)
.load(getAvatarUrl())
.preload()
}
// 待优化成自定义界面目前使用的华为sdk的默认界面
@ -182,7 +175,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
lifecycleScope.launch(Dispatchers.Main) {
delay(200)
loadingDialogFragment.showLoading(supportFragmentManager)
contactViewModel.loadFriendList(getUsername())
}
}

View File

@ -1,10 +1,10 @@
package com.kaixed.kchat.ui.base
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.kaixed.kchat.manager.AppManager
/**
* @Author: kaixed
@ -21,6 +21,12 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
binding = inflateBinding()
setContentView(binding.root)
initData()
AppManager.instance.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
AppManager.instance.finishActivity(this)
}
abstract fun initData()

View File

@ -56,6 +56,7 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
context = requireContext()
loadData()
setObservation()
loadFriendRequest()
@ -63,17 +64,7 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(mReceiver, filter)
}
override fun onResume() {
super.onResume()
loadFriendRequest()
}
override fun onDestroy() {
super.onDestroy()
LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(mReceiver);
}
private fun loadFriendRequest() {
private fun setObservation() {
contactViewModel.contactRequestListResult.observe(viewLifecycleOwner) { result ->
result.onSuccess {
val items = result.getOrNull()?.toMutableList() ?: emptyList()
@ -88,12 +79,25 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
}
result.onFailure { }
}
}
override fun onResume() {
super.onResume()
loadFriendRequest()
}
override fun onDestroy() {
super.onDestroy()
LocalBroadcastManager.getInstance(requireActivity()).unregisterReceiver(mReceiver);
}
private fun loadFriendRequest() {
contactViewModel.getContactRequestList(getUsername())
}
private fun loadData() {
loading = true
binding.tvLoading.visibility = View.VISIBLE
binding.includeLoading.main.visibility = View.VISIBLE
binding.recycleFriendList.visibility = View.INVISIBLE
val contacts = contactViewModel.loadFriendListInDb()
@ -102,7 +106,7 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
addAll(contacts)
}
friendAdapter.updateData(allItems)
binding.tvLoading.visibility = View.GONE
binding.includeLoading.main.visibility = View.GONE
binding.recycleFriendList.visibility = View.VISIBLE
loading = false
setupRecyclerView()

View File

@ -28,7 +28,6 @@ class DiscoveryFragment : BaseFragment<FragmentDiscoveryBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setListener()
}

View File

@ -0,0 +1,27 @@
package com.kaixed.kchat.utils
import com.tencent.mmkv.MMKV
/**
* @Author: kaixed
* @Date: 2025/1/22 18:06
*/
object CacheUtils {
fun getAccessToken(): String {
val accessToken = MMKV.defaultMMKV().getString(Constants.ACCESS_TOKEN, "")
return accessToken ?: ""
}
fun getRefreshToken(): String {
val refreshToken = MMKV.defaultMMKV().getString(Constants.REFRESH_TOKEN, "")
return refreshToken ?: ""
}
fun setAccessToken(accessToken: String) {
MMKV.defaultMMKV().putString(Constants.ACCESS_TOKEN, accessToken)
}
fun setRefreshToken(refreshToken: String) {
MMKV.defaultMMKV().putString(Constants.REFRESH_TOKEN, refreshToken)
}
}

View File

@ -22,6 +22,9 @@ object Constants {
const val SCREEN_HEIGHT = "screenHeight"
const val SCREEN_WIDTH = "screenWidth"
const val ACCESS_TOKEN = "accessToken"
const val REFRESH_TOKEN = "refreshToken"
const val KEYBOARD_HEIGHT_RATIO = 0.15F
const val KEYBOARD_DEFAULT_HEIGHT = 200
const val STATUS_BAR_DEFAULT_HEIGHT: Int = 50

View File

@ -26,13 +26,8 @@
android:background="@color/white"
android:overScrollMode="never" />
<TextView
android:id="@+id/tv_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/title"
android:gravity="center"
android:text="正在加载中..."
android:textColor="@color/black" />
<include
android:id="@+id/include_loading"
layout="@layout/layout_loading" />
</RelativeLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie"
android:layout_width="50dp"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:lottie_autoPlay="true"
app:lottie_fileName="loading2.json"
app:lottie_loop="true" />
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在加载中..."
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/lottie"
app:layout_constraintStart_toEndOf="@id/lottie"
app:layout_constraintTop_toTopOf="@id/lottie" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,5 +2,6 @@
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">49.233.105.103</domain>
<domain includeSubdomains="true">192.168.31.18</domain>
</domain-config>
</network-security-config>

View File

@ -32,10 +32,12 @@ window = "1.3.0"
kotlin = "1.9.23"
coreKtx = "1.13.1"
objectbox = "4.0.2"
workRuntimeKtx = "2.10.0"
[libraries]
agcp = { module = "com.huawei.agconnect:agcp", version.ref = "agcp" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
compress = { module = "io.github.lucksiege:compress", version.ref = "pictureselector" }
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" }