diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index d12d139a73..729603c4d7 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -25,6 +25,7 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -38,7 +39,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.plus -import okhttp3.logging.HttpLoggingInterceptor import java.io.File import java.util.concurrent.Executors @@ -64,8 +64,15 @@ object AppModule { @Provides @SingleIn(AppScope::class) - fun providesBuildMeta(@ApplicationContext context: Context) = BuildMeta( - isDebug = BuildConfig.DEBUG, + fun providesBuildType(): BuildType { + return BuildType.valueOf(BuildConfig.BUILD_TYPE.uppercase()) + } + + @Provides + @SingleIn(AppScope::class) + fun providesBuildMeta(@ApplicationContext context: Context, buildType: BuildType) = BuildMeta( + isDebuggable = BuildConfig.DEBUG, + buildType = buildType, applicationName = context.getString(R.string.app_name), applicationId = BuildConfig.APPLICATION_ID, lowPrivacyLoggingEnabled = false, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, @@ -75,7 +82,6 @@ object AppModule { gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME, flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION, flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION, - okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC, ) @Provides diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 272d0ed00f..8e96500aab 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.designsystem) + implementation(projects.libraries.featureflag.api) + implementation(projects.libraries.featureflag.ui) implementation(projects.libraries.elementresources) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) @@ -53,6 +55,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.featureflag.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.logout.impl) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 30b4d0a65c..cded5d4bf6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -25,10 +25,12 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode import io.element.android.features.preferences.impl.root.PreferencesRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -52,6 +54,9 @@ class PreferencesFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize object Root : NavTarget + + @Parcelize + object DeveloperSettings : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -61,9 +66,16 @@ class PreferencesFlowNode @AssistedInject constructor( override fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } + + override fun onOpenDeveloperSettings() { + backstack.push(NavTarget.DeveloperSettings) + } } createNode(buildContext, plugins = listOf(callback)) } + NavTarget.DeveloperSettings -> { + createNode(buildContext) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt new file mode 100644 index 0000000000..b79484592f --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.developer + +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel + +sealed interface DeveloperSettingsEvents { + data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt new file mode 100644 index 0000000000..5b0795257c --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.developer + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class DeveloperSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: DeveloperSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + DeveloperSettingsView( + state = state, + modifier = modifier, + onBackPressed = this::navigateUp + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt new file mode 100644 index 0000000000..9f9cd636eb --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.developer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.SnapshotStateMap +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class DeveloperSettingsPresenter @Inject constructor( + private val featureFlagService: FeatureFlagService, +) : Presenter { + + @Composable + override fun present(): DeveloperSettingsState { + + val features = remember { + mutableStateMapOf() + } + val enabledFeatures = remember { + mutableStateMapOf() + } + LaunchedEffect(Unit) { + FeatureFlags.values().forEach { feature -> + features[feature.key] = feature + enabledFeatures[feature.key] = featureFlagService.isFeatureEnabled(feature) + } + } + val featureUiModels = createUiModels(features, enabledFeatures) + val coroutineScope = rememberCoroutineScope() + + fun handleEvents(event: DeveloperSettingsEvents) { + when (event) { + is DeveloperSettingsEvents.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature( + features, + enabledFeatures, + event.feature, + event.isEnabled + ) + } + } + + return DeveloperSettingsState( + features = featureUiModels.toImmutableList(), + eventSink = ::handleEvents + ) + } + + @Composable + private fun createUiModels( + features: SnapshotStateMap, + enabledFeatures: SnapshotStateMap + ): List { + return features.values.map { feature -> + key(feature.key) { + val isEnabled = enabledFeatures[feature.key].orFalse() + remember(feature, isEnabled) { + FeatureUiModel( + key = feature.key, + title = feature.title, + isEnabled = isEnabled + ) + } + } + } + } + + private fun CoroutineScope.updateEnabledFeature( + features: SnapshotStateMap, + enabledFeatures: SnapshotStateMap, + featureUiModel: FeatureUiModel, + enabled: Boolean + ) = launch { + val feature = features[featureUiModel.key] ?: return@launch + if (featureFlagService.setFeatureEnabled(feature, enabled)) { + enabledFeatures[featureUiModel.key] = enabled + } + } +} + + + diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt new file mode 100644 index 0000000000..53ff80967e --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.developer + +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import kotlinx.collections.immutable.ImmutableList + +data class DeveloperSettingsState( + val features: ImmutableList, + val eventSink: (DeveloperSettingsEvents) -> Unit +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt new file mode 100644 index 0000000000..f69f73e6e5 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.developer + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList + +open class DeveloperSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aDeveloperSettingsState(), + ) +} + +fun aDeveloperSettingsState() = DeveloperSettingsState( + features = aFeatureUiModelList(), + eventSink = {} +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt new file mode 100644 index 0000000000..9b8c5e9cde --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.preferences.impl.developer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.components.preferences.PreferenceTopAppBar +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.featureflag.ui.FeatureListView +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.ui.strings.R + +@Composable +fun DeveloperSettingsView( + state: DeveloperSettingsState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit, +) { + Scaffold( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + contentWindowInsets = WindowInsets.statusBars, + topBar = { + PreferenceTopAppBar( + title = stringResource(id = R.string.common_developer_options), + onBackPressed = onBackPressed, + ) + }, + content = { + FeatureListContent(it, state) + } + ) +} + +@Composable +fun FeatureListContent( + paddingValues: PaddingValues, + state: DeveloperSettingsState, + modifier: Modifier = Modifier +) { + + fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) { + state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled)) + } + + Box( + modifier = modifier + .padding(paddingValues) + .fillMaxSize() + ) { + FeatureListView(features = state.features, onCheckedChange = ::onFeatureEnabled) + } +} + +@Preview +@Composable +fun DeveloperSettingsViewLightPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun DeveloperSettingsViewDarkPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: DeveloperSettingsState) { + DeveloperSettingsView( + state = state, + onBackPressed = {} + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 36e4afab84..0ddffefe59 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -36,12 +36,17 @@ class PreferencesRootNode @AssistedInject constructor( interface Callback : Plugin { fun onOpenBugReport() + fun onOpenDeveloperSettings() } private fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } } + private fun onOpenDeveloperSettings() { + plugins().forEach { it.onOpenDeveloperSettings() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -49,7 +54,9 @@ class PreferencesRootNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = this::navigateUp, - onOpenRageShake = this::onOpenBugReport + onOpenRageShake = this::onOpenBugReport, + onOpenDeveloperSettings = this::onOpenDeveloperSettings ) } + } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 68036cfa9b..b1f1762dfc 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -21,22 +21,25 @@ import io.element.android.features.logout.api.LogoutPreferencePresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildType import javax.inject.Inject class PreferencesRootPresenter @Inject constructor( private val logoutPresenter: LogoutPreferencePresenter, private val rageshakePresenter: RageshakePreferencesPresenter, + private val buildType: BuildType, ) : Presenter { @Composable override fun present(): PreferencesRootState { val logoutState = logoutPresenter.present() val rageshakeState = rageshakePresenter.present() - + val showDeveloperSettings = buildType != BuildType.RELEASE return PreferencesRootState( logoutState = logoutState, rageshakeState = rageshakeState, myUser = Async.Uninitialized, + showDeveloperSettings = showDeveloperSettings ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index fbf307f7e8..5603d7f508 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -25,4 +25,5 @@ data class PreferencesRootState( val logoutState: LogoutPreferenceState, val rageshakeState: RageshakePreferencesState, val myUser: Async, + val showDeveloperSettings: Boolean ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index a12b16a54b..87b5b36e0d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -23,5 +23,6 @@ import io.element.android.libraries.architecture.Async fun aPreferencesRootState() = PreferencesRootState( logoutState = aLogoutPreferenceState(), rageshakeState = aRageshakePreferencesState(), - myUser = Async.Uninitialized + myUser = Async.Uninitialized, + showDeveloperSettings = true ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 3704756d09..3f4230ee01 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -16,6 +16,8 @@ package io.element.android.features.preferences.impl.root +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -25,6 +27,8 @@ import io.element.android.features.logout.api.LogoutPreferenceView import io.element.android.features.preferences.impl.user.UserPreferences import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.components.preferences.PreferenceView import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -38,6 +42,7 @@ fun PreferencesRootView( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, onOpenRageShake: () -> Unit = {}, + onOpenDeveloperSettings: () -> Unit = {}, ) { // TODO Hierarchy! // Include pref from other modules @@ -54,6 +59,20 @@ fun PreferencesRootView( LogoutPreferenceView( state = state.logoutState, ) + if (state.showDeveloperSettings) { + DeveloperPreferencesView(onOpenDeveloperSettings) + } + } +} + +@Composable +fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { + PreferenceCategory(title = stringResource(id = StringR.string.common_developer_options)) { + PreferenceText( + title = stringResource(id = StringR.string.common_developer_options), + icon = Icons.Default.DeveloperMode, + onClick = onOpenDeveloperSettings + ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt new file mode 100644 index 0000000000..2e4bd005ca --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.preferences.impl.developer + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DeveloperSettingsPresenterTest { + @Test + fun `present - ensures initial state is correct`() = runTest { + val presenter = DeveloperSettingsPresenter( + FakeFeatureFlagService() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.features).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ensures feature list is loaded`() = runTest { + val presenter = DeveloperSettingsPresenter( + FakeFeatureFlagService() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val state = awaitItem() + assertThat(state.features).hasSize(FeatureFlags.values().size) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { + val presenter = DeveloperSettingsPresenter( + FakeFeatureFlagService() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val stateBeforeEvent = awaitItem() + val featureBeforeEvent = stateBeforeEvent.features.first() + stateBeforeEvent.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(featureBeforeEvent, !featureBeforeEvent.isEnabled)) + val stateAfterEvent = awaitItem() + val featureAfterEvent = stateAfterEvent.features.first() + assertThat(featureBeforeEvent.key).isEqualTo(featureAfterEvent.key) + assertThat(featureBeforeEvent.isEnabled).isNotEqualTo(featureAfterEvent.isEnabled) + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 83d1163e9e..38e97148ca 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -27,6 +27,7 @@ import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePr import io.element.android.features.rageshake.test.rageshake.FakeRageShake import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.matrix.test.FakeMatrixClient import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -38,7 +39,9 @@ class PreferencesRootPresenterTest { val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient()) val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()) val presenter = PreferencesRootPresenter( - logoutPresenter, rageshakePresenter + logoutPresenter, + rageshakePresenter, + BuildType.DEBUG ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -50,6 +53,7 @@ class PreferencesRootPresenterTest { assertThat(initialState.rageshakeState.isSupported).isTrue() assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f) assertThat(initialState.myUser).isEqualTo(Async.Uninitialized) + assertThat(initialState.showDeveloperSettings).isEqualTo(true) } } } diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index 6cc5f4b33e..40eaddfc06 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -26,9 +26,6 @@ java { dependencies { implementation(libs.coroutines.core) - implementation(platform(libs.network.okhttp.bom)) - implementation("com.squareup.okhttp3:logging-interceptor") - testImplementation(libs.test.junit) testImplementation(libs.test.truth) } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt index 8fefe19919..f816f13a39 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt @@ -16,10 +16,9 @@ package io.element.android.libraries.core.meta -import okhttp3.logging.HttpLoggingInterceptor - data class BuildMeta( - val isDebug: Boolean, + val buildType: BuildType, + val isDebuggable: Boolean, val applicationName: String, val applicationId: String, val lowPrivacyLoggingEnabled: Boolean, @@ -29,5 +28,4 @@ data class BuildMeta( val gitBranchName: String, val flavorDescription: String, val flavorShortDescription: String, - val okHttpLoggingLevel: HttpLoggingInterceptor.Level, ) diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildType.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildType.kt new file mode 100644 index 0000000000..085fea8e0f --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildType.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.core.meta + +enum class BuildType { + RELEASE, + NIGHTLY, + DEBUG +} diff --git a/libraries/featureflag/api/build.gradle.kts b/libraries/featureflag/api/build.gradle.kts new file mode 100644 index 0000000000..9420821932 --- /dev/null +++ b/libraries/featureflag/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.featureflag.api" +} + +dependencies { + implementation(libs.coroutines.core) +} diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt new file mode 100644 index 0000000000..8343bca8e4 --- /dev/null +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.api + +interface Feature { + /** + * Unique key to identify the feature. + */ + val key: String + + /** + * Title to show in the UI. Not needed to be translated as it's only dev accessible. + */ + val title: String + + /** + * Optional description to give more context on the feature. + */ + val description: String? + + /** + * The default value of the feature (enabled or disabled). + */ + val defaultValue: Boolean +} diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt new file mode 100644 index 0000000000..59e224a1ae --- /dev/null +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.api + +interface FeatureFlagService { + /** + * @param feature the feature to check for + * + * @return true if the feature is enabled + */ + suspend fun isFeatureEnabled(feature: Feature): Boolean + + /** + * @param feature the feature to enable or disable + * @param enabled true to enable the feature + * + * @return true if the method succeeds, ie if a RuntimeFeatureFlagProvider is registered + */ + suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean +} diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt new file mode 100644 index 0000000000..920be09389 --- /dev/null +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.api + +enum class FeatureFlags( + override val key: String, + override val title: String, + override val description: String? = null, + override val defaultValue: Boolean = true +) : Feature { + CollapseRoomStateEvents( + key = "feature.collapseroomstateevents", + title = "Collapse room state events", + ), + ShowStartChatFlow( + key = "feature.showstartchatflow", + title = "Show start chat flow", + ), + ShowMediaUploadingFlow( + key = "feature.showmediauploadingflow", + title = "Show media uploading flow", + ) +} diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts new file mode 100644 index 0000000000..95a7d6fa55 --- /dev/null +++ b/libraries/featureflag/impl/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.featureflag.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + api(projects.libraries.featureflag.api) + implementation(libs.dagger) + implementation(libs.androidx.datastore.preferences) + implementation(projects.libraries.di) + implementation(projects.libraries.core) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) +} diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt new file mode 100644 index 0000000000..ae498e67df --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlags +import javax.inject.Inject + +class BuildtimeFeatureFlagProvider @Inject constructor() : + FeatureFlagProvider { + + override val priority: Int + get() = LOW_PRIORITY + + override suspend fun isFeatureEnabled(feature: Feature): Boolean { + return if (feature is FeatureFlags) { + when (feature) { + FeatureFlags.CollapseRoomStateEvents -> false + FeatureFlags.ShowStartChatFlow -> false + FeatureFlags.ShowMediaUploadingFlow -> false + } + } else { + false + } + } + + override fun hasFeature(feature: Feature) = true +} diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt new file mode 100644 index 0000000000..7298929aea --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlagService +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultFeatureFlagService @Inject constructor( + private val providers: Set<@JvmSuppressWildcards FeatureFlagProvider> +) : FeatureFlagService { + + override suspend fun isFeatureEnabled(feature: Feature): Boolean { + return providers.filter { it.hasFeature(feature) } + .sortedByDescending(FeatureFlagProvider::priority) + .firstOrNull() + ?.isFeatureEnabled(feature) + ?: feature.defaultValue + } + + override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { + return providers.filterIsInstance(RuntimeFeatureFlagProvider::class.java) + .sortedBy(FeatureFlagProvider::priority) + .firstOrNull() + ?.setFeatureEnabled(feature, enabled) + ?.let { true } + ?: false + } +} diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt new file mode 100644 index 0000000000..833d9f9003 --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import io.element.android.libraries.featureflag.api.Feature + +interface FeatureFlagProvider { + val priority: Int + suspend fun isFeatureEnabled(feature: Feature): Boolean + fun hasFeature(feature: Feature): Boolean +} + +const val LOW_PRIORITY = 0 +const val MEDIUM_PRIORITY = 1 +const val HIGH_PRIORITY = 2 + diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt new file mode 100644 index 0000000000..15ab08b338 --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_featureflag") + +class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext context: Context) : RuntimeFeatureFlagProvider { + + private val store = context.dataStore + + override val priority: Int + get() = MEDIUM_PRIORITY + + override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) { + store.edit { prefs -> + prefs[booleanPreferencesKey(feature.key)] = enabled + } + } + + override suspend fun isFeatureEnabled(feature: Feature): Boolean { + return store.data.map { prefs -> + prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue + }.first() + } + + override fun hasFeature(feature: Feature): Boolean { + return true + } +} diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt new file mode 100644 index 0000000000..1238ad354c --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import io.element.android.libraries.featureflag.api.Feature + +interface RuntimeFeatureFlagProvider : FeatureFlagProvider { + suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) +} diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt new file mode 100644 index 0000000000..07ee53ceee --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import dagger.multibindings.ElementsIntoSet +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.featureflag.impl.BuildtimeFeatureFlagProvider +import io.element.android.libraries.featureflag.impl.FeatureFlagProvider +import io.element.android.libraries.featureflag.impl.PreferencesFeatureFlagProvider + +@Module +@ContributesTo(AppScope::class) +object FeatureFlagModule { + + @JvmStatic + @Provides + @ElementsIntoSet + fun providesFeatureFlagProvider( + buildType: BuildType, + runtimeFeatureFlagProvider: PreferencesFeatureFlagProvider, + buildtimeFeatureFlagProvider: BuildtimeFeatureFlagProvider, + ): Set { + val providers = HashSet() + if (buildType == BuildType.RELEASE) { + providers.add(buildtimeFeatureFlagProvider) + } else { + providers.add(runtimeFeatureFlagProvider) + } + return providers + } +} diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt new file mode 100644 index 0000000000..5f7f01423a --- /dev/null +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultFeatureFlagServiceTest { + + @Test + fun `given service without provider when feature is checked then it returns the default value`() = runTest { + val featureFlagService = DefaultFeatureFlagService(emptySet()) + val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow) + assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.ShowStartChatFlow.defaultValue) + } + + @Test + fun `given service without provider when set enabled feature is called then it returns false`() = runTest { + val featureFlagService = DefaultFeatureFlagService(emptySet()) + val result = featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true) + assertThat(result).isEqualTo(false) + } + + @Test + fun `given service with a runtime provider when set enabled feature is called then it returns true`() = runTest { + val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0) + val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider)) + val result = featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true) + assertThat(result).isEqualTo(true) + } + + @Test + fun `given service with a runtime provider and feature enabled when feature is checked then it returns the correct value`() = runTest { + val featureFlagProvider = FakeRuntimeFeatureFlagProvider(0) + val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider)) + featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true) + assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(true) + featureFlagService.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, false) + assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(false) + } + + @Test + fun `given service with 2 runtime providers when feature is checked then it uses the priority correctly`() = runTest { + val lowPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(LOW_PRIORITY) + val highPriorityfeatureFlagProvider = FakeRuntimeFeatureFlagProvider(HIGH_PRIORITY) + val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityfeatureFlagProvider, highPriorityfeatureFlagProvider)) + lowPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, false) + highPriorityfeatureFlagProvider.setFeatureEnabled(FeatureFlags.ShowStartChatFlow, true) + assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.ShowStartChatFlow)).isEqualTo(true) + } +} diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeRuntimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeRuntimeFeatureFlagProvider.kt new file mode 100644 index 0000000000..5ff5cf932f --- /dev/null +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeRuntimeFeatureFlagProvider.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.impl + +import io.element.android.libraries.featureflag.api.Feature + +class FakeRuntimeFeatureFlagProvider(override val priority: Int) : RuntimeFeatureFlagProvider { + + private val enabledFeatures = HashMap() + + override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) { + enabledFeatures[feature.key] = enabled + } + + override suspend fun isFeatureEnabled(feature: Feature): Boolean { + return enabledFeatures[feature.key] ?: feature.defaultValue + } + + override fun hasFeature(feature: Feature): Boolean = true +} diff --git a/libraries/featureflag/test/build.gradle.kts b/libraries/featureflag/test/build.gradle.kts new file mode 100644 index 0000000000..952b9323f6 --- /dev/null +++ b/libraries/featureflag/test/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.featureflag.test" + + dependencies { + api(projects.libraries.featureflag.api) + } +} diff --git a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt new file mode 100644 index 0000000000..548ffa7cc4 --- /dev/null +++ b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.test + +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlagService + +class FakeFeatureFlagService( + initialState: Map = emptyMap() +) : FeatureFlagService { + + private val enabledFeatures = HashMap(initialState) + + override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { + enabledFeatures[feature.key] = enabled + return true + } + + override suspend fun isFeatureEnabled(feature: Feature): Boolean { + return enabledFeatures[feature.key] ?: feature.defaultValue + } +} diff --git a/libraries/featureflag/ui/build.gradle.kts b/libraries/featureflag/ui/build.gradle.kts new file mode 100644 index 0000000000..8cf616377b --- /dev/null +++ b/libraries/featureflag/ui/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.featureflag.ui" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(projects.libraries.designsystem) + ksp(libs.showkase.processor) +} diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt new file mode 100644 index 0000000000..280e062110 --- /dev/null +++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.ui + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun FeatureListView( + features: ImmutableList, + onCheckedChange: (FeatureUiModel, Boolean) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier, + ) { + items( + items = features, + key = { it.key } + ) { feature -> + + fun onCheckedChange(isChecked: Boolean) { + onCheckedChange(feature, isChecked) + } + + FeaturePreferenceView(feature = feature, onCheckedChange = ::onCheckedChange) + } + } +} + +@Composable +fun FeaturePreferenceView( + feature: FeatureUiModel, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + + PreferenceSwitch( + title = feature.title, + isChecked = feature.isEnabled, + modifier = modifier, + onCheckedChange = onCheckedChange + ) +} + +@Preview +@Composable +internal fun FeatureListViewLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun FeatureListViewDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + FeatureListView( + features = aFeatureUiModelList(), + onCheckedChange = { _, _ -> } + ) +} diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt new file mode 100644 index 0000000000..5a3eecebe0 --- /dev/null +++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.ui.model + +data class FeatureUiModel( + val key: String, + val title: String, + val isEnabled: Boolean +) diff --git a/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt new file mode 100644 index 0000000000..233331d46d --- /dev/null +++ b/libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.featureflag.ui.model + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +fun aFeatureUiModelList(): ImmutableList { + return persistentListOf( + FeatureUiModel("key1", "Display State Events", true), + FeatureUiModel("key2", "Display Room Events", false) + ) +} diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt index 69c1d78369..07a039e248 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt @@ -22,11 +22,11 @@ import dagger.Provides import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger import okhttp3.OkHttpClient import okhttp3.Protocol -import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger -import java.util.concurrent.TimeUnit import okhttp3.logging.HttpLoggingInterceptor +import java.util.concurrent.TimeUnit @Module @ContributesTo(AppScope::class) @@ -35,9 +35,14 @@ object NetworkModule { @Provides @JvmStatic fun providesHttpLoggingInterceptor(buildMeta: BuildMeta): HttpLoggingInterceptor { - val logger = FormattedJsonHttpLogger(buildMeta.okHttpLoggingLevel) + val loggingLevel = if (buildMeta.isDebuggable) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.BASIC + } + val logger = FormattedJsonHttpLogger(loggingLevel) val interceptor = HttpLoggingInterceptor(logger) - interceptor.level = buildMeta.okHttpLoggingLevel + interceptor.level = loggingLevel return interceptor } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 89b24d6bc4..5d1ef9a20e 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -83,6 +83,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:pushproviders:firebase")) // Comment to not include unified push in the project implementation(project(":libraries:pushproviders:unifiedpush")) + implementation(project(":libraries:featureflag:impl")) implementation(project(":libraries:pushstore:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0298cc02c7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d07bdb794fe858dfc3e3122c10b24dcf5a4918d21deb3c87686139a2d94f4f43 +size 18918 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5ae68d249f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.developer_null_DefaultGroup_DeveloperSettingsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e944e15b9eb9477e57b002d6b16c8a28bc8bc25d8bef40cdf6982509e4424c7f +size 18211 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 9a09df0348..f3e4ce3aac 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4da4fd3b7c17b00bd17639a8ec5c6315abdf36b491c7da874f865bc386e622d8 -size 45198 +oid sha256:4da371d30e79682f1b62d613684a65bda3e685e36e9d85881f8fa0ea976037f5 +size 45433 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 3b86dfc9c4..caff2f7ac3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a191ed88e984ca94c890b644fcee602539bf72a2e402be64caeb189db12bb80 -size 44940 +oid sha256:77db5c9af4eda9dda35e93f488f9732adc5fdbc7deb0b958c9e059911b0d7897 +size 48716 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 93698a1ecd..042f141b08 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9083a3f1146c5aa30c39ced0f535eddd38cafb2bad6c509717c10b74356a844b -size 43877 +oid sha256:a0816cd40ef1317530c1ce5b52b00555b72dc3bdcbf17d9333b470143816b201 +size 44108 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 52fef485fb..921b3ba256 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4b03c928ec2cd4dbcc7fce37c0374b1e2b794d473d6ecc11bc5c33cc6953cca -size 43699 +oid sha256:707ec6660e60ced6e176c9d326ed8859f3365db0b5b9a2efb57e9d1e1bd2e899 +size 47290