185 lines
7.1 KiB
Kotlin
185 lines
7.1 KiB
Kotlin
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)
|
||
}
|
||
}
|
||
}
|
||
}
|