Merge pull request #1648 from vector-im/feature/bma/secureBackup
Secure backup
This commit is contained in:
@@ -14,9 +14,8 @@
|
||||
* 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 {
|
||||
const val LearnMoreUrl: String = "https://element.io/help#encryption5"
|
||||
}
|
||||
@@ -58,6 +58,7 @@ class LogoutNode @AssistedInject constructor(
|
||||
state = state,
|
||||
onChangeRecoveryKeyClicked = ::onChangeRecoveryKeyClicked,
|
||||
onSuccessLogout = { onSuccessLogout(activity, it) },
|
||||
onBackClicked = ::navigateUp,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -46,13 +47,15 @@ class LogoutPresenter @Inject constructor(
|
||||
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 isLastSession by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
isLastSession = encryptionService.isLastDevice().getOrNull() ?: false
|
||||
encryptionService.waitForBackupUploadSteadyState()
|
||||
}
|
||||
|
||||
fun handleEvents(event: LogoutEvents) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.pages.HeaderFooterPage
|
||||
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.preview.ElementPreview
|
||||
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.OutlinedButton
|
||||
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.utils.CommonDrawables
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LogoutView(
|
||||
state: LogoutState,
|
||||
onChangeRecoveryKeyClicked: () -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -56,6 +61,12 @@ fun LogoutView(
|
||||
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = { BackButton(onClick = onBackClicked) },
|
||||
title = {},
|
||||
)
|
||||
},
|
||||
header = {
|
||||
HeaderContent(state = state)
|
||||
},
|
||||
@@ -134,7 +145,7 @@ private fun HeaderContent(
|
||||
else -> null
|
||||
}
|
||||
|
||||
val paddingTop = 60.dp
|
||||
val paddingTop = 0.dp
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier.padding(top = paddingTop),
|
||||
iconResourceId = CommonDrawables.ic_key,
|
||||
@@ -219,6 +230,7 @@ internal fun LogoutViewPreview(
|
||||
LogoutView(
|
||||
state,
|
||||
onChangeRecoveryKeyClicked = {},
|
||||
onSuccessLogout = {}
|
||||
onSuccessLogout = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.encryption.FakeEncryptionService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -73,6 +75,15 @@ class LogoutPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state - backing up`() = runTest {
|
||||
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(
|
||||
encryptionService = encryptionService
|
||||
)
|
||||
@@ -84,10 +95,10 @@ class LogoutPresenterTest {
|
||||
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
|
||||
assertThat(initialState.showConfirmationDialog).isFalse()
|
||||
assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized)
|
||||
encryptionService.emitBackupUploadState(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
|
||||
val state = awaitItem()
|
||||
assertThat(state.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
|
||||
encryptionService.emitBackupUploadState(BackupUploadState.Done)
|
||||
val waitingState = awaitItem()
|
||||
assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting)
|
||||
val uploadingState = awaitItem()
|
||||
assertThat(uploadingState.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2))
|
||||
val doneState = awaitItem()
|
||||
assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done)
|
||||
}
|
||||
|
||||
@@ -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.model.RoomListRoomSummary
|
||||
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.PreviewsDayNight
|
||||
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.Icon
|
||||
@@ -190,19 +190,22 @@ private fun RoomListContent(
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
) {
|
||||
if (state.displayVerificationPrompt) {
|
||||
item {
|
||||
RequestVerificationHeader(
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
||||
)
|
||||
when {
|
||||
state.displayVerificationPrompt -> {
|
||||
item {
|
||||
RequestVerificationHeader(
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (state.displayRecoveryKeyPrompt) {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClicked = onOpenSettings,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
state.displayRecoveryKeyPrompt -> {
|
||||
item {
|
||||
ConfirmRecoveryKeyBanner(
|
||||
onContinueClicked = onOpenSettings,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
anvil(projects.anvilcodegen)
|
||||
implementation(projects.anvilannotations)
|
||||
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
|
||||
@@ -40,6 +40,7 @@ class SecureBackupDisableNode @AssistedInject constructor(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onDone = ::navigateUp,
|
||||
onBackClicked = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.IconTitleSubtitleMolecule
|
||||
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.ErrorDialog
|
||||
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.Icon
|
||||
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.theme.ElementTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SecureBackupDisableView(
|
||||
state: SecureBackupDisableState,
|
||||
onDone: () -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LaunchedEffect(state.disableAction) {
|
||||
@@ -56,6 +61,12 @@ fun SecureBackupDisableView(
|
||||
}
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = { BackButton(onClick = onBackClicked) },
|
||||
title = {},
|
||||
)
|
||||
},
|
||||
header = {
|
||||
HeaderContent()
|
||||
},
|
||||
@@ -95,7 +106,7 @@ private fun HeaderContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier.padding(top = 60.dp),
|
||||
modifier = modifier.padding(top = 0.dp),
|
||||
iconResourceId = CommonDrawables.ic_key_off,
|
||||
title = stringResource(id = R.string.screen_key_backup_disable_title),
|
||||
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
|
||||
@@ -158,5 +169,6 @@ internal fun SecureBackupDisableViewPreview(
|
||||
SecureBackupDisableView(
|
||||
state = state,
|
||||
onDone = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class SecureBackupEnableNode @AssistedInject constructor(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onDone = ::navigateUp,
|
||||
onBackClicked = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl.enable
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.IconTitleSubtitleMolecule
|
||||
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.preview.ElementPreview
|
||||
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.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SecureBackupEnableView(
|
||||
state: SecureBackupEnableState,
|
||||
onDone: () -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LaunchedEffect(state.enableAction) {
|
||||
@@ -48,6 +53,12 @@ fun SecureBackupEnableView(
|
||||
}
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = { BackButton(onClick = onBackClicked) },
|
||||
title = {},
|
||||
)
|
||||
},
|
||||
header = {
|
||||
HeaderContent()
|
||||
},
|
||||
@@ -68,7 +79,7 @@ private fun HeaderContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier.padding(top = 60.dp),
|
||||
modifier = modifier.padding(top = 0.dp),
|
||||
iconResourceId = CommonDrawables.ic_key,
|
||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
||||
subTitle = null,
|
||||
@@ -99,5 +110,6 @@ internal fun SecureBackupEnableViewPreview(
|
||||
SecureBackupEnableView(
|
||||
state = state,
|
||||
onDone = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
|
||||
onDone = {
|
||||
coroutineScope.postSuccessSnackbar()
|
||||
navigateUp()
|
||||
}
|
||||
},
|
||||
onBackClicked = ::navigateUp,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl.enter
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.IconTitleSubtitleMolecule
|
||||
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.preview.ElementPreview
|
||||
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.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SecureBackupEnterRecoveryKeyView(
|
||||
state: SecureBackupEnterRecoveryKeyState,
|
||||
onDone: () -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state.submitAction) {
|
||||
@@ -59,6 +64,12 @@ fun SecureBackupEnterRecoveryKeyView(
|
||||
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = { BackButton(onClick = onBackClicked) },
|
||||
title = {},
|
||||
)
|
||||
},
|
||||
header = {
|
||||
HeaderContent()
|
||||
},
|
||||
@@ -75,6 +86,9 @@ fun SecureBackupEnterRecoveryKeyView(
|
||||
state = state,
|
||||
onChange = {
|
||||
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it))
|
||||
},
|
||||
onSubmit = {
|
||||
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -84,7 +98,7 @@ private fun HeaderContent(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier.padding(top = 60.dp),
|
||||
modifier = modifier.padding(top = 0.dp),
|
||||
iconResourceId = CommonDrawables.ic_key,
|
||||
title = stringResource(id = R.string.screen_recovery_key_confirm_title),
|
||||
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
|
||||
@@ -112,13 +126,15 @@ private fun BottomMenu(
|
||||
@Composable
|
||||
private fun Content(
|
||||
state: SecureBackupEnterRecoveryKeyState,
|
||||
onChange: ((String) -> Unit)?,
|
||||
onChange: (String) -> Unit,
|
||||
onSubmit: () -> Unit,
|
||||
) {
|
||||
RecoveryKeyView(
|
||||
modifier = Modifier.padding(top = 52.dp),
|
||||
state = state.recoveryKeyViewState,
|
||||
onClick = null,
|
||||
onChange = onChange,
|
||||
onSubmit = onSubmit,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -130,5 +146,6 @@ internal fun SecureBackupEnterRecoveryKeyViewPreview(
|
||||
SecureBackupEnterRecoveryKeyView(
|
||||
state = state,
|
||||
onDone = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
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
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
||||
@@ -60,6 +60,7 @@ class SecureBackupSetupNode @AssistedInject constructor(
|
||||
coroutineScope.postSuccessSnackbar()
|
||||
navigateUp()
|
||||
},
|
||||
onBackClicked = ::navigateUp,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
package io.element.android.features.securebackup.impl.setup
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.IconTitleSubtitleMolecule
|
||||
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.preview.ElementPreview
|
||||
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.IconSource
|
||||
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.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SecureBackupSetupView(
|
||||
state: SecureBackupSetupState,
|
||||
onDone: () -> Unit,
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val canGoBack = state.canGoBack()
|
||||
BackHandler(enabled = canGoBack) {
|
||||
onBackClicked()
|
||||
}
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
if (canGoBack) {
|
||||
BackButton(onClick = onBackClicked)
|
||||
}
|
||||
},
|
||||
title = {},
|
||||
)
|
||||
},
|
||||
header = {
|
||||
HeaderContent(state = state)
|
||||
},
|
||||
@@ -109,6 +129,10 @@ fun SecureBackupSetupView(
|
||||
}
|
||||
}
|
||||
|
||||
private fun SecureBackupSetupState.canGoBack(): Boolean {
|
||||
return recoveryKeyViewState.formattedRecoveryKey == null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeaderContent(
|
||||
state: SecureBackupSetupState,
|
||||
@@ -136,7 +160,7 @@ private fun HeaderContent(
|
||||
stringResource(id = R.string.screen_recovery_key_save_description)
|
||||
}
|
||||
IconTitleSubtitleMolecule(
|
||||
modifier = modifier.padding(top = 60.dp),
|
||||
modifier = modifier.padding(top = 0.dp),
|
||||
iconResourceId = CommonDrawables.ic_key,
|
||||
title = title,
|
||||
subTitle = subTitle,
|
||||
@@ -192,6 +216,7 @@ private fun Content(
|
||||
state = state,
|
||||
onClick = onClick,
|
||||
onChange = null,
|
||||
onSubmit = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,5 +228,6 @@ internal fun SecureBackupSetupViewPreview(
|
||||
SecureBackupSetupView(
|
||||
state = state,
|
||||
onDone = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,5 +33,6 @@ internal fun SecureBackupSetupViewChangePreview(
|
||||
recoveryKeyViewState = state.recoveryKeyViewState.copy(recoveryKeyUserStory = RecoveryKeyUserStory.Change),
|
||||
),
|
||||
onDone = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,12 +25,15 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -52,6 +55,7 @@ internal fun RecoveryKeyView(
|
||||
state: RecoveryKeyViewState,
|
||||
onClick: (() -> Unit)?,
|
||||
onChange: ((String) -> Unit)?,
|
||||
onSubmit: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
@@ -63,7 +67,7 @@ internal fun RecoveryKeyView(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
RecoveryKeyContent(state, onClick, onChange)
|
||||
RecoveryKeyContent(state, onClick, onChange, onSubmit)
|
||||
RecoveryKeyFooter(state)
|
||||
}
|
||||
}
|
||||
@@ -73,11 +77,12 @@ private fun RecoveryKeyContent(
|
||||
state: RecoveryKeyViewState,
|
||||
onClick: (() -> Unit)?,
|
||||
onChange: ((String) -> Unit)?,
|
||||
onSubmit: (() -> Unit)?,
|
||||
) {
|
||||
when (state.recoveryKeyUserStory) {
|
||||
RecoveryKeyUserStory.Setup,
|
||||
RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick)
|
||||
RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange)
|
||||
RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange, onSubmit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +148,13 @@ private fun RecoveryKeyStaticContent(
|
||||
}
|
||||
|
||||
@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")
|
||||
onSubmit ?: error("onSubmit should not be null")
|
||||
val recoveryKeyVisualTransformation = remember {
|
||||
RecoveryKeyVisualTransformation()
|
||||
}
|
||||
@@ -155,6 +165,12 @@ private fun RecoveryKeyFormContent(state: RecoveryKeyViewState, onChange: ((Stri
|
||||
onValueChange = onChange,
|
||||
enabled = state.inProgress.not(),
|
||||
visualTransformation = recoveryKeyVisualTransformation,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onSubmit() }
|
||||
),
|
||||
label = { Text(text = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder)) }
|
||||
)
|
||||
}
|
||||
@@ -217,5 +233,6 @@ internal fun RecoveryKeyViewPreview(
|
||||
state = state,
|
||||
onClick = {},
|
||||
onChange = {},
|
||||
onSubmit = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.encryption
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface EncryptionService {
|
||||
val backupStateStateFlow: StateFlow<BackupState>
|
||||
val recoveryStateStateFlow: StateFlow<RecoveryState>
|
||||
val backupUploadStateStateFlow: StateFlow<BackupUploadState>
|
||||
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
|
||||
|
||||
suspend fun enableBackups(): Result<Unit>
|
||||
@@ -46,7 +46,7 @@ interface EncryptionService {
|
||||
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>
|
||||
}
|
||||
|
||||
@@ -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.EncryptionService
|
||||
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.callbackFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.BackupStateListener
|
||||
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 recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map))
|
||||
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Unknown)
|
||||
override val backupUploadStateStateFlow: MutableStateFlow<BackupUploadState> = MutableStateFlow(BackupUploadState.Unknown)
|
||||
|
||||
fun start() {
|
||||
service.backupStateListener(object : BackupStateListener {
|
||||
@@ -94,16 +96,19 @@ internal class RustEncryptionService(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun waitForBackupUploadSteadyState(
|
||||
): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
override fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> {
|
||||
return callbackFlow {
|
||||
service.waitForBackupUploadSteadyState(
|
||||
progressListener = object : BackupSteadyStateListener {
|
||||
override fun onUpdate(status: RustBackupUploadState) {
|
||||
backupUploadStateStateFlow.value = backupUploadStateMapper.map(status)
|
||||
trySend(backupUploadStateMapper.map(status))
|
||||
if (status == RustBackupUploadState.Done) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
awaitClose {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.RecoveryState
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class FakeEncryptionService : EncryptionService {
|
||||
private var disableRecoveryFailure: Exception? = null
|
||||
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
|
||||
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.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
|
||||
|
||||
@@ -73,12 +75,12 @@ class FakeEncryptionService : EncryptionService {
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun waitForBackupUploadSteadyState(): Result<Unit> {
|
||||
return Result.success(Unit)
|
||||
fun givenWaitForBackupUploadSteadyStateFlow(flow: Flow<BackupUploadState>) {
|
||||
waitForBackupUploadSteadyStateFlow = flow
|
||||
}
|
||||
|
||||
suspend fun emitBackupUploadState(state: BackupUploadState) {
|
||||
backupUploadStateStateFlow.emit(state)
|
||||
override fun waitForBackupUploadSteadyState(): Flow<BackupUploadState> {
|
||||
return waitForBackupUploadSteadyStateFlow
|
||||
}
|
||||
|
||||
suspend fun emitBackupState(state: BackupState) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user