diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7b3006b..20c7e48 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,9 +11,10 @@ - diff --git a/.idea/misc.xml b/.idea/misc.xml index a4f09e2..1a1bf72 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd90082..afbaf70 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("com.google.devtools.ksp") // id("therouter") id("org.jetbrains.kotlin.plugin.serialization") - id("kotlin-kapt") +// id("kotlin-kapt") } android { @@ -17,7 +17,7 @@ android { defaultConfig { applicationId = "com.kaixed.kchat" - minSdk = 28 + minSdk = 30 targetSdk = 34 versionCode = 1 versionName = "0.0.1" @@ -36,16 +36,17 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "17" + jvmTarget = "11" } } dependencies { + implementation(project(":core_common")) implementation(libs.androidx.core.ktx) implementation(libs.appcompat) @@ -59,13 +60,15 @@ dependencies { implementation(libs.emoji2) implementation(libs.preference) + implementation(libs.androidx.startup.runtime) + implementation(libs.okhttp) implementation(libs.mmkv) implementation(libs.gson) - implementation(libs.objectbox.kotlin) - debugImplementation(libs.objectbox.android.objectbrowser) - releaseImplementation(libs.objectbox.android) +// implementation(libs.objectbox.kotlin) +// debugImplementation(libs.objectbox.android.objectbrowser) +// releaseImplementation(libs.objectbox.android) implementation(libs.glide) implementation(libs.lottie) @@ -97,6 +100,15 @@ dependencies { implementation(libs.scanplus) // implementation(libs.therouter) // ksp(libs.therouter.ksp) + + // Room + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.paging) + implementation(libs.androidx.paging.runtime.ktx) + + debugImplementation(libs.leakcanary.android) } -apply(plugin = "io.objectbox") +//apply(plugin = "io.objectbox") diff --git a/app/objectbox-models/default.json.bak b/app/objectbox-models/default.json.bak index 1bfa6eb..69401e1 100644 --- a/app/objectbox-models/default.json.bak +++ b/app/objectbox-models/default.json.bak @@ -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": [] diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6738c46..909a2dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + - + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/App.kt b/app/src/main/kotlin/com/kaixed/kchat/App.kt index 4ee927a..0ffe6cf 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/App.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/App.kt @@ -1,11 +1,10 @@ package com.kaixed.kchat import android.app.Application -import com.kaixed.kchat.data.local.box.ObjectBox -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import android.content.Context +import com.kaixed.kchat.manager.NetworkManager import com.kaixed.kchat.utils.ScreenUtils import com.tencent.mmkv.MMKV -import io.objectbox.android.Admin /** @@ -14,12 +13,16 @@ import io.objectbox.android.Admin */ class App : Application() { + companion object { + private lateinit var appContext: Context + + fun getAppContext(): Context { + return appContext + } + } + override fun onCreate() { super.onCreate() - MMKV.initialize(this) - - ObjectBox.init(this) - Admin(getBoxStore()).start(this) - ScreenUtils.init(this) + appContext = this } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/DataBase.kt b/app/src/main/kotlin/com/kaixed/kchat/data/DataBase.kt index 41830b4..8a8cbe2 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/DataBase.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/DataBase.kt @@ -1,30 +1,47 @@ package com.kaixed.kchat.data -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Contact -import com.kaixed.kchat.data.local.entity.Conversation -import com.kaixed.kchat.data.local.entity.Messages import com.kaixed.kchat.data.local.entity.UserInfo -import io.objectbox.Box -import io.objectbox.kotlin.boxFor +import com.kaixed.kchat.utils.ConstantsUtils.getUsername +import kotlinx.coroutines.runBlocking /** * @Author: kaixed * @Date: 2024/12/14 23:06 */ object DataBase { - val contactBox: Box by lazy { getBoxStore().boxFor() } - val messagesBox: Box by lazy { getBoxStore().boxFor() } + private val contactDao = AppDatabase.getDatabase().contactDao() + private val userInfoDao = AppDatabase.getDatabase().userInfoDao() + private val conversationDao = AppDatabase.getDatabase().conversationDao() + private val messagesDao = AppDatabase.getDatabase().messagesDao() - val conversationBox: Box by lazy { getBoxStore().boxFor() } + fun clearAllDatabase() { + val db = AppDatabase.getDatabase() // 获取 Room 数据库实例 + db.clearAllTables() // 清空所有表的数据 + } - val userInfoBox: Box by lazy { getBoxStore().boxFor() } + fun getUserInfo(): UserInfo? { + return runBlocking { + userInfoDao.getUserInfoByUsername(getUsername()) + } + } - fun cleanAllData() { - contactBox.removeAll() - messagesBox.removeAll() - conversationBox.removeAll() - userInfoBox.removeAll() + fun deleteMsgByLocalId(localId: Long) { + } + + suspend fun getContactByUsername(contactId: String): Contact? { + return contactDao.getContactByUsername(contactId) + } + + fun saveUserInfo(info: UserInfo) { + + } + + fun isMyFriend(string: String): Boolean { + return runBlocking { + contactDao.isMyFriend(string) + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/LocalDatabase.kt b/app/src/main/kotlin/com/kaixed/kchat/data/LocalDatabase.kt deleted file mode 100644 index eda6e63..0000000 --- a/app/src/main/kotlin/com/kaixed/kchat/data/LocalDatabase.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.kaixed.kchat.data - -import com.kaixed.kchat.data.local.entity.Contact -import com.kaixed.kchat.data.local.entity.Contact_ -import com.kaixed.kchat.data.local.entity.Conversation_ -import com.kaixed.kchat.data.local.entity.Messages -import com.kaixed.kchat.data.local.entity.Messages_ -import com.kaixed.kchat.data.local.entity.UserInfo -import io.objectbox.query.QueryBuilder - -/** - * @Author: kaixed - * @Date: 2024/11/24 13:34 - */ -object LocalDatabase { - - private val contactBox = DataBase.contactBox - - private val messagesBox = DataBase.messagesBox - - private val conversationBox = DataBase.conversationBox - - private val userInfoBox = DataBase.userInfoBox - - fun saveUserInfo(userInfo: UserInfo) { - userInfoBox.put(userInfo) - } - - fun cleanChatHistory(contactId: String) { - messagesBox.query(Messages_.talkerId.equal(contactId)).build().remove() - conversationBox.query(Conversation_.talkerId.equal(contactId)).build().remove() - } - - fun isMyFriend(contactId: String): Boolean { - return getContactByUsername(contactId) != null - } - - fun getContactByUsername(contactId: String): Contact? { - return contactBox.query(Contact_.username.equal(contactId)).build().findFirst() - } - - fun getMessagesWithContact(contactId: String, offset: Long, limit: Long): List { - val query = messagesBox - .query(Messages_.talkerId.equal(contactId)) - .order(Messages_.timestamp, QueryBuilder.DESCENDING) - .build() - return query.find(offset, limit) - } - - fun getMoreMessages(contactId: String, msgLocalId: Long, limit: Long): List { - val query = messagesBox - .query(Messages_.talkerId.equal(contactId)) - .lessOrEqual(Messages_.msgLocalId, msgLocalId) - .order(Messages_.timestamp, QueryBuilder.DESCENDING) - .build() - val offset = 0 - return query.find(offset.toLong(), limit) - } - - fun getAllHistoryMessages(contactId: String, msgLocalId: Long): List { - val query = messagesBox - .query(Messages_.talkerId.equal(contactId)) - .greaterOrEqual(Messages_.msgLocalId, msgLocalId) - .order(Messages_.timestamp, QueryBuilder.DESCENDING) - .build() - return query.find() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/AppDatabase.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/AppDatabase.kt new file mode 100644 index 0000000..c8f568d --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/AppDatabase.kt @@ -0,0 +1,46 @@ +package com.kaixed.kchat.data.local + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.kaixed.kchat.App +import com.kaixed.kchat.data.local.dao.ContactDao +import com.kaixed.kchat.data.local.dao.ConversationDao +import com.kaixed.kchat.data.local.dao.MessagesDao +import com.kaixed.kchat.data.local.dao.UserInfoDao +import com.kaixed.kchat.data.local.entity.Contact +import com.kaixed.kchat.data.local.entity.Conversation +import com.kaixed.kchat.data.local.entity.Messages +import com.kaixed.kchat.data.local.entity.UserInfo + +@Database( + entities = [ + Contact::class, + Conversation::class, + Messages::class, + UserInfo::class + ], version = 1, exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract fun userInfoDao(): UserInfoDao + abstract fun contactDao(): ContactDao + abstract fun messagesDao(): MessagesDao + abstract fun conversationDao(): ConversationDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + App.getAppContext(), + AppDatabase::class.java, + "app_database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/box/ObjectBoxManager.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/box/ObjectBoxManager.kt deleted file mode 100644 index e1c98d8..0000000 --- a/app/src/main/kotlin/com/kaixed/kchat/data/local/box/ObjectBoxManager.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.kaixed.kchat.data.local.box - -import android.content.Context -import com.kaixed.kchat.data.local.entity.MyObjectBox -import io.objectbox.Box -import io.objectbox.BoxStore -import io.objectbox.kotlin.boxFor - -/** - * @Author: kaixed - * @Date: 2024/10/24 16:57 - */ -object ObjectBox { - private lateinit var boxStore: BoxStore - - fun init(context: Context) { - boxStore = MyObjectBox.builder() - .androidContext(context) - .build() - } - - fun getBoxStore(): BoxStore = boxStore - - fun getBox(entityClass: Class): Box { - return boxStore.boxFor(entityClass) - } -} diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/ContactDao.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/ContactDao.kt new file mode 100644 index 0000000..63e26ca --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/ContactDao.kt @@ -0,0 +1,35 @@ +package com.kaixed.kchat.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.kaixed.kchat.data.local.entity.Contact + +@Dao +interface ContactDao { + @Query("SELECT * FROM contact WHERE username = :contactId LIMIT 1") + suspend fun getContactByUsername(contactId: String): Contact? + + @Query("SELECT * FROM contact") + suspend fun getAllContacts(): List + + @Query("SELECT EXISTS (SELECT 1 FROM contact WHERE username = :contactId)") + suspend fun isMyFriend(contactId: String): Boolean + + @Query("DELETE FROM contact WHERE username = :contactId") + suspend fun deleteContactByUsername(contactId: String): Int + + // 更新联系人 + @Update + suspend fun updateContact(contact: Contact) + + @Update + suspend fun updateContacts(contacts: List) + + @Insert + suspend fun insertContact(contact: Contact) + + @Insert + suspend fun insertContacts(contacts: List) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/ConversationDao.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/ConversationDao.kt new file mode 100644 index 0000000..c483537 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/ConversationDao.kt @@ -0,0 +1,29 @@ +package com.kaixed.kchat.data.local.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.kaixed.kchat.data.local.entity.Conversation + +@Dao +interface ConversationDao { + @Query("DELETE FROM conversation WHERE talkerId = :contactId") + suspend fun deleteConversationsByContactId(contactId: String) + + @Query("SELECT * FROM conversation WHERE isShow = 1 ORDER BY timestamp DESC") + suspend fun getConversations(): List + + @Query("SELECT * FROM conversation WHERE talkerId = :contactId LIMIT 1") + suspend fun getConversationByUsername(contactId: String): Conversation? + + @Insert + suspend fun insertConversation(conversation: Conversation) + + @Delete + suspend fun deleteConversation(conversation: Conversation) + + @Update + suspend fun updateConversation(conversation: Conversation) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/MessagesDao.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/MessagesDao.kt new file mode 100644 index 0000000..2d8af0a --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/MessagesDao.kt @@ -0,0 +1,72 @@ +package com.kaixed.kchat.data.local.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import com.kaixed.kchat.data.local.entity.Messages + +@Dao +interface MessagesDao { + + @Query("select * from messages where talkerId = :contactId and msgLocalId <= :msgLocalId order by timestamp desc limit :limit") + suspend fun getMessageByContactId( + contactId: String, + msgLocalId: Long, + limit: Long + ): List + + @Query("select * from messages where talkerId = :contactId order by timestamp desc limit :limit") + suspend fun getMessageByContactId(contactId: String, limit: Long): List + + @Query("SELECT * FROM messages WHERE talkerId = :contactId ORDER BY timestamp DESC LIMIT :limit OFFSET :offset") + suspend fun getMessagesWithContact(contactId: String, offset: Long, limit: Long): List + + @Query("SELECT * FROM messages WHERE talkerId = :contactId AND msgLocalId <= :msgLocalId ORDER BY timestamp DESC LIMIT :limit") + suspend fun getMoreMessages(contactId: String, msgLocalId: Long, limit: Long): List + + @Query("SELECT * FROM messages WHERE talkerId = :contactId AND msgLocalId >= :msgLocalId ORDER BY timestamp DESC") + suspend fun getAllHistoryMessages(contactId: String, msgLocalId: Long): List + + @Query("DELETE FROM messages WHERE talkerId = :contactId") + suspend fun deleteMessagesByContactId(contactId: String) + + @Query("SELECT * FROM messages WHERE talkerId = :contactId ORDER BY timestamp ASC") + fun selectHistoryMessages(contactId: String): PagingSource + + // ASC 更早的消息在最上面 + @Query("SELECT * FROM messages WHERE talkerId = :contactId ORDER BY timestamp ASC") + fun getMessagesWithContact(contactId: String): PagingSource + + @Query("SELECT * FROM messages WHERE talkerId = :contactId AND msgLocalId < :maxMsgId ORDER BY timestamp ASC LIMIT :limit") + fun getMessagesBefore(contactId: String, maxMsgId: Long, limit: Int): List + + @Insert + suspend fun insertAll(messages: List) + + @Insert + suspend fun insert(messages: Messages): Long + + @Query("SELECT * FROM messages ORDER BY timestamp DESC") + fun getMessagesPaged(): PagingSource + + @Query("SELECT * FROM messages ORDER BY timestamp DESC") + fun getPagedMessages(): PagingSource + + @Query("SELECT * FROM messages WHERE content LIKE :keyword") + suspend fun getMessagesContainingKeyword(keyword: String): List + + @Query("SELECT * FROM messages WHERE talkerId = :talkerId AND timestamp <= :currentTimestamp ORDER BY timestamp DESC LIMIT 1") + suspend fun getLatestMessage(talkerId: String, currentTimestamp: Long): Messages? + + @Query("SELECT * FROM messages WHERE msgLocalId = :localId") + suspend fun getMessageByLocalId(localId: Long): Messages + + @Update + suspend fun updateMessage(messages: Messages) + + @Query("SELECT * FROM messages WHERE talkerId = :talkerId ORDER BY timestamp DESC") + fun getPagedMessages(talkerId: String): PagingSource +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/UserInfoDao.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/UserInfoDao.kt new file mode 100644 index 0000000..fc7a289 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/dao/UserInfoDao.kt @@ -0,0 +1,16 @@ +package com.kaixed.kchat.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.kaixed.kchat.data.local.entity.UserInfo + +@Dao +interface UserInfoDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveUserInfo(userInfo: UserInfo) + + @Query("SELECT * FROM user_info WHERE username = :username LIMIT 1") + suspend fun getUserInfoByUsername(username: String): UserInfo? +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Contact.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Contact.kt index 86d389d..b5c97e3 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Contact.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Contact.kt @@ -1,21 +1,19 @@ package com.kaixed.kchat.data.local.entity -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import io.objectbox.annotation.Index -import kotlinx.serialization.Serializable +import androidx.room.Entity +import androidx.room.PrimaryKey /** * @Author: kaixed * @Date: 2024/11/4 13:39 */ -@Entity -@Serializable +@Entity(tableName = "contact") data class Contact( - @Id var id: Long = 0L, - @Index var username: String, - @Index var nickname: String, - @Index var remark: String? = null, + @PrimaryKey(autoGenerate = true) + var id: Long = 0L, + var username: String, + var nickname: String, + var remark: String? = null, var signature: String? = null, var avatarUrl: String? = null, var status: String? = null, diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Conversation.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Conversation.kt index 48a5340..83f4e99 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Conversation.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Conversation.kt @@ -1,19 +1,17 @@ package com.kaixed.kchat.data.local.entity -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import io.objectbox.annotation.Index +import androidx.room.Entity +import androidx.room.PrimaryKey /** * @Author: kaixed * @Date: 2024/10/24 17:07 */ -@Entity +@Entity(tableName = "conversation") data class Conversation( - @Id + @PrimaryKey(autoGenerate = true) var id: Long = 0L, - @Index var talkerId: String, var nickname: String, var avatarUrl: String, diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Messages.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Messages.kt index 0517546..47e596c 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Messages.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/Messages.kt @@ -1,7 +1,7 @@ package com.kaixed.kchat.data.local.entity -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id +import androidx.room.Entity +import androidx.room.PrimaryKey import kotlinx.serialization.Serializable /** @@ -9,10 +9,9 @@ import kotlinx.serialization.Serializable * @Date: 2024/10/24 17:08 */ -@Entity -@Serializable +@Entity(tableName = "messages") data class Messages( - @Id + @PrimaryKey(autoGenerate = true) var msgLocalId: Long = 0L, var msgSvrId: Long = 0L, var content: String, diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/UserInfo.kt b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/UserInfo.kt index 8e42613..9543877 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/UserInfo.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/local/entity/UserInfo.kt @@ -1,8 +1,7 @@ package com.kaixed.kchat.data.local.entity -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id -import io.objectbox.annotation.Index +import androidx.room.Entity +import androidx.room.PrimaryKey import kotlinx.serialization.Serializable /** @@ -10,12 +9,10 @@ import kotlinx.serialization.Serializable * @Date: 2024/11/6 19:24 */ -@Entity -@Serializable +@Entity(tableName = "user_info") data class UserInfo( - @Id + @PrimaryKey(autoGenerate = true) var id: Long = 0, - @Index var username: String, var nickname: String, var avatarUrl: String, diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/repository/ContactRepository.kt b/app/src/main/kotlin/com/kaixed/kchat/data/repository/ContactRepository.kt index e0f308f..4e83bf9 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/repository/ContactRepository.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/repository/ContactRepository.kt @@ -1,25 +1,19 @@ package com.kaixed.kchat.data.repository -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.core.common.utils.Pinyin4jUtil +import com.kaixed.kchat.data.local.dao.ContactDao import com.kaixed.kchat.data.local.entity.Contact -import com.kaixed.kchat.data.local.entity.Contact_ import com.kaixed.kchat.data.model.friend.FriendRequestItem import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.network.ApiCall.apiCall import com.kaixed.kchat.network.RetrofitClient import com.kaixed.kchat.utils.ConstantsUtils.getUsername -import com.kaixed.kchat.utils.Pinyin4jUtil import com.kaixed.kchat.utils.handle.ContactUtil -import io.objectbox.Box -class ContactRepository { +class ContactRepository(private val contactDao: ContactDao) { private val friendApiService = RetrofitClient.friendApiService - private val contactBox: Box by lazy { - getBoxStore().boxFor(Contact::class.java) - } - // 获取联系人请求列表 suspend fun getContactRequestList(username: String): Result?> { return apiCall( @@ -53,9 +47,7 @@ class ContactRepository { apiCall = { friendApiService.deleteContact(username, contactId) }, errorMessage = "删除好友失败" ).onSuccess { - val con = - contactBox.query(Contact_.username.equal(contactId)).build().findFirst() - contactBox.remove(con!!) + contactDao.deleteContactByUsername(contactId) } } @@ -106,16 +98,26 @@ class ContactRepository { } } + suspend fun getContactsInDb(): List? { + val contacts = contactDao.getAllContacts() + return if (contacts.isNotEmpty()) { + contacts.sortedWith(::compareContacts) + } else { + null + } + } + suspend fun setRemark(userId: String, contactId: String, remark: String): Result { return apiCall( apiCall = { friendApiService.setRemark(userId, contactId, remark) }, errorMessage = "设置备注失败" ).onSuccess { - val con = contactBox.query(Contact_.username.equal(contactId)).build().findFirst() - if (con != null) { - con.remark = remark - con.remarkquanpin = Pinyin4jUtil.toPinyin(remark) - contactBox.put(con) + val contact = contactDao.getContactByUsername(contactId) + contact?.apply { + this.remark = remark + this.remarkquanpin = Pinyin4jUtil.toPinyin(remark) + }?.let { contact -> + contactDao.updateContact(contact) } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/repository/MessagesRepository.kt b/app/src/main/kotlin/com/kaixed/kchat/data/repository/MessagesRepository.kt index 42e2a59..e4778de 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/repository/MessagesRepository.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/repository/MessagesRepository.kt @@ -1,9 +1,28 @@ package com.kaixed.kchat.data.repository +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.kaixed.kchat.data.local.dao.MessagesDao +import com.kaixed.kchat.data.local.entity.Messages +import kotlinx.coroutines.flow.Flow + /** * @Author: kaixed * @Date: 2024/12/11 9:43 */ -class MessagesRepository { +class MessagesRepository(private val messagesDao: MessagesDao) { + + fun getMessageFlow(): Flow> { + return Pager( + config = PagingConfig( + pageSize = 20, // 每页加载的数量 + enablePlaceholders = false, + prefetchDistance = 5, // 当距离边缘还有5个项目时开始加载下一页 + initialLoadSize = 40 // 首次加载的数量 + ), + pagingSourceFactory = { messagesDao.getMessagesPaged() } + ).flow + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserAuthRepository.kt b/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserAuthRepository.kt index 5d60e1c..ff3e0bb 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserAuthRepository.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserAuthRepository.kt @@ -1,26 +1,22 @@ package com.kaixed.kchat.data.repository -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.kchat.data.local.dao.UserInfoDao import com.kaixed.kchat.data.local.entity.UserInfo -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.ApiCall.apiCall import com.kaixed.kchat.network.RetrofitClient import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION import com.tencent.mmkv.MMKV -import io.objectbox.Box /** * @Author: kaixed * @Date: 2024/11/23 14:15 */ -class UserAuthRepository { +class UserAuthRepository(private val userInfoDao: UserInfoDao) { private val authApiService = RetrofitClient.authApiService - private val userInfoBox: Box by lazy { getBoxStore().boxFor(UserInfo::class.java) } - private val mmkv by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) } /** @@ -72,7 +68,7 @@ class UserAuthRepository { } } - private fun insertUserInfo( + private suspend fun insertUserInfo( register: Register, telephone: String ) { @@ -83,22 +79,21 @@ class UserAuthRepository { signature = "", telephone = telephone ) - userInfoBox.put(userInfo) + userInfoDao.saveUserInfo(userInfo) } - private fun updateDb(userInfo: UserInfo) { + private suspend fun updateDb(userInfo: UserInfo) { mmkv.apply { encode("username", userInfo.username) encode("nickname", userInfo.nickname) encode("avatarUrl", userInfo.avatarUrl) } MMKV.defaultMMKV().encode("userLoginStatus", true) - val user = userInfoBox - .query(UserInfo_.username.equal(userInfo.username)) - .build() - .findFirst() - if (user == null) { - userInfoBox.put(userInfo) + val existingUser = userInfoDao.getUserInfoByUsername(userInfo.username) + + if (existingUser == null) { + // 如果用户不存在,插入新的用户信息 + userInfoDao.saveUserInfo(userInfo) } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserProfileRepository.kt b/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserProfileRepository.kt index cde5ec0..bf69822 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserProfileRepository.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserProfileRepository.kt @@ -1,8 +1,7 @@ package com.kaixed.kchat.data.repository -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.kchat.data.local.dao.UserInfoDao import com.kaixed.kchat.data.local.entity.UserInfo -import com.kaixed.kchat.data.local.entity.UserInfo_ import com.kaixed.kchat.data.model.request.UpdatePasswordRequest import com.kaixed.kchat.data.model.request.UserRequest import com.kaixed.kchat.data.model.search.SearchUser @@ -18,7 +17,7 @@ import okhttp3.MultipartBody * @Author: kaixed * @Date: 2024/11/23 14:15 */ -class UserProfileRepository { +class UserProfileRepository(private val userInfoDao: UserInfoDao) { private val userApiService = RetrofitClient.userApiService @@ -67,29 +66,22 @@ class UserProfileRepository { ) } - private fun updateNickname(newNickname: String) { - val userInfoBox = getBoxStore().boxFor(UserInfo::class.java) - val user = userInfoBox - .query(UserInfo_.username.equal(getUsername())) - .build() - .findFirst() - + private suspend fun updateNickname(newNickname: String) { + val userInfo = userInfoDao.getUserInfoByUsername(getUsername()) MMKV.mmkvWithID(MMKV_USER_SESSION).putString(NICKNAME_KEY, newNickname) - user?.nickname = newNickname - - if (user != null) { - userInfoBox.put(user) + userInfo?.nickname = newNickname + userInfo?.apply { + nickname = newNickname + userInfoDao.saveUserInfo(this) } } - private fun updateDb(url: String) { - val userInfoBox = getBoxStore().boxFor(UserInfo::class.java) - val userInfo = - userInfoBox.query(UserInfo_.username.equal(getUsername())).build().findFirst() - if (userInfo != null) { - userInfo.avatarUrl = url - userInfoBox.put(userInfo) + private suspend fun updateDb(url: String) { + val userInfo = userInfoDao.getUserInfoByUsername(getUsername()) + userInfo?.apply { + avatarUrl = url + userInfoDao.saveUserInfo(this) } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserSearchRepository.kt b/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserSearchRepository.kt index 94f3aab..7bea2e7 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserSearchRepository.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/data/repository/UserSearchRepository.kt @@ -1,6 +1,7 @@ package com.kaixed.kchat.data.repository import com.kaixed.kchat.data.model.response.search.User +import com.kaixed.kchat.network.ApiCall.apiCall import com.kaixed.kchat.network.RetrofitClient /** @@ -12,20 +13,11 @@ class UserSearchRepository { private val userApiService = RetrofitClient.userApiService // 获取用户列表 - suspend fun getUserList(username: String): Result> { + suspend fun getUserList(username: String): Result?> { val requestParams = mapOf("username" to username) - return try { - val response = userApiService.fetchUserList(requestParams) - if (response.isSuccess()) { - val userList = response.getResponseData() - userList?.let { - Result.success(userList) - } ?: Result.failure(Exception("用户列表为空")) - } else { - Result.failure(Exception(response.getResponseMsg())) - } - } catch (e: Exception) { - Result.failure(e) - } + return apiCall( + apiCall = { userApiService.fetchUserList(requestParams) }, + errorMessage = "用户列表为空" + ) } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/manager/ConversationManager.kt b/app/src/main/kotlin/com/kaixed/kchat/manager/ConversationManager.kt index 68ce62f..81790ab 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/manager/ConversationManager.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/manager/ConversationManager.kt @@ -2,16 +2,16 @@ package com.kaixed.kchat.manager import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.kaixed.kchat.data.DataBase -import com.kaixed.kchat.data.LocalDatabase.getContactByUsername +import com.kaixed.kchat.data.DataBase.getContactByUsername import com.kaixed.kchat.data.event.UnreadEvent -import com.kaixed.kchat.data.local.entity.Contact_ +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Conversation -import com.kaixed.kchat.data.local.entity.Conversation_ import com.kaixed.kchat.data.local.entity.Messages -import com.kaixed.kchat.data.local.entity.Messages_ import com.kaixed.kchat.utils.ConstantsUtils.getCurrentContactId import com.kaixed.kchat.utils.ConstantsUtils.getUsername +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus /** @@ -23,12 +23,10 @@ object ConversationManager { private val _conversationLiveData = MutableLiveData?>() val conversations: LiveData?> get() = _conversationLiveData - private val conversationBox by lazy { DataBase.conversationBox } - private val contactBox by lazy { DataBase.contactBox } - private val messagesBox by lazy { DataBase.messagesBox } - private val conversationMap: MutableMap = mutableMapOf() + private val conversationDao = AppDatabase.getDatabase().conversationDao() + private var unreadCount = 0 val unReadMsgCount get() = unreadCount @@ -37,25 +35,27 @@ object ConversationManager { } private fun loadConversations() { - val conversations = conversationBox.query(Conversation_.isShow.equal(true)) - .orderDesc(Conversation_.timestamp).build().find() + val scope = CoroutineScope(Dispatchers.IO) + scope.launch { + val conversations = conversationDao.getConversations() - conversations.forEach { conversation -> - if (conversation.unreadCount > 0) { - unreadCount += conversation.unreadCount + conversations.forEach { conversation -> + if (conversation.unreadCount > 0) { + unreadCount += conversation.unreadCount + } + conversationMap[conversation.talkerId] = conversation } - conversationMap[conversation.talkerId] = conversation + updateLiveData() } - updateLiveData() } - fun handleMessages(messages: Messages) { + suspend fun handleMessages(messages: Messages) { val talkerId = getCurrentContactId() if (messages.senderId != getUsername() && messages.talkerId != talkerId) { unreadCount++ } val updatedConversation = updateConversation(messages) - conversationBox.put(updatedConversation) + conversationDao.updateConversation(updatedConversation) updateLiveData() } @@ -78,7 +78,7 @@ object ConversationManager { updateLiveData() } - fun deleteConversation(conversation: Conversation) { + suspend fun deleteConversation(conversation: Conversation) { val count = conversationMap[conversation.talkerId]?.unreadCount count?.let { if (it > 0) { @@ -86,48 +86,50 @@ object ConversationManager { } } conversationMap.remove(conversation.talkerId) - conversationBox.query(Conversation_.talkerId.equal(conversation.talkerId)).build().remove() - messagesBox.query(Messages_.talkerId.equal(conversation.talkerId)).build().remove() + conversationDao.deleteConversationsByContactId(conversation.talkerId) + val messagesDao = AppDatabase.getDatabase().messagesDao() + messagesDao.deleteMessagesByContactId(conversation.talkerId) updateLiveData() } - fun cleanUnreadCount(talkerId: String) { + suspend fun cleanUnreadCount(talkerId: String) { val con = conversationMap[talkerId] unreadCount -= con?.unreadCount ?: 0 conversationMap[talkerId].apply { this?.unreadCount = 0 } - val conversation = - conversationBox.query(Conversation_.talkerId.equal(talkerId)).build().findFirst() + val conversation = conversationDao.getConversationByUsername(talkerId) - conversation?.let { - it.unreadCount = 0 - conversationBox.put(it) + conversation?.apply { + unreadCount = 0 + conversationDao.updateConversation(this) } updateLiveData() } - fun updateDisturbStatus(contactId: String, isDisturb: Boolean) { + private val contactDao = AppDatabase.getDatabase().contactDao() + + suspend fun updateDisturbStatus(contactId: String, isDisturb: Boolean) { conversationMap[contactId]?.apply { - this.doNotDisturb = isDisturb + doNotDisturb = isDisturb + conversationDao.updateConversation(this) } - conversationBox.put(conversationMap[contactId]!!) val dbContact = - contactBox.query(Contact_.username.equal(contactId)).build().findFirst().apply { + contactDao.getContactByUsername(contactId).apply { this?.doNotDisturb = isDisturb } - contactBox.put(dbContact!!) + contactDao.updateContact(dbContact!!) updateLiveData() } - fun updateUnreadCount(talkerId: String, isRead: Boolean) { + suspend fun updateUnreadCount(talkerId: String, isRead: Boolean) { unreadCount = if (isRead) unreadCount - 1 else unreadCount + 1 val conversation = conversationMap[talkerId]?.apply { this.unreadCount = if (isRead) 1 else 0 } if (conversation != null) { - conversationBox.put(conversation) + conversationDao.insertConversation(conversation) } updateLiveData() } @@ -138,7 +140,7 @@ object ConversationManager { _conversationLiveData.postValue(conversationMap.values.sortedByDescending { it.timestamp }) } - private fun updateConversation(messages: Messages): Conversation { + private suspend fun updateConversation(messages: Messages): Conversation { val existingConversation = conversationMap[messages.talkerId] return existingConversation?.apply { lastContent = if (messages.type == "4") "[图片]" else messages.content @@ -148,7 +150,7 @@ object ConversationManager { } ?: createConversation(messages) } - private fun createConversation(messages: Messages): Conversation { + private suspend fun createConversation(messages: Messages): Conversation { val contact = getContactByUsername(messages.talkerId) return Conversation( talkerId = messages.talkerId, diff --git a/app/src/main/kotlin/com/kaixed/kchat/manager/MessagesManager.kt b/app/src/main/kotlin/com/kaixed/kchat/manager/MessagesManager.kt index a3679ac..5308d02 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/manager/MessagesManager.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/manager/MessagesManager.kt @@ -1,13 +1,13 @@ package com.kaixed.kchat.manager import com.kaixed.kchat.data.DataBase -import com.kaixed.kchat.data.LocalDatabase -import com.kaixed.kchat.data.LocalDatabase.getAllHistoryMessages -import com.kaixed.kchat.data.LocalDatabase.getMessagesWithContact -import com.kaixed.kchat.data.LocalDatabase.getMoreMessages +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Messages +import com.kaixed.kchat.data.repository.MessagesRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlin.collections.isNotEmpty /** * @Author: kaixed @@ -22,41 +22,57 @@ object MessagesManager { val messages: StateFlow> get() = _messages private var contactId: String = "" + private var isHasHistory = false - private var loading = false + + private val messagesDao = AppDatabase.getDatabase().messagesDao() + + private var tempIndex: Long = 0 + fun setContactId(contactId: String) { + this.contactId = contactId + } + + suspend fun firstLoadMessages() { + val messages = messagesDao.getMessageByContactId(contactId, LIMIT + 1) + handleMessages(messages) + } + + suspend fun loadMoreMessages() { + if (!isHasHistory) { + return + } + val messages = messagesDao.getMessageByContactId(contactId, tempIndex, LIMIT + 1) + handleMessages(messages) + } + + private fun handleMessages(messages: List) { + if (messages.isNotEmpty()) { + val size = messages.size + isHasHistory = size > LIMIT + + _messages.value += if (isHasHistory) { + tempIndex = messages[size - 1].msgLocalId + messages.subList(0, LIMIT.toInt()) + } else { + messages + } + } + } + fun deleteMessage(msgLocalId: Long) { - DataBase.messagesBox.remove(msgLocalId) + DataBase.deleteMsgByLocalId(msgLocalId) val msg = _messages.value.toMutableList().apply { removeIf { it.msgLocalId == msgLocalId } } _messages.value = msg } - fun queryHistory(msgLocalId: Long): Int { - _messages.value = emptyList() - val msg = getAllHistoryMessages(contactId, msgLocalId) - _messages.value = msg - return msg.size - } - - fun setContactId(contactId: String) { - this.contactId = contactId - } - fun resetMessages() { _messages.value = emptyList() } - fun cleanMessages(timestamp: Long) { - val messages = _messages.value.toMutableList() - _messages.value = messages.apply { - removeIf { it.timestamp < timestamp } - } - LocalDatabase.cleanChatHistory(contactId) - } - fun receiveMessage(messages: Messages) { if (messages.talkerId != contactId) return if (_messages.value.first() == messages) return @@ -66,49 +82,9 @@ object MessagesManager { } fun sendMessages(messages: Messages) { - _messages.value = _messages.value.toMutableList().apply { - add(0, messages) - } - } - - fun firstLoadMessages() { - val messages = getMessagesWithContact(contactId, 0, LIMIT + 1) - if (messages.isNotEmpty()) { - val size = messages.size - isHasHistory = size > LIMIT - if (isHasHistory) { - val messages1 = messages.subList(0, LIMIT.toInt()) - _messages.value = messages1 - tempIndex = messages[size - 1].msgLocalId - } else { - _messages.value = messages - } - } - } - - fun loadMoreMessages() { - if (loading) return - - if (!isHasHistory) return - - loading = true - - val newMessages: List = - getMoreMessages(contactId, tempIndex, LIMIT + 1) - - val size = newMessages.size - tempIndex = newMessages[size - 1].msgLocalId - isHasHistory = size > LIMIT - - if (newMessages.isNotEmpty()) { - val messages1 = if (isHasHistory) { - newMessages.subList(0, LIMIT.toInt()).toMutableList() - } else { - newMessages.subList(0, newMessages.size).toMutableList() - } - _messages.value += messages1 - } - - loading = false + _messages.value = mutableListOf(messages) + _messages.value +// _messages.value = _messages.value.toMutableList().apply { +// add(0, messages) +// } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/manager/NetworkManager.kt b/app/src/main/kotlin/com/kaixed/kchat/manager/NetworkManager.kt new file mode 100644 index 0000000..2c482ed --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/manager/NetworkManager.kt @@ -0,0 +1,19 @@ +package com.kaixed.kchat.manager + +import android.content.Context + +object NetworkManager { + private lateinit var networkStateManager: NetworkStateManager + + fun init(context: Context) { + networkStateManager = NetworkStateManager(context.applicationContext) + } + + fun start() { + networkStateManager.startListening() + } + + fun release() { + networkStateManager.stopListening() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/manager/NetworkStateManager.kt b/app/src/main/kotlin/com/kaixed/kchat/manager/NetworkStateManager.kt new file mode 100644 index 0000000..48e2568 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/manager/NetworkStateManager.kt @@ -0,0 +1,86 @@ +package com.kaixed.kchat.manager + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import com.kaixed.kchat.App + +class NetworkStateManager(context: Context) { + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + currentNetworkState = NetworkState.CONNECTED + notifyListeners() + } + + override fun onLost(network: Network) { + currentNetworkState = NetworkState.DISCONNECTED + notifyListeners() + } + + override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) { + // 判断网络类型和质量 + currentNetworkState = when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> NetworkState.WIFI + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> NetworkState.CELLULAR + else -> NetworkState.CONNECTED + } + + // 判断信号强度 + signalStrength = when { + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) + && capabilities.linkDownstreamBandwidthKbps > 2000 -> SignalStrength.STRONG + + capabilities.linkDownstreamBandwidthKbps < 500 -> SignalStrength.WEAK + else -> SignalStrength.NORMAL + } + + notifyListeners() + } + } + + private var currentNetworkState: NetworkState = NetworkState.UNKNOWN + private var signalStrength: SignalStrength = SignalStrength.UNKNOWN + private val listeners = mutableListOf() + + fun startListening() { + val request = NetworkRequest.Builder().build() + connectivityManager.registerNetworkCallback(request, networkCallback) + } + + fun stopListening() { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + + fun getCurrentNetworkInfo(): Pair { + return Pair(currentNetworkState, signalStrength) + } + + fun addListener(listener: NetworkStateListener) { + listeners.add(listener) + } + + fun removeListener(listener: NetworkStateListener) { + listeners.remove(listener) + } + + private fun notifyListeners() { + listeners.forEach { it.onNetworkStateChanged(currentNetworkState, signalStrength) } + } + + enum class NetworkState { + UNKNOWN, CONNECTED, DISCONNECTED, WIFI, CELLULAR + } + + enum class SignalStrength { + UNKNOWN, WEAK, NORMAL, STRONG + } + + interface NetworkStateListener { + fun onNetworkStateChanged(state: NetworkState, strength: SignalStrength) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/network/NetworkInterface.kt b/app/src/main/kotlin/com/kaixed/kchat/network/NetworkInterface.kt index dc05114..19e4d61 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/network/NetworkInterface.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/network/NetworkInterface.kt @@ -7,7 +7,7 @@ package com.kaixed.kchat.network object NetworkInterface { // private const val URL = "app.kaixed.com/kchat" - private const val URL = "192.168.31.18:6196" + private const val URL = "192.168.225.209:6196" // private const val URL = "49.233.105.103:6000" // const val SERVER_URL = "https://$URL" const val SERVER_URL = "http://$URL" @@ -44,7 +44,7 @@ object NetworkInterface { // 获取好友列表 const val GET_FRIENDS = "friends/list" // 获取好友请求列表 - const val GET_FRIEND_LIST = "friends/requests/fetch" + const val GET_FRIEND_REQUEST_LIST = "friends/requests/fetch" // 删除好友 const val DELETE_FRIEND = "friends/delete-friend" // 设置好友备注 diff --git a/app/src/main/kotlin/com/kaixed/kchat/network/RetrofitClient.kt b/app/src/main/kotlin/com/kaixed/kchat/network/RetrofitClient.kt index 71b1aa8..4231a8b 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/network/RetrofitClient.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/network/RetrofitClient.kt @@ -1,7 +1,5 @@ package com.kaixed.kchat.network -import com.kaixed.kchat.network.interceptor.SignInterceptor -import com.kaixed.kchat.network.interceptor.TokenRefreshInterceptor import com.kaixed.kchat.network.service.AuthApiService import com.kaixed.kchat.network.service.FileApiService import com.kaixed.kchat.network.service.FriendApiService @@ -49,8 +47,8 @@ object RetrofitClient { private val client = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .pingInterval(15, TimeUnit.SECONDS) - .addInterceptor(SignInterceptor()) // 签名拦截器 - .addInterceptor(TokenRefreshInterceptor()) // Token 拦截器 +// .addInterceptor(SignInterceptor()) // 签名拦截器 +// .addInterceptor(TokenRefreshInterceptor()) // Token 拦截器 .build() // 创建 Retrofit 实例(用于普通 API 调用) diff --git a/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/SignInterceptor.kt b/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/SignInterceptor.kt index 80e7405..7e218b1 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/SignInterceptor.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/SignInterceptor.kt @@ -1,5 +1,6 @@ package com.kaixed.kchat.network.interceptor +import com.kaixed.kchat.utils.Constants import okhttp3.FormBody import okhttp3.Interceptor import okhttp3.Request @@ -10,8 +11,6 @@ import java.util.* class SignInterceptor : Interceptor { - private val secretKey = "YourSecretKey" // 后端秘钥 - @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() @@ -26,7 +25,7 @@ class SignInterceptor : Interceptor { val params = getRequestParams(request) // 生成签名 - val sign = generateServerSign(params, timestamp, nonce, secretKey) + val sign = generateServerSign(params, timestamp, nonce) // 构造新的请求,添加请求头 val newRequest = request.newBuilder() @@ -41,43 +40,46 @@ class SignInterceptor : Interceptor { // 获取请求的参数,处理 POST 请求的 Map 格式数据,并确保按字典顺序排序 private fun getRequestParams(request: Request): Map { - val params = mutableMapOf() + val params = TreeMap() - // 处理 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) + when (request.method) { + "GET" -> { + request.url.queryParameterNames.forEach { name -> + params[name] = request.url.queryParameter(name) ?: "" } } - } - // 按字典顺序排序参数 - return params.toSortedMap() + "POST" -> { + val body = request.body + if (body is FormBody) { + // 获取所有参数,并确保按字典顺序排序 + for (i in 0 until body.size) { + params[body.name(i)] = body.value(i) + } + } + } + + else -> { + // 其他请求方法不处理 + } + } + return params } // 生成服务端签名 - private fun generateServerSign(params: Map, timestamp: String, nonce: String, secretKey: String): String { - val sortedParams = params.toSortedMap() // 确保是按字典顺序排序 - + private fun generateServerSign( + sortedParams: Map, + timestamp: String, + nonce: String, + ): String { // 拼接参数 val sb = StringBuilder() - for ((key, value) in sortedParams) { + sortedParams.forEach { (key, value) -> sb.append("$key=$value&") } sb.append("timestamp=$timestamp&") sb.append("nonce=$nonce&") - sb.append("secretKey=$secretKey") + sb.append("secretKey=${Constants.Sign.SECRET_KEY}") // 使用 SHA-256 进行加密 return hashWithSHA256(sb.toString()) diff --git a/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/TokenRefreshInterceptor.kt b/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/TokenRefreshInterceptor.kt index 142706d..0aeea1b 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/TokenRefreshInterceptor.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/network/interceptor/TokenRefreshInterceptor.kt @@ -28,7 +28,6 @@ class TokenRefreshInterceptor() : Interceptor { 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) { diff --git a/app/src/main/kotlin/com/kaixed/kchat/network/service/FriendApiService.kt b/app/src/main/kotlin/com/kaixed/kchat/network/service/FriendApiService.kt index 6580d32..7fd7dc6 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/network/service/FriendApiService.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/network/service/FriendApiService.kt @@ -6,7 +6,7 @@ import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.network.ApiResponse import com.kaixed.kchat.network.NetworkInterface.ACCEPT_FRIEND_REQUEST import com.kaixed.kchat.network.NetworkInterface.DELETE_FRIEND -import com.kaixed.kchat.network.NetworkInterface.GET_FRIEND_LIST +import com.kaixed.kchat.network.NetworkInterface.GET_FRIEND_REQUEST_LIST import com.kaixed.kchat.network.NetworkInterface.SEND_FRIEND_REQUEST import com.kaixed.kchat.network.NetworkInterface.SET_FRIEND_REMARK import retrofit2.http.Body @@ -23,9 +23,9 @@ import retrofit2.http.Path interface FriendApiService { // 获取好友请求列表 @FormUrlEncoded - @POST(GET_FRIEND_LIST) + @POST(GET_FRIEND_REQUEST_LIST) suspend fun getContactRequestList( - @Field("userId") username: String + @Field("userAccount") username: String ): ApiResponse?> // 接受好友请求 diff --git a/app/src/main/kotlin/com/kaixed/kchat/processor/MessageProcessor.kt b/app/src/main/kotlin/com/kaixed/kchat/processor/MessageProcessor.kt index 0921067..fa01c9f 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/processor/MessageProcessor.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/processor/MessageProcessor.kt @@ -1,9 +1,11 @@ package com.kaixed.kchat.processor -import com.kaixed.kchat.data.local.box.ObjectBox.getBox +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Messages -import com.kaixed.kchat.data.local.entity.Messages_ -import io.objectbox.Box +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * @Author: kaixed @@ -13,20 +15,21 @@ object MessageProcessor { private const val INTERVAL = 5 - private val messagesBox: Box by lazy { getBox(Messages::class.java) } + private val messagesDao = AppDatabase.getDatabase().messagesDao() - fun processorMsg(messages: Messages): Messages { + suspend fun processorMsg(messages: Messages): Messages { val curTimestamp = System.currentTimeMillis() - val localMsg = messagesBox.query(Messages_.talkerId.equal(messages.talkerId)) - .lessOrEqual(Messages_.timestamp, curTimestamp) - .orderDesc(Messages_.timestamp).build().findFirst() - var showTimer = true - localMsg?.let { - showTimer = curTimestamp - it.timestamp >= INTERVAL * 1L * 60 * 1000 + return withContext(Dispatchers.IO) { + val localMsg = messagesDao.getLatestMessage(messages.talkerId, curTimestamp) + + var showTimer = true + localMsg?.let { + showTimer = curTimestamp - it.timestamp >= INTERVAL * 1L * 60 * 1000 + } + + messages.isShowTimer = showTimer + messages } - - messages.isShowTimer = showTimer - return messages } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/send/MessageRepo.kt b/app/src/main/kotlin/com/kaixed/kchat/send/MessageRepo.kt new file mode 100644 index 0000000..f32f80e --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/send/MessageRepo.kt @@ -0,0 +1,22 @@ +package com.kaixed.kchat.send + +// 模拟持久化存储,可考虑使用Room数据库代替。 +object MessageRepo { + private val taskStore = mutableMapOf() + + fun save(task: MessageTask) { + taskStore[task.id] = task + } + + fun update(task: MessageTask) { + taskStore[task.id] = task + } + + fun remove(taskId: Long) { + taskStore.remove(taskId) + } + + fun loadAll(): List { + return taskStore.values.toList() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/send/MessageTask.kt b/app/src/main/kotlin/com/kaixed/kchat/send/MessageTask.kt new file mode 100644 index 0000000..a800e7f --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/send/MessageTask.kt @@ -0,0 +1,14 @@ +package com.kaixed.kchat.send + +data class MessageTask( + val id: Long, + val payload: String, + val maxRetry: Int = 5, + var retryCount: Int = 0, + var nextRetryTime: Long = System.currentTimeMillis(), + var state: MessageState = MessageState.PENDING +) + +enum class MessageState { + PENDING, SENDING, SUCCESS, FAILED +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/send/NetworkMonitor.kt b/app/src/main/kotlin/com/kaixed/kchat/send/NetworkMonitor.kt new file mode 100644 index 0000000..f381dff --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/send/NetworkMonitor.kt @@ -0,0 +1,16 @@ +package com.kaixed.kchat.send + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +// NetworkMonitor 用于监听网络状态变化,并通知 MessageUploadManager +object NetworkMonitor { + // 这里可以用 LiveData 或 Flow 来实时通知变化 + private val _networkAvailable = MutableStateFlow(true) + val networkAvailable: StateFlow get() = _networkAvailable + + // 模拟切换,实际需集成 ConnectivityManager 回调 + fun setNetworkAvailable(available: Boolean) { + _networkAvailable.value = available + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/send/RetryQueueManager.kt b/app/src/main/kotlin/com/kaixed/kchat/send/RetryQueueManager.kt new file mode 100644 index 0000000..50425a5 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/send/RetryQueueManager.kt @@ -0,0 +1,37 @@ +package com.kaixed.kchat.send + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class RetryQueueManager { + // 使用 MutableStateFlow 保存当前任务列表 + private val _taskFlow = MutableStateFlow>(emptyList()) + val taskFlow: StateFlow> get() = _taskFlow + + // 添加任务到队列,并持久化 + fun addTask(task: MessageTask) { + MessageRepo.save(task) + _taskFlow.value = _taskFlow.value + task + } + + // 更新任务状态 + fun updateTask(task: MessageTask) { + MessageRepo.update(task) + _taskFlow.value = _taskFlow.value.map { if (it.id == task.id) task else it } + } + + // 从队列中移除任务 + fun removeTask(task: MessageTask) { + MessageRepo.remove(task.id) + _taskFlow.value = _taskFlow.value.filter { it.id != task.id } + } + + // 获取当前已到达可上传时间的任务(按 nextRetryTime 排序) + fun getReadyTask(): MessageTask? { + val now = System.currentTimeMillis() + return _taskFlow.value + .filter { it.state == MessageState.PENDING && it.nextRetryTime <= now } + .sortedWith(compareBy { it.nextRetryTime }) + .firstOrNull() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/send/UploadWorker.kt b/app/src/main/kotlin/com/kaixed/kchat/send/UploadWorker.kt new file mode 100644 index 0000000..fafa5b1 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/send/UploadWorker.kt @@ -0,0 +1,125 @@ +package com.kaixed.kchat.send + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.math.pow +import kotlin.random.Random + +class UploadWorker(private val semaphore: Semaphore) { + + // 模拟消息上传接口 + suspend fun upload(task: MessageTask): Boolean { + // 模拟上传耗时 & 网络请求,可用 Retrofit 或 OkHttp 实现 + delay(500L) // 模拟网络延迟 + // 模拟成功率 80% + return Random.nextInt(100) < 80 + } +} + +class MessageUploadManager( + private val retryQueue: RetryQueueManager, + private val worker: UploadWorker, + private val ackTimeout: Long = 5000L // ACK 超时设为 5 秒 +) { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + // 限制并发上传数量,例如最多 3 个同时上传 + private val semaphore = Semaphore(3) + + // 启动调度流程,监视重传队列 + fun start() { + // 持续监听任务队列 + scope.launch { + while (isActive) { + // 检查网络状态(这里应嵌入 NetworkMonitor 判断,可扩展为挂起函数) + if (!isNetworkAvailable()) { + delay(2000L) + continue + } + + // 从队列中选择可上传任务 + val task = retryQueue.getReadyTask() + if (task != null) { + // 进入发送状态 + task.state = MessageState.SENDING + retryQueue.updateTask(task) + + // 限流并发执行上传任务 + semaphore.acquire() + launch { + val success = worker.upload(task) + // 模拟等待 ACK 的逻辑 + // 如果上传成功,则等待 ackTimeout 时间后检查是否收到 ACK + if (success) { + // 调用 ackWaitTimer 监听 ACK + withTimeoutOrNull(ackTimeout) { + waitForAck(task.id.toString()) + }?.let { + // 成功收到 ACK + task.state = MessageState.SUCCESS + retryQueue.updateTask(task) + retryQueue.removeTask(task) + } ?: run { + // 超时未收到 ACK,则作为失败处理 + retryTask(task) + } + } else { + // 上传失败处理 + retryTask(task) + } + semaphore.release() + } + } else { + // 无任务时稍作延迟 + delay(1000L) + } + } + } + } + + // 模拟网络状态检测函数,可结合 ConnectivityManager 使用 + private fun isNetworkAvailable(): Boolean { + // 实际应检测网络连接 + return true + } + + // 模拟等待 ACK,实际可通过 Channel 或 WebSocket 收到 ack 后返回 + private suspend fun waitForAck(taskId: String) { + // 这里模拟 ack 延迟,实际情况中应由 AckHandler 回调触发 + delay(300L) // 模拟短延时后收到 ACK + } + + // 重试逻辑:更新重试次数、计算下次重试时间,然后回到队列 + private fun retryTask(task: MessageTask) { + task.retryCount++ + if (task.retryCount > task.maxRetry) { + task.state = MessageState.FAILED + retryQueue.updateTask(task) + // 可在此处上报失败或通知用户 + } else { + // 计算指数退避时间(如基于 2000ms 基数,再加上少量随机抖动) + val newDelay = + (2000L * 2.0.pow(task.retryCount.toDouble())).toLong() + Random.nextLong(500) + task.nextRetryTime = System.currentTimeMillis() + newDelay + task.state = MessageState.PENDING + retryQueue.updateTask(task) + } + } + + // 当收到 ACK 时调用,由 AckHandler 或网络消息回调触发 + fun onAckReceived(taskId: Long) { + // 查找任务并更新状态 + val task = retryQueue.taskFlow.value.find { it.id == taskId } + if (task != null) { + task.state = MessageState.SUCCESS + retryQueue.updateTask(task) + retryQueue.removeTask(task) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/ChatViewModel.kt b/app/src/main/kotlin/com/kaixed/kchat/service/ChatViewModel.kt new file mode 100644 index 0000000..19fe121 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/ChatViewModel.kt @@ -0,0 +1,24 @@ +package com.kaixed.kchat.service + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +class ChatViewModel : ViewModel() { + fun sendChatMessage(content: String, msgLocalId: Long) { + viewModelScope.launch { + MessageRepository.sendMessage(content, msgLocalId).fold( + onSuccess = { /* 消息发送成功 */ }, + onFailure = { /* 消息发送失败 */ } + ) + } + } + + fun observeMessages() { + viewModelScope.launch { + MessageRepository.observeMessages().collect { message -> + println("Received message: $message") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/MessageModel.kt b/app/src/main/kotlin/com/kaixed/kchat/service/MessageModel.kt new file mode 100644 index 0000000..2e6cef9 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/MessageModel.kt @@ -0,0 +1,16 @@ +package com.kaixed.kchat.service + +data class MessageModel( + var msgLocalId: Long = 0L, + var msgSvrId: Long = 0L, + var content: String, + var timestamp: Long, + var status: String = "normal", + var senderId: String, + var avatarUrl: String = "", + var talkerId: String, + var type: String, + var show: Boolean = true, + var isShowTimer: Boolean = false, + var isSender: Boolean = false +) diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/MessageReceiver.kt b/app/src/main/kotlin/com/kaixed/kchat/service/MessageReceiver.kt new file mode 100644 index 0000000..7674840 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/MessageReceiver.kt @@ -0,0 +1,7 @@ +package com.kaixed.kchat.service + +import kotlinx.coroutines.flow.Flow + +interface MessageReceiver { + fun observeMessages(): Flow +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/MessageRepository.kt b/app/src/main/kotlin/com/kaixed/kchat/service/MessageRepository.kt new file mode 100644 index 0000000..a052499 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/MessageRepository.kt @@ -0,0 +1,46 @@ +package com.kaixed.kchat.service + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +object MessageRepository : MessageSender, MessageReceiver { + + private val webSocketClient: WebSocketClient = OkHttpWebSocketClient + + // 创建一个 Flow 来发送 WebSocket 的消息 + private val _outgoingMessages = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 10 + ) + + // 创建一个 Flow 来接收 WebSocket 的消息 + private val _incomingMessages = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 10 + ) + + init { + // 设置 WebSocket 消息监听器 + webSocketClient.setMessageListener { text -> + _incomingMessages.tryEmit(text) + } + } + + override suspend fun sendMessage(message: String, msgLocalId: Long): Result { + return try { + if (webSocketClient.send(message, msgLocalId)) { + _outgoingMessages.emit(message) + Result.success(Unit) + } else { + Result.failure(Exception("WebSocket not connected")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override fun observeMessages(): Flow = _incomingMessages.asSharedFlow() + + fun observeOutgoingMessages(): Flow = _outgoingMessages.asSharedFlow() +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/MessageSender.kt b/app/src/main/kotlin/com/kaixed/kchat/service/MessageSender.kt new file mode 100644 index 0000000..0821e72 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/MessageSender.kt @@ -0,0 +1,5 @@ +package com.kaixed.kchat.service + +interface MessageSender { + suspend fun sendMessage(message: String, msgLocalId: Long): Result +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/OkHttpWebSocketClient.kt b/app/src/main/kotlin/com/kaixed/kchat/service/OkHttpWebSocketClient.kt new file mode 100644 index 0000000..3267e5d --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/OkHttpWebSocketClient.kt @@ -0,0 +1,124 @@ +package com.kaixed.kchat.service + +import android.util.Log +import com.google.gson.Gson +import com.kaixed.kchat.data.local.entity.Messages +import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET +import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL +import com.kaixed.kchat.network.OkhttpHelper +import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA +import com.kaixed.kchat.utils.ConstantsUtils +import com.kaixed.kchat.utils.ConstantsUtils.getUsername +import com.tencent.mmkv.MMKV +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener + +object OkHttpWebSocketClient : WebSocketClient { + + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val url: String = "$WEBSOCKET_SERVER_URL$WEBSOCKET${getUsername()}" + private lateinit var webSocket: WebSocket + private var messageListener: ((String) -> Unit)? = null + private var isConnected = false + + private const val TAG = "WebSocketService" + private const val WEBSOCKET_CLOSE_CODE = 1000 + + override fun connect() { + establishConnection() + } + + override fun send(message: String, msgLocalId: Long): Boolean { + return if (isConnected) { + val kv = MMKV.mmkvWithID(MMKV_COMMON_DATA) + kv.putLong("msgLocalId", msgLocalId) + webSocket.send(message) + } else { + false + } + } + + override fun close(code: Int, reason: String) { + webSocket.close(code, reason) + isConnected = false + } + + override fun setMessageListener(listener: (String) -> Unit) { + this.messageListener = listener + } + + override fun isConnected(): Boolean = isConnected + + private fun reconnect(attempt: Int = 1) { + scope.launch { + val delayMs = minOf(attempt * 1000L, 30000L) // 指数退避,最大 30 秒 + delay(delayMs) + establishConnection() + if (!isConnected()) { + reconnect(attempt + 1) + } + } + } + + private fun establishConnection() { + val request = Request.Builder() + .url(url).build() + val listener = EchoWebSocketListener() + val client = OkhttpHelper.getInstance() + webSocket = client.newWebSocket(request, listener) + } + + private class EchoWebSocketListener : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(TAG, "WebSocket Opened") + isConnected = true + } + + override fun onMessage(webSocket: WebSocket, text: String) { + messageListener?.invoke(text) + val messages = Gson().fromJson(text, Messages::class.java) + +// serviceScope.launch { +// if ("ack" == messages.type) { +// val existedMessage = +// messagesDao.getMessageByLocalId(messages.msgLocalId) +// +// existedMessage.apply { +// timestamp = messages.timestamp +// if (messages.msgSvrId == 0L) { +// messages.msgSvrId = messages.msgLocalId +// } +// messagesDao.updateMessage(this) +// } +// } else { +// messages.talkerId = messages.senderId +// _messagesMutableLiveData.postValue(messages) +// MessageProcessor.processorMsg(messages) +// messagesDao.insert(messages) +// ConversationManager.handleMessages(messages) +// } +// } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + isConnected = false + webSocket.close(WEBSOCKET_CLOSE_CODE, null) + Log.d(TAG, "WebSocket closing: $reason") + establishConnection() + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + isConnected = false + Log.d(TAG, "WebSocket closing: ${t.cause}") + Log.d(TAG, "WebSocket closing: $t") + establishConnection() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketClient.kt b/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketClient.kt new file mode 100644 index 0000000..cca32e5 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketClient.kt @@ -0,0 +1,9 @@ +package com.kaixed.kchat.service + +interface WebSocketClient { + fun send(message: String, msgLocalId: Long): Boolean + fun close(code: Int, reason: String) + fun setMessageListener(listener: (String) -> Unit) + fun connect() + fun isConnected(): Boolean +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketService.kt b/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketService.kt index db3a881..e69efdd 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketService.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/service/WebSocketService.kt @@ -6,9 +6,9 @@ import android.os.Binder import android.os.IBinder import android.util.Log import com.google.gson.Gson -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore -import com.kaixed.kchat.data.local.entity.Contact -import com.kaixed.kchat.data.local.entity.Contact_ +import com.kaixed.core.common.utils.SingleLiveEvent +import com.kaixed.kchat.data.DataBase +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Messages import com.kaixed.kchat.manager.ConversationManager import com.kaixed.kchat.manager.MessagesManager @@ -16,16 +16,17 @@ import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL import com.kaixed.kchat.network.OkhttpHelper import com.kaixed.kchat.processor.MessageProcessor +import com.kaixed.kchat.service.OkHttpWebSocketClient import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA import com.kaixed.kchat.utils.ConstantsUtils.getUsername -import com.kaixed.kchat.utils.SingleLiveEvent import com.tencent.mmkv.MMKV -import io.objectbox.Box -import io.objectbox.kotlin.boxFor import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket @@ -42,26 +43,16 @@ class WebSocketService : Service() { private const val WEBSOCKET_CLOSE_CODE = 1000 } - private val messagesBox: Box by lazy { getBoxStore().boxFor() } - - private val contactBox: Box by lazy { getBoxStore().boxFor() } - - private lateinit var username: String + private val webSocketClient: WebSocketClient = OkHttpWebSocketClient private val binder = LocalBinder() - private var webSocket: WebSocket? = null - - private var heartbeatJob: Job? = null - - private val serviceJob = Job() - - private val serviceScope = CoroutineScope(IO + serviceJob) - private val _messagesMutableLiveData = SingleLiveEvent() val messageLivedata: SingleLiveEvent get() = _messagesMutableLiveData + private val scope = CoroutineScope(IO + SupervisorJob()) + inner class LocalBinder : Binder() { fun getService(): WebSocketService { return this@WebSocketService @@ -74,99 +65,38 @@ class WebSocketService : Service() { override fun onCreate() { super.onCreate() - username = getUsername() - if (!EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().register(this) + webSocketClient.connect() + + collectOutgoingMessages() + } + + private fun collectOutgoingMessages() { + scope.launch { + MessageRepository.observeOutgoingMessages().collect { message -> +// val json = Json.encodeToString(MessageModel.serializer(), message) + val id = 0L + webSocketClient.send(message, id) + } } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - establishConnection() + webSocketClient.connect() return START_STICKY } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(contactId: String) { - val timestamp = System.currentTimeMillis() - MessagesManager.cleanMessages(timestamp) - ConversationManager.hideConversation(contactId) - } - - fun sendMessage(jsonObject: String, msgLocalId: Long) { - webSocket?.let { - it.send(jsonObject) - val kv = MMKV.mmkvWithID(MMKV_COMMON_DATA) - kv.putLong("msgLocalId", msgLocalId) - } - } - fun storeOwnerMsg(messages: Messages) { - if (messages.avatarUrl == "") { - val contact = - contactBox.query(Contact_.username.equal(messages.talkerId)).build().findFirst() - messages.avatarUrl = contact?.avatarUrl ?: "" - } - ConversationManager.handleMessages(messages) - } - - private fun establishConnection() { - if (webSocket == null) { - val request = Request.Builder() - .url("$WEBSOCKET_SERVER_URL$WEBSOCKET$username").build() - val listener = EchoWebSocketListener() - val client = OkhttpHelper.getInstance() - webSocket = client.newWebSocket(request, listener) - } - } - - private inner class EchoWebSocketListener : WebSocketListener() { - override fun onOpen(webSocket: WebSocket, response: Response) { - Log.d(TAG, "WebSocket Opened") - } - - override fun onMessage(webSocket: WebSocket, text: String) { - val messages = Gson().fromJson(text, Messages::class.java) - - serviceScope.launch { - if ("ack" == messages.type) { - val existingMessage = messagesBox.get(messages.msgLocalId) - existingMessage?.let { - it.timestamp = messages.timestamp - it.msgSvrId = messages.msgSvrId - messagesBox.put(it) - } ?: run { - messagesBox.put(messages) - } - } else { - messages.talkerId = messages.senderId - _messagesMutableLiveData.postValue(messages) - MessageProcessor.processorMsg(messages) - messagesBox.put(messages) - ConversationManager.handleMessages(messages) - } + scope.launch { + if (messages.avatarUrl == "") { + val contact = DataBase.getContactByUsername(messages.talkerId) + messages.avatarUrl = contact?.avatarUrl ?: "" } - } - - override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - webSocket.close(WEBSOCKET_CLOSE_CODE, null) - Log.d(TAG, "WebSocket closing: $reason") - establishConnection() - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - Log.d(TAG, "WebSocket closing: ${t.cause}") - Log.d(TAG, "WebSocket closing: $t") - establishConnection() + ConversationManager.handleMessages(messages) } } override fun onDestroy() { super.onDestroy() - if (EventBus.getDefault().isRegistered(this)) { - EventBus.getDefault().unregister(this) - } - serviceJob.cancel() - heartbeatJob?.cancel() - webSocket?.close(WEBSOCKET_CLOSE_CODE, "App exited") + webSocketClient.close(WEBSOCKET_CLOSE_CODE, "App exited") } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/startup/MMKVInitializer.kt b/app/src/main/kotlin/com/kaixed/kchat/startup/MMKVInitializer.kt new file mode 100644 index 0000000..0159030 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/startup/MMKVInitializer.kt @@ -0,0 +1,18 @@ +package com.kaixed.kchat.startup + +import android.content.Context +import android.util.Log +import androidx.startup.Initializer +import com.tencent.mmkv.MMKV + +class MMKVInitializer : Initializer { + override fun create(context: Context): String { + Log.d("StartupTest", "MMKV Initialized") + val rootDir = MMKV.initialize(context) + return rootDir + } + + override fun dependencies(): List>> { + return emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/startup/ScreenUtilsInitializer.kt b/app/src/main/kotlin/com/kaixed/kchat/startup/ScreenUtilsInitializer.kt new file mode 100644 index 0000000..98cb654 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/startup/ScreenUtilsInitializer.kt @@ -0,0 +1,18 @@ +package com.kaixed.kchat.startup + +import android.content.Context +import android.util.Log +import androidx.startup.Initializer +import com.kaixed.kchat.utils.ScreenUtils + +class ScreenUtilsInitializer : Initializer { + override fun create(context: Context): ScreenUtils { + Log.d("StartupTest", "ScreenUtils Initialized") + ScreenUtils.init(context.applicationContext) + return ScreenUtils + } + + + override fun dependencies(): List>> = + listOf(MMKVInitializer::class.java) +} diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/AddFriendsActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/AddFriendsActivity.kt index ce9ae5d..2a1cbdd 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/AddFriendsActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/AddFriendsActivity.kt @@ -7,13 +7,6 @@ import androidx.activity.enableEdgeToEdge import com.kaixed.kchat.databinding.ActivityAddFriendsBinding import com.kaixed.kchat.ui.base.BaseActivity -/** - * 该文件包含用于兼容不同 Android 版本的 Intent 获取 Parcelable 数据的扩展函数。 - * 通过扩展函数 getParcelableExtraCompat 解决了低版本和高版本 Android 系统间的差异。 - * - * @author kaixed - * @since 2025年1月26日 - */ class AddFriendsActivity : BaseActivity() { private val context: Context by lazy { this } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApplyAddFriendActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApplyAddFriendActivity.kt index b2b119d..e1aa936 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApplyAddFriendActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApplyAddFriendActivity.kt @@ -2,21 +2,23 @@ package com.kaixed.kchat.ui.activity import android.os.Bundle import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels +import com.kaixed.core.common.base.BaseActivity import com.kaixed.kchat.databinding.ActivityApplyAddFriendBinding -import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.viewmodel.ContactViewModel -class ApplyAddFriendActivity : BaseActivity() { - - private val contactViewModel: ContactViewModel by viewModels() +class ApplyAddFriendActivity : BaseActivity() { private lateinit var contactId: String + private lateinit var contactNickname: String override fun inflateBinding(): ActivityApplyAddFriendBinding = ActivityApplyAddFriendBinding.inflate(layoutInflater) + override fun getViewModelClass(): Class { + return ContactViewModel::class.java + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,7 +30,7 @@ class ApplyAddFriendActivity : BaseActivity() { } override fun observeData() { - contactViewModel.addContactResult + viewModel.addContactResult .observe(this) { result -> result.onSuccess { toast(result.getOrNull().toString()) @@ -52,7 +54,7 @@ class ApplyAddFriendActivity : BaseActivity() { private fun sendContactRequest(contactId: String?) { contactId?.let { - contactViewModel.addContact(contactId, binding.etMessage.text.toString()) + viewModel.addContact(contactId, binding.etMessage.text.toString()) } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApproveDetailActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApproveDetailActivity.kt index 01367f1..568f44a 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApproveDetailActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ApproveDetailActivity.kt @@ -4,9 +4,9 @@ import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge import com.bumptech.glide.Glide +import com.kaixed.core.common.extension.getParcelableExtraCompat import com.kaixed.kchat.data.model.friend.FriendRequestItem import com.kaixed.kchat.databinding.ActivityApproveDetailBinding -import com.kaixed.kchat.extensions.getParcelableExtraCompat import com.kaixed.kchat.ui.base.BaseActivity class ApproveDetailActivity : BaseActivity() { diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatActivity.kt index 1f7cac3..fd3ed48 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatActivity.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.os.Handler import android.os.IBinder @@ -21,6 +20,7 @@ import android.widget.LinearLayout import androidx.activity.OnBackPressedCallback import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.core.graphics.drawable.toDrawable import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager @@ -28,22 +28,21 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.drake.softinput.getSoftInputHeight import com.drake.softinput.hasSoftInput -import com.drake.softinput.setWindowSoftInput +import com.kaixed.core.common.base.BaseActivity import com.kaixed.kchat.R -import com.kaixed.kchat.data.event.UnreadEvent -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Messages import com.kaixed.kchat.data.model.FunctionItem import com.kaixed.kchat.databinding.ActivityChatBinding import com.kaixed.kchat.manager.ConversationManager import com.kaixed.kchat.manager.MessagesManager import com.kaixed.kchat.processor.MessageProcessor +import com.kaixed.kchat.service.ChatViewModel import com.kaixed.kchat.service.WebSocketService import com.kaixed.kchat.service.WebSocketService.LocalBinder import com.kaixed.kchat.ui.adapter.ChatAdapter import com.kaixed.kchat.ui.adapter.EmojiAdapter import com.kaixed.kchat.ui.adapter.FunctionPanelAdapter -import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.i.IOnItemClickListener import com.kaixed.kchat.ui.i.OnItemClickListener import com.kaixed.kchat.ui.widget.LoadingDialogFragment @@ -54,31 +53,27 @@ import com.kaixed.kchat.utils.ConstantsUtils.getKeyboardHeight import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.utils.ImageEngines import com.kaixed.kchat.utils.ImageSpanUtil.insertEmoji -import com.kaixed.kchat.utils.ScreenUtils import com.kaixed.kchat.viewmodel.FileViewModel import com.luck.picture.lib.basic.PictureSelector import com.luck.picture.lib.config.SelectMimeType import com.luck.picture.lib.entity.LocalMedia import com.luck.picture.lib.interfaces.OnResultCallbackListener import com.tencent.mmkv.MMKV -import io.objectbox.Box -import io.objectbox.kotlin.boxFor +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.withContext import org.json.JSONObject import java.io.File import kotlin.math.max -class ChatActivity : BaseActivity(), OnItemClickListener, +class ChatActivity : BaseActivity(), OnItemClickListener, IOnItemClickListener { private var chatAdapter: ChatAdapter? = null private var webSocketService: WebSocketService? = null - private val messagesBox: Box by lazy { getBoxStore().boxFor() } + private val messagesDao = AppDatabase.getDatabase().messagesDao() private var contactId: String = "" @@ -112,24 +107,36 @@ class ChatActivity : BaseActivity(), OnItemClickListener, return ActivityChatBinding.inflate(layoutInflater) } + override fun getViewModelClass(): Class { + return ChatViewModel::class.java + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - EventBus.getDefault().register(this) - firstLoadData() + MessagesManager.setContactId(contactId) + firstLoadMessage() bindWebSocketService() setPanelChange() - if (isSearchHistory) { - val size = MessagesManager.queryHistory(msgLocalId) - binding.recycleChatList.smoothScrollToPosition(size - 1) +// if (isSearchHistory) { +// val size = MessagesManager.queryHistory(msgLocalId) +// binding.rvChatList.smoothScrollToPosition(size - 1) +// } + } + + private fun firstLoadMessage() { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + MessagesManager.firstLoadMessages() + } } } override fun onResume() { super.onResume() - if (!isSearchHistory) { - MessagesManager.firstLoadMessages() - } +// if (!isSearchHistory) { +// firstLoadMessage() +// } } private val connection: ServiceConnection = object : ServiceConnection { @@ -210,17 +217,21 @@ class ChatActivity : BaseActivity(), OnItemClickListener, } private fun lockContentViewHeight() { - val layoutParams = binding.recycleChatList.layoutParams as LinearLayout.LayoutParams - layoutParams.height = binding.recycleChatList.height - layoutParams.weight = 0f - binding.recycleChatList.layoutParams = layoutParams + val layoutParams = binding.rvChatList.layoutParams as LinearLayout.LayoutParams + layoutParams.apply { + height = binding.rvChatList.height + weight = 0f + binding.rvChatList.layoutParams = this + } } private fun unlockContentViewHeight() { - binding.recycleChatList.postDelayed({ - val layoutParams = binding.recycleChatList.layoutParams as LinearLayout.LayoutParams - layoutParams.weight = 1f - binding.recycleChatList.layoutParams = layoutParams + binding.rvChatList.postDelayed({ + val layoutParams = binding.rvChatList.layoutParams as LinearLayout.LayoutParams + layoutParams.apply { + weight = 1F + binding.rvChatList.layoutParams = this + } }, UNBLOCK_DELAY_TIME) } @@ -328,31 +339,31 @@ class ChatActivity : BaseActivity(), OnItemClickListener, private fun setRecycleView() { val layoutManager = LinearLayoutManager(this) layoutManager.reverseLayout = true - binding.recycleChatList.layoutManager = layoutManager + binding.rvChatList.layoutManager = layoutManager chatAdapter = ChatAdapter(this) - binding.recycleChatList.adapter = chatAdapter - } - - private fun firstLoadData() { - MessagesManager.setContactId(contactId) - MessagesManager.firstLoadMessages() + binding.rvChatList.adapter = chatAdapter + binding.rvChatList.itemAnimator = null } private fun loadMoreMessages() { - MessagesManager.loadMoreMessages() + lifecycleScope.launch { + withContext(Dispatchers.IO) { + MessagesManager.loadMoreMessages() + } + } } override fun observeData() { - webSocketService!!.messageLivedata.observe(this) { - it?.let { - handleMsg(it) - } - } +// webSocketService!!.messageLivedata.observe(this) { +// it?.let { +// handleMsg(it) +// } +// } lifecycleScope.launch { MessagesManager.messages.collect { chatAdapter?.submitList(it) - binding.recycleChatList.post { - binding.recycleChatList.smoothScrollToPosition(0) + binding.rvChatList.post { + binding.rvChatList.smoothScrollToPosition(0) } } } @@ -360,31 +371,26 @@ class ChatActivity : BaseActivity(), OnItemClickListener, override fun setupListeners() { binding.etInput.setOnClickListener { - if (hasSoftInput()){ + if (hasSoftInput()) { MMKV.defaultMMKV().encode(KEYBOARD_HEIGHT, max(getSoftInputHeight(), 300)) } } setBackPressListener() - binding.gvFunctionPanel.selector = ColorDrawable(Color.TRANSPARENT) + binding.gvFunctionPanel.selector = Color.TRANSPARENT.toDrawable() binding.tvSend.setOnClickListener { val message = binding.etInput.text.toString().trim() sendMessage(contactId, message, "0") } - binding.recycleChatList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.rvChatList.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) - if (recyclerView.canScrollVertically(-1)) { + if (!recyclerView.canScrollVertically(-1)) { loadMoreMessages() } -// val layoutManager = checkNotNull(recyclerView.layoutManager as LinearLayoutManager?) -// val firstVisiblePosition = layoutManager.findLastVisibleItemPosition() -// if (tempIndex + 1 == messagesList[firstVisiblePosition].msgLocalId && hasHistory && !loading) { -// loadMoreMessages() -// } } }) @@ -416,11 +422,6 @@ class ChatActivity : BaseActivity(), OnItemClickListener, } } - private fun handleMsg(messages: Messages) { - MessagesManager.receiveMessage(messages) - binding.recycleChatList.smoothScrollToPosition(0) - } - private fun sendMessage(talkerId: String, message: String, type: String) { val messages = Messages( content = message, @@ -429,23 +430,33 @@ class ChatActivity : BaseActivity(), OnItemClickListener, talkerId = talkerId, type = type, ) - val msg = MessageProcessor.processorMsg(messages) - val id: Long = messagesBox.put(msg) + lifecycleScope.launch { - val jsonObject = JSONObject() - val jsonObject2 = JSONObject() - jsonObject2.put("receiverId", contactId) - jsonObject2.put("content", message) - jsonObject2.put("msgLocalId", id) - jsonObject.put("type", if (type == "4") "image" else "text") - jsonObject.put("body", jsonObject2) + withContext(Dispatchers.IO) { + val msg = MessageProcessor.processorMsg(messages) + val id: Long = messagesDao.insert(msg) - webSocketService!!.sendMessage(jsonObject.toString(), id) - webSocketService!!.storeOwnerMsg(messages) + val jsonObject = JSONObject() + val jsonObject2 = JSONObject() + jsonObject2.put("receiverId", contactId) + jsonObject2.put("content", message) + jsonObject2.put("msgLocalId", id) + jsonObject.put("type", if (type == "4") "image" else "text") + jsonObject.put("body", jsonObject2) - MessagesManager.sendMessages(messages) + viewModel.sendChatMessage(jsonObject.toString(), id) +// webSocketService!!.storeOwnerMsg(messages) + ConversationManager.handleMessages(messages) + + MessagesManager.sendMessages(messages) + } + + binding.etInput.setText("") + binding.rvChatList.post { + binding.rvChatList.smoothScrollToPosition(0) + } + } - binding.etInput.setText("") } private fun bindWebSocketService() { @@ -456,15 +467,8 @@ class ChatActivity : BaseActivity(), OnItemClickListener, ) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(unreadEvent: UnreadEvent) { - val unreadCount = unreadEvent.unreadCount - binding.ctb.setUnReadCount(unreadCount) - } - override fun onDestroy() { super.onDestroy() - EventBus.getDefault().unregister(this) MessagesManager.resetMessages() mmkv.putString(CURRENT_CONTACT_ID, "") if (bound) { @@ -497,7 +501,6 @@ class ChatActivity : BaseActivity(), OnItemClickListener, fileViewModel.uploadFile(file) } - //TODO: 待优化成微信选择上传模式,当前上传模式耗费用户等待时间 private fun selectPicture() { PictureSelector.create(this).openGallery(SelectMimeType.ofImage()) diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatDetailActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatDetailActivity.kt index 1a57d03..34f5740 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatDetailActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ChatDetailActivity.kt @@ -3,20 +3,24 @@ package com.kaixed.kchat.ui.activity import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide -import com.kaixed.kchat.data.LocalDatabase.getContactByUsername +import com.kaixed.kchat.data.DataBase +import com.kaixed.kchat.data.local.entity.Contact import com.kaixed.kchat.databinding.ActivityChatDetailBinding import com.kaixed.kchat.manager.ConversationManager import com.kaixed.kchat.manager.MessagesManager import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.widget.SwitchButton +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus class ChatDetailActivity : BaseActivity() { private lateinit var contactId: String - private val contact by lazy { getContactByUsername(contactId) } + private lateinit var contact: Contact override fun inflateBinding(): ActivityChatDetailBinding { return ActivityChatDetailBinding.inflate(layoutInflater) @@ -24,7 +28,7 @@ class ChatDetailActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() + } override fun initView() { @@ -34,7 +38,10 @@ class ChatDetailActivity : BaseActivity() { override fun initData() { contactId = intent.getStringExtra("contactId") ?: "" - binding.ciSetDisturb.setSwitchChecked(contact?.doNotDisturb ?: false) + lifecycleScope.launch { + contact = DataBase.getContactByUsername(contactId)!! + binding.ciSetDisturb.setSwitchChecked(contact?.doNotDisturb ?: false) + } } override fun observeData() { @@ -45,7 +52,9 @@ class ChatDetailActivity : BaseActivity() { binding.ciSetDisturb.sbSwitch.setOnCheckedChangeListener(object : SwitchButton.OnCheckedChangeListener { override fun onCheckedChanged(button: SwitchButton, isChecked: Boolean) { - ConversationManager.updateDisturbStatus(contactId, isChecked) + lifecycleScope.launch { + ConversationManager.updateDisturbStatus(contactId, isChecked) + } } }) binding.ciCleanChatHistory.setOnClickListener { diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ContactsDetailActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ContactsDetailActivity.kt index d439fb0..5c401c3 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ContactsDetailActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ContactsDetailActivity.kt @@ -5,15 +5,18 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.activity.enableEdgeToEdge +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import com.kaixed.kchat.data.DataBase import com.kaixed.kchat.data.local.entity.Contact -import com.kaixed.kchat.data.local.entity.Contact_ import com.kaixed.kchat.databinding.ActivityContactsDetailBinding import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.utils.ConstantsUtils.getAvatarUrl import com.kaixed.kchat.utils.ConstantsUtils.getNickName import com.kaixed.kchat.utils.ConstantsUtils.getUsername +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ContactsDetailActivity : BaseActivity() { @@ -95,19 +98,21 @@ class ContactsDetailActivity : BaseActivity() { binding.ciSetRemarkAndLabel.visibility = View.GONE return } - val contact: Contact by lazy { - getContactInfo(contactId!!) - } - binding.tvContactId.text = "星联号: $contactId" - binding.tvContactSignature.text = contact.signature - if (contact.remark?.isNotEmpty() == true) { - binding.tvRemark.text = contact.remark - contactNickname = contact.remark - binding.tvContactName.text = "昵称:${contact.nickname}" - } else { - contactNickname = contact.nickname - binding.tvRemark.text = contact.nickname - binding.tvContactName.visibility = View.GONE + lifecycleScope.launch { + val contact = getContactInfo(contactId!!) + + // 使用获取到的联系人信息更新UI(这部分已在主线程执行) + binding.tvContactId.text = "星联号: $contactId" + binding.tvContactSignature.text = contact?.signature + if (contact?.remark?.isNotEmpty() == true) { + binding.tvRemark.text = contact.remark + contactNickname = contact.remark + binding.tvContactName.text = "昵称:${contact.nickname}" + } else { + contactNickname = contact?.nickname + binding.tvRemark.text = contact?.nickname + binding.tvContactName.visibility = View.GONE + } } } @@ -116,10 +121,9 @@ class ContactsDetailActivity : BaseActivity() { updateContent() } - private fun getContactInfo(contactId: String): Contact { - return DataBase.contactBox - .query(Contact_.username.equal(contactId)) - .build() - .findFirst()!! + private suspend fun getContactInfo(contactId: String): Contact? { + return withContext(Dispatchers.IO) { + DataBase.getContactByUsername(contactId) + } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt index 0763716..42dc1c1 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/FriendCircleActivity.kt @@ -12,6 +12,7 @@ import androidx.core.view.updatePadding import androidx.core.widget.NestedScrollView import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide +import com.kaixed.core.common.utils.TextUtil import com.kaixed.kchat.R import com.kaixed.kchat.data.model.dynamic.Comment import com.kaixed.kchat.data.model.dynamic.FriendCircleItem @@ -22,7 +23,6 @@ import com.kaixed.kchat.utils.ConstantsUtils import com.kaixed.kchat.utils.ConstantsUtils.getAvatarUrl import com.kaixed.kchat.utils.ConstantsUtils.getNickName import com.kaixed.kchat.utils.ScreenUtils.dp2px -import com.kaixed.kchat.utils.TextUtil import kotlin.math.max import kotlin.math.min import kotlin.random.Random diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/LoginActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/LoginActivity.kt index f5a28ca..293e767 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/LoginActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/LoginActivity.kt @@ -12,7 +12,7 @@ import androidx.core.content.ContextCompat import com.drake.softinput.getSoftInputHeight import com.drake.softinput.hasSoftInput import com.kaixed.kchat.R -import com.kaixed.kchat.data.LocalDatabase +import com.kaixed.kchat.data.DataBase import com.kaixed.kchat.data.local.entity.UserInfo import com.kaixed.kchat.databinding.ActivityLoginBinding import com.kaixed.kchat.ui.base.BaseActivity @@ -153,8 +153,9 @@ class LoginActivity : BaseActivity() { return true } + //TODO 存储用户信息 private fun saveUserInfo(user: UserInfo) { - LocalDatabase.saveUserInfo(user) + DataBase.saveUserInfo(user) MMKV.defaultMMKV().putString(ACCESS_TOKEN, user.accessToken) MMKV.defaultMMKV().putString(REFRESH_TOKEN, user.refreshToken) } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/MainActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/MainActivity.kt index b04d076..a88ea46 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/MainActivity.kt @@ -15,14 +15,13 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide import com.huawei.hms.hmsscankit.ScanUtil import com.huawei.hms.ml.scan.HmsScan import com.kaixed.kchat.R -import com.kaixed.kchat.data.LocalDatabase +import com.kaixed.kchat.data.DataBase import com.kaixed.kchat.data.event.UnreadEvent -import com.kaixed.kchat.data.local.box.ObjectBox.getBox -import com.kaixed.kchat.data.local.entity.Contact +import com.kaixed.kchat.data.local.AppDatabase +import com.kaixed.kchat.data.local.dao.MessagesDao import com.kaixed.kchat.databinding.ActivityMainBinding import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.fragment.ContactFragment @@ -30,14 +29,13 @@ import com.kaixed.kchat.ui.fragment.DiscoveryFragment import com.kaixed.kchat.ui.fragment.HomeFragment import com.kaixed.kchat.ui.fragment.MineFragment import com.kaixed.kchat.ui.widget.LoadingDialogFragment -import com.kaixed.kchat.utils.ConstantsUtils.getAvatarUrl import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.utils.ConstantsUtils.isFirstLaunchApp import com.kaixed.kchat.viewmodel.ContactViewModel -import io.objectbox.Box import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -49,14 +47,14 @@ class MainActivity : BaseActivity() { private val colorBlack: Int by lazy { ContextCompat.getColor(this, R.color.black) } - private val contactBox: Box by lazy { getBox(Contact::class.java) } - private val contactViewModel: ContactViewModel by viewModels() private val navBinding by lazy { binding.bottomNavContainer } private val loadingDialogFragment by lazy { LoadingDialogFragment.newInstance("同步数据中") } + private val messagesDao: MessagesDao = AppDatabase.getDatabase().messagesDao() + private val navItems by lazy { listOf( NavItem(navBinding.ivHome, navBinding.tvHome), @@ -106,7 +104,9 @@ class MainActivity : BaseActivity() { ScanUtil.SUCCESS -> { val hmsScan: HmsScan? = data.getParcelableExtra(ScanUtil.RESULT) hmsScan?.let { - handleResult(it.showResult) + lifecycleScope.launch { + handleResult(it.showResult) + } } } ScanUtil.ERROR_NO_READ_PERMISSION -> { @@ -122,7 +122,7 @@ class MainActivity : BaseActivity() { } // 处理扫描结果 - private fun handleResult(result: String) { + private suspend fun handleResult(result: String) { if (result.startsWith("kchat@")) { val contactId = result.substringAfter("kchat@") if (contactId == getUsername()) { @@ -131,7 +131,7 @@ class MainActivity : BaseActivity() { putExtra("isMine", true) }) } - if (LocalDatabase.isMyFriend(contactId)) { + if (DataBase.isMyFriend(contactId)) { startActivity(Intent(this, ContactsDetailActivity::class.java).apply { putExtra("contactId", contactId) }) @@ -152,8 +152,14 @@ class MainActivity : BaseActivity() { private fun setListener() { contactViewModel.contactListResult.observe(this@MainActivity) { result -> result.onSuccess { - contactBox.put(it ?: emptyList()) - loadingDialogFragment.dismissLoading() + //TODO 优化,使用协程 + val contactDao = AppDatabase.getDatabase().contactDao() + lifecycleScope.launch { + withContext(Dispatchers.IO) { + contactDao.insertContacts(it ?: emptyList()) + loadingDialogFragment.dismissLoading() + } + } } } contactViewModel.searchContactResult.observe(this@MainActivity) { result -> diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt index 7d1acfe..edefdf8 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/ProfileDetailActivity.kt @@ -14,19 +14,16 @@ import androidx.activity.viewModels import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.bumptech.glide.Glide -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.core.common.utils.TextUtil.extractDimensionsAndPrefix import com.kaixed.kchat.data.local.entity.UserInfo -import com.kaixed.kchat.data.local.entity.UserInfo_ import com.kaixed.kchat.databinding.ActivityProfileDetailBinding import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.widget.LoadingDialogFragment import com.kaixed.kchat.utils.Constants.AVATAR_URL import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION import com.kaixed.kchat.utils.ConstantsUtils.getUsername -import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix import com.kaixed.kchat.viewmodel.UserViewModel import com.tencent.mmkv.MMKV -import io.objectbox.Box import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody @@ -35,8 +32,6 @@ import java.io.File class ProfileDetailActivity : BaseActivity() { - private val userInfoBox: Box by lazy { getBoxStore().boxFor(UserInfo::class.java) } - private val userSessionMMKV by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) } private val userViewModel: UserViewModel by viewModels() @@ -184,14 +179,15 @@ class ProfileDetailActivity : BaseActivity() { updateContent(username) } + //TODO 更新用户信息 private fun updateContent(username: String) { - val userInfo = userInfoBox.query(UserInfo_.username.equal(username)).build().findFirst() - if (userInfo != null) { - binding.ciKid.setItemDesc(userInfo.username) - binding.ciNickname.setItemDesc(userInfo.nickname) - val avatarUrl = - extractDimensionsAndPrefix(userInfo.avatarUrl)?.first ?: userInfo.avatarUrl - binding.ciAvatar.setItemIcon(avatarUrl) - } +// val userInfo = userInfoBox.query(UserInfo_.username.equal(username)).build().findFirst() +// if (userInfo != null) { +// binding.ciKid.setItemDesc(userInfo.username) +// binding.ciNickname.setItemDesc(userInfo.nickname) +// val avatarUrl = +// extractDimensionsAndPrefix(userInfo.avatarUrl)?.first ?: userInfo.avatarUrl +// binding.ciAvatar.setItemIcon(avatarUrl) +// } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/RenameActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/RenameActivity.kt index 531ab92..16cf227 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/RenameActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/RenameActivity.kt @@ -6,8 +6,6 @@ import android.os.Looper import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.core.widget.addTextChangedListener -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore -import com.kaixed.kchat.data.local.entity.UserInfo import com.kaixed.kchat.data.model.request.UserRequest import com.kaixed.kchat.databinding.ActivityRenameBinding import com.kaixed.kchat.ui.base.BaseActivity @@ -15,7 +13,6 @@ import com.kaixed.kchat.ui.widget.LoadingDialogFragment import com.kaixed.kchat.utils.ConstantsUtils.getNickName import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.viewmodel.UserViewModel -import io.objectbox.Box class RenameActivity : BaseActivity() { diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchChatHistory.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchChatHistory.kt index ca6eece..13bf5b1 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchChatHistory.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchChatHistory.kt @@ -9,16 +9,16 @@ import androidx.activity.enableEdgeToEdge import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.kaixed.kchat.data.local.box.ObjectBox.getBox +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Messages -import com.kaixed.kchat.data.local.entity.Messages_ import com.kaixed.kchat.data.model.search.ChatHistoryItem import com.kaixed.kchat.databinding.ActivitySearchChatHistoryBinding import com.kaixed.kchat.ui.adapter.ChatHistoryAdapter import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.utils.ConstantsUtils.getUsername -import io.objectbox.Box +import kotlinx.coroutines.launch class SearchChatHistory : BaseActivity() { @@ -28,8 +28,6 @@ class SearchChatHistory : BaseActivity() { private const val VIEW_SELECT = 2 } - private val messageBox: Box by lazy { getBox(Messages::class.java) } - private val chatHistoryAdapter by lazy { ChatHistoryAdapter( contactAvatarUrl, @@ -62,10 +60,14 @@ class SearchChatHistory : BaseActivity() { chatHistoryAdapter.setContactId(contactId) chatHistoryAdapter.let { adapter -> - val viewHolder = adapter.createViewHolder(binding.rvChatHistory, adapter.getItemViewType(0)) + val viewHolder = + adapter.createViewHolder(binding.rvChatHistory, adapter.getItemViewType(0)) val itemView = viewHolder.itemView itemView.measure( - View.MeasureSpec.makeMeasureSpec(binding.rvChatHistory.width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec( + binding.rvChatHistory.width, + View.MeasureSpec.EXACTLY + ), View.MeasureSpec.UNSPECIFIED ) val itemHeight = itemView.measuredHeight @@ -76,15 +78,18 @@ class SearchChatHistory : BaseActivity() { private fun setListener() { binding.tvCancel.setOnClickListener { finish() } - binding.etSearch.addTextChangedListener(afterTextChanged = - { - if (it?.length == 0) { - chatHistoryAdapter.submitList(null) - showView(VIEW_SELECT) - } else { - getData(it.toString()) - } - }) + binding.etSearch.addTextChangedListener( + afterTextChanged = + { + if (it?.length == 0) { + chatHistoryAdapter.submitList(null) + showView(VIEW_SELECT) + } else { + lifecycleScope.launch { + getData(it.toString()) + } + } + }) } private fun showView(viewToShowIndex: Int) { @@ -94,7 +99,7 @@ class SearchChatHistory : BaseActivity() { } } - private fun getData(matchedField: String) { + private suspend fun getData(matchedField: String) { val data = loadDataFromDb(matchedField) val items = data.map { @@ -130,9 +135,10 @@ class SearchChatHistory : BaseActivity() { } } - private fun loadDataFromDb(content: String): List { - return messageBox.query(Messages_.content.contains(content)).orderDesc(Messages_.timestamp) - .build().find() + private suspend fun loadDataFromDb(content: String): List { + val messagesDao = AppDatabase.getDatabase().messagesDao() + + return messagesDao.getMessagesContainingKeyword(content) } override fun initData() { diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt index 007c814..359e0da 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SearchFriendsActivity.kt @@ -12,7 +12,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.core.widget.addTextChangedListener import com.bumptech.glide.Glide -import com.kaixed.kchat.data.LocalDatabase +import com.kaixed.kchat.data.DataBase import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.databinding.ActivitySearchFriendsBinding import com.kaixed.kchat.ui.base.BaseActivity @@ -106,7 +106,7 @@ class SearchFriendsActivity : BaseActivity() { result.onSuccess { it?.let { isSearchedMine = it.username == getUsername() - if (LocalDatabase.isMyFriend(it.username)) { + if (DataBase.isMyFriend(it.username)) { val username = it.username val intent = Intent(this, ContactsDetailActivity::class.java).apply { diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SetRemarkAndLabelActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SetRemarkAndLabelActivity.kt index 3abf441..43198f6 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SetRemarkAndLabelActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/SetRemarkAndLabelActivity.kt @@ -12,8 +12,7 @@ import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.kaixed.kchat.R -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore -import com.kaixed.kchat.data.LocalDatabase +import com.kaixed.kchat.data.DataBase import com.kaixed.kchat.data.local.entity.Contact import com.kaixed.kchat.databinding.ActivitySetRemarkAndLabelBinding import com.kaixed.kchat.ui.base.BaseActivity @@ -22,6 +21,7 @@ import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.viewmodel.ContactViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class SetRemarkAndLabelActivity : BaseActivity() { @@ -32,7 +32,9 @@ class SetRemarkAndLabelActivity : BaseActivity private var remark = "" private val contact: Contact? by lazy { - LocalDatabase.getContactByUsername(contactId!!) + runBlocking { + DataBase.getContactByUsername(contactId!!) + } } private val contactViewModel by lazy { ContactViewModel() } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/AccountSecurityActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/AccountSecurityActivity.kt index 1a53509..448699e 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/AccountSecurityActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/AccountSecurityActivity.kt @@ -19,7 +19,7 @@ class AccountSecurityActivity : BaseActivity() { } override fun initData() { - val userInfo = DataBase.userInfoBox.query().build().findFirst() + val userInfo = DataBase.getUserInfo() userInfo?.let { binding.ciAccount.setItemDesc(userInfo.username) val telephone = userInfo.telephone.take(3) + "******" + userInfo.telephone.takeLast(2) diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/UpdateTelephoneActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/UpdateTelephoneActivity.kt index b07d54d..db4f11c 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/UpdateTelephoneActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/activity/setting/UpdateTelephoneActivity.kt @@ -24,14 +24,11 @@ class UpdateTelephoneActivity : BaseActivity() { } override fun initView() { - TODO("Not yet implemented") } override fun setupListeners() { - TODO("Not yet implemented") } override fun observeData() { - TODO("Not yet implemented") } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatAdapter.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatAdapter.kt index 3f0764e..d52c0c8 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatAdapter.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatAdapter.kt @@ -1,19 +1,21 @@ package com.kaixed.kchat.ui.adapter import android.content.Context +import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.updateLayoutParams +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.drake.spannable.replaceSpan import com.drake.spannable.span.CenterImageSpan +import com.kaixed.core.common.utils.TextUtil.extractDimensionsAndPrefix import com.kaixed.kchat.R -import com.kaixed.kchat.data.local.box.ObjectBox.getBox import com.kaixed.kchat.data.local.entity.Messages import com.kaixed.kchat.databinding.ChatRecycleItemCustomNormalBinding import com.kaixed.kchat.databinding.ChatRecycleItemImageNormalBinding @@ -22,24 +24,13 @@ import com.kaixed.kchat.manager.MessagesManager import com.kaixed.kchat.utils.ConstantsUtils import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow -import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility import com.kaixed.kchat.utils.ViewUtil.setViewVisibility -import io.objectbox.Box class ChatAdapter( private val context: Context -) : ListAdapter(object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Messages, newItem: Messages): Boolean { - return oldItem.msgLocalId == newItem.msgLocalId - } - - override fun areContentsTheSame(oldItem: Messages, newItem: Messages): Boolean { - return oldItem == newItem - } - -}) { +) : ListAdapter(DIFF_CALLBACK) { companion object { const val CUSTOM = 0 @@ -50,6 +41,25 @@ class ChatAdapter( const val EMOJI = 10 const val RED_PACKET = 12 private const val TAG = "ChatAdapter" + + // 移动到外部作为静态对象 + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Messages, newItem: Messages): Boolean { + val isSame = oldItem.msgLocalId == newItem.msgLocalId + if (!isSame) { + Log.d(TAG, "Item changed: old=${oldItem.msgLocalId}, new=${newItem.msgLocalId}") + } + return isSame + } + + override fun areContentsTheSame(oldItem: Messages, newItem: Messages): Boolean { + val changed = oldItem != newItem + if (changed) { + Log.d(TAG, "Item content changed: id=${oldItem.msgLocalId}") + } + return !changed + } + } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -81,7 +91,7 @@ class ChatAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val singleMessage = getItem(position) + val singleMessage = getItem(position) ?: return when (holder) { is CustomViewHolder -> { holder.bindData(singleMessage) @@ -209,11 +219,6 @@ class ChatAdapter( } override fun getItemViewType(position: Int): Int { - return getItem(position).type.toInt() - } - - private fun updateDb(message: Messages) { - val messagesBox: Box = getBox(Messages::class.java) - messagesBox.put(message) + return getItem(position)?.type?.toInt() ?: 0 } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatHistoryAdapter.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatHistoryAdapter.kt index 4e295c3..203b610 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatHistoryAdapter.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ChatHistoryAdapter.kt @@ -10,11 +10,11 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.bumptech.glide.Glide import com.drake.spannable.replaceSpanFirst import com.drake.spannable.span.HighlightSpan +import com.kaixed.core.common.utils.TextUtil import com.kaixed.kchat.data.model.search.ChatHistoryItem import com.kaixed.kchat.databinding.ChatHistoryRecycleItemBinding import com.kaixed.kchat.ui.activity.ChatActivity import com.kaixed.kchat.utils.ConstantsUtils -import com.kaixed.kchat.utils.TextUtil /** * @Author: kaixed diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ConversationAdapter.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ConversationAdapter.kt index 1323314..56f1332 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ConversationAdapter.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/ConversationAdapter.kt @@ -14,12 +14,12 @@ import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.drake.spannable.replaceSpan import com.drake.spannable.span.CenterImageSpan +import com.kaixed.core.common.utils.TextUtil import com.kaixed.kchat.R import com.kaixed.kchat.data.local.entity.Conversation import com.kaixed.kchat.databinding.ChatMainItemBinding import com.kaixed.kchat.ui.activity.ChatActivity import com.kaixed.kchat.ui.i.OnItemListener -import com.kaixed.kchat.utils.TextUtil /** * @Author: kaixed diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/FriendCircleAdapter.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/FriendCircleAdapter.kt index 813776f..6a1b451 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/FriendCircleAdapter.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/adapter/FriendCircleAdapter.kt @@ -5,11 +5,11 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.kaixed.core.common.utils.TextUtil import com.kaixed.kchat.data.model.dynamic.FriendCircleItem import com.kaixed.kchat.databinding.ItemFriendCircleBinding import com.kaixed.kchat.utils.ScreenUtils.dp2px import com.kaixed.kchat.utils.ScreenUtils.getScreenWidth -import com.kaixed.kchat.utils.TextUtil /** * @Author: kaixed diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/base/BaseActivity.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/base/BaseActivity.kt index 1302f2d..ab3a7d7 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/base/BaseActivity.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/base/BaseActivity.kt @@ -2,6 +2,7 @@ package com.kaixed.kchat.ui.base import android.os.Bundle import android.widget.Toast +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding import com.kaixed.kchat.manager.AppManager @@ -16,6 +17,7 @@ abstract class BaseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() binding = inflateBinding() setContentView(binding.root) initData() diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/ContactFragment.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/ContactFragment.kt index 528a8a4..c1ec1c1 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/ContactFragment.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/ContactFragment.kt @@ -11,17 +11,15 @@ import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.LinearLayoutManager +import com.kaixed.core.common.base.BaseFragment import com.kaixed.kchat.data.local.entity.Contact import com.kaixed.kchat.databinding.FragmentContactBinding import com.kaixed.kchat.ui.adapter.FriendListAdapter -import com.kaixed.kchat.ui.base.BaseFragment import com.kaixed.kchat.utils.ConstantsUtils.getUsername import com.kaixed.kchat.viewmodel.ContactViewModel -class ContactFragment : BaseFragment() { - - private val contactViewModel: ContactViewModel by viewModels() +class ContactFragment : BaseFragment() { private var loading = false @@ -64,8 +62,26 @@ class ContactFragment : BaseFragment() { LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(mReceiver, filter) } + override fun getViewModelClass(): Class { + return ContactViewModel::class.java + } + private fun setObservation() { - contactViewModel.contactRequestListResult.observe(viewLifecycleOwner) { result -> + // 监听本地联系人列表加载结果 + viewModel.contactListInDbResult.observe(viewLifecycleOwner) { contacts -> + val allItems = mutableListOf().apply { + addAll(getDefaultItems()) + addAll(contacts) + } + friendAdapter.updateData(allItems) + binding.includeLoading.main.visibility = View.GONE + binding.recycleFriendList.visibility = View.VISIBLE + loading = false + setupRecyclerView() + } + + // 监听请求列表 + viewModel.contactRequestListResult.observe(viewLifecycleOwner) { result -> result.onSuccess { val items = result.getOrNull()?.toMutableList() ?: emptyList() if (items.isNotEmpty()) { @@ -92,7 +108,7 @@ class ContactFragment : BaseFragment() { } private fun loadFriendRequest() { - contactViewModel.getContactRequestList(getUsername()) + viewModel.getContactRequestList(getUsername()) } private fun loadData() { @@ -100,16 +116,7 @@ class ContactFragment : BaseFragment() { binding.includeLoading.main.visibility = View.VISIBLE binding.recycleFriendList.visibility = View.INVISIBLE - val contacts = contactViewModel.loadFriendListInDb() - val allItems = mutableListOf().apply { - addAll(getDefaultItems()) - addAll(contacts) - } - friendAdapter.updateData(allItems) - binding.includeLoading.main.visibility = View.GONE - binding.recycleFriendList.visibility = View.VISIBLE - loading = false - setupRecyclerView() + viewModel.loadFriendListInDb() // 启动加载 } private fun setupRecyclerView() { diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/HomeFragment.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/HomeFragment.kt index 898e532..6259310 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/HomeFragment.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/fragment/HomeFragment.kt @@ -26,6 +26,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.huawei.hms.hmsscankit.ScanUtil import com.huawei.hms.ml.scan.HmsScan @@ -47,6 +48,7 @@ import com.kaixed.kchat.ui.base.BaseFragment import com.kaixed.kchat.ui.i.OnDialogFragmentClickListener import com.kaixed.kchat.ui.i.OnItemListener import com.kaixed.kchat.ui.widget.HomeDialogFragment +import kotlinx.coroutines.launch class HomeFragment : BaseFragment(), OnItemListener, OnDialogFragmentClickListener { @@ -322,7 +324,9 @@ class HomeFragment : BaseFragment(), OnItemListener, override fun onItemClick(talkerId: String) { this.talkerId = talkerId - ConversationManager.cleanUnreadCount(talkerId) + lifecycleScope.launch { + ConversationManager.cleanUnreadCount(talkerId) + } } private var longClickTalkerId = "" @@ -350,7 +354,9 @@ class HomeFragment : BaseFragment(), OnItemListener, override fun onClickSetUnread() { if (con != null) { - ConversationManager.updateUnreadCount(con!!.talkerId, con!!.unreadCount == 0) + lifecycleScope.launch { + ConversationManager.updateUnreadCount(con!!.talkerId, con!!.unreadCount == 0) + } } } @@ -364,6 +370,8 @@ class HomeFragment : BaseFragment(), OnItemListener, override fun onClickDeleteConversation() { val con = getConversation() - ConversationManager.deleteConversation(con) + lifecycleScope.launch { + ConversationManager.deleteConversation(con) + } } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt b/app/src/main/kotlin/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt index 3c90716..a1c652c 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/ui/widget/MyBottomSheetFragment.kt @@ -95,7 +95,7 @@ class MyBottomSheetFragment : BottomSheetDialogFragment() { val mmkv = MMKV.mmkvWithID(MMKV_USER_SESSION) mmkv.clearAll() MMKV.defaultMMKV().putBoolean(USER_LOGIN_STATUS, false) - DataBase.cleanAllData() + DataBase.clearAllDatabase() } override fun onDestroyView() { diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/AccountUtils.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/AccountUtils.kt deleted file mode 100644 index 47e30a6..0000000 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/AccountUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.kaixed.kchat.utils - -/** - * @Author: kaixed - * @Date: 2025/1/24 20:41 - */ -class AccountUtils { - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/CacheUtils.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/CacheUtils.kt index d1ccc9b..3f3ecbd 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/CacheUtils.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/CacheUtils.kt @@ -7,6 +7,7 @@ import com.tencent.mmkv.MMKV * @Date: 2025/1/22 18:06 */ object CacheUtils { + fun getAccessToken(): String { val accessToken = MMKV.defaultMMKV().getString(Constants.ACCESS_TOKEN, "") return accessToken ?: "" diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/Constants.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/Constants.kt index ee1a847..e98838b 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/Constants.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/Constants.kt @@ -6,6 +6,10 @@ package com.kaixed.kchat.utils */ object Constants { + object Sign { + const val SECRET_KEY = "123" + } + // mmkv const val MMKV_USER_SESSION: String = "userSession" const val MMKV_COMMON_DATA: String = "commonData" diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/ConstantsUtils.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/ConstantsUtils.kt index d0d8d8a..5424a21 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/ConstantsUtils.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/ConstantsUtils.kt @@ -1,5 +1,6 @@ package com.kaixed.kchat.utils +import com.kaixed.core.common.utils.TextUtil.extractUrl import com.kaixed.kchat.utils.Constants.AVATAR_URL import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID import com.kaixed.kchat.utils.Constants.FIRST_LAUNCH_APP @@ -31,7 +32,7 @@ object ConstantsUtils { fun getAvatarUrl(): String { val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: "" - return TextUtil.extractUrl(avatarUrl) + return extractUrl(avatarUrl) } fun getStatusBarHeight(): Int = diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/DownloadManager.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/DownloadManager.kt new file mode 100644 index 0000000..f0a939b --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/DownloadManager.kt @@ -0,0 +1,99 @@ +package com.kaixed.kchat.utils + +import okhttp3.* +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile + +class DownloadManager( + private val url: String, + private val destinationFile: File, + private val callback: DownloadCallback +) { + + private val client = OkHttpClient() + private var downloadedBytes: Long = 0L + private var totalBytes: Long = 0L + + init { + // 如果目标文件已存在,则获取已下载的字节数 + if (destinationFile.exists()) { + downloadedBytes = destinationFile.length() + } + } + + // 开始下载文件 + fun startDownload() { + val request = Request.Builder() + .url(url) + .header("Range", "bytes=$downloadedBytes-") // 请求从已下载的字节数继续下载 + .build() + + client.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + // 获取文件总大小 + val contentRange = response.header("Content-Range") + if (contentRange != null) { + totalBytes = contentRange.split("/")[1].toLong() + } + + // 获取响应体的输入流 + val body = response.body ?: return + + // 使用 RandomAccessFile 写入文件 + val raf = RandomAccessFile(destinationFile, "rw") + raf.seek(downloadedBytes) + + val inputStream = body.byteStream() + val buffer = ByteArray(8192) + var bytesRead: Int + + try { + // 开始下载并写入文件 + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + raf.write(buffer, 0, bytesRead) + downloadedBytes += bytesRead + + // 计算下载进度 + val progress = (downloadedBytes * 100) / totalBytes + + // 回调进度更新 + callback.onProgress(progress) + } + + // 下载完成 + callback.onDownloadComplete() + + } catch (e: IOException) { + // 发生错误时回调 + callback.onError(e.message ?: "Unknown error") + } finally { + raf.close() + inputStream.close() + body.close() + } + } else { + callback.onError("Failed to download: ${response.code}") + } + } + + override fun onFailure(call: Call, e: IOException) { + // 网络请求失败时回调 + callback.onError("Download failed: ${e.message}") + } + }) + } + + // 取消下载 + fun cancelDownload() { + // 可以扩展实现下载取消的功能,关闭请求等 + } + + // 下载进度回调接口 + interface DownloadCallback { + fun onProgress(progress: Long) // 下载进度(百分比) + fun onDownloadComplete() // 下载完成 + fun onError(errorMessage: String) // 错误信息 + } +} diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/ScreenUtils.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/ScreenUtils.kt index 6a46a24..0ef06b9 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/ScreenUtils.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/ScreenUtils.kt @@ -20,9 +20,12 @@ object ScreenUtils { private val defaultMMKV by lazy { MMKV.defaultMMKV() } fun init(context: Context) { - appContext = context + if (!::appContext.isInitialized) { + appContext = context.applicationContext + } } + fun getScreenHeight(): Int { val screenHeight = defaultMMKV.getInt(SCREEN_HEIGHT, 0) if (screenHeight != 0) { diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/ViewUtil.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/ViewUtil.kt index 5b97efb..8b24651 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/ViewUtil.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/ViewUtil.kt @@ -1,10 +1,10 @@ package com.kaixed.kchat.utils -import android.util.Log import android.view.View import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import com.kaixed.core.common.utils.TextUtil.getTimestampString import com.kaixed.kchat.data.local.entity.Messages /** @@ -48,11 +48,11 @@ object ViewUtil { if (showTimer) { tvTimer.visibility = View.VISIBLE - tvTimer.text = TextUtil.getTimestampString(singleMessage.timestamp) + tvTimer.text = getTimestampString(singleMessage.timestamp) } else { tvTimer.visibility = View.GONE } tvTimer.visibility = if (showTimer) View.VISIBLE else View.GONE - tvTimer.text = TextUtil.getTimestampString(singleMessage.timestamp) + tvTimer.text = getTimestampString(singleMessage.timestamp) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/handle/ContactUtil.kt b/app/src/main/kotlin/com/kaixed/kchat/utils/handle/ContactUtil.kt index 3d74814..53c5910 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/handle/ContactUtil.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/utils/handle/ContactUtil.kt @@ -1,9 +1,8 @@ package com.kaixed.kchat.utils.handle -import com.kaixed.kchat.data.local.box.ObjectBox.getBoxStore +import com.kaixed.core.common.utils.Pinyin4jUtil +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Contact -import com.kaixed.kchat.utils.Pinyin4jUtil -import io.objectbox.Box /** * @Author: kaixed @@ -11,9 +10,9 @@ import io.objectbox.Box */ object ContactUtil { - private val contactBox: Box by lazy { getBoxStore().boxFor(Contact::class.java) } + private val contactDao = AppDatabase.getDatabase().contactDao() - fun handleContact(contact: Contact) { + suspend fun handleContact(contact: Contact) { val contactLists = getDbContactLists() contactLists.add(contact) contact.apply { @@ -28,10 +27,10 @@ object ContactUtil { index - 1 )) } - contactBox.put(contactLists) + contactDao.updateContacts(contactLists) } - private fun getDbContactLists(): MutableList { - return contactBox.all.toMutableList() + private suspend fun getDbContactLists(): MutableList { + return contactDao.getAllContacts() as MutableList } } diff --git a/app/src/main/kotlin/com/kaixed/kchat/viewmodel/ContactViewModel.kt b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/ContactViewModel.kt index 015c6ad..7fdb64d 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/viewmodel/ContactViewModel.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/ContactViewModel.kt @@ -4,18 +4,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kaixed.kchat.data.local.box.ObjectBox.getBox +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.Contact import com.kaixed.kchat.data.model.friend.FriendRequestItem import com.kaixed.kchat.data.model.search.SearchUser import com.kaixed.kchat.data.repository.ContactRepository -import com.kaixed.kchat.utils.SingleLiveEvent -import io.objectbox.Box import kotlinx.coroutines.launch class ContactViewModel : ViewModel() { - private val contactRepository = ContactRepository() + private val contactRepository = ContactRepository(AppDatabase.getDatabase().contactDao()) // 获取联系人请求列表 private val _contactRequestListResult = @@ -96,38 +94,14 @@ class ContactViewModel : ViewModel() { } } - fun loadFriendListInDb(): List { - val contactBox: Box = getBox(Contact::class.java) + private val _contactListInDbResult = MutableLiveData>() + val contactListInDbResult: LiveData> = _contactListInDbResult - val sortedContacts = contactBox.query() - .sort { contact1, contact2 -> - val str1 = contact1.remarkquanpin ?: contact1.quanpin ?: contact1.nickname - val str2 = contact2.remarkquanpin ?: contact2.quanpin ?: contact2.nickname - - val type1 = when { - str1.matches(Regex("[a-zA-Z]+")) -> 1 // 字母 - str1.matches(Regex("[0-9]+")) -> 2 // 数字 - else -> 3 // 特殊字符 - } - - val type2 = when { - str2.matches(Regex("[a-zA-Z]+")) -> 1 // 字母 - str2.matches(Regex("[0-9]+")) -> 2 // 数字 - else -> 3 // 特殊字符 - } - - // 比较类型,如果相同再按字母升序排序 - if (type1 != type2) { - type1 - type2 // 按类型排序 - } else { - str1.compareTo(str2) // 如果类型相同,按字母顺序排序 - } - - } - .build() - .find() - - return sortedContacts.toMutableList() + fun loadFriendListInDb() { + viewModelScope.launch { + val result = contactRepository.getContactsInDb() + _contactListInDbResult.postValue(result ?: emptyList()) + } } fun loadFriendList(username: String) { diff --git a/app/src/main/kotlin/com/kaixed/kchat/viewmodel/MessagesViewModel.kt b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/MessagesViewModel.kt new file mode 100644 index 0000000..5e6669a --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/MessagesViewModel.kt @@ -0,0 +1,28 @@ +package com.kaixed.kchat.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.kaixed.kchat.data.local.dao.MessagesDao +import com.kaixed.kchat.data.local.entity.Messages +import kotlinx.coroutines.flow.Flow + +class MessagesViewModel( + private val messagesDao: MessagesDao +) : ViewModel() { + private val pagingConfig = PagingConfig( + pageSize = 20, // 每页加载的数量 + enablePlaceholders = false, + prefetchDistance = 5, // 当距离边缘还有5个项目时开始加载下一页 + initialLoadSize = 40 // 首次加载的数量 + ) + + // 接收当前联系人ID的消息流 + fun getMessageFlow(contactId: String): Flow> = Pager( + config = pagingConfig, + pagingSourceFactory = { messagesDao.getPagedMessages(contactId) } + ).flow.cachedIn(viewModelScope) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/kaixed/kchat/viewmodel/UserViewModel.kt b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/UserViewModel.kt index 2f45fb5..d44a5b7 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/viewmodel/UserViewModel.kt +++ b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/UserViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.kaixed.kchat.data.local.AppDatabase import com.kaixed.kchat.data.local.entity.UserInfo import com.kaixed.kchat.data.model.request.RegisterRequest import com.kaixed.kchat.data.model.request.UpdatePasswordRequest @@ -19,9 +20,9 @@ import okhttp3.MultipartBody class UserViewModel : ViewModel() { - private val userAuthRepo = UserAuthRepository() + private val userAuthRepo = UserAuthRepository(AppDatabase.getDatabase().userInfoDao()) - private val userProfileRepo = UserProfileRepository() + private val userProfileRepo = UserProfileRepository(AppDatabase.getDatabase().userInfoDao()) private val userSearchRepo = UserSearchRepository() @@ -65,8 +66,8 @@ class UserViewModel : ViewModel() { } } - private val _userListResult = MutableLiveData>>() - val userListResult: LiveData>> = _userListResult + private val _userListResult = MutableLiveData?>>() + val userListResult: LiveData?>> = _userListResult // 获取用户列表 fun getUserList(username: String) { diff --git a/app/src/main/kotlin/com/kaixed/kchat/viewmodel/factory/MessagesViewModelFactory.kt b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/factory/MessagesViewModelFactory.kt new file mode 100644 index 0000000..fb0ec81 --- /dev/null +++ b/app/src/main/kotlin/com/kaixed/kchat/viewmodel/factory/MessagesViewModelFactory.kt @@ -0,0 +1,16 @@ +package com.kaixed.kchat.viewmodel.factory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.kaixed.kchat.data.local.dao.MessagesDao +import com.kaixed.kchat.viewmodel.MessagesViewModel + +class MessagesViewModelFactory(private val messagesDao: MessagesDao) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MessagesViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return MessagesViewModel(messagesDao) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 886ce5c..9da724d 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -17,7 +17,7 @@ app:titleName="contact" /> 49.233.105.103 - 192.168.31.18 + 192.168.225.209 diff --git a/build.gradle.kts b/build.gradle.kts index c00b658..f87b9da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false // id ("cn.therouter.agp8") version "1.2.1" apply false id ("org.jetbrains.kotlin.plugin.serialization") version "1.9.23" + alias(libs.plugins.androidLibrary) apply false } buildscript { @@ -14,7 +15,7 @@ buildscript { mavenCentral() } dependencies { - classpath(libs.objectbox.gradle.plugin) +// classpath(libs.objectbox.gradle.plugin) // classpath("com.android.tools.build:gradle:8.6.0") classpath(libs.agcp) } diff --git a/core_common/.gitignore b/core_common/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core_common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core_common/build.gradle.kts b/core_common/build.gradle.kts new file mode 100644 index 0000000..f18699a --- /dev/null +++ b/core_common/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) +} + +android { + namespace = "com.kaixed.core.common" + compileSdk = 34 + + defaultConfig { + minSdk = 30 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + viewBinding { + enable = true + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + + implementation(libs.pinyin4j) + + implementation(libs.mmkv) +} \ No newline at end of file diff --git a/core_common/consumer-rules.pro b/core_common/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/core_common/proguard-rules.pro b/core_common/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/core_common/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core_common/src/androidTest/java/com/kaixed/core/common/ExampleInstrumentedTest.kt b/core_common/src/androidTest/java/com/kaixed/core/common/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d21b963 --- /dev/null +++ b/core_common/src/androidTest/java/com/kaixed/core/common/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.kaixed.core.common + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.kaixed.core.common.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core_common/src/main/AndroidManifest.xml b/core_common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/core_common/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core_common/src/main/java/com/kaixed/core/common/base/BaseActivity.kt b/core_common/src/main/java/com/kaixed/core/common/base/BaseActivity.kt new file mode 100644 index 0000000..1f0ae1b --- /dev/null +++ b/core_common/src/main/java/com/kaixed/core/common/base/BaseActivity.kt @@ -0,0 +1,57 @@ +package com.kaixed.core.common.base + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.viewbinding.ViewBinding + +/** + * @Author: kaixed + * @Date: 2024/11/4 10:43 + */ +abstract class BaseActivity : AppCompatActivity() { + + protected lateinit var binding: VB + + protected val viewModel: VM by lazy { ViewModelProvider(this)[getViewModelClass()] } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + binding = inflateBinding() + setContentView(binding.root) + initData() + initView() + setupListeners() + observeData() +// AppManager.instance.addActivity(this) + } + + override fun onDestroy() { + super.onDestroy() +// AppManager.instance.finishActivity(this) + } + + abstract fun initData() + + abstract fun initView() + + abstract fun setupListeners() + + abstract fun observeData() + + fun toast(msg: String) { + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() + } + + fun toast() { + Toast.makeText(this, "暂未开发", Toast.LENGTH_SHORT).show() + } + + abstract fun inflateBinding(): VB + + abstract fun getViewModelClass(): Class +} \ No newline at end of file diff --git a/core_common/src/main/java/com/kaixed/core/common/base/BaseFragment.kt b/core_common/src/main/java/com/kaixed/core/common/base/BaseFragment.kt new file mode 100644 index 0000000..236f159 --- /dev/null +++ b/core_common/src/main/java/com/kaixed/core/common/base/BaseFragment.kt @@ -0,0 +1,53 @@ +package com.kaixed.core.common.base + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.viewbinding.ViewBinding +import com.kaixed.core.common.utils.ConstantsUtils.getStatusBarHeight + +/** + * @Author: kaixed + * @Date: 2024/10/31 11:04 + */ +abstract class BaseFragment : Fragment() { + + private var _binding: VB? = null + protected val binding get() = _binding!! + protected val viewModel: VM by lazy { ViewModelProvider(this)[getViewModelClass()] } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = inflateBinding(inflater, container) + return binding.root + } + + abstract fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?): VB + +// abstract fun isSetStatusBarPadding(): Boolean + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupStatusBarPadding(binding.root) + } + + private fun setupStatusBarPadding(rootView: View) { + val statusBarHeight = getStatusBarHeight() + rootView.updatePadding(top = statusBarHeight) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + abstract fun getViewModelClass(): Class +} diff --git a/app/src/main/kotlin/com/kaixed/kchat/extensions/CommonExtensions.kt b/core_common/src/main/java/com/kaixed/core/common/extension/CommonExtensions.kt similarity index 80% rename from app/src/main/kotlin/com/kaixed/kchat/extensions/CommonExtensions.kt rename to core_common/src/main/java/com/kaixed/core/common/extension/CommonExtensions.kt index 58cf6d1..b620065 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/extensions/CommonExtensions.kt +++ b/core_common/src/main/java/com/kaixed/core/common/extension/CommonExtensions.kt @@ -1,4 +1,4 @@ -package com.kaixed.kchat.extensions +package com.kaixed.core.common.extension import android.content.Intent import android.os.Build @@ -16,4 +16,8 @@ inline fun Intent.getParcelableExtraCompat(key: String) @Suppress("DEPRECATION") getParcelableExtra(key) } +} + +fun Int.dp2px(density: Float): Int { + return (this * density + 0.5f).toInt() } \ No newline at end of file diff --git a/core_common/src/main/java/com/kaixed/core/common/utils/Constants.kt b/core_common/src/main/java/com/kaixed/core/common/utils/Constants.kt new file mode 100644 index 0000000..63e4073 --- /dev/null +++ b/core_common/src/main/java/com/kaixed/core/common/utils/Constants.kt @@ -0,0 +1,35 @@ +package com.kaixed.core.common.utils + +/** + * @Author: kaixed + * @Date: 2024/10/24 22:03 + */ +object Constants { + + object Sign { + const val SECRET_KEY = "123" + } + + // mmkv + const val MMKV_USER_SESSION: String = "userSession" + const val MMKV_COMMON_DATA: String = "commonData" + + const val USERNAME_KEY = "username" + const val NICKNAME_KEY = "nickname" + const val AVATAR_URL = "avatarUrl" + const val FIRST_LAUNCH_APP = "firstLaunchApp" + const val USER_LOGIN_STATUS = "userLoginStatus" + const val STATUS_BAR_HEIGHT = "status_bar_height" + const val KEYBOARD_HEIGHT = "keyboardHeight" + const val CURRENT_CONTACT_ID = "currentContactId" + + 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 = 300 + const val STATUS_BAR_DEFAULT_HEIGHT: Int = 50 +} diff --git a/core_common/src/main/java/com/kaixed/core/common/utils/ConstantsUtils.kt b/core_common/src/main/java/com/kaixed/core/common/utils/ConstantsUtils.kt new file mode 100644 index 0000000..c8d4b02 --- /dev/null +++ b/core_common/src/main/java/com/kaixed/core/common/utils/ConstantsUtils.kt @@ -0,0 +1,55 @@ +package com.kaixed.core.common.utils + +import com.kaixed.core.common.utils.Constants.AVATAR_URL +import com.kaixed.core.common.utils.Constants.CURRENT_CONTACT_ID +import com.kaixed.core.common.utils.Constants.FIRST_LAUNCH_APP +import com.kaixed.core.common.utils.Constants.KEYBOARD_DEFAULT_HEIGHT +import com.kaixed.core.common.utils.Constants.KEYBOARD_HEIGHT +import com.kaixed.core.common.utils.Constants.MMKV_COMMON_DATA +import com.kaixed.core.common.utils.Constants.MMKV_USER_SESSION +import com.kaixed.core.common.utils.Constants.NICKNAME_KEY +import com.kaixed.core.common.utils.Constants.STATUS_BAR_DEFAULT_HEIGHT +import com.kaixed.core.common.utils.Constants.STATUS_BAR_HEIGHT +import com.kaixed.core.common.utils.Constants.USERNAME_KEY +import com.kaixed.core.common.utils.TextUtil.extractUrl +import com.tencent.mmkv.MMKV +import kotlin.getValue + +/** + * @Author: kaixed + * @Date: 2024/10/24 22:04 + */ +object ConstantsUtils { + + private val commonDataMMKV by lazy { MMKV.mmkvWithID(MMKV_COMMON_DATA) } + private val userSessionMMKV by lazy { MMKV.mmkvWithID(MMKV_USER_SESSION) } + private val defaultMMKV by lazy { MMKV.defaultMMKV() } + + fun getKeyboardHeight(): Int = + defaultMMKV.getInt(KEYBOARD_HEIGHT, KEYBOARD_DEFAULT_HEIGHT) + + fun getUsername(): String = + userSessionMMKV.getString(USERNAME_KEY, "") ?: "" + + fun getAvatarUrl(): String { + val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: "" + return extractUrl(avatarUrl) + } + + fun getStatusBarHeight(): Int = + commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT) + + fun getNickName(): String = + userSessionMMKV.getString(NICKNAME_KEY, "") ?: "" + + fun getCurrentContactId(): String = + userSessionMMKV.getString(CURRENT_CONTACT_ID, "") ?: "" + + fun isFirstLaunchApp(): Boolean { + val isFirstLaunch = defaultMMKV.getBoolean(FIRST_LAUNCH_APP, true) + if (isFirstLaunch) { + defaultMMKV.putBoolean(FIRST_LAUNCH_APP, false) + } + return isFirstLaunch + } +} diff --git a/core_common/src/main/java/com/kaixed/core/common/utils/NetworkInterface.kt b/core_common/src/main/java/com/kaixed/core/common/utils/NetworkInterface.kt new file mode 100644 index 0000000..fe52ff1 --- /dev/null +++ b/core_common/src/main/java/com/kaixed/core/common/utils/NetworkInterface.kt @@ -0,0 +1,55 @@ +package com.kaixed.core.common.utils + +/** + * @Author: kaixed + * @Date: 2024/10/14 13:46 + */ +object NetworkInterface { + +// private const val URL = "app.kaixed.com/kchat" + private const val URL = "192.168.225.209: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 = "ws://$URL" + const val WEBSOCKET = "/websocket/single/" + + // 获取access-token + const val ACCESS_TOKEN = "auth/access-token" + // 获取refresh-token + const val REFRESH_TOKEN = "auth/refresh-token" + // 登录接口(根据用户名登录) + const val LOGIN_BY_USERNAME = "auth/login/username" + // 登录接口(根据电话登录) + const val LOGIN_BY_TELEPHONE = "auth/login/telephone" + // 注册接口 + const val REGISTER = "auth/register" + + // 上传文件 + const val UPLOAD_FILE = "file/upload" + + // 更新用户密码 + const val UPDATE_PASSWORD = "user/password/update" + // 获取用户信息 + const val GET_USER_INFO = "user/info/fetch" + // 更新用户信息 + const val UPDATE_USER_INFO = "user/info/update" + // 上传用户头像 + const val UPLOAD_AVATAR = "user/upload-avatar" + + // 发送好友请求 + const val SEND_FRIEND_REQUEST = "friends/requests/send" + // 接受好友请求 + const val ACCEPT_FRIEND_REQUEST = "friends/requests/accept" + // 获取好友列表 + const val GET_FRIENDS = "friends/list" + // 获取好友请求列表 + const val GET_FRIEND_REQUEST_LIST = "friends/requests/fetch" + // 删除好友 + const val DELETE_FRIEND = "friends/delete-friend" + // 设置好友备注 + const val SET_FRIEND_REMARK = "friends/set-remark" + + //撤回消息 + const val RECALL_MESSAGE = "message/withdraw" +} diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/Pinyin4jUtil.kt b/core_common/src/main/java/com/kaixed/core/common/utils/Pinyin4jUtil.kt similarity index 96% rename from app/src/main/kotlin/com/kaixed/kchat/utils/Pinyin4jUtil.kt rename to core_common/src/main/java/com/kaixed/core/common/utils/Pinyin4jUtil.kt index 3457e22..413ab0e 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/Pinyin4jUtil.kt +++ b/core_common/src/main/java/com/kaixed/core/common/utils/Pinyin4jUtil.kt @@ -1,8 +1,9 @@ -package com.kaixed.kchat.utils +package com.kaixed.core.common.utils import net.sourceforge.pinyin4j.PinyinHelper import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat import net.sourceforge.pinyin4j.format.HanyuPinyinToneType +import kotlin.collections.isNotEmpty /** diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/SingleLiveEvent.kt b/core_common/src/main/java/com/kaixed/core/common/utils/SingleLiveEvent.kt similarity index 97% rename from app/src/main/kotlin/com/kaixed/kchat/utils/SingleLiveEvent.kt rename to core_common/src/main/java/com/kaixed/core/common/utils/SingleLiveEvent.kt index 75cf144..5ac24f2 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/SingleLiveEvent.kt +++ b/core_common/src/main/java/com/kaixed/core/common/utils/SingleLiveEvent.kt @@ -1,4 +1,4 @@ -package com.kaixed.kchat.utils +package com.kaixed.core.common.utils import androidx.annotation.MainThread import androidx.lifecycle.LifecycleOwner diff --git a/app/src/main/kotlin/com/kaixed/kchat/utils/TextUtil.kt b/core_common/src/main/java/com/kaixed/core/common/utils/TextUtil.kt similarity index 96% rename from app/src/main/kotlin/com/kaixed/kchat/utils/TextUtil.kt rename to core_common/src/main/java/com/kaixed/core/common/utils/TextUtil.kt index a297156..9133596 100644 --- a/app/src/main/kotlin/com/kaixed/kchat/utils/TextUtil.kt +++ b/core_common/src/main/java/com/kaixed/core/common/utils/TextUtil.kt @@ -1,6 +1,5 @@ -package com.kaixed.kchat.utils +package com.kaixed.core.common.utils -import com.kaixed.kchat.utils.ScreenUtils.getScreenWidth import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -18,6 +17,11 @@ object TextUtil { return matchResult?.groups?.get(1)?.value ?: url } + //TODO 获取屏幕宽度 + fun getScreenWidth(): Int { + return 100 + } + /** * 从 URL 中提取图片的宽度和高度 * diff --git a/core_common/src/test/java/com/kaixed/core/common/ExampleUnitTest.kt b/core_common/src/test/java/com/kaixed/core/common/ExampleUnitTest.kt new file mode 100644 index 0000000..c3ce2a4 --- /dev/null +++ b/core_common/src/test/java/com/kaixed/core/common/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.kaixed.core.common + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/core_network/.gitignore b/core_network/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core_network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core_network/build.gradle.kts b/core_network/build.gradle.kts new file mode 100644 index 0000000..139f8af --- /dev/null +++ b/core_network/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) + id("org.jetbrains.kotlin.plugin.serialization") + id("com.google.devtools.ksp") +} + +android { + namespace = "com.kaixed.core.network" + compileSdk = 34 + + defaultConfig { + minSdk = 30 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(project(":core_common")) + + implementation(libs.androidx.core.ktx) + implementation(libs.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + // OkHttp + implementation(libs.okhttp) + implementation(libs.gson) + + implementation(libs.retrofit) + implementation(libs.retrofit2.converter.gson) + + implementation(libs.okhttp3.logging.interceptor) +} \ No newline at end of file diff --git a/core_network/consumer-rules.pro b/core_network/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/core_network/proguard-rules.pro b/core_network/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/core_network/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core_network/src/androidTest/java/com/kaixed/core/network/ExampleInstrumentedTest.kt b/core_network/src/androidTest/java/com/kaixed/core/network/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..2cc4f8d --- /dev/null +++ b/core_network/src/androidTest/java/com/kaixed/core/network/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.kaixed.core.network + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.kaixed.core.network.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core_network/src/main/AndroidManifest.xml b/core_network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/core_network/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/ApiCall.kt b/core_network/src/main/java/com/kaixed/core/network/ApiCall.kt new file mode 100644 index 0000000..122a249 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/ApiCall.kt @@ -0,0 +1,24 @@ +package com.kaixed.core.network + +/** + * @Author: kaixed + * @Date: 2024/11/24 9:29 + */ +object ApiCall { + suspend fun apiCall( + apiCall: suspend () -> ApiResponse, errorMessage: String = "操作失败" + ): Result { + return try { + val response = apiCall() + if (response.isSuccess()) { + response.getResponseData()?.let { + Result.success(it) + } ?: Result.failure(Exception(errorMessage)) + } else { + Result.failure(Exception(response.getResponseMsg())) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/core_network/src/main/java/com/kaixed/core/network/ApiResponse.kt b/core_network/src/main/java/com/kaixed/core/network/ApiResponse.kt new file mode 100644 index 0000000..60a2bd3 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/ApiResponse.kt @@ -0,0 +1,17 @@ +package com.kaixed.core.network + +/** + * @Author: kaixed + * @Date: 2024/11/23 13:25 + */ +data class ApiResponse( + val code: String, + val msg: String, + val `data`: T? +) : BaseResponse() { + + override fun isSuccess(): Boolean = code == "200" + override fun getResponseCode() = code + override fun getResponseData() = data + override fun getResponseMsg() = msg +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/BaseResponse.kt b/core_network/src/main/java/com/kaixed/core/network/BaseResponse.kt new file mode 100644 index 0000000..a97f9e1 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/BaseResponse.kt @@ -0,0 +1,12 @@ +package com.kaixed.core.network + +abstract class BaseResponse { + + abstract fun isSuccess(): Boolean + + abstract fun getResponseData(): T + + abstract fun getResponseCode(): String + + abstract fun getResponseMsg(): String +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/NetworkManager.kt b/core_network/src/main/java/com/kaixed/core/network/NetworkManager.kt new file mode 100644 index 0000000..07a1685 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/NetworkManager.kt @@ -0,0 +1,29 @@ +package com.kaixed.core.network + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object NetworkManager { + + val okHttpClient: OkHttpClient by lazy { + OkhttpHelper.getInstance() +// OkHttpClient.Builder() +// .addInterceptor(CommonHeaderInterceptor()) +// .addInterceptor(HttpLoggingInterceptor().apply { +// level = HttpLoggingInterceptor.Level.BODY +// }) +// // ... 其他全局配置 +// .build() + } + + fun createService(service: Class): T { + return Retrofit.Builder() + .baseUrl("https://api.xxx.com/") + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(service) + } +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/OkhttpHelper.kt b/core_network/src/main/java/com/kaixed/core/network/OkhttpHelper.kt new file mode 100644 index 0000000..812f4e3 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/OkhttpHelper.kt @@ -0,0 +1,24 @@ +package com.kaixed.core.network + +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit + +/** + * @Author: kaixed + * @Date: 2024/5/27 8:58 + */ +object OkhttpHelper { + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .pingInterval(30, TimeUnit.SECONDS) + .build() + } + + fun getInstance(): OkHttpClient { + return client + } +} + diff --git a/core_network/src/main/java/com/kaixed/core/network/RetrofitClient.kt b/core_network/src/main/java/com/kaixed/core/network/RetrofitClient.kt new file mode 100644 index 0000000..d7f354b --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/RetrofitClient.kt @@ -0,0 +1,76 @@ +package com.kaixed.core.network + +import com.kaixed.core.common.utils.NetworkInterface +import com.kaixed.core.network.service.AuthApiService +import com.kaixed.core.network.service.FileApiService +import com.kaixed.core.network.service.FriendApiService +import com.kaixed.core.network.service.UserApiService +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +/** + * @Author: kaixed + * @Date: 2024/11/15 11:05 + */ +object RetrofitClient { + + private const val BASE_URL = "${NetworkInterface.SERVER_URL}/" + + // 添加日志拦截器 + private val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + // 创建一个不包含 Token 拦截器的 OkHttpClient,专门用于授权 API + private val authClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .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(SignInterceptor()) // 签名拦截器 +// .addInterceptor(TokenRefreshInterceptor()) // Token 拦截器 + .build() + + // 创建 Retrofit 实例(用于普通 API 调用) + private val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) // 使用包含 Token 拦截器的 OkHttpClient + .build() + } + + // API 服务实例 + val userApiService: UserApiService by lazy { + retrofit.create(UserApiService::class.java) + } + + val friendApiService: FriendApiService by lazy { + retrofit.create(FriendApiService::class.java) + } + + val fileApiService: FileApiService by lazy { + retrofit.create(FileApiService::class.java) + } +} diff --git a/core_network/src/main/java/com/kaixed/core/network/SignUtil.kt b/core_network/src/main/java/com/kaixed/core/network/SignUtil.kt new file mode 100644 index 0000000..5030d1f --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/SignUtil.kt @@ -0,0 +1,96 @@ +package com.kaixed.core.network + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import okhttp3.Request +import okio.Buffer +import java.io.IOException +import java.security.MessageDigest + +object SignUtil { + + // 生成签名 + fun generateSign(request: Request, timestamp: String): String { + val secretKey = "your_secret_key" + val stringBuilder = StringBuilder("timestamp=$timestamp") + + // 根据请求类型处理 GET 或 POST 请求 + when (request.method) { + "GET" -> appendUrlParams(request, stringBuilder) + "POST" -> appendPostParams(request, stringBuilder) + } + + // 拼接密钥并生成签名 + stringBuilder.append(secretKey) + println(stringBuilder.toString()) + return md5(stringBuilder.toString()) + } + + // 处理 GET 请求 URL 参数 + private fun appendUrlParams(request: Request, stringBuilder: StringBuilder) { + val url = request.url + url.queryParameterNames.forEach { name -> + val value = url.queryParameter(name) + stringBuilder.append("&$name=$value") + } + } + + // 处理 POST 请求参数,包括表单数据和 JSON 数据 + private fun appendPostParams(request: Request, stringBuilder: StringBuilder) { + val body = request.body + body?.let { + when (body.contentType()?.subtype) { + "x-www-form-urlencoded" -> appendFormParams(it, stringBuilder) + "json" -> appendJsonBody(it, stringBuilder) + } + } + } + + // 处理表单数据 + private fun appendFormParams(body: okhttp3.RequestBody, stringBuilder: StringBuilder) { + val formBody = body as okhttp3.FormBody + val sortedParams = (0 until formBody.size) + .map { Pair(formBody.name(it), formBody.value(it)) } + .sortedBy { it.first } + + sortedParams.forEach { (name, value) -> + stringBuilder.append("&$name=$value") + } + } + + // 处理 JSON 数据并排序 + private fun appendJsonBody(body: okhttp3.RequestBody, stringBuilder: StringBuilder) { + val jsonBody = getRequestBodyAsJsonObject(body) + jsonBody?.let { + val sortedJson = sortJson(it) + stringBuilder.append("&body=$sortedJson") + } + } + + // 读取 RequestBody 并解析为 JsonObject + private fun getRequestBodyAsJsonObject(body: okhttp3.RequestBody): JsonObject? { + val buffer = Buffer() + try { + body.writeTo(buffer) + val bodyString = buffer.readUtf8() + return JsonParser.parseString(bodyString).asJsonObject + } catch (e: IOException) { + e.printStackTrace() + return null + } + } + + // 排序 JSON 对象 + private fun sortJson(jsonObject: JsonObject): String { + return jsonObject.keySet() + .sorted() + .joinToString(",", "{", "}") { key -> "\"$key\":${jsonObject[key]}" } + } + + // MD5 加密函数 + private fun md5(input: String): String { + val digest = MessageDigest.getInstance("MD5") + val bytes = digest.digest(input.toByteArray()) + return bytes.joinToString("") { "%02x".format(it) } + } +} diff --git a/core_network/src/main/java/com/kaixed/core/network/interceptor/SignInterceptor.kt b/core_network/src/main/java/com/kaixed/core/network/interceptor/SignInterceptor.kt new file mode 100644 index 0000000..07777f5 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/interceptor/SignInterceptor.kt @@ -0,0 +1,101 @@ +package com.kaixed.core.network.interceptor + +import com.kaixed.core.common.utils.Constants +import okhttp3.FormBody +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okio.IOException +import java.security.MessageDigest +import java.util.* + +class SignInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + // 获取当前时间戳 + val timestamp = System.currentTimeMillis().toString() + + // 生成随机 nonce + val nonce = UUID.randomUUID().toString() + + // 获取请求参数 + val params = getRequestParams(request) + + // 生成签名 + val sign = generateServerSign(params, timestamp, nonce) + + // 构造新的请求,添加请求头 + val newRequest = request.newBuilder() + .addHeader("sign", sign) + .addHeader("timestamp", timestamp) + .addHeader("nonce", nonce) + .build() + + // 继续请求 + return chain.proceed(newRequest) + } + + // 获取请求的参数,处理 POST 请求的 Map 格式数据,并确保按字典顺序排序 + private fun getRequestParams(request: Request): Map { + val params = TreeMap() + + when (request.method) { + "GET" -> { + request.url.queryParameterNames.forEach { name -> + params[name] = request.url.queryParameter(name) ?: "" + } + } + + "POST" -> { + val body = request.body + if (body is FormBody) { + // 获取所有参数,并确保按字典顺序排序 + for (i in 0 until body.size) { + params[body.name(i)] = body.value(i) + } + } + } + + else -> { + // 其他请求方法不处理 + } + } + return params + } + + // 生成服务端签名 + private fun generateServerSign( + sortedParams: Map, + timestamp: String, + nonce: String, + ): String { + // 拼接参数 + val sb = StringBuilder() + sortedParams.forEach { (key, value) -> + sb.append("$key=$value&") + } + sb.append("timestamp=$timestamp&") + sb.append("nonce=$nonce&") + sb.append("secretKey=${Constants.Sign.SECRET_KEY}") + + // 使用 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() + } +} diff --git a/core_network/src/main/java/com/kaixed/core/network/interceptor/TokenRefreshInterceptor.kt b/core_network/src/main/java/com/kaixed/core/network/interceptor/TokenRefreshInterceptor.kt new file mode 100644 index 0000000..1e46633 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/interceptor/TokenRefreshInterceptor.kt @@ -0,0 +1,127 @@ +package com.kaixed.core.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 + } +} diff --git a/core_network/src/main/java/com/kaixed/core/network/message/MessageCenter.kt b/core_network/src/main/java/com/kaixed/core/network/message/MessageCenter.kt new file mode 100644 index 0000000..fae6a44 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/MessageCenter.kt @@ -0,0 +1,12 @@ +package com.kaixed.core.network.message + +class MessageCenter( + private val messageRepository: MessageRepository +) { + suspend fun sendChatMessage(content: String, msgLocalId: Long) { + messageRepository.sendMessage(content, msgLocalId).fold( + onSuccess = { }, + onFailure = { } + ) + } +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/message/MessageModel.kt b/core_network/src/main/java/com/kaixed/core/network/message/MessageModel.kt new file mode 100644 index 0000000..e119f36 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/MessageModel.kt @@ -0,0 +1,16 @@ +package com.kaixed.core.network.message + +data class MessageModel( + var msgLocalId: Long = 0L, + var msgSvrId: Long = 0L, + var content: String, + var timestamp: Long, + var status: String = "normal", + var senderId: String, + var avatarUrl: String = "", + var talkerId: String, + var type: String, + var show: Boolean = true, + var isShowTimer: Boolean = false, + var isSender: Boolean = false +) diff --git a/core_network/src/main/java/com/kaixed/core/network/message/MessageRepository.kt b/core_network/src/main/java/com/kaixed/core/network/message/MessageRepository.kt new file mode 100644 index 0000000..fa412e3 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/MessageRepository.kt @@ -0,0 +1,66 @@ +package com.kaixed.core.network.message + +import android.util.Log +import com.google.gson.Gson +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class MessageRepository( + private val webSocketClient: WebSocketClient +) : MessageService { + + private val scope = CoroutineScope(IO + SupervisorJob()) + + // 创建一个 Flow 来发送 WebSocket 的消息 + private val _sendMessages = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 10 + ) + + private val _receivedMessages = MutableSharedFlow(replay = 0, extraBufferCapacity = 100) + + val receivedMessages: SharedFlow = _receivedMessages.asSharedFlow() + + init { + // 监听WebSocket消息 + scope.launch { + webSocketClient.observeMessages().collect { text -> + try { + val wsMessage = Gson().fromJson(text, MessageModel::class.java) + if (wsMessage.type == "ack") { +// messageDao.updateMessageStatus( +// wsMessage.payload.msgLocalId, +// wsMessage.payload.msgSvrId, +// "sent" +// ) + } else { +// // 存储接收到的消息 +// messageDao.insertMessages(listOf(wsMessage.payload)) +// // 广播给数据处理层 + _receivedMessages.emit(wsMessage) + } + } catch (e: Exception) { + Log.e("MessageRepository", "消息解析失败: ${e.message}") + } + } + } + } + + override suspend fun sendMessage(message: String, msgLocalId: Long): Result { + return try { + if (webSocketClient.send(message, msgLocalId)) { + _sendMessages.emit(message) + Result.success(Unit) + } else { + Result.failure(Exception("WebSocket not connected")) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/message/MessageService.kt b/core_network/src/main/java/com/kaixed/core/network/message/MessageService.kt new file mode 100644 index 0000000..6f4c37c --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/MessageService.kt @@ -0,0 +1,5 @@ +package com.kaixed.core.network.message + +interface MessageService { + suspend fun sendMessage(message: String, msgLocalId: Long): Result +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/message/OkHttpWebSocketClient.kt b/core_network/src/main/java/com/kaixed/core/network/message/OkHttpWebSocketClient.kt new file mode 100644 index 0000000..6f5d82c --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/OkHttpWebSocketClient.kt @@ -0,0 +1,98 @@ +package com.kaixed.core.network.message + +import android.util.Log +import com.kaixed.core.common.utils.ConstantsUtils.getUsername +import com.kaixed.core.network.OkhttpHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener + +class OkHttpWebSocketClient : WebSocketClient { + + companion object { + const val TAG = "WebSocketService" + const val WEBSOCKET_CLOSE_CODE = 1000 + const val MAX_RECONNECT_ATTEMPTS = 5 + } + + private var reconnectAttempts = 0 + + private val _messages = Channel(capacity = 100) + + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val url: String = "$WEBSOCKET_SERVER_URL$WEBSOCKET${getUsername()}" + private lateinit var webSocket: WebSocket + private var isConnected = false + + override fun connect() { + val request = Request.Builder() + .url(url).build() + val listener = EchoWebSocketListener() + val client = OkhttpHelper.getInstance() + webSocket = client.newWebSocket(request, listener) + } + + override fun send(message: String, msgLocalId: Long): Boolean { + return if (isConnected) { + webSocket.send(message) + } else { + false + } + } + + override fun close(code: Int, reason: String) { + webSocket.close(code, reason) + isConnected = false + } + + override fun isConnected(): Boolean = isConnected + + private fun reconnect() { + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + Log.e("WebSocket", "达到最大重连次数") + return + } + reconnectAttempts++ + scope.launch { + val delayMs = minOf(reconnectAttempts * 1000L, 30000L) // 指数退避,最大 30 秒 + delay(delayMs) + connect() + } + } + + override fun observeMessages(): Flow = _messages.receiveAsFlow() + + inner class EchoWebSocketListener : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(TAG, "WebSocket Opened") + isConnected = true + reconnectAttempts = 0 + } + + override fun onMessage(webSocket: WebSocket, text: String) { + scope.launch { _messages.send(text) } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + isConnected = false + webSocket.close(WEBSOCKET_CLOSE_CODE, null) + Log.d(TAG, "WebSocket closing: $reason") + connect() + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + isConnected = false + Log.d(TAG, "WebSocket closing: ${t.cause}") + reconnect() + } + } +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/message/WebSocketClient.kt b/core_network/src/main/java/com/kaixed/core/network/message/WebSocketClient.kt new file mode 100644 index 0000000..36a8d67 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/WebSocketClient.kt @@ -0,0 +1,11 @@ +package com.kaixed.core.network.message + +import kotlinx.coroutines.flow.Flow + +interface WebSocketClient { + fun send(message: String, msgLocalId: Long): Boolean + fun close(code: Int, reason: String) + fun connect() + fun observeMessages(): Flow + fun isConnected(): Boolean +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/message/WebSocketService.kt b/core_network/src/main/java/com/kaixed/core/network/message/WebSocketService.kt new file mode 100644 index 0000000..93a99e3 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/message/WebSocketService.kt @@ -0,0 +1,37 @@ +package com.kaixed.core.network.message + +import android.app.Service +import android.content.Intent +import android.os.IBinder + + +class WebSocketService : Service() { + + companion object { + private const val TAG = "WebSocketService" + private const val WEBSOCKET_CLOSE_CODE = 1000 + } + + private lateinit var messageRepository: MessageRepository + + private lateinit var webSocketClient: WebSocketClient + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onCreate() { + super.onCreate() + webSocketClient = OkHttpWebSocketClient() + messageRepository = MessageRepository(webSocketClient) + webSocketClient.connect() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + webSocketClient.connect() + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + webSocketClient.close(WEBSOCKET_CLOSE_CODE, "App exited") + } +} diff --git a/core_network/src/main/java/com/kaixed/core/network/service/AuthApiService.kt b/core_network/src/main/java/com/kaixed/core/network/service/AuthApiService.kt new file mode 100644 index 0000000..96bd978 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/service/AuthApiService.kt @@ -0,0 +1,49 @@ +package com.kaixed.core.network.service + +import com.kaixed.core.common.utils.Constants.ACCESS_TOKEN +import com.kaixed.core.common.utils.Constants.REFRESH_TOKEN +import com.kaixed.core.common.utils.NetworkInterface.LOGIN_BY_TELEPHONE +import com.kaixed.core.common.utils.NetworkInterface.LOGIN_BY_USERNAME +import com.kaixed.core.common.utils.NetworkInterface.REGISTER +import com.kaixed.core.network.ApiResponse +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query + +/** + * @Author: kaixed + * @Date: 2025/1/22 18:35 + */ +interface AuthApiService { + + @POST(ACCESS_TOKEN) + suspend fun auth( + @Header("Authorization") token: String, + @Query("username") username: String + ): ApiResponse + + @POST(REFRESH_TOKEN) + suspend fun refresh( + @Header("Authorization") token: String, + @Query("username") username: String + ): ApiResponse + + // 登录接口(根据用户名登录) + @POST(LOGIN_BY_USERNAME) + suspend fun loginByUsername( + @Body requestParams: Map, + ): ApiResponse + + // 登录接口(根据电话登录) + @POST(LOGIN_BY_TELEPHONE) + suspend fun loginByTelephone( + @Body requestParams: Map, + ): ApiResponse + + // 注册接口 + @POST(REGISTER) + suspend fun register( + @Body registerRequest: RegisterRequest + ): ApiResponse +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/service/FileApiService.kt b/core_network/src/main/java/com/kaixed/core/network/service/FileApiService.kt new file mode 100644 index 0000000..fe8a159 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/service/FileApiService.kt @@ -0,0 +1,21 @@ +package com.kaixed.core.network.service + +import com.kaixed.kchat.network.ApiResponse +import com.kaixed.kchat.network.NetworkInterface.UPLOAD_FILE +import okhttp3.MultipartBody +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +/** + * @Author: kaixed + * @Date: 2024/11/29 15:11 + */ +interface FileApiService { + + @Multipart + @POST(UPLOAD_FILE) + suspend fun uploadFile( + @Part file: MultipartBody.Part + ): ApiResponse +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/service/FriendApiService.kt b/core_network/src/main/java/com/kaixed/core/network/service/FriendApiService.kt new file mode 100644 index 0000000..807c483 --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/service/FriendApiService.kt @@ -0,0 +1,76 @@ +package com.kaixed.core.network.service + +import com.kaixed.kchat.data.local.entity.Contact +import com.kaixed.kchat.data.model.friend.FriendRequestItem +import com.kaixed.kchat.data.model.search.SearchUser +import com.kaixed.kchat.network.ApiResponse +import com.kaixed.kchat.network.NetworkInterface.ACCEPT_FRIEND_REQUEST +import com.kaixed.kchat.network.NetworkInterface.DELETE_FRIEND +import com.kaixed.kchat.network.NetworkInterface.GET_FRIEND_REQUEST_LIST +import com.kaixed.kchat.network.NetworkInterface.SEND_FRIEND_REQUEST +import com.kaixed.kchat.network.NetworkInterface.SET_FRIEND_REMARK +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +/** + * @Author: kaixed + * @Date: 2025/1/25 16:38 + */ +interface FriendApiService { + // 获取好友请求列表 + @FormUrlEncoded + @POST(GET_FRIEND_REQUEST_LIST) + suspend fun getContactRequestList( + @Field("userAccount") username: String + ): ApiResponse?> + + // 接受好友请求 + @FormUrlEncoded + @POST(ACCEPT_FRIEND_REQUEST) + suspend fun acceptContactRequest( + @Field("requestId") contactId: String, + @Field("receiverId") username: String, + @Field("remark") remark: String + ): ApiResponse + + // 添加联系人 + @FormUrlEncoded + @POST(SEND_FRIEND_REQUEST) + suspend fun addContact( + @Field("senderId") senderId: String, + @Field("receiverId") receiverId: String, + @Field("message") message: String + ): ApiResponse + + // 搜索联系人 + @GET("users/{username}") + suspend fun searchContact( + @Path("username") username: String + ): ApiResponse + + // 获取联系人列表 + @POST("friends/list") + suspend fun getContactList( + @Body requestParams: Map, + ): ApiResponse?> + + // 删除联系人 + @POST(DELETE_FRIEND) + suspend fun deleteContact( + @Field("userId") username: String, + @Field("contactId") contactId: String, + ): ApiResponse + + // 设置好友备注 + @FormUrlEncoded + @POST(SET_FRIEND_REMARK) + suspend fun setRemark( + @Field("userId") userId: String, + @Field("contactId") contactId: String, + @Field("remark") remark: String, + ): ApiResponse +} \ No newline at end of file diff --git a/core_network/src/main/java/com/kaixed/core/network/service/UserApiService.kt b/core_network/src/main/java/com/kaixed/core/network/service/UserApiService.kt new file mode 100644 index 0000000..db4232f --- /dev/null +++ b/core_network/src/main/java/com/kaixed/core/network/service/UserApiService.kt @@ -0,0 +1,56 @@ +package com.kaixed.core.network.service + +import com.kaixed.kchat.data.model.request.UpdatePasswordRequest +import com.kaixed.kchat.data.model.request.UserRequest +import com.kaixed.kchat.data.model.response.search.User +import com.kaixed.kchat.data.model.search.SearchUser +import com.kaixed.kchat.network.ApiResponse +import com.kaixed.kchat.network.NetworkInterface.GET_FRIENDS +import com.kaixed.kchat.network.NetworkInterface.GET_USER_INFO +import com.kaixed.kchat.network.NetworkInterface.UPDATE_PASSWORD +import com.kaixed.kchat.network.NetworkInterface.UPDATE_USER_INFO +import com.kaixed.kchat.network.NetworkInterface.UPLOAD_AVATAR +import okhttp3.MultipartBody +import retrofit2.http.Body +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +/** + * @Author: kaixed + * @Date: 2024/11/15 11:00 + */ +interface UserApiService { + + // 获取用户列表 + @POST(GET_FRIENDS) + suspend fun fetchUserList( + @Body requestParams: Map + ): ApiResponse> + + // 获取用户信息 + @POST(GET_USER_INFO) + suspend fun getUserInfo( + @Body requestParams: Map + ): ApiResponse + + // 更改昵称接口 + @POST(UPDATE_USER_INFO) + suspend fun changeNickname( + @Body userRequest: UserRequest + ): ApiResponse + + // 上传头像接口 + @Multipart + @POST(UPLOAD_AVATAR) + suspend fun uploadAvatar( + @Part("username") username: String, + @Part file: MultipartBody.Part + ): ApiResponse + + // 更改密码 + @POST(UPDATE_PASSWORD) + suspend fun updatePassword( + @Body updatePasswordRequest: UpdatePasswordRequest + ): ApiResponse +} diff --git a/core_network/src/test/java/com/kaixed/core/network/ExampleUnitTest.kt b/core_network/src/test/java/com/kaixed/core/network/ExampleUnitTest.kt new file mode 100644 index 0000000..0758f2a --- /dev/null +++ b/core_network/src/test/java/com/kaixed/core/network/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.kaixed.core.network + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3a05024..fa49af6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,12 +6,14 @@ emoji2 = "1.5.0" eventbus = "3.3.1" glide = "4.16.0" gson = "2.11.0" +hiltAndroid = "2.50" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" appcompat = "1.7.0" kotlinxCoroutinesCore = "1.7.3" kotlinxSerializationJson = "1.6.3" +leakcanaryAndroid = "2.14" loggingInterceptorVersion = "5.0.0-alpha.2" lottie = "6.5.2" material = "1.12.0" @@ -20,13 +22,16 @@ constraintlayout = "2.2.0" mmkv = "1.3.9" objectboxGradlePlugin = "4.0.3" okhttp = "4.12.0" +pagingRuntimeKtx = "3.3.6" pictureselector = "v3.11.2" pinyin4j = "2.5.1" preference = "1.2.1" retrofit = "2.11.0" +roomKtx = "2.6.1" scanplus = "2.12.0.301" softInputEvent = "1.0.9" spannable = "1.2.7" +startupRuntime = "1.2.0" therouter = "1.2.2" window = "1.3.0" #noinspection GradleDependency @@ -38,11 +43,19 @@ 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" } +androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntimeKtx" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomKtx" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" } +androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomKtx" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomKtx" } +androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startupRuntime" } compress = { module = "io.github.lucksiege:compress", version.ref = "pictureselector" } eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroid" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } +leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } objectbox-gradle-plugin = { module = "io.objectbox:objectbox-gradle-plugin", version.ref = "objectboxGradlePlugin" } okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptorVersion" } pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" } @@ -78,3 +91,4 @@ window = { module = "androidx.window:window", version.ref = "window" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } diff --git a/plantuml/时序图.puml b/plantuml/时序图.puml new file mode 100644 index 0000000..717302a --- /dev/null +++ b/plantuml/时序图.puml @@ -0,0 +1,22 @@ +@startuml +actor 用户 +participant 客户端 +participant WebSocket +participant 服务器 + +== 发送消息 == + +用户 -> 客户端 : 输入消息 +客户端 -> 客户端 : 本地存储为 sending 状态 +客户端 -> WebSocket : 发送消息 JSON +WebSocket -> 服务器 : 转发消息 +服务器 -> WebSocket : ACK + 推送给对方 +WebSocket -> 客户端 : 返回ACK +客户端 -> 客户端 : 更新消息状态为 sent + +== 同步消息 == + +客户端 -> 服务器 : 请求 /sync?last_msg_id=xxx +服务器 -> 客户端 : 返回缺失的消息列表 +客户端 -> 客户端 : 写入本地 + UI展示 +@enduml diff --git a/settings.gradle.kts b/settings.gradle.kts index e5799b3..bd1adb5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,3 +24,5 @@ dependencyResolutionManagement { rootProject.name = "KChat-Android" include(":app") +include(":core_common") +include(":core_network")