Merge pull request #1716 from vector-im/feature/bma/secureBackupFeatureFlag

Secure backup: create a feature flag (disabled)
This commit is contained in:
Benoit Marty 2023-10-31 22:52:18 +01:00 committed by GitHub
commit d764536af5
13 changed files with 68 additions and 11 deletions

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
@ -48,5 +49,6 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
} }

View file

@ -28,16 +28,21 @@ import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Async 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.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
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.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.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class LogoutPresenter @Inject constructor( class LogoutPresenter @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
) : Presenter<LogoutState> { ) : Presenter<LogoutState> {
@Composable @Composable
@ -47,8 +52,15 @@ class LogoutPresenter @Inject constructor(
mutableStateOf(Async.Uninitialized) mutableStateOf(Async.Uninitialized)
} }
val backupUploadState: BackupUploadState by remember { val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
encryptionService.waitForBackupUploadSteadyState() .collectAsState(initial = null)
val backupUploadState: BackupUploadState by remember(secureStorageFlag) {
when (secureStorageFlag) {
true -> encryptionService.waitForBackupUploadSteadyState()
false -> flowOf(BackupUploadState.Done)
else -> emptyFlow()
}
} }
.collectAsState(initial = BackupUploadState.Unknown) .collectAsState(initial = BackupUploadState.Unknown)

View file

@ -21,6 +21,8 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
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.BackupUploadState
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -28,6 +30,7 @@ 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 io.element.android.tests.testutils.awaitLastSequentialItem
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -45,7 +48,7 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitLastSequentialItem()
assertThat(initialState.isLastSession).isFalse() assertThat(initialState.isLastSession).isFalse()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.showConfirmationDialog).isFalse()
@ -90,6 +93,7 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isLastSession).isFalse() assertThat(initialState.isLastSession).isFalse()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)
@ -110,7 +114,7 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.showConfirmationDialog).isTrue()
@ -126,7 +130,7 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.showConfirmationDialog).isTrue()
@ -151,7 +155,7 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.showConfirmationDialog).isTrue()
@ -179,7 +183,7 @@ class LogoutPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitLastSequentialItem()
initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false))
val confirmationState = awaitItem() val confirmationState = awaitItem()
assertThat(confirmationState.showConfirmationDialog).isTrue() assertThat(confirmationState.showConfirmationDialog).isTrue()
@ -205,6 +209,7 @@ class LogoutPresenterTest {
): LogoutPresenter = LogoutPresenter( ): LogoutPresenter = LogoutPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
encryptionService = encryptionService, encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
) )
} }

View file

@ -78,6 +78,9 @@ class PreferencesRootPresenter @Inject constructor(
val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator() val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator()
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
.collectAsState(initial = null)
val accountManagementUrl: MutableState<String?> = remember { val accountManagementUrl: MutableState<String?> = remember {
mutableStateOf(null) mutableStateOf(null)
} }
@ -94,7 +97,7 @@ class PreferencesRootPresenter @Inject constructor(
myUser = matrixUser.value, myUser = matrixUser.value,
version = versionFormatter.get(), version = versionFormatter.get(),
showCompleteVerification = showCompleteVerification, showCompleteVerification = showCompleteVerification,
showSecureBackup = !showCompleteVerification, showSecureBackup = !showCompleteVerification && secureStorageFlag == true,
showSecureBackupBadge = showSecureBackupIndicator, showSecureBackupBadge = showSecureBackupIndicator,
accountManagementUrl = accountManagementUrl.value, accountManagementUrl = accountManagementUrl.value,
devicesManagementUrl = devicesManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value,

View file

@ -56,6 +56,7 @@ class PreferencesRootPresenterTest {
DefaultIndicatorService( DefaultIndicatorService(
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
encryptionService = FakeEncryptionService(), encryptionService = FakeEncryptionService(),
featureFlagService = FakeFeatureFlagService(),
), ),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
@ -64,6 +65,7 @@ dependencies {
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.eventformatter.test)
testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.indicator.impl)

View file

@ -35,6 +35,8 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState 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.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -57,6 +59,7 @@ class RoomListPresenter @Inject constructor(
private val leaveRoomPresenter: LeaveRoomPresenter, private val leaveRoomPresenter: LeaveRoomPresenter,
private val roomListDataSource: RoomListDataSource, private val roomListDataSource: RoomListDataSource,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
@ -84,10 +87,14 @@ class RoomListPresenter @Inject constructor(
derivedStateOf { canVerifySession && !verificationPromptDismissed } derivedStateOf { canVerifySession && !verificationPromptDismissed }
} }
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
.collectAsState(initial = null)
var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) } var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) }
val displayRecoveryKeyPrompt by remember { val displayRecoveryKeyPrompt by remember {
derivedStateOf { derivedStateOf {
recoveryState == RecoveryState.INCOMPLETE && !recoveryKeyPromptDismissed secureStorageFlag == true &&
recoveryState == RecoveryState.INCOMPLETE &&
!recoveryKeyPromptDismissed
} }
} }

View file

@ -37,6 +37,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
@ -417,9 +419,11 @@ class RoomListPresenterTests {
appScope = coroutineScope appScope = coroutineScope
), ),
encryptionService = encryptionService, encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
indicatorService = DefaultIndicatorService( indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService, encryptionService = encryptionService,
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)),
), ),
) )
} }

View file

@ -66,5 +66,11 @@ enum class FeatureFlags(
title = "Mentions", title = "Mentions",
description = "Type `@` to get mention suggestions and insert them", description = "Type `@` to get mention suggestions and insert them",
defaultValue = false, defaultValue = false,
),
SecureStorage(
key = "feature.securestorage",
title = "Chat backup",
description = "Allow access to backup and restore chat history settings",
defaultValue = false,
) )
} }

View file

@ -41,6 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.PinUnlock -> true FeatureFlags.PinUnlock -> true
FeatureFlags.InRoomCalls -> true FeatureFlags.InRoomCalls -> true
FeatureFlags.Mentions -> false FeatureFlags.Mentions -> false
FeatureFlags.SecureStorage -> false
} }
} else { } else {
false false

View file

@ -31,6 +31,7 @@ dependencies {
anvil(projects.anvilcodegen) anvil(projects.anvilcodegen)
implementation(libs.dagger) implementation(libs.dagger)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
@ -38,6 +39,7 @@ dependencies {
api(projects.libraries.indicator.api) api(projects.libraries.indicator.api)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)

View file

@ -24,6 +24,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
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.indicator.api.IndicatorService
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -35,6 +37,7 @@ import javax.inject.Inject
class DefaultIndicatorService @Inject constructor( class DefaultIndicatorService @Inject constructor(
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
) : IndicatorService { ) : IndicatorService {
@Composable @Composable
@ -51,6 +54,8 @@ class DefaultIndicatorService @Inject constructor(
@Composable @Composable
override fun showSettingChatBackupIndicator(): State<Boolean> { override fun showSettingChatBackupIndicator(): State<Boolean> {
val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage)
.collectAsState(initial = null)
val backupState by encryptionService.backupStateStateFlow.collectAsState() val backupState by encryptionService.backupStateStateFlow.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
@ -63,7 +68,7 @@ class DefaultIndicatorService @Inject constructor(
RecoveryState.DISABLED, RecoveryState.DISABLED,
RecoveryState.INCOMPLETE, RecoveryState.INCOMPLETE,
) )
showForBackup || showForRecovery secureStorageFlag == true && (showForBackup || showForRecovery)
} }
} }
} }

View file

@ -36,6 +36,8 @@ import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFo
import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter
import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter
import io.element.android.libraries.eventformatter.impl.StateContentFormatter import io.element.android.libraries.eventformatter.impl.StateContentFormatter
import io.element.android.libraries.featureflag.impl.DefaultFeatureFlagService
import io.element.android.libraries.featureflag.impl.StaticFeatureFlagProvider
import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -62,6 +64,9 @@ class RoomListScreen(
private val sessionVerificationService = matrixClient.sessionVerificationService() private val sessionVerificationService = matrixClient.sessionVerificationService()
private val encryptionService = matrixClient.encryptionService() private val encryptionService = matrixClient.encryptionService()
private val stringProvider = AndroidStringProvider(context.resources) private val stringProvider = AndroidStringProvider(context.resources)
private val featureFlagService = DefaultFeatureFlagService(
providers = setOf(StaticFeatureFlagProvider())
)
private val presenter = RoomListPresenter( private val presenter = RoomListPresenter(
client = matrixClient, client = matrixClient,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
@ -87,7 +92,9 @@ class RoomListScreen(
indicatorService = DefaultIndicatorService( indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService, encryptionService = encryptionService,
) featureFlagService = featureFlagService,
),
featureFlagService = featureFlagService,
) )
@Composable @Composable