226 lines
7.9 KiB
Kotlin
226 lines
7.9 KiB
Kotlin
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<MessageSyncWorker>()
|
|
.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<chats.data.remote.api.AttachmentRequest>? {
|
|
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
|
|
}
|
|
}
|
|
}
|