Кэш чатов
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user