Secure backup

This commit is contained in:
Benoit Marty 2023-10-27 11:14:00 +02:00 committed by Benoit Marty
parent bf905dd79b
commit 9807ebf649
115 changed files with 4698 additions and 393 deletions

View file

@ -30,6 +30,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode
@ -53,6 +54,7 @@ class PreferencesFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val lockScreenEntryPoint: LockScreenEntryPoint,
private val logoutEntryPoint: LogoutEntryPoint,
) : BackstackNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
@ -92,6 +94,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data class UserProfile(val matrixUser: MatrixUser) : NavTarget
@Parcelize
data object SignOut : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -106,6 +111,10 @@ class PreferencesFlowNode @AssistedInject constructor(
plugins<PreferencesEntryPoint.Callback>().forEach { it.onVerifyClicked() }
}
override fun onSecureBackupClicked() {
plugins<PreferencesEntryPoint.Callback>().forEach { it.onSecureBackupClicked() }
}
override fun onOpenAnalytics() {
backstack.push(NavTarget.AnalyticsSettings)
}
@ -133,6 +142,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onOpenUserProfile(matrixUser: MatrixUser) {
backstack.push(NavTarget.UserProfile(matrixUser))
}
override fun onSignOutClicked() {
backstack.push(NavTarget.SignOut)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@ -182,6 +195,16 @@ class PreferencesFlowNode @AssistedInject constructor(
.target(LockScreenEntryPoint.Target.Settings)
.build()
}
NavTarget.SignOut -> {
val callBack: LogoutEntryPoint.Callback = object : LogoutEntryPoint.Callback {
override fun onChangeRecoveryKeyClicked() {
plugins<PreferencesEntryPoint.Callback>().forEach { it.onSecureBackupClicked() }
}
}
logoutEntryPoint.nodeBuilder(this, buildContext)
.callback(callBack)
.build()
}
}
}

View file

@ -31,7 +31,6 @@ import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTa
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.theme.ElementTheme
import timber.log.Timber
@ContributesNode(SessionScope::class)
class PreferencesRootNode @AssistedInject constructor(
@ -43,6 +42,7 @@ class PreferencesRootNode @AssistedInject constructor(
interface Callback : Plugin {
fun onOpenBugReport()
fun onVerifyClicked()
fun onSecureBackupClicked()
fun onOpenAnalytics()
fun onOpenAbout()
fun onOpenDeveloperSettings()
@ -50,6 +50,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenLockScreenSettings()
fun onOpenAdvancedSettings()
fun onOpenUserProfile(matrixUser: MatrixUser)
fun onSignOutClicked()
}
private fun onOpenBugReport() {
@ -60,6 +61,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onVerifyClicked() }
}
private fun onSecureBackupClicked() {
plugins<Callback>().forEach { it.onSecureBackupClicked() }
}
private fun onOpenDeveloperSettings() {
plugins<Callback>().forEach { it.onOpenDeveloperSettings() }
}
@ -102,6 +107,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenUserProfile(matrixUser) }
}
private fun onSignOutClicked() {
plugins<Callback>().forEach { it.onSignOutClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -115,20 +124,14 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenAnalytics = this::onOpenAnalytics,
onOpenAbout = this::onOpenAbout,
onVerifyClicked = this::onVerifyClicked,
onSecureBackupClicked = this::onSecureBackupClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings,
onOpenLockScreenSettings = this::onOpenLockScreenSettings,
onOpenUserProfile = this::onOpenUserProfile,
onSignOutClicked = this::onSignOutClicked,
)
}
private fun onSuccessLogout(activity: Activity, url: String?) {
Timber.d("Success logout with result url: $url")
url?.let {
activity.openUrlInChromeCustomTab(null, false, it)
}
}
}

View file

@ -24,13 +24,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.logout.api.LogoutPreferencePresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -42,7 +42,6 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
class PreferencesRootPresenter @Inject constructor(
private val logoutPresenter: LogoutPreferencePresenter,
private val matrixClient: MatrixClient,
private val sessionVerificationService: SessionVerificationService,
private val analyticsService: AnalyticsService,
@ -50,6 +49,7 @@ class PreferencesRootPresenter @Inject constructor(
private val versionFormatter: VersionFormatter,
private val snackbarDispatcher: SnackbarDispatcher,
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService,
) : Presenter<PreferencesRootState> {
@Composable
@ -76,6 +76,8 @@ class PreferencesRootPresenter @Inject constructor(
// We should display the 'complete verification' option if the current session can be verified
val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false)
val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator()
val accountManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
@ -87,13 +89,13 @@ class PreferencesRootPresenter @Inject constructor(
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
}
val logoutState = logoutPresenter.present()
val showDeveloperSettings = buildType != BuildType.RELEASE
return PreferencesRootState(
logoutState = logoutState,
myUser = matrixUser.value,
version = versionFormatter.get(),
showCompleteVerification = showCompleteVerification,
showSecureBackup = !showCompleteVerification,
showSecureBackupBadge = showSecureBackupIndicator,
accountManagementUrl = accountManagementUrl.value,
devicesManagementUrl = devicesManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders,

View file

@ -16,15 +16,15 @@
package io.element.android.features.preferences.impl.root
import io.element.android.features.logout.api.LogoutPreferenceState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
data class PreferencesRootState(
val logoutState: LogoutPreferenceState,
val myUser: MatrixUser?,
val version: String,
val showCompleteVerification: Boolean,
val showSecureBackup: Boolean,
val showSecureBackupBadge: Boolean,
val accountManagementUrl: String?,
val devicesManagementUrl: String?,
val showAnalyticsSettings: Boolean,

View file

@ -16,15 +16,15 @@
package io.element.android.features.preferences.impl.root
import io.element.android.features.logout.api.aLogoutPreferenceState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.ui.strings.CommonStrings
fun aPreferencesRootState() = PreferencesRootState(
logoutState = aLogoutPreferenceState(),
myUser = null,
version = "Version 1.1 (1)",
showCompleteVerification = true,
showSecureBackup = true,
showSecureBackupBadge = true,
accountManagementUrl = "aUrl",
devicesManagementUrl = "anOtherUrl",
showAnalyticsSettings = true,

View file

@ -29,10 +29,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.logout.api.LogoutPreferenceView
import io.element.android.features.preferences.impl.user.UserPreferences
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
@ -51,6 +50,7 @@ fun PreferencesRootView(
state: PreferencesRootState,
onBackPressed: () -> Unit,
onVerifyClicked: () -> Unit,
onSecureBackupClicked: () -> Unit,
onManageAccountClicked: (url: String) -> Unit,
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
@ -58,9 +58,9 @@ fun PreferencesRootView(
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
onOpenNotificationSettings: () -> Unit,
onOpenUserProfile: (MatrixUser) -> Unit,
onSignOutClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@ -84,6 +84,16 @@ fun PreferencesRootView(
icon = Icons.Outlined.VerifiedUser,
onClick = onVerifyClicked,
)
}
if (state.showSecureBackup) {
PreferenceText(
title = stringResource(id = CommonStrings.common_chat_backup),
iconResourceId = CommonDrawables.ic_key_filled,
showEndBadge = state.showSecureBackupBadge,
onClick = onSecureBackupClicked,
)
}
if (state.showCompleteVerification || state.showSecureBackup) {
HorizontalDivider()
}
if (state.accountManagementUrl != null) {
@ -143,9 +153,10 @@ fun PreferencesRootView(
DeveloperPreferencesView(onOpenDeveloperSettings)
}
HorizontalDivider()
LogoutPreferenceView(
state = state.logoutState,
onSuccessLogout = onSuccessLogout,
PreferenceText(
title = stringResource(id = CommonStrings.action_signout),
iconResourceId = CommonDrawables.ic_compound_leave,
onClick = onSignOutClicked,
)
Text(
modifier = Modifier
@ -189,10 +200,11 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onOpenAdvancedSettings = {},
onOpenAbout = {},
onVerifyClicked = {},
onSuccessLogout = {},
onSecureBackupClicked = {},
onManageAccountClicked = {},
onOpenNotificationSettings = {},
onOpenLockScreenSettings = {},
onOpenUserProfile = {},
onSignOutClicked = {},
)
}

View file

@ -20,15 +20,15 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@ -44,16 +44,19 @@ class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val matrixClient = FakeMatrixClient()
val logoutPresenter = DefaultLogoutPreferencePresenter(matrixClient)
val sessionVerificationService = FakeSessionVerificationService()
val presenter = PreferencesRootPresenter(
logoutPresenter,
matrixClient,
FakeSessionVerificationService(),
sessionVerificationService,
FakeAnalyticsService(),
BuildType.DEBUG,
FakeVersionFormatter(),
SnackbarDispatcher(),
FakeFeatureFlagService()
FakeFeatureFlagService(),
DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = FakeEncryptionService(),
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -62,7 +65,6 @@ class PreferencesRootPresenterTest {
assertThat(initialState.myUser).isNull()
assertThat(initialState.version).isEqualTo("A Version")
val loadedState = awaitItem()
assertThat(loadedState.logoutState.logoutAction).isEqualTo(Async.Uninitialized)
assertThat(loadedState.myUser).isEqualTo(
MatrixUser(
userId = matrixClient.sessionId,