package chats.data.sync import android.content.Context import android.util.Log import androidx.hilt.work.HiltWorker import androidx.work.* import chats.data.remote.api.ChatApi import chats.data.remote.api.SendMessageRequest import core.database.data.MessageDao import core.database.data.MessageEntity import core.database.data.SyncStatus import core.network.ServerConfig import core.security.TokenManager import com.google.gson.Gson import dagger.assisted.Assisted import dagger.assisted.AssistedInject import java.util.concurrent.TimeUnit /** * WorkManager Worker для фоновой синхронизации сообщений * * Обрабатывает: * 1. Отправку новых сообщений (SYNCING) * 2. Обновление отредактированных сообщений (isEditedLocally = true) * 3. Удаление сообщений (isDeletedLocally = true) * 4. Повторную отправку при ошибках (FAILED) * * Политика повторных попыток: * - Экспоненциальная задержка (backoff) * - Максимум 3 попытки * - Таймаут 10 минут на выполнение */ @HiltWorker class MessageSyncWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted params: WorkerParameters, private val dao: MessageDao, private val api: ChatApi, private val serverConfig: ServerConfig, private val tokenManager: TokenManager ) : CoroutineWorker(appContext, params) { private val gson = Gson() companion object { const val WORK_NAME = "message_sync_worker" private const val TAG = "MessageSyncWorker" /** * Планирует синхронизацию * Вызывается при изменении сообщений в базе */ fun scheduleSync(context: Context) { Log.d(TAG, "Scheduling sync") val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(false) .build() val workRequest = OneTimeWorkRequestBuilder() .setConstraints(constraints) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS ) .addTag(WORK_NAME) .build() WorkManager.getInstance(context).enqueueUniqueWork( WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest ) Log.d(TAG, "Sync scheduled") } /** * Отменяет запланированную синхронизацию */ fun cancelSync(context: Context) { WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) Log.d(TAG, "Sync cancelled") } } override suspend fun doWork(): Result { Log.d(TAG, "Starting message sync. Attempt: ${runAttemptCount + 1}") if (runAttemptCount >= 3) { Log.e(TAG, "Max retry attempts reached") return Result.failure() } return try { val pendingMessages = dao.getPendingSyncMessages() Log.d(TAG, "Found ${pendingMessages.size} messages to sync") if (pendingMessages.isEmpty()) { Log.d(TAG, "No pending messages. Sync complete.") return Result.success() } var successCount = 0 var failureCount = 0 for (message in pendingMessages) { try { when { message.isDeletedLocally -> { handleDeleteMessage(message) successCount++ } message.isEditedLocally -> { handleEditMessage(message) successCount++ } message.syncStatus == SyncStatus.SYNCING -> { handleSendMessage(message) successCount++ } } } catch (e: Exception) { Log.e(TAG, "Failed to sync message ${message.id}", e) dao.markAsSyncFailed(message.id) failureCount++ } } Log.d(TAG, "Sync completed. Success: $successCount, Failed: $failureCount") if (failureCount > 0 && successCount == 0) { Result.retry() } else { Result.success() } } catch (e: Exception) { Log.e(TAG, "Sync failed with exception", e) Result.retry() } } private suspend fun handleSendMessage(message: MessageEntity) { Log.d(TAG, "Sending message: ${message.id}") val attachments = parseAttachments(message.mediaJson) val request = SendMessageRequest( content = message.content, type = message.mediaType.lowercase(), attachments = attachments, replyToId = message.replyToId ) val response = api.sendMessage(message.chatId, request) val syncedMessage = message.copy( id = response.id, sequenceId = response.sequenceId ?: message.sequenceId, createdAt = response.createdAt ?: message.createdAt, syncStatus = SyncStatus.SYNCED, isDeletedLocally = false, isEditedLocally = false, editedContent = null, lastUpdated = System.currentTimeMillis() ) dao.insertMessage(syncedMessage) Log.d(TAG, "Message sent successfully: ${response.id}") } private suspend fun handleEditMessage(message: MessageEntity) { Log.d(TAG, "Editing message: ${message.id}") val newContent = message.editedContent ?: message.content val request = SendMessageRequest(content = newContent) val response = api.editMessage(message.id, request) val syncedMessage = message.copy( content = response.content, syncStatus = SyncStatus.SYNCED, isEditedLocally = false, editedContent = null, lastUpdated = System.currentTimeMillis() ) dao.insertMessage(syncedMessage) Log.d(TAG, "Message edited successfully: ${message.id}") } private suspend fun handleDeleteMessage(message: MessageEntity) { Log.d(TAG, "Deleting message: ${message.id}") val response = api.deleteMessage(message.id, forEveryone = false) if (response.isSuccessful || response.code() == 404) { dao.deleteMessage(message.id) Log.d(TAG, "Message deleted successfully: ${message.id}") } else { throw Exception("Delete failed with code: ${response.code()}") } } private fun parseAttachments(mediaJson: String): List? { return try { val mediaList = gson.fromJson(mediaJson, Array::class.java) ?.map { elem -> val map = elem as Map<*, *> chats.data.remote.api.AttachmentRequest( type = map["type"] as? String ?: "file", url = map["url"] as? String ?: "", fileName = map["filename"] as? String ?: "file", fileSize = (map["size"] as? Number)?.toLong() ?: 0L ) } mediaList?.takeIf { it.isNotEmpty() } } catch (e: Exception) { Log.e(TAG, "Failed to parse attachments", e) null } } }