151 lines
5.9 KiB
Kotlin
151 lines
5.9 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.MessageDao
|
||
import chats.data.local.database.MessageEntity
|
||
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<SendMessageWorker>()
|
||
.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
|
||
val request = SendMessageRequest(
|
||
content = message.content,
|
||
type = message.mediaType.lowercase(),
|
||
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"))
|
||
)
|
||
}
|
||
}
|
||
}
|