From 279185b1758094ce1946fc22b1e26a043619ac89 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 17 Apr 2023 19:44:29 +0200 Subject: [PATCH] FeatureFlag: first implementation --- features/preferences/impl/build.gradle.kts | 2 + .../preferences/impl/PreferencesFlowNode.kt | 12 ++ .../impl/developer/DeveloperSettingsEvents.kt | 23 ++++ .../impl/developer/DeveloperSettingsNode.kt | 45 ++++++++ .../developer/DeveloperSettingsPresenter.kt | 109 ++++++++++++++++++ .../impl/developer/DeveloperSettingsState.kt | 25 ++++ .../DeveloperSettingsStateProvider.kt | 32 +++++ .../impl/developer/DeveloperSettingsView.kt | 103 +++++++++++++++++ .../impl/root/PreferencesRootNode.kt | 9 +- .../impl/root/PreferencesRootPresenter.kt | 4 +- .../impl/root/PreferencesRootState.kt | 1 + .../impl/root/PreferencesRootStateProvider.kt | 3 +- .../impl/root/PreferencesRootView.kt | 19 +++ libraries/featureflag/api/build.gradle.kts | 27 +++++ .../libraries/featureflag/api/Feature.kt | 39 +++++++ .../featureflag/api/FeatureFlagService.kt | 35 ++++++ .../libraries/featureflag/api/FeatureFlags.kt | 37 ++++++ libraries/featureflag/impl/build.gradle.kts | 44 +++++++ .../impl/BuildtimeFeatureFlagProvider.kt | 42 +++++++ .../impl/DefaultFeatureFlagService.kt | 48 ++++++++ .../featureflag/impl/FeatureFlagProvider.kt | 30 +++++ .../impl/PreferencesFeatureFlagProvider.kt | 55 +++++++++ .../impl/RuntimeFeatureFlagProvider.kt | 23 ++++ .../featureflag/impl/di/FeatureFlagModule.kt | 56 +++++++++ libraries/featureflag/ui/build.gradle.kts | 39 +++++++ .../featureflag/ui/FeatureListView.kt | 85 ++++++++++++++ .../featureflag/ui/model/FeatureUiModel.kt | 23 ++++ .../ui/model/FeatureUiModelProvider.kt | 27 +++++ .../kotlin/extension/DependencyHandleScope.kt | 1 + settings.gradle.kts | 2 + 30 files changed, 997 insertions(+), 3 deletions(-) create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt create mode 100644 libraries/featureflag/api/build.gradle.kts create mode 100644 libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt create mode 100644 libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt create mode 100644 libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt create mode 100644 libraries/featureflag/impl/build.gradle.kts create mode 100644 libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt create mode 100644 libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt create mode 100644 libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt create mode 100644 libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt create mode 100644 libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/RuntimeFeatureFlagProvider.kt create mode 100644 libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt create mode 100644 libraries/featureflag/ui/build.gradle.kts create mode 100644 libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/FeatureListView.kt create mode 100644 libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModel.kt create mode 100644 libraries/featureflag/ui/src/main/kotlin/io/element/android/libraries/featureflag/ui/model/FeatureUiModelProvider.kt diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index c181cfa74c..013672990b 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -38,6 +38,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) 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..b7b92c88c7 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,24 @@ 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.BuildMeta import javax.inject.Inject class PreferencesRootPresenter @Inject constructor( private val logoutPresenter: LogoutPreferencePresenter, private val rageshakePresenter: RageshakePreferencesPresenter, + private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): PreferencesRootState { val logoutState = logoutPresenter.present() val rageshakeState = rageshakePresenter.present() - return PreferencesRootState( logoutState = logoutState, rageshakeState = rageshakeState, myUser = Async.Uninitialized, + showDeveloperSettings = buildMeta.isDebug ) } } 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/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..a94724e155 --- /dev/null +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt @@ -0,0 +1,35 @@ +/* + * 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..28db4f1e35 --- /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) } + .sortedBy(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..2cafdc03f8 --- /dev/null +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt @@ -0,0 +1,56 @@ +/* + * 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.BuildMeta +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 +import io.element.android.libraries.featureflag.impl.RuntimeFeatureFlagProvider + +@Module +@ContributesTo(AppScope::class) +object FeatureFlagModule { + + @Provides + fun providesRuntimeFeatureFlagProvider(preferencesFeatureFlagProvider: PreferencesFeatureFlagProvider): RuntimeFeatureFlagProvider { + return preferencesFeatureFlagProvider + } + + @JvmStatic + @Provides + @ElementsIntoSet + fun providesFeatureFlagProvider( + buildMeta: BuildMeta, + buildtimeFeatureFlagProvider: BuildtimeFeatureFlagProvider, + runtimeFeatureFlagProvider: RuntimeFeatureFlagProvider, + ): Set { + val providers = HashSet() + //TODO change this condition? + if (buildMeta.isDebug) { + providers.add(runtimeFeatureFlagProvider) + } else { + providers.add(buildtimeFeatureFlagProvider) + } + return providers + } +} 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/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 1427269755..8991c23013 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -80,6 +80,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/settings.gradle.kts b/settings.gradle.kts index 1173288adb..43b227476f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,7 @@ import java.net.URI +include(":libraries:featureflag:ui") + /* * Copyright (c) 2022 New Vector Ltd *