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")