228 lines
9.6 KiB
Kotlin
228 lines
9.6 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.data.remote.dto.*
|
||
import chats.data.repository.toDomain
|
||
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,
|
||
private val serverConfig: core.network.ServerConfig
|
||
) : 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>()
|
||
val currentUserId = tokenManager.getUserId() ?: ""
|
||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||
val gson = com.google.gson.Gson()
|
||
|
||
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 lastMessageDomain = lastMessage?.toDomain(currentUserId, baseUrl)
|
||
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,
|
||
lastMessageJson = lastMessageDomain?.let { gson.toJson(it) }
|
||
)
|
||
localChats.add(chatEntity)
|
||
}
|
||
|
||
// Если сервер не вернул Избранное — создаём/получаем его отдельно
|
||
val hasFavorites = remoteChats.any { it.type == "favorites" }
|
||
if (!hasFavorites) {
|
||
try {
|
||
val favDto = chatApi.getOrCreateFavorites()
|
||
val existingChat = chatDao.getChatByRemoteId(favDto.id)
|
||
val favTimestamp = favDto.messages.firstOrNull()?.createdAt?.let {
|
||
try { java.time.ZonedDateTime.parse(it).toInstant().toEpochMilli() } catch (e: Exception) { null }
|
||
}
|
||
val favLastMessageDomain = favDto.messages.firstOrNull()?.toDomain(currentUserId, baseUrl)
|
||
val favEntity = chats.data.local.database.ChatEntity(
|
||
localId = existingChat?.localId ?: java.util.UUID.randomUUID().toString(),
|
||
remoteId = favDto.id,
|
||
type = favDto.type,
|
||
name = favDto.name ?: "Избранное",
|
||
avatar = favDto.avatar,
|
||
unreadCount = favDto.unreadCount,
|
||
lastMessageText = favDto.messages.firstOrNull()?.content,
|
||
lastMessageTimestamp = favTimestamp,
|
||
lastMessageJson = favLastMessageDomain?.let { gson.toJson(it) }
|
||
)
|
||
localChats.add(favEntity)
|
||
Log.d("ChatSyncWorker", "Created favorites chat: ${favDto.id}")
|
||
} catch (e: Exception) {
|
||
Log.e("ChatSyncWorker", "Failed to get or create favorites", e)
|
||
}
|
||
}
|
||
|
||
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()
|
||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||
|
||
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?.let { if (it.startsWith("http")) it else "$baseUrl$it" },
|
||
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 { mediaItem ->
|
||
chats.domain.model.Media(
|
||
id = mediaItem.id,
|
||
type = mediaItem.type,
|
||
url = (mediaItem.url as? String ?: "").let { url -> if (url.startsWith("http")) url else "$baseUrl$url" },
|
||
filename = mediaItem.filename,
|
||
size = mediaItem.size,
|
||
duration = mediaItem.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)
|
||
}
|
||
}
|
||
}
|
||
}
|