Browse Source

Merge pull request #1648 from vector-im/feature/bma/secureBackup

Secure backup
pull/1698/head
Benoit Marty 11 months ago committed by GitHub
parent
commit
5728d621bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt
  2. 1
      features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt
  3. 7
      features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt
  4. 16
      features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt
  5. 19
      features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt
  6. 29
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt
  7. 1
      features/securebackup/impl/build.gradle.kts
  8. 1
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt
  9. 14
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt
  10. 1
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt
  11. 14
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt
  12. 3
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt
  13. 21
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt
  14. 2
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt
  15. 1
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt
  16. 28
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt
  17. 1
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt
  18. 23
      features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
  19. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt
  20. 15
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt
  21. 12
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt
  22. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png
  23. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png
  24. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png
  25. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png
  26. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png
  27. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png
  28. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png
  29. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png
  30. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png
  31. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png
  32. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png
  33. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png
  34. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png
  35. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png
  36. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png
  37. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png
  38. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png
  39. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png
  40. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png
  41. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png
  42. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png
  43. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png
  44. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png
  45. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png
  46. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png
  47. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png
  48. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png
  49. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png
  50. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png
  51. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png
  52. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png
  53. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png
  54. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png
  55. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png
  56. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png
  57. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png
  58. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png
  59. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png
  60. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png
  61. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png
  62. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png
  63. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png
  64. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png
  65. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png
  66. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png
  67. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png
  68. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png
  69. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png
  70. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png
  71. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png
  72. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png
  73. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png
  74. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png
  75. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png
  76. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png
  77. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png

3
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt → appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt

@ -14,9 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.securebackup.impl package io.element.android.appconfig
// TODO Move to appconfig module when it will be available
object SecureBackupConfig { object SecureBackupConfig {
const val LearnMoreUrl: String = "https://element.io/help#encryption5" const val LearnMoreUrl: String = "https://element.io/help#encryption5"
} }

1
features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt

@ -58,6 +58,7 @@ class LogoutNode @AssistedInject constructor(
state = state, state = state,
onChangeRecoveryKeyClicked = ::onChangeRecoveryKeyClicked, onChangeRecoveryKeyClicked = ::onChangeRecoveryKeyClicked,
onSuccessLogout = { onSuccessLogout(activity, it) }, onSuccessLogout = { onSuccessLogout(activity, it) },
onBackClicked = ::navigateUp,
modifier = modifier, modifier = modifier,
) )
} }

7
features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt

@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -46,13 +47,15 @@ class LogoutPresenter @Inject constructor(
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
val backupUploadState by encryptionService.backupUploadStateStateFlow.collectAsState() val backupUploadState: BackupUploadState by remember {
encryptionService.waitForBackupUploadSteadyState()
}
.collectAsState(initial = BackupUploadState.Unknown)
var showLogoutDialog by remember { mutableStateOf(false) } var showLogoutDialog by remember { mutableStateOf(false) }
var isLastSession by remember { mutableStateOf(false) } var isLastSession by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
encryptionService.waitForBackupUploadSteadyState()
} }
fun handleEvents(event: LogoutEvents) { fun handleEvents(event: LogoutEvents) {

16
features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt

@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -32,6 +33,7 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -39,16 +41,19 @@ import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LogoutView( fun LogoutView(
state: LogoutState, state: LogoutState,
onChangeRecoveryKeyClicked: () -> Unit, onChangeRecoveryKeyClicked: () -> Unit,
onBackClicked: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit, onSuccessLogout: (logoutUrlResult: String?) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -56,6 +61,12 @@ fun LogoutView(
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = { header = {
HeaderContent(state = state) HeaderContent(state = state)
}, },
@ -134,7 +145,7 @@ private fun HeaderContent(
else -> null else -> null
} }
val paddingTop = 60.dp val paddingTop = 0.dp
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = modifier.padding(top = paddingTop), modifier = modifier.padding(top = paddingTop),
iconResourceId = CommonDrawables.ic_key, iconResourceId = CommonDrawables.ic_key,
@ -219,6 +230,7 @@ internal fun LogoutViewPreview(
LogoutView( LogoutView(
state, state,
onChangeRecoveryKeyClicked = {}, onChangeRecoveryKeyClicked = {},
onSuccessLogout = {} onSuccessLogout = {},
onBackClicked = {},
) )
} }

19
features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt

@ -28,6 +28,8 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -73,6 +75,15 @@ class LogoutPresenterTest {
@Test @Test
fun `present - initial state - backing up`() = runTest { fun `present - initial state - backing up`() = runTest {
val encryptionService = FakeEncryptionService() val encryptionService = FakeEncryptionService()
encryptionService.givenWaitForBackupUploadSteadyStateFlow(
flow {
emit(BackupUploadState.Waiting)
delay(1)
emit(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
delay(1)
emit(BackupUploadState.Done)
}
)
val presenter = createLogoutPresenter( val presenter = createLogoutPresenter(
encryptionService = encryptionService encryptionService = encryptionService
) )
@ -84,10 +95,10 @@ class LogoutPresenterTest {
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.showConfirmationDialog).isFalse()
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
encryptionService.emitBackupUploadState(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) val waitingState = awaitItem()
val state = awaitItem() assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
assertThat(state.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) val uploadingState = awaitItem()
encryptionService.emitBackupUploadState(BackupUploadState.Done) assertThat(uploadingState.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
val doneState = awaitItem() val doneState = awaitItem()
assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done) assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done)
} }

29
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt

@ -52,8 +52,8 @@ import io.element.android.features.roomlist.impl.components.RoomListTopBar
import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
@ -190,19 +190,22 @@ private fun RoomListContent(
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
state = lazyListState, state = lazyListState,
) { ) {
if (state.displayVerificationPrompt) { when {
item { state.displayVerificationPrompt -> {
RequestVerificationHeader( item {
onVerifyClicked = onVerifyClicked, RequestVerificationHeader(
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } onVerifyClicked = onVerifyClicked,
) onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
)
}
} }
} else if (state.displayRecoveryKeyPrompt) { state.displayRecoveryKeyPrompt -> {
item { item {
ConfirmRecoveryKeyBanner( ConfirmRecoveryKeyBanner(
onContinueClicked = onOpenSettings, onContinueClicked = onOpenSettings,
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
) )
}
} }
} }

1
features/securebackup/impl/build.gradle.kts

@ -33,6 +33,7 @@ dependencies {
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
implementation(projects.appconfig)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)

1
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt

@ -40,6 +40,7 @@ class SecureBackupDisableNode @AssistedInject constructor(
state = state, state = state,
modifier = modifier, modifier = modifier,
onDone = ::navigateUp, onDone = ::navigateUp,
onBackClicked = ::navigateUp,
) )
} }
} }

14
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt

@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -33,6 +34,7 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -40,13 +42,16 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupDisableView( fun SecureBackupDisableView(
state: SecureBackupDisableState, state: SecureBackupDisableState,
onDone: () -> Unit, onDone: () -> Unit,
onBackClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LaunchedEffect(state.disableAction) { LaunchedEffect(state.disableAction) {
@ -56,6 +61,12 @@ fun SecureBackupDisableView(
} }
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = { header = {
HeaderContent() HeaderContent()
}, },
@ -95,7 +106,7 @@ private fun HeaderContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 60.dp), modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key_off, iconResourceId = CommonDrawables.ic_key_off,
title = stringResource(id = R.string.screen_key_backup_disable_title), title = stringResource(id = R.string.screen_key_backup_disable_title),
subTitle = stringResource(id = R.string.screen_key_backup_disable_description), subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
@ -158,5 +169,6 @@ internal fun SecureBackupDisableViewPreview(
SecureBackupDisableView( SecureBackupDisableView(
state = state, state = state,
onDone = {}, onDone = {},
onBackClicked = {},
) )
} }

1
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt

@ -40,6 +40,7 @@ class SecureBackupEnableNode @AssistedInject constructor(
state = state, state = state,
modifier = modifier, modifier = modifier,
onDone = ::navigateUp, onDone = ::navigateUp,
onBackClicked = ::navigateUp,
) )
} }
} }

14
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt

@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl.enable
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -29,16 +30,20 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupEnableView( fun SecureBackupEnableView(
state: SecureBackupEnableState, state: SecureBackupEnableState,
onDone: () -> Unit, onDone: () -> Unit,
onBackClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LaunchedEffect(state.enableAction) { LaunchedEffect(state.enableAction) {
@ -48,6 +53,12 @@ fun SecureBackupEnableView(
} }
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = { header = {
HeaderContent() HeaderContent()
}, },
@ -68,7 +79,7 @@ private fun HeaderContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 60.dp), modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key, iconResourceId = CommonDrawables.ic_key,
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable), title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
subTitle = null, subTitle = null,
@ -99,5 +110,6 @@ internal fun SecureBackupEnableViewPreview(
SecureBackupEnableView( SecureBackupEnableView(
state = state, state = state,
onDone = {}, onDone = {},
onBackClicked = {},
) )
} }

3
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt

@ -50,7 +50,8 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
onDone = { onDone = {
coroutineScope.postSuccessSnackbar() coroutineScope.postSuccessSnackbar()
navigateUp() navigateUp()
} },
onBackClicked = ::navigateUp,
) )
} }

21
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt

@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl.enter
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -30,17 +31,21 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupEnterRecoveryKeyView( fun SecureBackupEnterRecoveryKeyView(
state: SecureBackupEnterRecoveryKeyState, state: SecureBackupEnterRecoveryKeyState,
onDone: () -> Unit, onDone: () -> Unit,
onBackClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (state.submitAction) { when (state.submitAction) {
@ -59,6 +64,12 @@ fun SecureBackupEnterRecoveryKeyView(
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = { header = {
HeaderContent() HeaderContent()
}, },
@ -75,6 +86,9 @@ fun SecureBackupEnterRecoveryKeyView(
state = state, state = state,
onChange = { onChange = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it)) state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it))
},
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
}) })
} }
} }
@ -84,7 +98,7 @@ private fun HeaderContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 60.dp), modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key, iconResourceId = CommonDrawables.ic_key,
title = stringResource(id = R.string.screen_recovery_key_confirm_title), title = stringResource(id = R.string.screen_recovery_key_confirm_title),
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description), subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
@ -112,13 +126,15 @@ private fun BottomMenu(
@Composable @Composable
private fun Content( private fun Content(
state: SecureBackupEnterRecoveryKeyState, state: SecureBackupEnterRecoveryKeyState,
onChange: ((String) -> Unit)?, onChange: (String) -> Unit,
onSubmit: () -> Unit,
) { ) {
RecoveryKeyView( RecoveryKeyView(
modifier = Modifier.padding(top = 52.dp), modifier = Modifier.padding(top = 52.dp),
state = state.recoveryKeyViewState, state = state.recoveryKeyViewState,
onClick = null, onClick = null,
onChange = onChange, onChange = onChange,
onSubmit = onSubmit,
) )
} }
@ -130,5 +146,6 @@ internal fun SecureBackupEnterRecoveryKeyViewPreview(
SecureBackupEnterRecoveryKeyView( SecureBackupEnterRecoveryKeyView(
state = state, state = state,
onDone = {}, onDone = {},
onBackClicked = {},
) )
} }

2
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt

@ -27,7 +27,7 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.securebackup.impl.SecureBackupConfig import io.element.android.appconfig.SecureBackupConfig
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)

1
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt

@ -60,6 +60,7 @@ class SecureBackupSetupNode @AssistedInject constructor(
coroutineScope.postSuccessSnackbar() coroutineScope.postSuccessSnackbar()
navigateUp() navigateUp()
}, },
onBackClicked = ::navigateUp,
modifier = modifier, modifier = modifier,
) )
} }

28
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt

@ -16,8 +16,10 @@
package io.element.android.features.securebackup.impl.setup package io.element.android.features.securebackup.impl.setup
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -32,24 +34,42 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupSetupView( fun SecureBackupSetupView(
state: SecureBackupSetupState, state: SecureBackupSetupState,
onDone: () -> Unit, onDone: () -> Unit,
onBackClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val canGoBack = state.canGoBack()
BackHandler(enabled = canGoBack) {
onBackClicked()
}
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
if (canGoBack) {
BackButton(onClick = onBackClicked)
}
},
title = {},
)
},
header = { header = {
HeaderContent(state = state) HeaderContent(state = state)
}, },
@ -109,6 +129,10 @@ fun SecureBackupSetupView(
} }
} }
private fun SecureBackupSetupState.canGoBack(): Boolean {
return recoveryKeyViewState.formattedRecoveryKey == null
}
@Composable @Composable
private fun HeaderContent( private fun HeaderContent(
state: SecureBackupSetupState, state: SecureBackupSetupState,
@ -136,7 +160,7 @@ private fun HeaderContent(
stringResource(id = R.string.screen_recovery_key_save_description) stringResource(id = R.string.screen_recovery_key_save_description)
} }
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 60.dp), modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key, iconResourceId = CommonDrawables.ic_key,
title = title, title = title,
subTitle = subTitle, subTitle = subTitle,
@ -192,6 +216,7 @@ private fun Content(
state = state, state = state,
onClick = onClick, onClick = onClick,
onChange = null, onChange = null,
onSubmit = null,
) )
} }
@ -203,5 +228,6 @@ internal fun SecureBackupSetupViewPreview(
SecureBackupSetupView( SecureBackupSetupView(
state = state, state = state,
onDone = {}, onDone = {},
onBackClicked = {},
) )
} }

1
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt

@ -33,5 +33,6 @@ internal fun SecureBackupSetupViewChangePreview(
recoveryKeyViewState = state.recoveryKeyViewState.copy(recoveryKeyUserStory = RecoveryKeyUserStory.Change), recoveryKeyViewState = state.recoveryKeyViewState.copy(recoveryKeyUserStory = RecoveryKeyUserStory.Change),
), ),
onDone = {}, onDone = {},
onBackClicked = {},
) )
} }

23
features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt

@ -25,12 +25,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.progressSemantics
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -52,6 +55,7 @@ internal fun RecoveryKeyView(
state: RecoveryKeyViewState, state: RecoveryKeyViewState,
onClick: (() -> Unit)?, onClick: (() -> Unit)?,
onChange: ((String) -> Unit)?, onChange: ((String) -> Unit)?,
onSubmit: (() -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -63,7 +67,7 @@ internal fun RecoveryKeyView(
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
) )
RecoveryKeyContent(state, onClick, onChange) RecoveryKeyContent(state, onClick, onChange, onSubmit)
RecoveryKeyFooter(state) RecoveryKeyFooter(state)
} }
} }
@ -73,11 +77,12 @@ private fun RecoveryKeyContent(
state: RecoveryKeyViewState, state: RecoveryKeyViewState,
onClick: (() -> Unit)?, onClick: (() -> Unit)?,
onChange: ((String) -> Unit)?, onChange: ((String) -> Unit)?,
onSubmit: (() -> Unit)?,
) { ) {
when (state.recoveryKeyUserStory) { when (state.recoveryKeyUserStory) {
RecoveryKeyUserStory.Setup, RecoveryKeyUserStory.Setup,
RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick) RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick)
RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange) RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange, onSubmit)
} }
} }
@ -143,8 +148,13 @@ private fun RecoveryKeyStaticContent(
} }
@Composable @Composable
private fun RecoveryKeyFormContent(state: RecoveryKeyViewState, onChange: ((String) -> Unit)?) { private fun RecoveryKeyFormContent(
state: RecoveryKeyViewState,
onChange: ((String) -> Unit)?,
onSubmit: (() -> Unit)?,
) {
onChange ?: error("onChange should not be null") onChange ?: error("onChange should not be null")
onSubmit ?: error("onSubmit should not be null")
val recoveryKeyVisualTransformation = remember { val recoveryKeyVisualTransformation = remember {
RecoveryKeyVisualTransformation() RecoveryKeyVisualTransformation()
} }
@ -155,6 +165,12 @@ private fun RecoveryKeyFormContent(state: RecoveryKeyViewState, onChange: ((Stri
onValueChange = onChange, onValueChange = onChange,
enabled = state.inProgress.not(), enabled = state.inProgress.not(),
visualTransformation = recoveryKeyVisualTransformation, visualTransformation = recoveryKeyVisualTransformation,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = { onSubmit() }
),
label = { Text(text = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder)) } label = { Text(text = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder)) }
) )
} }
@ -217,5 +233,6 @@ internal fun RecoveryKeyViewPreview(
state = state, state = state,
onClick = {}, onClick = {},
onChange = {}, onChange = {},
onSubmit = {},
) )
} }

6
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt

@ -16,12 +16,12 @@
package io.element.android.libraries.matrix.api.encryption package io.element.android.libraries.matrix.api.encryption
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface EncryptionService { interface EncryptionService {
val backupStateStateFlow: StateFlow<BackupState> val backupStateStateFlow: StateFlow<BackupState>
val recoveryStateStateFlow: StateFlow<RecoveryState> val recoveryStateStateFlow: StateFlow<RecoveryState>
val backupUploadStateStateFlow: StateFlow<BackupUploadState>
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress> val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
suspend fun enableBackups(): Result<Unit> suspend fun enableBackups(): Result<Unit>
@ -46,7 +46,7 @@ interface EncryptionService {
suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit> suspend fun fixRecoveryIssues(recoveryKey: String): Result<Unit>
/** /**
* Observe [backupUploadStateStateFlow] to get progress. * Wait for backup upload steady state.
*/ */
suspend fun waitForBackupUploadSteadyState(): Result<Unit> fun waitForBackupUploadSteadyState(): Flow<BackupUploadState>
} }

15
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt

@ -22,7 +22,10 @@ import io.element.android.libraries.matrix.api.encryption.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.BackupStateListener import org.matrix.rustcomponents.sdk.BackupStateListener
import org.matrix.rustcomponents.sdk.BackupSteadyStateListener import org.matrix.rustcomponents.sdk.BackupSteadyStateListener
@ -50,7 +53,6 @@ internal class RustEncryptionService(
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(service.backupState().let(backupStateMapper::map)) override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(service.backupState().let(backupStateMapper::map))
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map))
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown) override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown)
override val backupUploadStateStateFlow: MutableStateFlow<BackupUploadState> = MutableStateFlow(BackupUploadState.Unknown)
fun start() { fun start() {
service.backupStateListener(object : BackupStateListener { service.backupStateListener(object : BackupStateListener {
@ -94,16 +96,19 @@ internal class RustEncryptionService(
} }
} }
override suspend fun waitForBackupUploadSteadyState( override fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> {
): Result<Unit> = withContext(dispatchers.io) { return callbackFlow {
runCatching {
service.waitForBackupUploadSteadyState( service.waitForBackupUploadSteadyState(
progressListener = object : BackupSteadyStateListener { progressListener = object : BackupSteadyStateListener {
override fun onUpdate(status: RustBackupUploadState) { override fun onUpdate(status: RustBackupUploadState) {
backupUploadStateStateFlow.value = backupUploadStateMapper.map(status) trySend(backupUploadStateMapper.map(status))
if (status == RustBackupUploadState.Done) {
close()
}
} }
} }
) )
awaitClose {}
} }
} }

12
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt

@ -22,14 +22,16 @@ import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
class FakeEncryptionService : EncryptionService { class FakeEncryptionService : EncryptionService {
private var disableRecoveryFailure: Exception? = null private var disableRecoveryFailure: Exception? = null
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN) override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN)
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown) override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown)
override val backupUploadStateStateFlow: MutableStateFlow<BackupUploadState> = MutableStateFlow(BackupUploadState.Unknown) private var waitForBackupUploadSteadyStateFlow: Flow<BackupUploadState> = flowOf()
private var fixRecoveryIssuesFailure: Exception? = null private var fixRecoveryIssuesFailure: Exception? = null
@ -73,12 +75,12 @@ class FakeEncryptionService : EncryptionService {
return Result.success(Unit) return Result.success(Unit)
} }
override suspend fun waitForBackupUploadSteadyState(): Result<Unit> { fun givenWaitForBackupUploadSteadyStateFlow(flow: Flow<BackupUploadState>) {
return Result.success(Unit) waitForBackupUploadSteadyStateFlow = flow
} }
suspend fun emitBackupUploadState(state: BackupUploadState) { override fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> {
backupUploadStateStateFlow.emit(state) return waitForBackupUploadSteadyStateFlow
} }
suspend fun emitBackupState(state: BackupState) { suspend fun emitBackupState(state: BackupState) {

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save