Files
forkmessager/client-mobile/chats/data/workers/ChatSyncWorker.kt
Халимов Рустам 9daa786cfb Заготовка
2026-05-07 23:27:37 +03:00

185 lines
7.1 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package chats.data.workers
import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.work.*
import chats.data.local.dao.ChatDao
import chats.data.local.dao.MessageDao
import chats.domain.model.MessageStatus
import core.security.TokenManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.net.SocketTimeoutException
import java.util.concurrent.TimeUnit
/**
* Worker для периодической синхронизации чатов и сообщений.
* Запускается при наличии сети для получения новых сообщений с сервера.
*/
@HiltWorker
class ChatSyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val chatDao: ChatDao,
private val messageDao: MessageDao,
private val chatApi: chats.data.remote.api.ChatApi
) : CoroutineWorker(context, params) {
companion object {
const val WORK_TAG = "chat_sync_worker"
const val KEY_CHAT_ID = "chat_id"
const val KEY_CURSOR = "cursor"
/**
* Создает периодический запрос на синхронизацию всех чатов
*/
fun createPeriodicWorkRequest(): PeriodicWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
return PeriodicWorkRequestBuilder<ChatSyncWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES
)
.setConstraints(constraints)
.addTag(WORK_TAG)
.build()
}
/**
* Создает разовый запрос на синхронизацию конкретного чата
*/
fun createOneTimeWorkRequest(chatId: String? = null, cursor: String? = null): OneTimeWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val inputData = workDataOf(
KEY_CHAT_ID to chatId,
KEY_CURSOR to cursor
)
return OneTimeWorkRequestBuilder<ChatSyncWorker>()
.setConstraints(constraints)
.setInputData(inputData)
.addTag(WORK_TAG)
.build()
}
}
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
Log.d("ChatSyncWorker", "Starting chat sync")
try {
// 1. Синхронизируем список чатов
syncChats()
// 2. Если указан chatId - синхронизируем сообщения
val chatId = inputData.getString(KEY_CHAT_ID)
if (chatId != null) {
syncMessagesForChat(chatId)
}
Log.i("ChatSyncWorker", "Sync completed successfully")
Result.success()
} catch (e: IOException) {
Log.w("ChatSyncWorker", "Network error during sync: ${e.message}")
Result.retry()
} catch (e: SocketTimeoutException) {
Log.w("ChatSyncWorker", "Timeout during sync: ${e.message}")
Result.retry()
} catch (e: Exception) {
Log.e("ChatSyncWorker", "Error during sync", e)
Result.failure()
}
}
private suspend fun syncChats() {
val remoteChats = chatApi.getChats()
val localChats = mutableListOf<chats.data.local.database.ChatEntity>()
remoteChats.forEach { dto ->
val existingChat = chatDao.getChatByRemoteId(dto.id)
val chatEntity = chats.data.local.database.ChatEntity(
localId = existingChat?.localId ?: java.util.UUID.randomUUID().toString(),
remoteId = dto.id,
type = dto.type,
name = dto.name ?: "",
avatar = dto.avatar,
unreadCount = dto.unreadCount,
lastMessageText = dto.messages.firstOrNull()?.content,
lastMessageTimestamp = dto.messages.firstOrNull()?.createdAt
)
localChats.add(chatEntity)
}
chatDao.insertChats(localChats)
Log.d("ChatSyncWorker", "Synced ${localChats.size} chats")
}
private suspend fun syncMessagesForChat(chatId: String) {
val lastMessage = messageDao.getMessagesByStatus(
chatId = chatId,
statuses = listOf(MessageStatus.SENT, MessageStatus.DELIVERED, MessageStatus.READ)
).maxByOrNull { it.sequenceId }
val cursor = lastMessage?.serverId
val messages = chatApi.getMessages(chatId, cursor = cursor, limit = 50)
if (messages.isNotEmpty()) {
saveMessagesToLocal(messages, chatId)
Log.d("ChatSyncWorker", "Synced ${messages.size} messages for chat $chatId")
}
}
private suspend fun saveMessagesToLocal(
messages: List<chats.data.remote.dto.MessageDto>,
chatId: String
) {
val currentUserId = tokenManager.getUserId() ?: ""
val gson = com.google.gson.Gson()
messages.forEach { dto ->
val existing = messageDao.getMessageByServerId(dto.id)
if (existing == null) {
val entity = chats.data.local.database.MessageEntity(
localId = java.util.UUID.randomUUID().toString(),
serverId = dto.id,
idempotencyKey = dto.id,
chatId = chatId,
senderId = dto.senderId ?: dto.sender?.id ?: currentUserId,
senderName = dto.sender?.displayName ?: dto.sender?.username ?: "",
senderAvatar = dto.sender?.avatarUrl,
content = dto.content,
sequenceId = dto.sequenceId?.toLong() ?: 0L,
createdAt = dto.createdAt ?: java.time.ZonedDateTime.now().toString(),
mediaType = dto.type ?: "TEXT",
mediaJson = gson.toJson(dto.media.map {
chats.domain.model.Media(
id = it.id,
type = it.type,
url = it.url,
filename = it.filename,
size = it.size,
duration = it.duration
)
}),
reactionsJson = gson.toJson(
dto.reactions?.associate { it.emoji to it.count } ?: emptyMap<String, Int>()
),
status = if (dto.senderId == currentUserId) MessageStatus.SENT else MessageStatus.DELIVERED,
createdAtMillis = System.currentTimeMillis(),
updatedAtMillis = System.currentTimeMillis()
)
messageDao.insertMessage(entity)
}
}
}
}