Use better progress for local backup v2 in regV5.

This commit is contained in:
Greyson Parrelli
2026-07-16 09:42:09 -04:00
committed by Cody Henthorne
parent fd83ad9625
commit 2f70d780d0
23 changed files with 280 additions and 136 deletions
@@ -55,6 +55,8 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.util.mebiBytes
import org.signal.registration.R
import org.signal.registration.screens.shared.RestoreProgress
import org.signal.registration.screens.shared.RestoreProgressDialog
import org.signal.registration.test.TestTags
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@@ -95,10 +97,18 @@ fun LocalBackupRestoreScreen(
NoBackupFoundContent(onEvent = onEvent, modifier = modifier)
}
LocalBackupRestoreState.RestorePhase.Preparing -> {
PreparingContent(modifier = modifier)
if (state.backupInfo?.type == LocalBackupInfo.BackupType.V2) {
V2RestoreInProgressContent(state = state, onEvent = onEvent, modifier = modifier)
} else {
V1PreparingContent(modifier = modifier)
}
}
LocalBackupRestoreState.RestorePhase.InProgress -> {
InProgressContent(progressFraction = state.progressFraction, onEvent = onEvent, modifier = modifier)
if (state.backupInfo?.type == LocalBackupInfo.BackupType.V2) {
V2RestoreInProgressContent(state = state, onEvent = onEvent, modifier = modifier)
} else {
V1InProgressContent(progressFraction = state.progressFraction, onEvent = onEvent, modifier = modifier)
}
}
LocalBackupRestoreState.RestorePhase.IncorrectCredential -> {
IncorrectCredentialContent(backupType = state.backupInfo?.type, onEvent = onEvent, modifier = modifier)
@@ -488,7 +498,7 @@ private fun BackupOptionCard(
}
@Composable
private fun PreparingContent(modifier: Modifier = Modifier) {
private fun V1PreparingContent(modifier: Modifier = Modifier) {
Loading(
label = stringResource(R.string.LocalBackupRestoreScreen__preparing_restore),
modifier = modifier
@@ -496,7 +506,7 @@ private fun PreparingContent(modifier: Modifier = Modifier) {
}
@Composable
private fun InProgressContent(
private fun V1InProgressContent(
progressFraction: Float,
onEvent: (LocalBackupRestoreEvents) -> Unit,
modifier: Modifier = Modifier
@@ -533,6 +543,22 @@ private fun InProgressContent(
)
}
@Composable
private fun V2RestoreInProgressContent(
state: LocalBackupRestoreState,
onEvent: (LocalBackupRestoreEvents) -> Unit,
modifier: Modifier = Modifier
) {
BackupFoundContent(
backupInfo = state.backupInfo!!,
allBackups = state.allBackups,
onEvent = onEvent,
modifier = modifier
)
RestoreProgressDialog(restoreProgress = state.restoreProgress)
}
@Composable
private fun IncorrectCredentialContent(
backupType: LocalBackupInfo.BackupType?,
@@ -699,6 +725,31 @@ private fun LocalBackupRestoreScreenInProgressPreview() {
}
}
@AllDevicePreviews
@Composable
private fun LocalBackupRestoreScreenV2InProgressPreview() {
Previews.Preview {
LocalBackupRestoreScreen(
state = LocalBackupRestoreState(
restorePhase = LocalBackupRestoreState.RestorePhase.InProgress,
backupInfo = LocalBackupInfo(
type = LocalBackupInfo.BackupType.V2,
date = LocalDateTime.of(2026, 3, 15, 14, 30, 0),
name = "signal-backup-2026-03-15-14-30-00",
uri = Uri.EMPTY,
sizeBytes = 511.mebiBytes.bytes
),
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Restoring,
bytesCompleted = 332.mebiBytes.bytes,
totalBytes = 511.mebiBytes.bytes
)
),
onEvent = {}
)
}
}
@AllDevicePreviews
@Composable
private fun LocalBackupRestoreScreenErrorPreview() {
@@ -8,6 +8,7 @@ package org.signal.registration.screens.localbackuprestore
import android.net.Uri
import org.signal.core.models.AccountEntropyPool
import org.signal.core.util.censor
import org.signal.registration.screens.shared.RestoreProgress
data class LocalBackupRestoreState(
val restorePhase: RestorePhase = RestorePhase.SelectFolder,
@@ -15,6 +16,7 @@ data class LocalBackupRestoreState(
val allBackups: List<LocalBackupInfo> = emptyList(),
val selectedFolderUri: Uri? = null,
val progressFraction: Float = 0f,
val restoreProgress: RestoreProgress? = null,
val errorMessage: String? = null,
val launchFolderPicker: Boolean = false,
val aep: AccountEntropyPool? = null,
@@ -22,7 +24,7 @@ data class LocalBackupRestoreState(
val storageCapable: Boolean = true
) {
override fun toString(): String = "LocalBackupRestoreState(restorePhase=$restorePhase, backupInfo=$backupInfo, allBackups=$allBackups, selectedFolderUri=$selectedFolderUri, progressFraction=$progressFraction, errorMessage=$errorMessage, launchFolderPicker=$launchFolderPicker, aep=${aep?.displayValue?.censor()}, v1Passphrase=${v1Passphrase?.censor()}, storageCapable=$storageCapable)"
override fun toString(): String = "LocalBackupRestoreState(restorePhase=$restorePhase, backupInfo=$backupInfo, allBackups=$allBackups, selectedFolderUri=$selectedFolderUri, progressFraction=$progressFraction, restoreProgress=$restoreProgress, errorMessage=$errorMessage, launchFolderPicker=$launchFolderPicker, aep=${aep?.displayValue?.censor()}, v1Passphrase=${v1Passphrase?.censor()}, storageCapable=$storageCapable)"
enum class RestorePhase {
/** Waiting for user to select a backup folder. */
@@ -27,6 +27,7 @@ import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.shared.RestoreProgress
import org.signal.registration.screens.util.navigateBack
import org.signal.registration.screens.util.navigateTo
@@ -223,18 +224,19 @@ class LocalBackupRestoreViewModel(
}
restoreFlow.collect { progress ->
_state.value = when (progress) {
is LocalBackupRestoreProgress.Preparing -> LocalBackupRestoreState(
is LocalBackupRestoreProgress.Preparing -> currentState.copy(
restorePhase = LocalBackupRestoreState.RestorePhase.Preparing,
aep = currentState.aep,
v1Passphrase = currentState.v1Passphrase,
storageCapable = currentState.storageCapable
progressFraction = 0f,
restoreProgress = null
)
is LocalBackupRestoreProgress.InProgress -> LocalBackupRestoreState(
is LocalBackupRestoreProgress.InProgress -> currentState.copy(
restorePhase = LocalBackupRestoreState.RestorePhase.InProgress,
progressFraction = progress.progressFraction,
aep = currentState.aep,
v1Passphrase = currentState.v1Passphrase,
storageCapable = currentState.storageCapable
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Restoring,
bytesCompleted = progress.bytesRead,
totalBytes = progress.totalBytes
)
)
is LocalBackupRestoreProgress.Complete -> {
onRestoreComplete(_state.value.copy(aep = aep, v1Passphrase = currentState.v1Passphrase, storageCapable = currentState.storageCapable), progress, backup.type)
@@ -6,10 +6,8 @@
package org.signal.registration.screens.remotebackuprestore
import android.text.format.DateFormat
import android.text.format.Formatter
import androidx.compose.foundation.background
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
@@ -19,13 +17,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -52,6 +46,7 @@ import org.signal.registration.screens.OnePaneRegistrationScaffold
import org.signal.registration.screens.RegistrationScaffold
import org.signal.registration.screens.TwoPaneRegistrationScaffold
import org.signal.registration.screens.attachDebugLogHelper
import org.signal.registration.screens.shared.RestoreProgressDialog
import org.signal.registration.test.TestTags
import java.util.Date
@@ -356,69 +351,6 @@ private fun RestoreStateDialogs(
}
}
@Composable
private fun RestoreProgressDialog(restoreProgress: RemoteBackupRestoreState.RestoreProgress?) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = {},
confirmButton = {},
dismissButton = {},
text = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.wrapContentSize()
) {
if (restoreProgress == null || restoreProgress.phase == RemoteBackupRestoreState.RestoreProgress.Phase.Finalizing) {
CircularProgressIndicator(
modifier = Modifier
.padding(top = 55.dp, bottom = 16.dp)
.width(48.dp)
.height(48.dp)
)
} else {
CircularProgressIndicator(
progress = { restoreProgress.progress },
modifier = Modifier
.padding(top = 55.dp, bottom = 16.dp)
.width(48.dp)
.height(48.dp)
)
}
val progressText = when (restoreProgress?.phase) {
RemoteBackupRestoreState.RestoreProgress.Phase.Downloading -> stringResource(R.string.RemoteRestoreScreen__downloading_backup)
RemoteBackupRestoreState.RestoreProgress.Phase.Restoring -> stringResource(R.string.RemoteRestoreScreen__restoring_messages)
RemoteBackupRestoreState.RestoreProgress.Phase.Finalizing -> stringResource(R.string.RemoteRestoreScreen__finishing_restore)
null -> stringResource(R.string.RemoteRestoreScreen__restoring)
}
Text(
text = progressText,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 12.dp)
)
if (restoreProgress != null && restoreProgress.phase != RemoteBackupRestoreState.RestoreProgress.Phase.Finalizing && restoreProgress.totalBytes > 0) {
val progressBytes = Formatter.formatShortFileSize(context, restoreProgress.bytesCompleted)
val totalBytes = Formatter.formatShortFileSize(context, restoreProgress.totalBytes)
Text(
text = stringResource(R.string.RemoteRestoreScreen__s_of_s_s, progressBytes, totalBytes, "%.2f%%".format(restoreProgress.progress * 100)),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 12.dp)
)
}
}
}
},
modifier = Modifier.width(212.dp)
)
}
@AllDevicePreviews
@Composable
private fun RemoteRestoreScreenLoadedPreview() {
@@ -7,6 +7,7 @@ package org.signal.registration.screens.remotebackuprestore
import org.signal.core.models.AccountEntropyPool
import org.signal.core.util.censor
import org.signal.registration.screens.shared.RestoreProgress
data class RemoteBackupRestoreState(
val aep: AccountEntropyPool,
@@ -42,19 +43,4 @@ data class RemoteBackupRestoreState(
data object Failed : RestoreState
}
data class RestoreProgress(
val phase: Phase,
val bytesCompleted: Long,
val totalBytes: Long
) {
val progress: Float
get() = if (totalBytes > 0) bytesCompleted.toFloat() / totalBytes.toFloat() else 0f
enum class Phase {
Downloading,
Restoring,
Finalizing
}
}
}
@@ -27,6 +27,7 @@ import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.shared.RestoreProgress
import org.signal.registration.screens.util.navigateBack
import org.signal.registration.screens.util.navigateTo
import kotlin.coroutines.CoroutineContext
@@ -89,8 +90,8 @@ class RemoteBackupRestoreViewModel(
Log.i(TAG, "[restoreBackup] Restoring...")
_state.value = _state.value.copy(
restoreState = RemoteBackupRestoreState.RestoreState.InProgress,
restoreProgress = RemoteBackupRestoreState.RestoreProgress(
phase = RemoteBackupRestoreState.RestoreProgress.Phase.Downloading,
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Downloading,
bytesCompleted = progress.bytesDownloaded,
totalBytes = progress.totalBytes
)
@@ -100,8 +101,8 @@ class RemoteBackupRestoreViewModel(
Log.i(TAG, "[restoreBackup] Restoring...")
_state.value = _state.value.copy(
restoreState = RemoteBackupRestoreState.RestoreState.InProgress,
restoreProgress = RemoteBackupRestoreState.RestoreProgress(
phase = RemoteBackupRestoreState.RestoreProgress.Phase.Restoring,
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Restoring,
bytesCompleted = progress.bytesRead,
totalBytes = progress.totalBytes
)
@@ -111,8 +112,8 @@ class RemoteBackupRestoreViewModel(
Log.i(TAG, "[restoreBackup] Finalizing...")
_state.value = _state.value.copy(
restoreState = RemoteBackupRestoreState.RestoreState.InProgress,
restoreProgress = RemoteBackupRestoreState.RestoreProgress(
phase = RemoteBackupRestoreState.RestoreProgress.Phase.Finalizing,
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Finalizing,
bytesCompleted = 0,
totalBytes = 0
)
@@ -0,0 +1,127 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.shared
import android.text.format.Formatter
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews
import org.signal.registration.R
/**
* Non-dismissable popup showing restore progress: a circular indicator, a phase label, and a byte count.
* A null [restoreProgress] (or the [RestoreProgress.Phase.Finalizing] phase) renders an indeterminate spinner.
*/
@Composable
fun RestoreProgressDialog(restoreProgress: RestoreProgress?) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = {},
confirmButton = {},
dismissButton = {},
text = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.wrapContentSize()
) {
if (restoreProgress == null || restoreProgress.phase == RestoreProgress.Phase.Finalizing) {
CircularProgressIndicator(
modifier = Modifier
.padding(top = 55.dp, bottom = 16.dp)
.width(48.dp)
.height(48.dp)
)
} else {
CircularProgressIndicator(
progress = { restoreProgress.progress },
modifier = Modifier
.padding(top = 55.dp, bottom = 16.dp)
.width(48.dp)
.height(48.dp)
)
}
val progressText = when (restoreProgress?.phase) {
RestoreProgress.Phase.Downloading -> stringResource(R.string.RemoteRestoreScreen__downloading_backup)
RestoreProgress.Phase.Restoring -> stringResource(R.string.RemoteRestoreScreen__restoring_messages)
RestoreProgress.Phase.Finalizing -> stringResource(R.string.RemoteRestoreScreen__finishing_restore)
null -> stringResource(R.string.RemoteRestoreScreen__restoring)
}
Text(
text = progressText,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 12.dp)
)
if (restoreProgress != null && restoreProgress.phase != RestoreProgress.Phase.Finalizing && restoreProgress.totalBytes > 0) {
val progressBytes = Formatter.formatShortFileSize(context, restoreProgress.bytesCompleted)
val totalBytes = Formatter.formatShortFileSize(context, restoreProgress.totalBytes)
Text(
text = stringResource(R.string.RemoteRestoreScreen__s_of_s_s, progressBytes, totalBytes, "%.2f%%".format(restoreProgress.progress * 100)),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 12.dp)
)
}
}
}
},
modifier = Modifier.width(212.dp)
)
}
@AllDevicePreviews
@Composable
private fun RestoreProgressDialogPreview() {
Previews.Preview {
RestoreProgressDialog(
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Restoring,
bytesCompleted = 512_000,
totalBytes = 1_024_000
)
)
}
}
/**
* Progress of an in-flight backup restore, shared between the remote and local restore flows.
*/
data class RestoreProgress(
val phase: Phase,
val bytesCompleted: Long,
val totalBytes: Long
) {
val progress: Float
get() = if (totalBytes > 0) bytesCompleted.toFloat() / totalBytes.toFloat() else 0f
enum class Phase {
Downloading,
Restoring,
Finalizing
}
}
@@ -372,7 +372,7 @@ private fun PrimaryDeviceCallToActionButtons(
Text(stringResource(R.string.RegistrationActivity_continue))
}
Spacer(modifier = Modifier.height(17.dp))
Spacer(modifier = Modifier.height(16.dp))
Buttons.LargeTonal(
onClick = onRestoreOrTransferClick,
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca027a262cd5878c40e4abd08f966c0ee6bf8f02eeef787f9832e6b9235a5d37
size 279488
oid sha256:8d91eb170a945e2ae40b73cfdf00cd7ed694b705d391fc24ef7097a586aad3c4
size 279157
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6df52102839dd5fff99d087b01b3f59e4bb7ecf4b7cfc97f28e33075b667ab26
size 253635
oid sha256:a89fcaf8db0b61f35e952bb6bdc8bbba68b2545e6b19d182a03fc8d0be98e6ea
size 253292
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3af78bd0c9865d3788e824d9075b9d285dc5b721375688a06a5927d1561eff5e
size 265333
oid sha256:bdfb102aca844d4428e203c022e2fbfb0768248108224fcc03b1efa2c1257a7d
size 265605
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4e224fd2c468d206149d54cb7d42e156d9bc1ef43000e5b6f8c71a3e920c6ecb
size 52900
oid sha256:9c648dec6bfe1a7db0cc9847e8cfca1ac3dadad5f3f3a9c177b59209ea259854
size 52917
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00c24756136f9ec19f52114a3b3505588b3ace0f35df07272a9c16823ef4c1dd
size 53201
oid sha256:ef5676fc65e169f0a82efbccc458187788e0c561ad91f97a07392430ed0e9160
size 53219
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00ee05dceb245e7789fabe844e7d8b78a317edafe913ad7d599909f05565d7a1
size 104487
oid sha256:0b33a06e42560c13c29c3ecdd383a5016bcd20a151bf5b076aa6cec2361d9343
size 105020
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8bdcf51c97838c57ddede736892461939d7ad13d88b266eddd589a3b30388b3f
size 241919
oid sha256:097e505e759ac652ea162b2a9571429daac0bd9ede00418e7350ba81adcf6e57
size 241783
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d6ef9c041521feeb0cda423cbb612f2ecdffe29a4eb97a9310e5c76192f27e6
size 192932
oid sha256:3c972a56a010492bf63eeafd107fd1a898f104f0f413d2c8ea018f3706487094
size 192897
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6ba17b3a59fc897a23e55f3a76c4ae9613a6f547919f60c077ee8ea59f8d231
size 182338
oid sha256:1f2cd9e0964d19f3833ebc6c647d6d4787d3ca1da345e237f9d445b583258033
size 182541
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c27372e794bcb63f49bf0e89cb495cf29c12d7024db7aeffde0db301f0951432
size 165171
oid sha256:8604d50efb86b60216707b41b67eba3894e489be5948d97467e28b9f6cf3a289
size 165254
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b333000ff39c5d726307d9be2691de4657032cbf4a9a5d946afa94ce3001c73
size 435557
oid sha256:cd6b3679f8458db22ae3c86bc9c659ee9e89da975fa87618f41c45981bb4a236
size 435556
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58ab68ce8d815d9f7aa094e0261075759083f67fcd4d40d8a38b519c9ac3ee78
size 395238
oid sha256:7c99fdb26eca7d0aa2c95fa6bd45cc79b592cc4ac2d9dae33269551f3685b916
size 395279
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:34263308f2b19c65983e82c7d775478612529b9174a98c11a23ec3b3e3dbebb6
size 297926
oid sha256:e6c58d8f3e5f9ac5b386747fb81d4422f5e40ec8913a9bd179086536dee1b633
size 297909
@@ -8,8 +8,10 @@ package org.signal.registration.screens.localbackuprestore
import android.app.Application
import android.net.Uri
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.core.app.ApplicationProvider
@@ -20,6 +22,8 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.ui.CoreUiDependenciesRule
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.registration.R
import org.signal.registration.screens.shared.RestoreProgress
import org.signal.registration.test.TestTags
import java.time.LocalDateTime
@@ -95,12 +99,19 @@ class LocalBackupRestoreScreenTest {
}
@Test
fun `InProgress phase displays progress bar`() {
fun `V1 InProgress phase displays progress bar`() {
composeTestRule.setContent {
SignalTheme {
LocalBackupRestoreScreen(
state = LocalBackupRestoreState(
restorePhase = LocalBackupRestoreState.RestorePhase.InProgress,
backupInfo = LocalBackupInfo(
type = LocalBackupInfo.BackupType.V1,
date = LocalDateTime.of(2026, 3, 15, 14, 30, 0),
name = "signal-2026-03-15-14-30-00.backup",
uri = Uri.EMPTY,
sizeBytes = 1_482_184_499
),
progressFraction = 0.5f
),
onEvent = {}
@@ -110,4 +121,35 @@ class LocalBackupRestoreScreenTest {
composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_PROGRESS_BAR).performScrollTo().assertIsDisplayed()
}
@Test
fun `V2 InProgress phase displays restore progress dialog and not the linear progress bar`() {
val context = ApplicationProvider.getApplicationContext<Application>()
composeTestRule.setContent {
SignalTheme {
LocalBackupRestoreScreen(
state = LocalBackupRestoreState(
restorePhase = LocalBackupRestoreState.RestorePhase.InProgress,
backupInfo = LocalBackupInfo(
type = LocalBackupInfo.BackupType.V2,
date = LocalDateTime.of(2026, 3, 15, 14, 30, 0),
name = "signal-backup-2026-03-15-14-30-00",
uri = Uri.EMPTY,
sizeBytes = 1_482_184_499
),
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Restoring,
bytesCompleted = 50,
totalBytes = 100
)
),
onEvent = {}
)
}
}
composeTestRule.onNodeWithText(context.getString(R.string.RemoteRestoreScreen__restoring_messages)).assertIsDisplayed()
composeTestRule.onNodeWithTag(TestTags.LOCAL_BACKUP_RESTORE_PROGRESS_BAR).assertIsNotDisplayed()
}
}
@@ -35,6 +35,7 @@ import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.shared.RestoreProgress
import java.io.IOException
import kotlin.time.Duration.Companion.seconds
@@ -156,8 +157,8 @@ class RemoteBackupRestoreViewModelTest {
val initialState = RemoteBackupRestoreState(
aep = aep,
restoreState = RemoteBackupRestoreState.RestoreState.Failed,
restoreProgress = RemoteBackupRestoreState.RestoreProgress(
phase = RemoteBackupRestoreState.RestoreProgress.Phase.Downloading,
restoreProgress = RestoreProgress(
phase = RestoreProgress.Phase.Downloading,
bytesCompleted = 50,
totalBytes = 100
)
@@ -313,7 +314,7 @@ class RemoteBackupRestoreViewModelTest {
val last = states.last()
assertThat(last.restoreState).isEqualTo(RemoteBackupRestoreState.RestoreState.InProgress)
assertThat(last.restoreProgress?.phase).isEqualTo(RemoteBackupRestoreState.RestoreProgress.Phase.Downloading)
assertThat(last.restoreProgress?.phase).isEqualTo(RestoreProgress.Phase.Downloading)
assertThat(last.restoreProgress?.bytesCompleted).isEqualTo(30L)
assertThat(last.restoreProgress?.totalBytes).isEqualTo(100L)
}
@@ -330,7 +331,7 @@ class RemoteBackupRestoreViewModelTest {
val last = states.last()
assertThat(last.restoreState).isEqualTo(RemoteBackupRestoreState.RestoreState.InProgress)
assertThat(last.restoreProgress?.phase).isEqualTo(RemoteBackupRestoreState.RestoreProgress.Phase.Restoring)
assertThat(last.restoreProgress?.phase).isEqualTo(RestoreProgress.Phase.Restoring)
assertThat(last.restoreProgress?.bytesCompleted).isEqualTo(75L)
}
@@ -346,7 +347,7 @@ class RemoteBackupRestoreViewModelTest {
val last = states.last()
assertThat(last.restoreState).isEqualTo(RemoteBackupRestoreState.RestoreState.InProgress)
assertThat(last.restoreProgress?.phase).isEqualTo(RemoteBackupRestoreState.RestoreProgress.Phase.Finalizing)
assertThat(last.restoreProgress?.phase).isEqualTo(RestoreProgress.Phase.Finalizing)
}
@Test