diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt index 6f861efb00..017ca5a81c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import org.signal.core.util.throttleLatest +import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState @@ -116,4 +117,49 @@ object ArchiveUploadProgress { _progress.tryEmit(Unit) } } + + object ArchiveBackupProgressListener : BackupRepository.ExportProgressListener { + override fun onAccount() { + updatePhase(ArchiveUploadProgressState.BackupPhase.Account) + } + + override fun onRecipient() { + updatePhase(ArchiveUploadProgressState.BackupPhase.Recipient) + } + + override fun onThread() { + updatePhase(ArchiveUploadProgressState.BackupPhase.Thread) + } + + override fun onCall() { + updatePhase(ArchiveUploadProgressState.BackupPhase.Call) + } + + override fun onSticker() { + updatePhase(ArchiveUploadProgressState.BackupPhase.Sticker) + } + + override fun onMessage(currentProgress: Long, approximateCount: Long) { + updatePhase(ArchiveUploadProgressState.BackupPhase.Message, currentProgress, approximateCount) + } + + override fun onAttachment(currentProgress: Long, totalCount: Long) { + updatePhase(ArchiveUploadProgressState.BackupPhase.BackupPhaseNone) + } + + private fun updatePhase( + phase: ArchiveUploadProgressState.BackupPhase, + completedObjects: Long = 0L, + totalObjects: Long = 0L + ) { + updateState( + state = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = phase, + completedAttachments = completedObjects, + totalAttachments = totalObjects + ) + ) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index c388e7b9dd..d7fc780643 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -450,6 +450,7 @@ object BackupRepository { plaintext: Boolean = false, currentTime: Long = System.currentTimeMillis(), mediaBackupEnabled: Boolean = SignalStore.backup.backsUpMedia, + progressEmitter: ExportProgressListener? = null, cancellationSignal: () -> Boolean = { false }, exportExtras: ((SignalDatabase) -> Unit)? = null ) { @@ -464,7 +465,15 @@ object BackupRepository { ) } - export(currentTime = currentTime, isLocal = false, writer = writer, mediaBackupEnabled = mediaBackupEnabled, cancellationSignal = cancellationSignal, exportExtras = exportExtras) + export( + currentTime = currentTime, + isLocal = false, + writer = writer, + progressEmitter = progressEmitter, + mediaBackupEnabled = mediaBackupEnabled, + cancellationSignal = cancellationSignal, + exportExtras = exportExtras + ) } /** @@ -567,7 +576,9 @@ object BackupRepository { return@export } - progressEmitter?.onMessage() + val approximateMessageCount = dbSnapshot.messageTable.getApproximateExportableMessageCount(exportState.threadIds) + val frameCountStart = frameCount + progressEmitter?.onMessage(0, approximateMessageCount) ChatItemArchiveProcessor.export(dbSnapshot, exportState, selfRecipientId, cancellationSignal) { frame -> writer.write(frame) eventTimer.emit("message") @@ -575,6 +586,7 @@ object BackupRepository { if (frameCount % 1000 == 0L) { Log.d(TAG, "[export] Exported $frameCount frames so far.") + progressEmitter?.onMessage(frameCount - frameCountStart, approximateMessageCount) if (cancellationSignal()) { Log.w(TAG, "[export] Cancelled! Stopping") return@export @@ -1436,7 +1448,7 @@ object BackupRepository { fun onThread() fun onCall() fun onSticker() - fun onMessage() + fun onMessage(currentProgress: Long, approximateCount: Long) fun onAttachment(currentProgress: Long, totalCount: Long) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt index e851f789b4..52800e768f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt @@ -169,8 +169,10 @@ object LocalArchiver { EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_STICKER)) } - override fun onMessage() { - EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_MESSAGE)) + override fun onMessage(currentProgress: Long, approximateCount: Long) { + if (currentProgress == 0L) { + EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_MESSAGE)) + } } override fun onAttachment(currentProgress: Long, totalCount: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 3b82b0adb5..0179edee43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -105,6 +105,7 @@ import org.thoughtcrime.securesms.util.viewModel import java.math.BigDecimal import java.util.Currency import java.util.Locale +import kotlin.math.max import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds @@ -928,9 +929,6 @@ private fun SubscriptionMismatchMissingGooglePlayCard( private fun InProgressBackupRow( archiveUploadProgressState: ArchiveUploadProgressState ) { - val progress = archiveUploadProgressState.completedAttachments - val totalProgress = archiveUploadProgressState.totalAttachments - Row( modifier = Modifier .padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter)) @@ -939,23 +937,18 @@ private fun InProgressBackupRow( Column( modifier = Modifier.weight(1f) ) { - if (totalProgress == 0L) { + val backupProgress = getBackupProgress(archiveUploadProgressState) + if (backupProgress.total == 0L) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } else { LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - progress = { ((progress ?: 0) / totalProgress).toFloat() } + progress = { backupProgress.progress } ) } - val inProgressText = if (totalProgress == 0L) { - getProgressStateMessage(archiveUploadProgressState.state) - } else { - stringResource(R.string.RemoteBackupsSettingsFragment__d_slash_d, progress ?: 0, totalProgress) - } - Text( - text = inProgressText, + text = getProgressStateMessage(archiveUploadProgressState), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -963,15 +956,46 @@ private fun InProgressBackupRow( } } -@Composable -private fun getProgressStateMessage(state: ArchiveUploadProgressState.State): String { - val stringId = when (state) { - ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.BackingUpMessages -> R.string.RemoteBackupsSettingsFragment__processing_backup - ArchiveUploadProgressState.State.UploadingMessages -> R.string.RemoteBackupsSettingsFragment__uploading_messages - ArchiveUploadProgressState.State.UploadingAttachments -> R.string.RemoteBackupsSettingsFragment__processing_backup - } +private fun getBackupProgress(state: ArchiveUploadProgressState): BackupProgress { + val approximateMessageCount = max(state.completedAttachments, state.totalAttachments) + return BackupProgress(state.completedAttachments, approximateMessageCount) +} - return stringResource(stringId) +@Composable +private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String { + return when (archiveUploadProgressState.state) { + ArchiveUploadProgressState.State.None -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup) + ArchiveUploadProgressState.State.BackingUpMessages -> getBackupPhaseMessage(archiveUploadProgressState) + ArchiveUploadProgressState.State.UploadingMessages -> stringResource(R.string.RemoteBackupsSettingsFragment__uploading_messages) + ArchiveUploadProgressState.State.UploadingAttachments -> getUploadingAttachmentsMessage(archiveUploadProgressState) + } +} + +@Composable +private fun getBackupPhaseMessage(state: ArchiveUploadProgressState): String { + return when (state.backupPhase) { + ArchiveUploadProgressState.BackupPhase.BackupPhaseNone -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup) + ArchiveUploadProgressState.BackupPhase.Message -> { + val progress = getBackupProgress(state) + pluralStringResource( + R.plurals.RemoteBackupsSettingsFragment__processing_d_of_d_d_messages, + progress.total.toInt(), + progress.completed, + progress.total, + (progress.progress * 100).toInt() + ) + } + else -> stringResource(R.string.RemoteBackupsSettingsFragment__preparing_backup) + } +} + +@Composable +private fun getUploadingAttachmentsMessage(state: ArchiveUploadProgressState): String { + return if (state.totalAttachments == 0L) { + stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup) + } else { + stringResource(R.string.RemoteBackupsSettingsFragment__d_slash_d, state.completedAttachments, state.totalAttachments) + } } @Composable @@ -1368,7 +1392,69 @@ private fun LastBackupRowPreview() { @Composable private fun InProgressRowPreview() { Previews.Preview { - InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState()) + Column { + InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState()) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Account + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Call + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Sticker + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Recipient + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Thread + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Message, + completedAttachments = 1, + totalAttachments = 1 + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Message, + completedAttachments = 1000, + totalAttachments = 100_000 + ) + ) + InProgressBackupRow( + archiveUploadProgressState = ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.BackingUpMessages, + backupPhase = ArchiveUploadProgressState.BackupPhase.Message, + completedAttachments = 1_000_000, + totalAttachments = 100_000 + ) + ) + } } } @@ -1434,3 +1520,10 @@ private fun BackupFrequencyDialogPreview() { ) } } + +private data class BackupProgress( + val completed: Long, + val total: Long +) { + val progress: Float = if (total > 0) completed / total.toFloat() else 0f +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index cf32a949fc..0b0fffb0e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -1808,6 +1808,21 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat .readToSingleInt() } + /** + * Given a set of thread ids, return the count of all messages in the table that match that thread id. This will include *all* messages, and is + * explicitly for use as a "fuzzy total" + */ + fun getApproximateExportableMessageCount(threadIds: Set): Long { + val queries = SqlUtil.buildCollectionQuery(THREAD_ID, threadIds) + return queries.sumOf { + readableDatabase.count() + .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_COUNT") + .where(it.where, it.whereArgs) + .run() + .readToSingleLong(0L) + } + } + fun canSetUniversalTimer(threadId: Long): Boolean { if (threadId == -1L) { return true diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index ce593034b7..dcc38570da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -209,7 +209,7 @@ class BackupMessagesJob private constructor( val outputStream = FileOutputStream(tempBackupFile) val backupKey = SignalStore.backup.messageBackupKey val currentTime = System.currentTimeMillis() - BackupRepository.export(outputStream = outputStream, messageBackupKey = backupKey, append = { tempBackupFile.appendBytes(it) }, plaintext = false, cancellationSignal = { this.isCanceled }, currentTime = currentTime) { + BackupRepository.export(outputStream = outputStream, messageBackupKey = backupKey, progressEmitter = ArchiveUploadProgress.ArchiveBackupProgressListener, append = { tempBackupFile.appendBytes(it) }, plaintext = false, cancellationSignal = { this.isCanceled }, currentTime = currentTime) { writeMediaCursorToTemporaryTable(it, currentTime = currentTime, mediaBackupEnabled = SignalStore.backup.backsUpMedia) } diff --git a/app/src/main/protowire/KeyValue.proto b/app/src/main/protowire/KeyValue.proto index 5dd3588864..550dfb0a2e 100644 --- a/app/src/main/protowire/KeyValue.proto +++ b/app/src/main/protowire/KeyValue.proto @@ -24,7 +24,22 @@ message ArchiveUploadProgressState { UploadingAttachments = 3; } + /** + * Describes the current phase the backup is in when we are exporting the database + * to the temporary file. + */ + enum BackupPhase { + BackupPhaseNone = 0; + Account = 1; + Recipient = 2; + Thread = 3; + Call = 4; + Sticker = 5; + Message = 6; + } + State state = 1; uint64 completedAttachments = 2; uint64 totalAttachments = 3; + BackupPhase backupPhase = 4; } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc9cedc9eb..86f695cb5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7772,6 +7772,13 @@ Learn more Processing backup… + + Preparing backup… + + + Processing %1$d of %2$d (%3$d%%) message + Processing %1$d of %2$d (%3$d%%) messages + You have %1$s of backup data that’s not on this device. Your backup will be deleted when your subscription ends in %2$d day.