feat: 增加接口签名验证

- 增加接口签名验证
- 优化登陆界面以适应密码保险箱
- 修复表情面板高度错误
This commit is contained in:
糕小菜 2025-01-26 09:51:28 +08:00
parent 632cd5c4e6
commit da1fc07599
22 changed files with 690 additions and 417 deletions

View File

@ -20,7 +20,7 @@ android {
minSdk = 28 minSdk = 28
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.0.01" versionName = "0.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -91,6 +91,9 @@ dependencies {
// 自定义spannable // 自定义spannable
implementation(libs.spannable) implementation(libs.spannable)
implementation(libs.soft.input.event)
implementation(libs.scanplus) implementation(libs.scanplus)
// implementation(libs.therouter) // implementation(libs.therouter)
// ksp(libs.therouter.ksp) // ksp(libs.therouter.ksp)

View File

@ -14,7 +14,7 @@ import io.objectbox.Box
class ContactRepository { class ContactRepository {
private val contactApiService = RetrofitClient.contactApiService private val friendApiService = RetrofitClient.friendApiService
private val contactBox: Box<Contact> by lazy { private val contactBox: Box<Contact> by lazy {
getBoxStore().boxFor(Contact::class.java) getBoxStore().boxFor(Contact::class.java)
@ -23,7 +23,7 @@ class ContactRepository {
// 获取联系人请求列表 // 获取联系人请求列表
suspend fun getContactRequestList(username: String): Result<List<FriendRequestItem>?> { suspend fun getContactRequestList(username: String): Result<List<FriendRequestItem>?> {
return apiCall( return apiCall(
apiCall = { contactApiService.getContactRequestList(username) }, apiCall = { friendApiService.getContactRequestList(username) },
errorMessage = "获取好友申请列表失败" errorMessage = "获取好友申请列表失败"
) )
} }
@ -35,7 +35,7 @@ class ContactRepository {
remark: String remark: String
): Result<Contact?> { ): Result<Contact?> {
return apiCall( return apiCall(
apiCall = { contactApiService.acceptContactRequest(contactId, username, remark) }, apiCall = { friendApiService.acceptContactRequest(contactId, username, remark) },
errorMessage = "添加好友失败" errorMessage = "添加好友失败"
).onSuccess { ).onSuccess {
it?.let { it?.let {
@ -50,7 +50,7 @@ class ContactRepository {
contactId: String, contactId: String,
): Result<String?> { ): Result<String?> {
return apiCall( return apiCall(
apiCall = { contactApiService.deleteContact(username, contactId) }, apiCall = { friendApiService.deleteContact(username, contactId) },
errorMessage = "删除好友失败" errorMessage = "删除好友失败"
).onSuccess { ).onSuccess {
val con = val con =
@ -62,7 +62,7 @@ class ContactRepository {
// 添加联系人 // 添加联系人
suspend fun addContact(contactId: String, message: String): Result<String?> { suspend fun addContact(contactId: String, message: String): Result<String?> {
return apiCall( return apiCall(
apiCall = { contactApiService.addContact(getUsername(), contactId, message) }, apiCall = { friendApiService.addContact(getUsername(), contactId, message) },
errorMessage = "添加联系人失败" errorMessage = "添加联系人失败"
) )
} }
@ -70,21 +70,16 @@ class ContactRepository {
// 搜索联系人 // 搜索联系人
suspend fun searchContact(username: String): Result<SearchUser?> { suspend fun searchContact(username: String): Result<SearchUser?> {
return apiCall( return apiCall(
apiCall = { contactApiService.searchContact(username) }, apiCall = { friendApiService.searchContact(username) },
errorMessage = "搜索用户失败" errorMessage = "搜索用户失败"
) )
} }
// 获取联系人列表 // 获取联系人列表
suspend fun getContactList(username: String): Result<List<Contact>?> { suspend fun getContactList(username: String): Result<List<Contact>?> {
// return safeApiCall(
// apiCall = { contactApiService.getContactList(username) },
// errorMessage = "获取好友列表失败"
// ).onSuccess {
//
// }
return try { return try {
val response = contactApiService.getContactList(username) val maps = mapOf("username" to username)
val response = friendApiService.getContactList(maps)
if (response.isSuccess()) { if (response.isSuccess()) {
val searchUsers = response.getResponseData() val searchUsers = response.getResponseData()
searchUsers?.let { searchUsers?.let {
@ -113,7 +108,7 @@ class ContactRepository {
suspend fun setRemark(userId: String, contactId: String, remark: String): Result<String?> { suspend fun setRemark(userId: String, contactId: String, remark: String): Result<String?> {
return apiCall( return apiCall(
apiCall = { contactApiService.setRemark(userId, contactId, remark) }, apiCall = { friendApiService.setRemark(userId, contactId, remark) },
errorMessage = "设置备注失败" errorMessage = "设置备注失败"
).onSuccess { ).onSuccess {
val con = contactBox.query(Contact_.username.equal(contactId)).build().findFirst() val con = contactBox.query(Contact_.username.equal(contactId)).build().findFirst()

View File

@ -23,7 +23,10 @@ class UserAuthRepository {
private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) } private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
// 用户注册 /**
* 注册方法
* @param registerRequest 注册请求参数
*/
suspend fun register(registerRequest: RegisterRequest): Result<Register?> { suspend fun register(registerRequest: RegisterRequest): Result<Register?> {
return apiCall( return apiCall(
apiCall = { authApiService.register(registerRequest) }, apiCall = { authApiService.register(registerRequest) },
@ -35,10 +38,15 @@ class UserAuthRepository {
} }
} }
// 登录方法 /**
* 登录方法
* @param username 用户名
* @param password 密码
*/
suspend fun loginByUsername(username: String, password: String): Result<UserInfo?> { suspend fun loginByUsername(username: String, password: String): Result<UserInfo?> {
val requestParams = mapOf("username" to username, "password" to password)
return apiCall( return apiCall(
apiCall = { authApiService.loginByUsername(username, password) }, apiCall = { authApiService.loginByUsername(requestParams) },
errorMessage = "登录成功,但未返回用户数据" errorMessage = "登录成功,但未返回用户数据"
).onSuccess { userInfo -> ).onSuccess { userInfo ->
userInfo?.let { userInfo?.let {
@ -47,10 +55,15 @@ class UserAuthRepository {
} }
} }
/**
* 登录方法
* @param telephone 手机号
* @param password 密码
*/
suspend fun loginByTelephone(telephone: String, password: String): Result<UserInfo?> { suspend fun loginByTelephone(telephone: String, password: String): Result<UserInfo?> {
val requestParams = mapOf("telephone" to telephone, "password" to password)
return apiCall( return apiCall(
apiCall = { authApiService.loginByTelephone(telephone, password) }, apiCall = { authApiService.loginByTelephone(requestParams) },
errorMessage = "登录成功,但未返回用户数据" errorMessage = "登录成功,但未返回用户数据"
).onSuccess { userInfo -> ).onSuccess { userInfo ->
userInfo?.let { userInfo?.let {
@ -58,7 +71,7 @@ class UserAuthRepository {
} }
} }
} }
private fun insertUserInfo( private fun insertUserInfo(
register: Register, register: Register,
telephone: String telephone: String

View File

@ -23,34 +23,21 @@ class UserProfileRepository {
private val userApiService = RetrofitClient.userApiService private val userApiService = RetrofitClient.userApiService
// 获取用户信息 // 获取用户信息
suspend fun getUserInfo(username: String): Result<SearchUser> { suspend fun getUserInfo(username: String): Result<SearchUser?> {
return try { val requestParams = mapOf("username" to username)
val response = userApiService.getUserInfo(username) return apiCall(
if (response.isSuccess()) { apiCall = { userApiService.getUserInfo(requestParams) },
val searchUser = response.getResponseData() errorMessage = "获取用户信息失败"
searchUser?.let { )
Result.success(searchUser)
} ?: Result.failure(Exception("用户不存在"))
} else {
Result.failure(Exception(response.getResponseMsg()))
}
} catch (e: Exception) {
Result.failure(e)
}
} }
// 修改昵称 // 修改昵称
suspend fun changeNickname(userRequest: UserRequest): Result<Boolean> { suspend fun changeNickname(userRequest: UserRequest): Result<Boolean?> {
return try { return apiCall(
val response = userApiService.changeNickname(userRequest) apiCall = { userApiService.changeNickname(userRequest) },
if (response.isSuccess()) { errorMessage = "修改昵称失败"
updateNickname(userRequest.nickname!!) ).onSuccess {
Result.success(true) updateNickname(userRequest.nickname!!)
} else {
Result.failure(Exception(response.getResponseMsg()))
}
} catch (e: Exception) {
Result.failure(e)
} }
} }

View File

@ -13,8 +13,9 @@ class UserSearchRepository {
// 获取用户列表 // 获取用户列表
suspend fun getUserList(username: String): Result<List<User>> { suspend fun getUserList(username: String): Result<List<User>> {
val requestParams = mapOf("username" to username)
return try { return try {
val response = userApiService.getUserListByNickname(username) val response = userApiService.fetchUserList(requestParams)
if (response.isSuccess()) { if (response.isSuccess()) {
val userList = response.getResponseData() val userList = response.getResponseData()
userList?.let { userList?.let {

View File

@ -11,7 +11,7 @@ object NetworkInterface {
// private const val URL = "49.233.105.103:6000" // private const val URL = "49.233.105.103:6000"
// const val SERVER_URL = "https://$URL" // const val SERVER_URL = "https://$URL"
const val SERVER_URL = "http://$URL" const val SERVER_URL = "http://$URL"
const val WEBSOCKET_SERVER_URL = "wss://$URL" const val WEBSOCKET_SERVER_URL = "ws://$URL"
const val WEBSOCKET = "/websocket/single/" const val WEBSOCKET = "/websocket/single/"
const val USER_INFO = "/users/info/" const val USER_INFO = "/users/info/"
const val USER_LOGIN_BY_USERNAME = "/users/login/username" const val USER_LOGIN_BY_USERNAME = "/users/login/username"

View File

@ -1,9 +1,10 @@
package com.kaixed.kchat.network package com.kaixed.kchat.network
import com.kaixed.kchat.network.interceptor.SignInterceptor
import com.kaixed.kchat.network.interceptor.TokenRefreshInterceptor import com.kaixed.kchat.network.interceptor.TokenRefreshInterceptor
import com.kaixed.kchat.network.service.AuthApiService 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.FileApiService
import com.kaixed.kchat.network.service.FriendApiService
import com.kaixed.kchat.network.service.UserApiService import com.kaixed.kchat.network.service.UserApiService
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
@ -48,6 +49,7 @@ object RetrofitClient {
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor) .addInterceptor(loggingInterceptor)
.pingInterval(15, TimeUnit.SECONDS) .pingInterval(15, TimeUnit.SECONDS)
.addInterceptor(SignInterceptor()) // 签名拦截器
.addInterceptor(TokenRefreshInterceptor()) // Token 拦截器 .addInterceptor(TokenRefreshInterceptor()) // Token 拦截器
.build() .build()
@ -65,8 +67,8 @@ object RetrofitClient {
retrofit.create(UserApiService::class.java) retrofit.create(UserApiService::class.java)
} }
val contactApiService: ContactService by lazy { val friendApiService: FriendApiService by lazy {
retrofit.create(ContactService::class.java) retrofit.create(FriendApiService::class.java)
} }
val fileApiService: FileApiService by lazy { val fileApiService: FileApiService by lazy {

View File

@ -1,37 +1,99 @@
package com.kaixed.kchat.network.interceptor package com.kaixed.kchat.network.interceptor
import com.kaixed.kchat.network.SignUtil.generateSign import okhttp3.FormBody
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okio.IOException
import java.security.MessageDigest
import java.util.*
class SignInterceptor : Interceptor { class SignInterceptor : Interceptor {
private val secretKey = "YourSecretKey" // 后端秘钥
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val request = chain.request()
val urlPath = originalRequest.url.encodedPath
val skipSignPaths = listOf(
"/users/avatar",
)
// 检查是否需要跳过签名
if (skipSignPaths.any { urlPath.endsWith(it) }) {
// 直接放行请求,不做签名处理
return chain.proceed(originalRequest)
}
// 获取当前时间戳 // 获取当前时间戳
val timestamp = System.currentTimeMillis().toString() val timestamp = System.currentTimeMillis().toString()
// 生成请求的签名 // 生成随机 nonce
val sign = generateSign(originalRequest, timestamp) val nonce = UUID.randomUUID().toString()
// 创建新的请求,加入 sign 和 timestamp // 获取请求参数
val newRequest = originalRequest.newBuilder() val params = getRequestParams(request)
// 生成签名
val sign = generateServerSign(params, timestamp, nonce, secretKey)
// 构造新的请求,添加请求头
val newRequest = request.newBuilder()
.addHeader("sign", sign) .addHeader("sign", sign)
.addHeader("timestamp", timestamp) .addHeader("timestamp", timestamp)
.addHeader("nonce", nonce)
.build() .build()
// 继续请求
return chain.proceed(newRequest) return chain.proceed(newRequest)
} }
// 获取请求的参数,处理 POST 请求的 Map 格式数据,并确保按字典顺序排序
private fun getRequestParams(request: Request): Map<String, String> {
val params = mutableMapOf<String, String>()
// 处理 GET 请求
if (request.method == "GET") {
val url = request.url
for (i in 0 until url.querySize) {
params[url.queryParameterName(i)] = url.queryParameterValue(i) ?: ""
}
}
// 处理 POST 请求Map 格式)
else if (request.method == "POST") {
val body = request.body
if (body is FormBody) {
// 获取所有参数,并确保按字典顺序排序
for (i in 0 until body.size) {
params[body.name(i)] = body.value(i)
}
}
}
// 按字典顺序排序参数
return params.toSortedMap()
}
// 生成服务端签名
private fun generateServerSign(params: Map<String, String>, timestamp: String, nonce: String, secretKey: String): String {
val sortedParams = params.toSortedMap() // 确保是按字典顺序排序
// 拼接参数
val sb = StringBuilder()
for ((key, value) in sortedParams) {
sb.append("$key=$value&")
}
sb.append("timestamp=$timestamp&")
sb.append("nonce=$nonce&")
sb.append("secretKey=$secretKey")
// 使用 SHA-256 进行加密
return hashWithSHA256(sb.toString())
}
// 使用 SHA-256 进行哈希加密
private fun hashWithSHA256(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(input.toByteArray(Charsets.UTF_8))
val hexString = StringBuilder()
for (b in hashBytes) {
val hex = Integer.toHexString(0xff and b.toInt())
if (hex.length == 1) hexString.append('0')
hexString.append(hex)
}
return hexString.toString()
}
} }

View File

@ -5,8 +5,6 @@ import com.kaixed.kchat.data.model.request.RegisterRequest
import com.kaixed.kchat.data.model.response.register.Register import com.kaixed.kchat.data.model.response.register.Register
import com.kaixed.kchat.network.ApiResponse import com.kaixed.kchat.network.ApiResponse
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
@ -17,36 +15,32 @@ import retrofit2.http.Query
*/ */
interface AuthApiService { interface AuthApiService {
@POST("users/access-token") @POST("auth/access-token")
suspend fun auth( suspend fun auth(
@Header("Authorization") token: String, @Header("Authorization") token: String,
@Query("username") username: String @Query("username") username: String
): ApiResponse<String> ): ApiResponse<String>
@POST("users/refresh-token") @POST("auth/refresh-token")
suspend fun refresh( suspend fun refresh(
@Header("Authorization") token: String, @Header("Authorization") token: String,
@Query("username") username: String @Query("username") username: String
): ApiResponse<String> ): ApiResponse<String>
// 登录接口(根据用户名登录) // 登录接口(根据用户名登录)
@FormUrlEncoded @POST("auth/login/username")
@POST("users/login/username")
suspend fun loginByUsername( suspend fun loginByUsername(
@Field("username") username: String, @Body requestParams: Map<String, String>,
@Field("password") password: String
): ApiResponse<UserInfo?> ): ApiResponse<UserInfo?>
// 登录接口(根据电话登录) // 登录接口(根据电话登录)
@FormUrlEncoded @POST("auth/login/telephone")
@POST("users/login/telephone")
suspend fun loginByTelephone( suspend fun loginByTelephone(
@Field("telephone") telephone: String, @Body requestParams: Map<String, String>,
@Field("password") password: String
): ApiResponse<UserInfo> ): ApiResponse<UserInfo>
// 注册接口 // 注册接口
@POST("users/register") @POST("auth/register")
suspend fun register( suspend fun register(
@Body registerRequest: RegisterRequest @Body registerRequest: RegisterRequest
): ApiResponse<Register> ): ApiResponse<Register>

View File

@ -4,6 +4,7 @@ import com.kaixed.kchat.data.local.entity.Contact
import com.kaixed.kchat.data.model.friend.FriendRequestItem import com.kaixed.kchat.data.model.friend.FriendRequestItem
import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.data.model.search.SearchUser
import com.kaixed.kchat.network.ApiResponse import com.kaixed.kchat.network.ApiResponse
import retrofit2.http.Body
import retrofit2.http.Field import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET import retrofit2.http.GET
@ -12,10 +13,9 @@ import retrofit2.http.Path
/** /**
* @Author: kaixed * @Author: kaixed
* @Date: 2024/11/15 11:00 * @Date: 2025/1/25 16:38
*/ */
interface ContactService { interface FriendApiService {
// 获取联系人请求列表 // 获取联系人请求列表
@FormUrlEncoded @FormUrlEncoded
@POST("friend/request/list") @POST("friend/request/list")
@ -48,15 +48,13 @@ interface ContactService {
): ApiResponse<SearchUser?> ): ApiResponse<SearchUser?>
// 获取联系人列表 // 获取联系人列表
@FormUrlEncoded @POST("friends/list")
@POST("friend/list")
suspend fun getContactList( suspend fun getContactList(
@Field("userId") username: String @Body requestParams: Map<String, String>,
): ApiResponse<List<Contact>?> ): ApiResponse<List<Contact>?>
// 删除联系人 // 删除联系人
@FormUrlEncoded @POST("friends/delete")
@POST("friend/delete")
suspend fun deleteContact( suspend fun deleteContact(
@Field("userId") username: String, @Field("userId") username: String,
@Field("contactId") contactId: String, @Field("contactId") contactId: String,
@ -70,4 +68,4 @@ interface ContactService {
@Field("contactId") contactId: String, @Field("contactId") contactId: String,
@Field("remark") remark: String, @Field("remark") remark: String,
): ApiResponse<String?> ): ApiResponse<String?>
} }

View File

@ -1,22 +1,15 @@
package com.kaixed.kchat.network.service 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.request.UpdatePasswordRequest import com.kaixed.kchat.data.model.request.UpdatePasswordRequest
import com.kaixed.kchat.data.model.request.UserRequest import com.kaixed.kchat.data.model.request.UserRequest
import com.kaixed.kchat.data.model.response.register.Register
import com.kaixed.kchat.data.model.response.search.User import com.kaixed.kchat.data.model.response.search.User
import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.data.model.search.SearchUser
import com.kaixed.kchat.network.ApiResponse import com.kaixed.kchat.network.ApiResponse
import okhttp3.MultipartBody import okhttp3.MultipartBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.Path
/** /**
* @Author: kaixed * @Author: kaixed
@ -25,22 +18,22 @@ import retrofit2.http.Path
interface UserApiService { interface UserApiService {
// 获取用户列表 // 获取用户列表
@GET("userList/{username}") @POST("friends/list")
suspend fun getUserListByNickname( suspend fun fetchUserList(
@Path("username") username: String @Body requestParams: Map<String, String>
): ApiResponse<List<User>> ): ApiResponse<List<User>>
// 获取用户信息 // 获取用户信息
@GET("users/{username}") @POST("users/info/fetch")
suspend fun getUserInfo( suspend fun getUserInfo(
@Path("username") username: String @Body requestParams: Map<String, String>
): ApiResponse<SearchUser> ): ApiResponse<SearchUser>
// 更改昵称接口 // 更改昵称接口
@POST("users/info") @POST("users/info")
suspend fun changeNickname( suspend fun changeNickname(
@Body userRequest: UserRequest @Body userRequest: UserRequest
): ApiResponse<String> ): ApiResponse<Boolean?>
// 上传头像接口 // 上传头像接口
@Multipart @Multipart
@ -50,7 +43,7 @@ interface UserApiService {
@Part file: MultipartBody.Part @Part file: MultipartBody.Part
): ApiResponse<String> ): ApiResponse<String>
// 更改昵称接口 // 更改密码
@POST("users/password") @POST("users/password")
suspend fun updatePassword( suspend fun updatePassword(
@Body updatePasswordRequest: UpdatePasswordRequest @Body updatePasswordRequest: UpdatePasswordRequest

View File

@ -155,6 +155,7 @@ class WebSocketService : Service() {
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.d(TAG, "WebSocket closing: ${t.cause}") Log.d(TAG, "WebSocket closing: ${t.cause}")
Log.d(TAG, "WebSocket closing: $t")
establishConnection() establishConnection()
} }
} }

View File

@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -27,6 +26,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drake.softinput.getSoftInputHeight
import com.drake.softinput.hasSoftInput
import com.drake.softinput.setWindowSoftInput
import com.kaixed.kchat.R import com.kaixed.kchat.R
import com.kaixed.kchat.data.event.UnreadEvent import com.kaixed.kchat.data.event.UnreadEvent
import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore
@ -46,12 +48,13 @@ import com.kaixed.kchat.ui.i.IOnItemClickListener
import com.kaixed.kchat.ui.i.OnItemClickListener import com.kaixed.kchat.ui.i.OnItemClickListener
import com.kaixed.kchat.ui.widget.LoadingDialogFragment import com.kaixed.kchat.ui.widget.LoadingDialogFragment
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.ConstantsUtils.getKeyboardHeight import com.kaixed.kchat.utils.ConstantsUtils.getKeyboardHeight
import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.utils.ConstantsUtils.getUsername
import com.kaixed.kchat.utils.ImageEngines import com.kaixed.kchat.utils.ImageEngines
import com.kaixed.kchat.utils.ImageSpanUtil.insertEmoji import com.kaixed.kchat.utils.ImageSpanUtil.insertEmoji
import com.kaixed.kchat.utils.ScreenUtils
import com.kaixed.kchat.viewmodel.FileViewModel import com.kaixed.kchat.viewmodel.FileViewModel
import com.luck.picture.lib.basic.PictureSelector import com.luck.picture.lib.basic.PictureSelector
import com.luck.picture.lib.config.SelectMimeType import com.luck.picture.lib.config.SelectMimeType
@ -66,6 +69,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import kotlin.math.max
class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener, class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
IOnItemClickListener { IOnItemClickListener {
@ -90,8 +94,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
private var softKeyboardHeight = 0 private var softKeyboardHeight = 0
private var keyboardShown = false
private var bound = false private var bound = false
private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) } private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) }
@ -120,7 +122,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
setListener() setListener()
bindWebSocketService() bindWebSocketService()
setPanelChange() setPanelChange()
getKeyBoardVisibility()
observeStateFlow() observeStateFlow()
if (isSearchHistory) { if (isSearchHistory) {
val size = MessagesManager.queryHistory(msgLocalId) val size = MessagesManager.queryHistory(msgLocalId)
@ -158,18 +159,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
} }
} }
private fun getKeyBoardVisibility() {
val rootView = binding.root
rootView.viewTreeObserver.addOnGlobalLayoutListener {
val r = Rect()
rootView.getWindowVisibleDisplayFrame(r)
val screenHeight = rootView.rootView.height
val keypadHeight = screenHeight - r.bottom
keyboardShown = keypadHeight > screenHeight * KEYBOARD_HEIGHT_RATIO
}
}
private fun setBackPressListener() { private fun setBackPressListener() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@ -220,7 +209,7 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
} }
private fun switchPanel(isSwitchToEmoji: Boolean) { private fun switchPanel(isSwitchToEmoji: Boolean) {
if (keyboardShown) { if (hasSoftInput()) {
lockContentViewHeight() lockContentViewHeight()
handlePanelSwitch(isSwitchToEmoji) handlePanelSwitch(isSwitchToEmoji)
unlockContentViewHeight() unlockContentViewHeight()
@ -278,6 +267,12 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
} }
private fun setListener() { private fun setListener() {
binding.etInput.setOnClickListener {
if (hasSoftInput()){
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 300))
}
}
setBackPressListener() setBackPressListener()
binding.gvFunctionPanel.selector = ColorDrawable(Color.TRANSPARENT) binding.gvFunctionPanel.selector = ColorDrawable(Color.TRANSPARENT)

View File

@ -2,188 +2,215 @@ package com.kaixed.kchat.ui.activity
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import android.widget.EditText
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.drake.softinput.getSoftInputHeight
import com.drake.softinput.hasSoftInput
import com.kaixed.kchat.R import com.kaixed.kchat.R
import com.kaixed.kchat.data.LocalDatabase import com.kaixed.kchat.data.LocalDatabase
import com.kaixed.kchat.data.local.entity.UserInfo
import com.kaixed.kchat.databinding.ActivityLoginBinding import com.kaixed.kchat.databinding.ActivityLoginBinding
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.ui.widget.MyBottomSheetFragment
import com.kaixed.kchat.utils.Constants.ACCESS_TOKEN import com.kaixed.kchat.utils.Constants.ACCESS_TOKEN
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
import com.kaixed.kchat.utils.Constants.REFRESH_TOKEN import com.kaixed.kchat.utils.Constants.REFRESH_TOKEN
import com.kaixed.kchat.utils.DrawableUtil.createDrawable import com.kaixed.kchat.utils.DrawableUtil.createDrawable
import com.kaixed.kchat.utils.ScreenUtils.dp2px import com.kaixed.kchat.utils.ScreenUtils.dp2px
import com.kaixed.kchat.viewmodel.UserViewModel import com.kaixed.kchat.viewmodel.UserViewModel
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import kotlin.math.max
class LoginActivity : BaseActivity<ActivityLoginBinding>() { class LoginActivity : BaseActivity<ActivityLoginBinding>() {
private val userViewModel: UserViewModel by viewModels() private val userViewModel: UserViewModel by viewModels()
private var previousKeyboardHeight = 0 private var isLoginByTelephone = true
private var loginByUsername = false
private lateinit var etUsername: EditText
private lateinit var etPassword: EditText
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setupEdittext()
etUsername.addTextChangedListener(textWatcher)
etPassword.addTextChangedListener(textWatcher)
initView() initView()
setListener() setListeners()
setObservers()
binding.tvLogin.setOnClickListener { binding.tvLogin.setOnClickListener {
val username = etUsername.text.toString().trim() val username =
val password = etPassword.text.toString().trim() if (isLoginByTelephone) binding.etTelephone.text.toString() else binding.etUsername.text.toString()
val password =
if (isLoginByTelephone) binding.etTelephonePassword.text.toString() else binding.etPassword.text.toString()
login(username, password) login(username, password)
} }
getKeyboardHeight()
} }
private fun setupEdittext() { private fun setObservers() {
etUsername = if (loginByUsername) binding.etUsername else binding.etTelephone
etPassword = binding.etPassword
}
override fun initData() {
}
private fun login(username: String, password: String) {
if (!loginByUsername) {
if (etUsername.text.toString().length != 11) {
toast("请输入正确的手机号码")
return
}
}
if (username.isEmpty() || password.isEmpty()) {
toast("请输入${if (loginByUsername) "用户名" else "手机号"}或密码")
}
userViewModel.loginResult.observe(this) { loginResult -> userViewModel.loginResult.observe(this) { loginResult ->
loginResult.onSuccess { loginResult.onSuccess {
it?.let { it?.let { saveUserInfo(it) }
LocalDatabase.saveUserInfo(it)
MMKV.defaultMMKV().putString(ACCESS_TOKEN, it.accessToken)
MMKV.defaultMMKV().putString(REFRESH_TOKEN, it.refreshToken)
}
navigateToMain() navigateToMain()
} }
loginResult.onFailure { loginResult.onFailure { toast(it.message.toString()) }
toast(it.message.toString())
}
}
if (loginByUsername) {
userViewModel.loginByUsername(username, password)
} else {
userViewModel.loginByTelephone(username, password)
} }
} }
private fun initView() { private fun initView() {
setView() setupLoginView()
} }
private fun setListener() { private fun setListeners() {
binding.ivClose.setOnClickListener { binding.ivClose.setOnClickListener { finish() }
finish()
}
binding.tvChangeLoginWay.setOnClickListener { binding.tvChangeLoginWay.setOnClickListener {
loginByUsername = !loginByUsername isLoginByTelephone = !isLoginByTelephone
setView() setupLoginView()
} if (isLoginByTelephone) {
} binding.etTelephone.requestFocus()
} else {
private fun setView() { binding.etUsername.requestFocus()
setupEdittext()
binding.tvTitle.text = if (loginByUsername) "用户名登录" else "手机号登录"
binding.etTelephone.visibility = if (loginByUsername) View.INVISIBLE else View.VISIBLE
binding.etUsername.visibility = if (loginByUsername) View.VISIBLE else View.INVISIBLE
binding.tvUsername.text = if (loginByUsername) "用户名" else "手机号"
binding.tvChangeLoginWay.text = if (loginByUsername) "手机号登录" else "用户名登录"
binding.tvTip.text =
if (loginByUsername) "上述账号仅用于登陆验证" else "上述手机号仅用于登陆验证"
}
private fun getKeyboardHeight() {
val rootLayout = binding.root
rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
val rect = Rect()
rootLayout.getWindowVisibleDisplayFrame(rect)
val screenHeight = rootLayout.rootView.height
val keypadHeight = screenHeight - rect.bottom
// 通过15%的屏幕高度差来判断是否为键盘
if (keypadHeight > (screenHeight * KEYBOARD_HEIGHT_RATIO)) {
if (keypadHeight != previousKeyboardHeight) {
previousKeyboardHeight = keypadHeight
val kv = MMKV.defaultMMKV()
kv.encode(KEYBOARD_HEIGHT, keypadHeight)
}
} }
} }
binding.tvMore.setOnClickListener { showBottomSheet() }
}
private fun setupLoginView() {
resetFields()
updateViewBasedOnLoginType()
}
private fun resetFields() {
if (isLoginByTelephone) {
binding.etTelephone.setText("")
binding.etTelephonePassword.setText("")
} else {
binding.etUsername.setText("")
binding.etPassword.setText("")
}
}
private fun updateViewBasedOnLoginType() {
if (isLoginByTelephone) {
configureForPhoneLogin()
} else {
configureForUsernameLogin()
}
}
private fun configureForPhoneLogin() {
binding.etTelephone.setOnClickListener {
if (hasSoftInput()) {
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 400))
}
}
binding.etTelephonePassword.addTextChangedListener(textWatcher)
binding.etTelephone.addTextChangedListener(textWatcher)
binding.clUsername.visibility = View.GONE
binding.clTelephone.visibility = View.VISIBLE
binding.tvTitle.text = "手机号登录"
binding.tvUsername.text = "手机号"
binding.tvChangeLoginWay.text = "用户名登录"
binding.tvTip.text = "上述手机号仅用于登陆验证"
}
private fun configureForUsernameLogin() {
binding.etUsername.setOnClickListener {
if (hasSoftInput()) {
MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 400))
}
}
binding.etPassword.addTextChangedListener(textWatcher)
binding.etUsername.addTextChangedListener(textWatcher)
binding.clUsername.visibility = View.VISIBLE
binding.clTelephone.visibility = View.GONE
binding.tvTitle.text = "用户名登录"
binding.tvUsername.text = "用户名"
binding.tvChangeLoginWay.text = "手机号登录"
binding.tvTip.text = "上述账号仅用于登陆验证"
}
private fun login(username: String, password: String) {
if (isInputValid(username, password)) {
if (isLoginByTelephone) {
userViewModel.loginByTelephone(username, password)
} else {
userViewModel.loginByUsername(username, password)
}
}
}
private fun isInputValid(username: String, password: String): Boolean {
if (username.isEmpty() || password.isEmpty()) {
toast("请输入${if (!isLoginByTelephone) "用户名" else "手机号"}或密码")
return false
}
if (isLoginByTelephone && username.length != 11) {
toast("请输入正确的手机号码")
return false
}
return true
}
private fun saveUserInfo(user: UserInfo) {
LocalDatabase.saveUserInfo(user)
MMKV.defaultMMKV().putString(ACCESS_TOKEN, user.accessToken)
MMKV.defaultMMKV().putString(REFRESH_TOKEN, user.refreshToken)
}
private fun showBottomSheet() {
val bottomSheetFragment = MyBottomSheetFragment().apply {
arguments = Bundle().apply {
putStringArrayList("list", arrayListOf("紧急冻结", "安全中心", "反馈问题", "取消"))
}
}
bottomSheetFragment.show(supportFragmentManager, bottomSheetFragment.tag)
} }
private val textWatcher = object : TextWatcher { private val textWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
val isInputValid = when {
val isInputValid = etUsername.text.isNotEmpty() && etPassword.text.isNotEmpty() isLoginByTelephone -> binding.etTelephonePassword.text.isNotEmpty() && binding.etTelephone.text.length == 11
else -> binding.etUsername.text.isNotEmpty() && binding.etPassword.text.isNotEmpty()
binding.tvLogin.apply {
setTextColor(
if (isInputValid) {
ContextCompat.getColor(
this@LoginActivity, R.color.white
)
} else {
Color.parseColor("#B4B4B4")
}
)
background = createDrawable(
if (isInputValid) {
ContextCompat.getColor(
this@LoginActivity, R.color.green
)
} else {
Color.parseColor("#E1E1E1")
}, dp2px(8)
)
} }
updateLoginButtonState(isInputValid)
}
}
private fun updateLoginButtonState(isInputValid: Boolean) {
binding.tvLogin.apply {
isEnabled = isInputValid
setTextColor(
if (isInputValid) ContextCompat.getColor(
this@LoginActivity,
R.color.white
) else Color.parseColor("#B4B4B4")
)
background = createDrawable(
if (isInputValid) ContextCompat.getColor(
this@LoginActivity,
R.color.green
) else Color.parseColor("#E1E1E1"), dp2px(8)
)
} }
} }
private fun navigateToMain() { private fun navigateToMain() {
toast("登录成功") toast("登录成功")
val intent = Intent(this, MainActivity::class.java) startActivity(Intent(this, MainActivity::class.java))
startActivity(intent)
finish() finish()
} }
override fun onDestroy() {
super.onDestroy()
binding.root.viewTreeObserver.removeOnGlobalLayoutListener { }
}
override fun initData() {}
override fun inflateBinding(): ActivityLoginBinding { override fun inflateBinding(): ActivityLoginBinding {
return ActivityLoginBinding.inflate(layoutInflater) return ActivityLoginBinding.inflate(layoutInflater)
} }

View File

@ -3,9 +3,14 @@ package com.kaixed.kchat.ui.widget
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.kaixed.kchat.R import com.kaixed.kchat.R
import com.kaixed.kchat.data.DataBase import com.kaixed.kchat.data.DataBase
@ -13,6 +18,7 @@ import com.kaixed.kchat.databinding.BottomSheetLayoutBinding
import com.kaixed.kchat.ui.activity.LoginActivity import com.kaixed.kchat.ui.activity.LoginActivity
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
import com.kaixed.kchat.utils.Constants.USER_LOGIN_STATUS import com.kaixed.kchat.utils.Constants.USER_LOGIN_STATUS
import com.kaixed.kchat.utils.ScreenUtils
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
/** /**
@ -39,32 +45,56 @@ class MyBottomSheetFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.tvQuitAccount.setOnClickListener { val list = arguments?.getStringArrayList("list")
deleteAllUserData() if (list != null) {
activity?.finish() initList(list)
}
}
activity?.startActivity(Intent( private fun initList(list: ArrayList<String>) {
activity, LoginActivity::class.java list.forEach {
addTextToContainer(it)
}
}
private fun addTextToContainer(item: String) {
val textView = TextView(requireContext()).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
ScreenUtils.dp2px(50)
).apply { ).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK topMargin = if (item == "取消") ScreenUtils.dp2px(8) else ScreenUtils.dp2px(1)
}) }
} setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white))
text = item
setTextColor(ContextCompat.getColor(requireContext(), R.color.black))
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL
binding.tvCancel.setOnClickListener { setOnClickListener {
dismiss() when (item) {
} "退出账号" -> {
deleteAllUserData()
activity?.finish()
activity?.startActivity(Intent(
activity, LoginActivity::class.java
).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
})
}
binding.tvQuitApp.setOnClickListener { "取消" -> dismiss()
activity?.finishAffinity() "退出应用" -> activity?.finishAffinity()
}
}
} }
binding.llBottomSheet.addView(textView)
} }
private fun deleteAllUserData() { private fun deleteAllUserData() {
val mmkv = MMKV.mmkvWithID(MMKV_USER_SESSION) val mmkv = MMKV.mmkvWithID(MMKV_USER_SESSION)
mmkv.clearAll() mmkv.clearAll()
MMKV.defaultMMKV().putBoolean(USER_LOGIN_STATUS, false) MMKV.defaultMMKV().putBoolean(USER_LOGIN_STATUS, false)
DataBase.cleanAllData() DataBase.cleanAllData()
} }

View File

@ -0,0 +1,9 @@
package com.kaixed.kchat.utils
/**
* @Author: kaixed
* @Date: 2025/1/24 20:41
*/
class AccountUtils {
}

View File

@ -26,6 +26,6 @@ object Constants {
const val REFRESH_TOKEN = "refreshToken" const val REFRESH_TOKEN = "refreshToken"
const val KEYBOARD_HEIGHT_RATIO = 0.15F const val KEYBOARD_HEIGHT_RATIO = 0.15F
const val KEYBOARD_DEFAULT_HEIGHT = 200 const val KEYBOARD_DEFAULT_HEIGHT = 300
const val STATUS_BAR_DEFAULT_HEIGHT: Int = 50 const val STATUS_BAR_DEFAULT_HEIGHT: Int = 50
} }

View File

@ -0,0 +1,145 @@
package com.kaixed.kchat.utils
import android.app.Activity
import android.content.res.Resources
import android.graphics.Rect
import android.os.Build
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowInsets
import android.view.WindowInsetsAnimation
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.annotation.RequiresApi
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
object KeyboardUtils {
private var sDecorViewInvisibleHeightPre: Int = 0
private var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
private var mNavHeight: Int = 0
private var sDecorViewDelta: Int = 0
private fun getDecorViewInvisibleHeight(activity: Activity): Int {
val decorView = activity.window.decorView ?: return sDecorViewInvisibleHeightPre
val outRect = Rect()
decorView.getWindowVisibleDisplayFrame(outRect)
val delta = Math.abs(decorView.bottom - outRect.bottom)
if (delta <= mNavHeight) {
sDecorViewDelta = delta
return 0
}
return delta - sDecorViewDelta
}
fun registerKeyboardHeightListener(activity: Activity, listener: KeyboardHeightListener) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
invokeAbove31(activity, listener)
} else {
invokeBelow31(activity, listener)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun invokeAbove31(activity: Activity, listener: KeyboardHeightListener) {
activity.window.decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
override fun onProgress(windowInsets: WindowInsets, animations: List<WindowInsetsAnimation>): WindowInsets {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val navHeight = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
val hasNavigationBar = windowInsets.isVisible(WindowInsetsCompat.Type.navigationBars()) &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0
listener.onKeyboardHeightChanged(if (hasNavigationBar) Math.max(imeHeight - navHeight, 0) else imeHeight)
return windowInsets
}
})
}
private fun invokeBelow31(activity: Activity, listener: KeyboardHeightListener) {
val flags = activity.window.attributes.flags
if (flags and WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS != 0) {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
}
val contentView = activity.findViewById<FrameLayout>(android.R.id.content)
sDecorViewInvisibleHeightPre = getDecorViewInvisibleHeight(activity)
onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val height = getDecorViewInvisibleHeight(activity)
if (sDecorViewInvisibleHeightPre != height) {
listener.onKeyboardHeightChanged(height)
sDecorViewInvisibleHeightPre = height
}
}
}
getNavigationBarHeight(activity) { height, hasNav ->
mNavHeight = height
contentView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
}
}
fun unregisterKeyboardHeightListener(activity: Activity) {
onGlobalLayoutListener = null
val contentView = activity.window.decorView.findViewById<View>(android.R.id.content) ?: return
contentView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
private fun getNavBarHeight(): Int {
val res = Resources.getSystem()
val resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId != 0) {
res.getDimensionPixelSize(resourceId)
} else {
0
}
}
private fun getNavigationBarHeight(activity: Activity, callback: (height: Int, hasNav: Boolean) -> Unit) {
val view = activity.window.decorView
val attachedToWindow = view.isAttachedToWindow
if (attachedToWindow) {
val windowInsets = ViewCompat.getRootWindowInsets(view)
val height = windowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
val hasNavigationBar = windowInsets?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0
if (height > 0) {
callback(height, hasNavigationBar)
} else {
callback(getNavBarHeight(), hasNavigationBar)
}
} else {
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
val windowInsets = ViewCompat.getRootWindowInsets(v)
val height = windowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
val hasNavigationBar = windowInsets?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true &&
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0
if (height > 0) {
callback(height, hasNavigationBar)
} else {
callback(getNavBarHeight(), hasNavigationBar)
}
}
override fun onViewDetachedFromWindow(v: View) {}
})
}
}
interface KeyboardHeightListener {
fun onKeyboardHeightChanged(height: Int)
}
interface NavigationBarCallback {
fun onHeight(height: Int, hasNav: Boolean)
}
}

View File

@ -76,8 +76,8 @@ class UserViewModel : ViewModel() {
} }
} }
private val _userInfoResult = MutableLiveData<Result<SearchUser>>() private val _userInfoResult = MutableLiveData<Result<SearchUser?>>()
val userInfoResult: LiveData<Result<SearchUser>> = _userInfoResult val userInfoResult: LiveData<Result<SearchUser?>> = _userInfoResult
// 获取用户信息 // 获取用户信息
fun getUserInfo(username: String) { fun getUserInfo(username: String) {
@ -87,8 +87,8 @@ class UserViewModel : ViewModel() {
} }
} }
private val _changeNicknameResult = MutableLiveData<Result<Boolean>>() private val _changeNicknameResult = MutableLiveData<Result<Boolean?>>()
val changeNicknameResult: LiveData<Result<Boolean>> = _changeNicknameResult val changeNicknameResult: LiveData<Result<Boolean?>> = _changeNicknameResult
// 修改昵称 // 修改昵称
fun changeNickname(userRequest: UserRequest) { fun changeNickname(userRequest: UserRequest) {

View File

@ -38,106 +38,178 @@
android:layout_marginTop="50dp" android:layout_marginTop="50dp"
app:layout_constraintTop_toBottomOf="@id/tv_title" /> app:layout_constraintTop_toBottomOf="@id/tv_title" />
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/tv_username" android:id="@+id/cl_telephone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="15dp"
android:text="手机号"
android:textColor="@color/black"
android:textSize="17sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view" />
<EditText
android:id="@+id/et_username"
android:layout_width="0dp"
android:layout_height="0dp"
android:maxLength="16"
android:layout_marginStart="35dp"
android:background="@null"
android:hint="请填写用户名"
android:textCursorDrawable="@drawable/cursor"
android:textSize="17sp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/view1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_username"
app:layout_constraintTop_toBottomOf="@id/view" />
<EditText
android:id="@+id/et_telephone"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="35dp"
android:background="@null"
android:hint="请填写手机号"
android:maxLength="11"
android:textCursorDrawable="@drawable/cursor"
android:textSize="17sp"
app:layout_constraintBottom_toTopOf="@id/view1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_username"
app:layout_constraintTop_toBottomOf="@id/view" />
<View
android:id="@+id/view1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0.2dp" android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="15dp" android:layout_marginTop="15dp"
android:background="#D8D8D8" app:layout_constraintTop_toBottomOf="@id/view">
app:layout_constraintTop_toBottomOf="@id/tv_username" />
<TextView <TextView
android:id="@+id/tv_password" android:id="@+id/tv_telephone"
android:layout_width="100dp"
android:layout_height="45dp"
android:layout_marginStart="15dp"
android:gravity="center_vertical"
android:text="手机号"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_telephone"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:hint="请填写手机号"
android:maxLength="16"
android:textCursorDrawable="@drawable/cursor"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/tv_telephone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_telephone"
app:layout_constraintTop_toTopOf="@id/tv_telephone" />
<View
android:id="@+id/view1"
android:layout_width="match_parent"
android:layout_height="0.2dp"
android:layout_marginHorizontal="10dp"
android:background="#D8D8D8"
app:layout_constraintBottom_toTopOf="@id/tv_telephone_password"
app:layout_constraintTop_toBottomOf="@id/tv_telephone" />
<TextView
android:id="@+id/tv_telephone_password"
android:layout_width="100dp"
android:layout_height="45dp"
android:gravity="center_vertical"
android:text="密码"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintStart_toStartOf="@id/tv_telephone"
app:layout_constraintTop_toBottomOf="@id/tv_telephone" />
<EditText
android:id="@+id/et_telephone_password"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:hint="请填写密码"
android:inputType="textPassword"
android:maxLength="16"
android:maxLines="1"
android:textColor="#A5A5A5"
android:textCursorDrawable="@drawable/cursor"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/tv_telephone_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_telephone_password"
app:layout_constraintTop_toTopOf="@id/tv_telephone_password" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:visibility="invisible"
app:layout_constraintTop_toBottomOf="@id/view">
<TextView
android:id="@+id/tv_username"
android:layout_width="100dp"
android:layout_height="45dp"
android:layout_marginStart="15dp"
android:gravity="center_vertical"
android:text="账号"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_username"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:hint="请填写账号"
android:maxLength="16"
android:textCursorDrawable="@drawable/cursor"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/tv_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_username"
app:layout_constraintTop_toTopOf="@id/tv_username" />
<View
android:layout_width="match_parent"
android:layout_height="0.2dp"
android:background="#D8D8D8"
android:layout_marginHorizontal="10dp"
app:layout_constraintBottom_toTopOf="@id/tv_password"
app:layout_constraintTop_toBottomOf="@id/tv_username" />
<TextView
android:id="@+id/tv_password"
android:layout_width="100dp"
android:layout_height="45dp"
android:gravity="center_vertical"
android:text="密码"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintStart_toStartOf="@id/tv_username"
app:layout_constraintTop_toBottomOf="@id/tv_username" />
<EditText
android:id="@+id/et_password"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:hint="请填写密码"
android:inputType="textPassword"
android:maxLength="16"
android:maxLines="1"
android:textColor="#A5A5A5"
android:textCursorDrawable="@drawable/cursor"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/tv_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_password"
app:layout_constraintTop_toTopOf="@id/tv_password" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp" app:barrierDirection="bottom"
android:layout_marginTop="15dp" app:constraint_referenced_ids="cl_username, cl_telephone" />
android:text="密码"
android:textColor="@color/black"
android:textSize="17sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view1" />
<EditText
android:id="@+id/et_password"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@null"
android:hint="请填写密码"
android:inputType="textPassword"
android:maxLength="16"
android:maxLines="1"
android:textCursorDrawable="@drawable/cursor"
android:textSize="17sp"
app:layout_constraintBottom_toTopOf="@id/view2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/et_username"
app:layout_constraintTop_toBottomOf="@id/view1" />
<View <View
android:id="@+id/view2" android:id="@+id/view2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0.2dp" android:layout_height="0.2dp"
android:layout_marginHorizontal="10dp" android:layout_marginHorizontal="10dp"
android:layout_marginTop="15dp"
android:background="#D8D8D8" android:background="#D8D8D8"
app:layout_constraintTop_toBottomOf="@id/tv_password" /> app:layout_constraintTop_toBottomOf="@id/barrier" />
<TextView <TextView
android:id="@+id/tv_change_login_way" android:id="@+id/tv_change_login_way"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="15dp" android:layout_marginTop="15dp"
android:text="用户名登录" android:text="用户名登录"
android:textColor="@color/normal" android:textColor="@color/normal"
android:textSize="16sp" android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/tv_username" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view2" /> app:layout_constraintTop_toBottomOf="@id/barrier" />
<TextView <TextView
android:id="@+id/tv_tip" android:id="@+id/tv_tip"
@ -166,62 +238,42 @@
app:layout_constraintTop_toBottomOf="@id/view2" app:layout_constraintTop_toBottomOf="@id/view2"
app:layout_constraintVertical_bias="0.7" /> app:layout_constraintVertical_bias="0.7" />
<TextView
android:id="@+id/tv_urgent_freeze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="紧急冻结"
android:textColor="@color/normal"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_login"
app:layout_constraintVertical_bias="0.8" />
<TextView <TextView
android:id="@+id/tv_forgot_password" android:id="@+id/tv_forgot_password"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="找回密码" android:text="找回密码"
android:textColor="@color/normal" android:textColor="@color/normal"
android:textSize="15sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze" app:layout_constraintBottom_toBottomOf="@id/view_2"
app:layout_constraintEnd_toStartOf="@id/tv_urgent_freeze" app:layout_constraintEnd_toStartOf="@id/view_2"
app:layout_constraintHorizontal_bias="0.7" app:layout_constraintHorizontal_bias="0.9"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" /> app:layout_constraintTop_toTopOf="@id/view_2" />
<TextView <TextView
android:id="@+id/tv_security_center" android:id="@+id/tv_more"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="安全中心" android:text="更多选项"
android:textColor="@color/normal" android:textColor="@color/normal"
android:textSize="15sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze" app:layout_constraintBottom_toBottomOf="@id/view_2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3" app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toEndOf="@id/tv_urgent_freeze" app:layout_constraintStart_toEndOf="@id/view_2"
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" /> app:layout_constraintTop_toTopOf="@id/view_2" />
<View <View
android:id="@+id/view_2"
android:layout_width="1dp" android:layout_width="1dp"
android:layout_height="0dp" android:layout_height="20dp"
android:background="#e5e5e5" android:background="#e5e5e5"
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tv_urgent_freeze" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv_forgot_password" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" /> app:layout_constraintTop_toBottomOf="@id/tv_login"
app:layout_constraintVertical_bias="0.7" />
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="#e5e5e5"
app:layout_constraintBottom_toBottomOf="@id/tv_urgent_freeze"
app:layout_constraintEnd_toStartOf="@id/tv_security_center"
app:layout_constraintStart_toEndOf="@id/tv_urgent_freeze"
app:layout_constraintTop_toTopOf="@id/tv_urgent_freeze" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -8,47 +8,11 @@
app:cardMaxElevation="0dp"> app:cardMaxElevation="0dp">
<LinearLayout <LinearLayout
android:id="@+id/ll_bottom_sheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="#F7F7F7" android:background="#F7F7F7"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:id="@+id/tv_quit_account"
android:layout_width="match_parent"
android:layout_height="55dp"
android:background="@color/white"
android:gravity="center_horizontal|center_vertical"
android:text="退出登录"
android:textColor="@color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_quit_app"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginTop="1dp"
android:background="@color/white"
android:gravity="center_horizontal|center_vertical"
android:text="关闭应用"
android:textColor="@color/black"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/divider_height"
android:background="#E5E5E5" />
<TextView
android:id="@+id/tv_cancel"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginTop="8dp"
android:background="@color/white"
android:gravity="center_horizontal|center_vertical"
android:text="取消"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@ -25,6 +25,7 @@ pinyin4j = "2.5.1"
preference = "1.2.1" preference = "1.2.1"
retrofit = "2.11.0" retrofit = "2.11.0"
scanplus = "2.12.0.301" scanplus = "2.12.0.301"
softInputEvent = "1.0.9"
spannable = "1.2.7" spannable = "1.2.7"
therouter = "1.2.2" therouter = "1.2.2"
window = "1.3.0" window = "1.3.0"
@ -49,6 +50,7 @@ pinyin4j = { module = "com.belerweb:pinyin4j", version.ref = "pinyin4j" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
scanplus = { module = "com.huawei.hms:scanplus", version.ref = "scanplus" } scanplus = { module = "com.huawei.hms:scanplus", version.ref = "scanplus" }
soft-input-event = { module = "com.github.liangjingkanji:soft-input-event", version.ref = "softInputEvent" }
spannable = { module = "com.github.liangjingkanji:spannable", version.ref = "spannable" } spannable = { module = "com.github.liangjingkanji:spannable", version.ref = "spannable" }
therouter-ksp = { module = "cn.therouter:apt", version.ref = "therouter" } therouter-ksp = { module = "cn.therouter:apt", version.ref = "therouter" }
objectbox-android-objectbrowser = { group = "io.objectbox", name = "objectbox-android-objectbrowser", version.ref = "objectbox" } objectbox-android-objectbrowser = { group = "io.objectbox", name = "objectbox-android-objectbrowser", version.ref = "objectbox" }