Add improved archive upload progress.

This commit is contained in:
Alex Hart
2024-12-06 15:38:08 -04:00
committed by Greyson Parrelli
parent 981808d074
commit 6d5be0b445
8 changed files with 217 additions and 27 deletions

View File

@@ -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
)
)
}
}
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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>): 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

View File

@@ -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)
}

View File

@@ -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;
}

View File

@@ -7772,6 +7772,13 @@
<string name="RemoteBackupsSettingsFragment__learn_more">Learn more</string>
<!-- Linear progress dialog text shown when creating a backup -->
<string name="RemoteBackupsSettingsFragment__processing_backup">Processing backup…</string>
<!-- Linear progress dialog text shown when preparing a backup -->
<string name="RemoteBackupsSettingsFragment__preparing_backup">Preparing backup…</string>
<!-- Linear progress dialog text shown when processing messages for backup. First placeholder is completed count, second is approximate total count, third is percent completed. -->
<plurals name="RemoteBackupsSettingsFragment__processing_d_of_d_d_messages">
<item quantity="one">Processing %1$d of %2$d (%3$d%%) message</item>
<item quantity="other">Processing %1$d of %2$d (%3$d%%) messages</item>
</plurals>
<!-- Displayed in row when backup is available for download and users subscription has expired. First placeholder is data size e.g. 12MB, second is days before expiration -->
<plurals name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data">
<item quantity="one">You have %1$s of backup data thats not on this device. Your backup will be deleted when your subscription ends in %2$d day.</item>