191 lines
7.3 KiB
Kotlin
191 lines
7.3 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,
|
|
private val tokenManager: TokenManager
|
|
) : 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 lastMessage = dto.messages.firstOrNull()
|
|
val timestamp = lastMessage?.createdAt?.let {
|
|
try { java.time.ZonedDateTime.parse(it).toInstant().toEpochMilli() }
|
|
catch (e: Exception) { null }
|
|
}
|
|
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 = lastMessage?.content,
|
|
lastMessageTimestamp = timestamp
|
|
)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|