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.MessageDao import chats.data.local.database.MessageEntity import chats.data.remote.api.AttachmentRequest import chats.data.remote.api.ChatApi import chats.data.remote.api.SendMessageRequest import chats.domain.model.MessageStatus 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 для отправки сообщений с поддержкой офлайн-режима. * Использует WorkManager для гарантированной доставки при появлении сети. */ @HiltWorker class SendMessageWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val messageDao: MessageDao, private val chatApi: ChatApi ) : CoroutineWorker(context, params) { companion object { const val WORK_TAG = "send_message_worker" const val KEY_LOCAL_ID = "local_id" const val MAX_RETRY_COUNT = 5 /** * Создает запрос на отправку сообщения через WorkManager */ fun createWorkRequest(localId: String): OneTimeWorkRequest { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val inputData = workDataOf(KEY_LOCAL_ID to localId) return OneTimeWorkRequestBuilder() .setConstraints(constraints) .setInputData(inputData) .addTag(WORK_TAG) .addTag("$WORK_TAG:$localId") .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS ) .build() } } override suspend fun doWork(): Result = withContext(Dispatchers.IO) { val localId = inputData.getString(KEY_LOCAL_ID) ?: return@withContext Result.failure() Log.d("SendMessageWorker", "Starting work for message: $localId") try { // Получаем сообщение из локальной БД val message = messageDao.getMessageByLocalId(localId) ?: return@withContext Result.failure( workDataOf("error" to "Message not found: $localId") ) // Проверяем, не отправлено ли уже if (message.status == MessageStatus.SENT || message.status == MessageStatus.DELIVERED || message.status == MessageStatus.READ) { Log.d("SendMessageWorker", "Message already sent: $localId") return@withContext Result.success() } // Проверяем лимит повторных попыток if (message.retryCount >= MAX_RETRY_COUNT) { Log.w("SendMessageWorker", "Max retries reached for message: $localId") messageDao.updateMessageError( localId = localId, status = MessageStatus.FAILED, errorMessage = "Max retry count exceeded", updatedAtMillis = System.currentTimeMillis() ) return@withContext Result.failure( workDataOf("error" to "Max retries reached") ) } // Обновляем статус на SENDING messageDao.updateMessageStatus( localId = localId, status = MessageStatus.SENDING, updatedAtMillis = System.currentTimeMillis() ) // Выполняем запрос к API с Idempotency-Key // Десериализуем media из JSON val mediaList = try { val gson = com.google.gson.Gson() val type = object : com.google.gson.reflect.TypeToken>() {}.type gson.fromJson>(message.mediaJson, type) ?: emptyList() } catch (e: Exception) { emptyList() } val attachments = mediaList.map { AttachmentRequest( type = it.type, url = it.url, fileName = it.filename ?: "", fileSize = it.size ?: 0L ) }.takeIf { it.isNotEmpty() } val request = SendMessageRequest( content = message.content, type = message.mediaType.lowercase(), attachments = attachments, replyToId = message.replyToServerId ) val serverId = chatApi.sendMessage( chatId = message.chatId, request = request, idempotencyKey = message.idempotencyKey ) // Успешная отправка - обновляем статус и serverId messageDao.updateMessageWithServerId( localId = localId, serverId = serverId, status = MessageStatus.SENT, updatedAtMillis = System.currentTimeMillis() ) Log.i("SendMessageWorker", "Message sent successfully: local=$localId, server=$serverId") Result.success() } catch (e: IOException) { // Сетевая ошибка - повторяем позже Log.w("SendMessageWorker", "Network error for message $localId: ${e.message}") Result.retry() } catch (e: SocketTimeoutException) { Log.w("SendMessageWorker", "Timeout for message $localId: ${e.message}") Result.retry() } catch (e: Exception) { // Остальные ошибки - помечаем как FAILED Log.e("SendMessageWorker", "Error sending message $localId", e) try { messageDao.updateMessageError( localId = localId, status = MessageStatus.FAILED, errorMessage = e.message ?: "Unknown error", updatedAtMillis = System.currentTimeMillis() ) } catch (dbError: Exception) { Log.e("SendMessageWorker", "Failed to update error status", dbError) } Result.failure( workDataOf("error" to (e.message ?: "Unknown error")) ) } } }