Remove Media tab from backup playground.

This commit is contained in:
Cody Henthorne
2025-05-29 12:29:56 -04:00
parent 9083359b33
commit 21e53e360e
3 changed files with 2 additions and 490 deletions

View File

@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.backup.v2
import android.database.Cursor
import android.os.Environment
import android.os.StatFs
import androidx.annotation.Discouraged
import androidx.annotation.WorkerThread
import okio.ByteString
import okio.ByteString.Companion.toByteString
@@ -35,7 +34,6 @@ import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
@@ -1105,21 +1103,6 @@ object BackupRepository {
.also { Log.i(TAG, "UploadBackupFileResult: $it") }
}
/**
* A simple test method that just hits various network endpoints. Only useful for the playground.
*
* @return True if successful, otherwise false.
*/
@Discouraged("This will upload the entire backup file on every execution.")
fun debugUploadBackupFile(backupStream: InputStream, backupStreamLength: Long): NetworkResult<Unit> {
return getResumableMessagesBackupUploadSpec()
.then { formAndUploadUrl ->
val (form, resumableUploadUrl) = formAndUploadUrl
SignalNetwork.archive.uploadBackupFile(form, resumableUploadUrl, backupStream, backupStreamLength)
.also { Log.i(TAG, "UploadBackupFileResult: $it") }
}
}
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): NetworkResult<Unit> {
return initBackupAndFetchAuth()
.then { credential ->
@@ -1208,74 +1191,6 @@ object BackupRepository {
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun copyAttachmentToArchive(databaseAttachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResult> {
return initBackupAndFetchAuth()
.then { credential ->
val requests = mutableListOf<ArchiveMediaRequest>()
val mediaIdToAttachmentId = mutableMapOf<String, AttachmentId>()
val attachmentIdToMediaName = mutableMapOf<AttachmentId, String>()
databaseAttachments.forEach {
val mediaName = it.requireMediaName()
val request = it.toArchiveMediaRequest(mediaName, credential.mediaBackupAccess.backupKey)
requests += request
mediaIdToAttachmentId[request.mediaId] = it.attachmentId
attachmentIdToMediaName[it.attachmentId] = mediaName.name
}
SignalNetwork.archive
.copyAttachmentToArchive(
aci = SignalStore.account.requireAci(),
archiveServiceAccess = credential.mediaBackupAccess,
items = requests
)
.map { credential to BatchArchiveMediaResult(it, mediaIdToAttachmentId, attachmentIdToMediaName) }
}
.map { (credential, result) ->
result
.successfulResponses
.forEach {
val attachmentId = result.mediaIdToAttachmentId(it.mediaId)
val mediaName = result.attachmentIdToMediaName(attachmentId)
val thumbnailId = credential.mediaBackupAccess.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode()
SignalDatabase.attachments.setArchiveCdn(attachmentId = attachmentId, archiveCdn = it.cdn!!)
}
result
}
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
val mediaToDelete = attachments
.filter { it.archiveTransferState == AttachmentTable.ArchiveTransferState.FINISHED }
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.archiveCdn,
mediaId = it.requireMediaName().toMediaId(mediaRootBackupKey).encode()
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
return NetworkResult.Success(Unit)
}
return initBackupAndFetchAuth()
.then { credential ->
SignalNetwork.archive.deleteArchivedMedia(
aci = SignalStore.account.requireAci(),
archiveServiceAccess = credential.mediaBackupAccess,
mediaToDelete = mediaToDelete
)
}
.map {
SignalDatabase.attachments.clearArchiveData(attachments.map { it.attachmentId })
}
.also { Log.i(TAG, "deleteArchivedMediaResult: $it") }
}
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
val mediaToDelete = mediaObjects
.map {

View File

@@ -11,26 +11,16 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -45,7 +35,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -54,9 +43,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
@@ -81,9 +68,7 @@ import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.getLength
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.compose.RoundCheckbox
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.DialogState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
import org.thoughtcrime.securesms.compose.ComposeFragment
@@ -148,15 +133,9 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
override fun FragmentContent() {
val context = LocalContext.current
val state by viewModel.state
val mediaState by viewModel.mediaState
LaunchedEffect(Unit) {
viewModel.loadMedia()
}
Tabs(
onBack = { findNavController().popBackStack() },
onDeleteAllArchivedMedia = { viewModel.deleteAllArchivedMedia() },
mainContent = {
Screen(
state = state,
@@ -246,19 +225,6 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
.show()
}
)
},
mediaContent = { snackbarHostState ->
MediaList(
enabled = SignalStore.backup.backsUpMedia,
state = mediaState,
snackbarHostState = snackbarHostState,
archiveAttachmentMedia = { viewModel.archiveAttachmentMedia(it) },
deleteArchivedMedia = { viewModel.deleteArchivedMedia(it) },
batchArchiveAttachmentMedia = { viewModel.archiveAttachmentMedia(it) },
batchDeleteBackupAttachmentMedia = { viewModel.deleteArchivedMedia(it) },
restoreArchivedMedia = { viewModel.restoreArchivedMedia(it, asThumbnail = false) },
restoreArchivedMediaThumbnail = { viewModel.restoreArchivedMedia(it, asThumbnail = true) }
)
}
)
}
@@ -268,11 +234,9 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
@Composable
fun Tabs(
onBack: () -> Unit,
onDeleteAllArchivedMedia: () -> Unit,
mainContent: @Composable () -> Unit,
mediaContent: @Composable (snackbarHostState: SnackbarHostState) -> Unit
mainContent: @Composable () -> Unit
) {
val tabs = listOf("Main", "Media")
val tabs = listOf("Main")
var tabIndex by remember { mutableIntStateOf(0) }
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
@@ -293,13 +257,6 @@ fun Tabs(
contentDescription = null
)
}
},
actions = {
if (tabIndex == 1 && SignalStore.backup.backsUpMedia) {
TextButton(onClick = onDeleteAllArchivedMedia) {
Text(text = "Delete All")
}
}
}
)
TabRow(selectedTabIndex = tabIndex) {
@@ -317,7 +274,6 @@ fun Tabs(
Surface(modifier = Modifier.padding(it)) {
when (tabIndex) {
0 -> mainContent()
1 -> mediaContent(snackbarHostState)
}
}
}
@@ -604,181 +560,6 @@ private fun ImportCredentialsDialog(onSubmit: (aci: String, backupKey: String) -
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MediaList(
enabled: Boolean,
state: InternalBackupPlaygroundViewModel.MediaState,
snackbarHostState: SnackbarHostState,
archiveAttachmentMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
deleteArchivedMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
batchArchiveAttachmentMedia: (Set<AttachmentId>) -> Unit,
batchDeleteBackupAttachmentMedia: (Set<AttachmentId>) -> Unit,
restoreArchivedMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
restoreArchivedMediaThumbnail: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit
) {
if (!enabled) {
Text(
text = "You do not have read/write to archive cdn enabled via SignalStore.backup",
modifier = Modifier
.padding(16.dp)
)
return
}
LaunchedEffect(state.error?.id) {
state.error?.let {
snackbarHostState.showSnackbar(it.errorText)
}
}
val haptics = LocalHapticFeedback.current
var selectionState by remember { mutableStateOf(MediaMultiSelectState()) }
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(
count = state.attachments.size,
key = { index -> state.attachments[index].id }
) { index ->
val attachment = state.attachments[index]
Row(
modifier = Modifier
.combinedClickable(onClick = {
if (selectionState.selecting) {
selectionState = selectionState.copy(selected = if (selectionState.selected.contains(attachment.id)) selectionState.selected - attachment.id else selectionState.selected + attachment.id)
}
}, onLongClick = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
selectionState = if (selectionState.selecting) MediaMultiSelectState() else MediaMultiSelectState(selecting = true, selected = setOf(attachment.id))
})
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if (selectionState.selecting) {
RoundCheckbox(
checked = selectionState.selected.contains(attachment.id),
onCheckedChange = { selected ->
selectionState = selectionState.copy(selected = if (selected) selectionState.selected + attachment.id else selectionState.selected - attachment.id)
}
)
}
Column(modifier = Modifier.weight(1f, true)) {
Text(text = attachment.title)
Text(text = "State: ${attachment.state}")
}
if (attachment.state == InternalBackupPlaygroundViewModel.BackupAttachment.State.IN_PROGRESS) {
CircularProgressIndicator()
} else {
Button(
enabled = !selectionState.selecting,
onClick = {
when (attachment.state) {
InternalBackupPlaygroundViewModel.BackupAttachment.State.ATTACHMENT_CDN,
InternalBackupPlaygroundViewModel.BackupAttachment.State.LOCAL_ONLY -> archiveAttachmentMedia(attachment)
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_UNDOWNLOADED,
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_FINAL -> selectionState = selectionState.copy(expandedOption = attachment.dbAttachment.attachmentId)
else -> throw AssertionError("Unsupported state: ${attachment.state}")
}
}
) {
Text(
text = when (attachment.state) {
InternalBackupPlaygroundViewModel.BackupAttachment.State.ATTACHMENT_CDN,
InternalBackupPlaygroundViewModel.BackupAttachment.State.LOCAL_ONLY -> "Backup"
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_UNDOWNLOADED,
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_FINAL -> "Options..."
else -> throw AssertionError("Unsupported state: ${attachment.state}")
}
)
DropdownMenu(
expanded = attachment.dbAttachment.attachmentId == selectionState.expandedOption,
onDismissRequest = { selectionState = selectionState.copy(expandedOption = null) }
) {
DropdownMenuItem(
text = { Text("Remote Delete") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
deleteArchivedMedia(attachment)
}
)
DropdownMenuItem(
text = { Text("Pseudo Restore") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
restoreArchivedMedia(attachment)
}
)
DropdownMenuItem(
text = { Text("Pseudo Restore Thumbnail") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
restoreArchivedMediaThumbnail(attachment)
}
)
if (attachment.dbAttachment.dataHash != null && attachment.state == InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_UNDOWNLOADED) {
DropdownMenuItem(
text = { Text("Re-copy with hash") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
archiveAttachmentMedia(attachment)
}
)
}
}
}
}
}
}
}
if (selectionState.selecting) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
.background(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(8.dp)
)
.padding(8.dp)
) {
Button(onClick = { selectionState = MediaMultiSelectState() }) {
Text("Cancel")
}
Button(onClick = {
batchArchiveAttachmentMedia(selectionState.selected)
selectionState = MediaMultiSelectState()
}) {
Text("Backup")
}
Button(onClick = {
batchDeleteBackupAttachmentMedia(selectionState.selected)
selectionState = MediaMultiSelectState()
}) {
Text("Delete")
}
}
}
}
}
private data class MediaMultiSelectState(
val selecting: Boolean = false,
val selected: Set<AttachmentId> = emptySet(),
val expandedOption: AttachmentId? = null
)
@SignalPreview
@Composable
fun PreviewScreen() {

View File

@@ -12,7 +12,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
@@ -43,19 +42,12 @@ import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver.FailureCause
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader.Companion.MAC_SIZE
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageType
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.CopyAttachmentToArchiveJob
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
import org.thoughtcrime.securesms.jobs.RestoreAttachmentThumbnailJob
import org.thoughtcrime.securesms.jobs.RestoreLocalAttachmentJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.NetworkResult
@@ -92,9 +84,6 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
)
val state: State<ScreenState> = _state
private val _mediaState: MutableState<MediaState> = mutableStateOf(MediaState())
val mediaState: State<MediaState> = _mediaState
enum class DialogState {
None,
ImportCredentials
@@ -359,179 +348,6 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
}
}
fun loadMedia() {
disposables += Single
.fromCallable { SignalDatabase.attachments.debugGetLatestAttachments() }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.subscribeBy {
_mediaState.set { update(attachments = it.map { a -> BackupAttachment(dbAttachment = a) }) }
}
}
fun archiveAttachmentMedia(attachments: Set<AttachmentId>) {
disposables += Single
.fromCallable {
val toArchive = mediaState.value
.attachments
.filter { attachments.contains(it.dbAttachment.attachmentId) }
.map { it.dbAttachment }
BackupRepository.copyAttachmentToArchive(toArchive)
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + attachments) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - attachments) } }
.subscribeBy { result ->
when (result) {
is NetworkResult.Success -> {
loadMedia()
result
.result
.sourceNotFoundResponses
.forEach {
reUploadAndArchiveMedia(result.result.mediaIdToAttachmentId(it.mediaId))
}
}
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
}
fun archiveAttachmentMedia(attachment: BackupAttachment) {
disposables += Single.fromCallable { BackupRepository.copyAttachmentToArchive(attachment.dbAttachment) }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + attachment.dbAttachment.attachmentId) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - attachment.dbAttachment.attachmentId) } }
.subscribeBy { result ->
when (result) {
is NetworkResult.Success -> loadMedia()
is NetworkResult.StatusCodeError -> {
if (result.code == 410) {
reUploadAndArchiveMedia(attachment.id)
} else {
_mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
}
private fun reUploadAndArchiveMedia(attachmentId: AttachmentId) {
disposables += Single
.fromCallable {
AppDependencies
.jobManager
.startChain(AttachmentUploadJob(attachmentId))
.then(CopyAttachmentToArchiveJob(attachmentId))
.enqueueAndBlockUntilCompletion(15.seconds.inWholeMilliseconds)
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + attachmentId) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - attachmentId) } }
.subscribeBy {
if (it.isPresent && it.get().isComplete) {
loadMedia()
} else {
_mediaState.set { copy(error = MediaStateError(errorText = "Reupload slow or failed, try again")) }
}
}
}
fun deleteArchivedMedia(attachmentIds: Set<AttachmentId>) {
deleteArchivedMedia(mediaState.value.attachments.filter { attachmentIds.contains(it.dbAttachment.attachmentId) })
}
fun deleteArchivedMedia(attachment: BackupAttachment) {
deleteArchivedMedia(listOf(attachment))
}
private fun deleteArchivedMedia(attachments: List<BackupAttachment>) {
val ids = attachments.map { it.dbAttachment.attachmentId }.toSet()
disposables += Single.fromCallable { BackupRepository.deleteArchivedMedia(attachments.map { it.dbAttachment }) }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + ids) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - ids) } }
.subscribeBy {
when (it) {
is NetworkResult.Success -> loadMedia()
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$it")) }
}
}
}
fun deleteAllArchivedMedia() {
disposables += Single
.fromCallable { BackupRepository.debugDeleteAllArchivedMedia() }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.subscribeBy { result ->
when (result) {
is NetworkResult.Success -> loadMedia()
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
}
fun restoreArchivedMedia(attachment: BackupAttachment, asThumbnail: Boolean) {
disposables += Completable
.fromCallable {
val recipientId = SignalStore.releaseChannel.releaseChannelRecipientId!!
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
val message = IncomingMessage(
type = MessageType.NORMAL,
from = recipientId,
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
body = "Restored from Archive!?",
serverGuid = UUID.randomUUID().toString()
)
val insertMessage = SignalDatabase.messages.insertMessageInbox(message, threadId).get()
SignalDatabase.attachments.debugCopyAttachmentForArchiveRestore(
mmsId = insertMessage.messageId,
attachment = attachment.dbAttachment,
forThumbnail = asThumbnail
)
val archivedAttachment = SignalDatabase.attachments.getAttachmentsForMessage(insertMessage.messageId).first()
if (asThumbnail) {
AppDependencies.jobManager.add(
RestoreAttachmentThumbnailJob(
messageId = insertMessage.messageId,
attachmentId = archivedAttachment.attachmentId,
highPriority = false
)
)
} else {
AppDependencies.jobManager.add(
RestoreAttachmentJob.forInitialRestore(
messageId = insertMessage.messageId,
attachmentId = archivedAttachment.attachmentId
)
)
}
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.subscribeBy(
onError = {
_mediaState.set { copy(error = MediaStateError(errorText = "$it")) }
}
)
}
suspend fun deleteRemoteBackupData(): Boolean = withContext(Dispatchers.IO) {
when (val result = BackupRepository.debugDeleteAllArchivedMedia()) {
is NetworkResult.Success -> Log.i(TAG, "Remote data deleted")