Merge pull request #334 from vector-im/feature/fga/feature_flag
Feature/fga/feature flag
This commit is contained in:
commit
f98fe8e52c
46 changed files with 1296 additions and 27 deletions
|
|
@ -25,6 +25,7 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
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.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
|
@ -38,7 +39,6 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
@ -64,8 +64,15 @@ object AppModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
fun providesBuildMeta(@ApplicationContext context: Context) = BuildMeta(
|
fun providesBuildType(): BuildType {
|
||||||
isDebug = BuildConfig.DEBUG,
|
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),
|
applicationName = context.getString(R.string.app_name),
|
||||||
applicationId = BuildConfig.APPLICATION_ID,
|
applicationId = BuildConfig.APPLICATION_ID,
|
||||||
lowPrivacyLoggingEnabled = false, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
lowPrivacyLoggingEnabled = false, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
||||||
|
|
@ -75,7 +82,6 @@ object AppModule {
|
||||||
gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME,
|
gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME,
|
||||||
flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION,
|
flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION,
|
||||||
flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION,
|
||||||
okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.featureflag.api)
|
||||||
|
implementation(projects.libraries.featureflag.ui)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
@ -53,6 +55,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.features.rageshake.impl)
|
testImplementation(projects.features.rageshake.impl)
|
||||||
testImplementation(projects.features.logout.impl)
|
testImplementation(projects.features.logout.impl)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
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.features.preferences.impl.root.PreferencesRootNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
|
|
@ -52,6 +54,9 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object DeveloperSettings : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -61,9 +66,16 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
override fun onOpenBugReport() {
|
override fun onOpenBugReport() {
|
||||||
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenBugReport() }
|
plugins<PreferencesEntryPoint.Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOpenDeveloperSettings() {
|
||||||
|
backstack.push(NavTarget.DeveloperSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
|
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
|
NavTarget.DeveloperSettings -> {
|
||||||
|
createNode<DeveloperSettingsNode>(buildContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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<Plugin>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<DeveloperSettingsState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): DeveloperSettingsState {
|
||||||
|
|
||||||
|
val features = remember {
|
||||||
|
mutableStateMapOf<String, Feature>()
|
||||||
|
}
|
||||||
|
val enabledFeatures = remember {
|
||||||
|
mutableStateMapOf<String, Boolean>()
|
||||||
|
}
|
||||||
|
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<String, Feature>,
|
||||||
|
enabledFeatures: SnapshotStateMap<String, Boolean>
|
||||||
|
): List<FeatureUiModel> {
|
||||||
|
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<String, Feature>,
|
||||||
|
enabledFeatures: SnapshotStateMap<String, Boolean>,
|
||||||
|
featureUiModel: FeatureUiModel,
|
||||||
|
enabled: Boolean
|
||||||
|
) = launch {
|
||||||
|
val feature = features[featureUiModel.key] ?: return@launch
|
||||||
|
if (featureFlagService.setFeatureEnabled(feature, enabled)) {
|
||||||
|
enabledFeatures[featureUiModel.key] = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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<FeatureUiModel>,
|
||||||
|
val eventSink: (DeveloperSettingsEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -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<DeveloperSettingsState> {
|
||||||
|
override val values: Sequence<DeveloperSettingsState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aDeveloperSettingsState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aDeveloperSettingsState() = DeveloperSettingsState(
|
||||||
|
features = aFeatureUiModelList(),
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
@ -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 = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -36,12 +36,17 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
|
fun onOpenDeveloperSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOpenBugReport() {
|
private fun onOpenBugReport() {
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onOpenDeveloperSettings() {
|
||||||
|
plugins<Callback>().forEach { it.onOpenDeveloperSettings() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -49,7 +54,9 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = this::navigateUp,
|
onBackPressed = this::navigateUp,
|
||||||
onOpenRageShake = this::onOpenBugReport
|
onOpenRageShake = this::onOpenBugReport,
|
||||||
|
onOpenDeveloperSettings = this::onOpenDeveloperSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PreferencesRootPresenter @Inject constructor(
|
class PreferencesRootPresenter @Inject constructor(
|
||||||
private val logoutPresenter: LogoutPreferencePresenter,
|
private val logoutPresenter: LogoutPreferencePresenter,
|
||||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||||
|
private val buildType: BuildType,
|
||||||
) : Presenter<PreferencesRootState> {
|
) : Presenter<PreferencesRootState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PreferencesRootState {
|
override fun present(): PreferencesRootState {
|
||||||
val logoutState = logoutPresenter.present()
|
val logoutState = logoutPresenter.present()
|
||||||
val rageshakeState = rageshakePresenter.present()
|
val rageshakeState = rageshakePresenter.present()
|
||||||
|
val showDeveloperSettings = buildType != BuildType.RELEASE
|
||||||
return PreferencesRootState(
|
return PreferencesRootState(
|
||||||
logoutState = logoutState,
|
logoutState = logoutState,
|
||||||
rageshakeState = rageshakeState,
|
rageshakeState = rageshakeState,
|
||||||
myUser = Async.Uninitialized,
|
myUser = Async.Uninitialized,
|
||||||
|
showDeveloperSettings = showDeveloperSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,5 @@ data class PreferencesRootState(
|
||||||
val logoutState: LogoutPreferenceState,
|
val logoutState: LogoutPreferenceState,
|
||||||
val rageshakeState: RageshakePreferencesState,
|
val rageshakeState: RageshakePreferencesState,
|
||||||
val myUser: Async<MatrixUser>,
|
val myUser: Async<MatrixUser>,
|
||||||
|
val showDeveloperSettings: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,6 @@ import io.element.android.libraries.architecture.Async
|
||||||
fun aPreferencesRootState() = PreferencesRootState(
|
fun aPreferencesRootState() = PreferencesRootState(
|
||||||
logoutState = aLogoutPreferenceState(),
|
logoutState = aLogoutPreferenceState(),
|
||||||
rageshakeState = aRageshakePreferencesState(),
|
rageshakeState = aRageshakePreferencesState(),
|
||||||
myUser = Async.Uninitialized
|
myUser = Async.Uninitialized,
|
||||||
|
showDeveloperSettings = true
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
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.preferences.impl.user.UserPreferences
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
|
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
|
||||||
import io.element.android.libraries.architecture.Async
|
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.components.preferences.PreferenceView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
@ -38,6 +42,7 @@ fun PreferencesRootView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onOpenRageShake: () -> Unit = {},
|
onOpenRageShake: () -> Unit = {},
|
||||||
|
onOpenDeveloperSettings: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
// TODO Hierarchy!
|
// TODO Hierarchy!
|
||||||
// Include pref from other modules
|
// Include pref from other modules
|
||||||
|
|
@ -54,6 +59,20 @@ fun PreferencesRootView(
|
||||||
LogoutPreferenceView(
|
LogoutPreferenceView(
|
||||||
state = state.logoutState,
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.FakeRageShake
|
||||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -38,7 +39,9 @@ class PreferencesRootPresenterTest {
|
||||||
val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient())
|
val logoutPresenter = DefaultLogoutPreferencePresenter(FakeMatrixClient())
|
||||||
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
|
val rageshakePresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore())
|
||||||
val presenter = PreferencesRootPresenter(
|
val presenter = PreferencesRootPresenter(
|
||||||
logoutPresenter, rageshakePresenter
|
logoutPresenter,
|
||||||
|
rageshakePresenter,
|
||||||
|
BuildType.DEBUG
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -50,6 +53,7 @@ class PreferencesRootPresenterTest {
|
||||||
assertThat(initialState.rageshakeState.isSupported).isTrue()
|
assertThat(initialState.rageshakeState.isSupported).isTrue()
|
||||||
assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f)
|
assertThat(initialState.rageshakeState.sensitivity).isEqualTo(1.0f)
|
||||||
assertThat(initialState.myUser).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.myUser).isEqualTo(Async.Uninitialized)
|
||||||
|
assertThat(initialState.showDeveloperSettings).isEqualTo(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ java {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(platform(libs.network.okhttp.bom))
|
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.libraries.core.meta
|
package io.element.android.libraries.core.meta
|
||||||
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
|
|
||||||
data class BuildMeta(
|
data class BuildMeta(
|
||||||
val isDebug: Boolean,
|
val buildType: BuildType,
|
||||||
|
val isDebuggable: Boolean,
|
||||||
val applicationName: String,
|
val applicationName: String,
|
||||||
val applicationId: String,
|
val applicationId: String,
|
||||||
val lowPrivacyLoggingEnabled: Boolean,
|
val lowPrivacyLoggingEnabled: Boolean,
|
||||||
|
|
@ -29,5 +28,4 @@ data class BuildMeta(
|
||||||
val gitBranchName: String,
|
val gitBranchName: String,
|
||||||
val flavorDescription: String,
|
val flavorDescription: String,
|
||||||
val flavorShortDescription: String,
|
val flavorShortDescription: String,
|
||||||
val okHttpLoggingLevel: HttpLoggingInterceptor.Level,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
27
libraries/featureflag/api/build.gradle.kts
Normal file
27
libraries/featureflag/api/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
44
libraries/featureflag/impl/build.gradle.kts
Normal file
44
libraries/featureflag/impl/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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<Preferences> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<FeatureFlagProvider> {
|
||||||
|
val providers = HashSet<FeatureFlagProvider>()
|
||||||
|
if (buildType == BuildType.RELEASE) {
|
||||||
|
providers.add(buildtimeFeatureFlagProvider)
|
||||||
|
} else {
|
||||||
|
providers.add(runtimeFeatureFlagProvider)
|
||||||
|
}
|
||||||
|
return providers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, Boolean>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
27
libraries/featureflag/test/build.gradle.kts
Normal file
27
libraries/featureflag/test/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, Boolean> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
39
libraries/featureflag/ui/build.gradle.kts
Normal file
39
libraries/featureflag/ui/build.gradle.kts
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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<FeatureUiModel>,
|
||||||
|
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 = { _, _ -> }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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<FeatureUiModel> {
|
||||||
|
return persistentListOf(
|
||||||
|
FeatureUiModel("key1", "Display State Events", true),
|
||||||
|
FeatureUiModel("key2", "Display Room Events", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -22,11 +22,11 @@ import dagger.Provides
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
|
|
@ -35,9 +35,14 @@ object NetworkModule {
|
||||||
@Provides
|
@Provides
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun providesHttpLoggingInterceptor(buildMeta: BuildMeta): HttpLoggingInterceptor {
|
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)
|
val interceptor = HttpLoggingInterceptor(logger)
|
||||||
interceptor.level = buildMeta.okHttpLoggingLevel
|
interceptor.level = loggingLevel
|
||||||
return interceptor
|
return interceptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:pushproviders:firebase"))
|
implementation(project(":libraries:pushproviders:firebase"))
|
||||||
// Comment to not include unified push in the project
|
// Comment to not include unified push in the project
|
||||||
implementation(project(":libraries:pushproviders:unifiedpush"))
|
implementation(project(":libraries:pushproviders:unifiedpush"))
|
||||||
|
implementation(project(":libraries:featureflag:impl"))
|
||||||
implementation(project(":libraries:pushstore:impl"))
|
implementation(project(":libraries:pushstore:impl"))
|
||||||
implementation(project(":libraries:architecture"))
|
implementation(project(":libraries:architecture"))
|
||||||
implementation(project(":libraries:dateformatter:impl"))
|
implementation(project(":libraries:dateformatter:impl"))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d07bdb794fe858dfc3e3122c10b24dcf5a4918d21deb3c87686139a2d94f4f43
|
||||||
|
size 18918
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e944e15b9eb9477e57b002d6b16c8a28bc8bc25d8bef40cdf6982509e4424c7f
|
||||||
|
size 18211
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4da4fd3b7c17b00bd17639a8ec5c6315abdf36b491c7da874f865bc386e622d8
|
oid sha256:4da371d30e79682f1b62d613684a65bda3e685e36e9d85881f8fa0ea976037f5
|
||||||
size 45198
|
size 45433
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3a191ed88e984ca94c890b644fcee602539bf72a2e402be64caeb189db12bb80
|
oid sha256:77db5c9af4eda9dda35e93f488f9732adc5fdbc7deb0b958c9e059911b0d7897
|
||||||
size 44940
|
size 48716
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9083a3f1146c5aa30c39ced0f535eddd38cafb2bad6c509717c10b74356a844b
|
oid sha256:a0816cd40ef1317530c1ce5b52b00555b72dc3bdcbf17d9333b470143816b201
|
||||||
size 43877
|
size 44108
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e4b03c928ec2cd4dbcc7fce37c0374b1e2b794d473d6ecc11bc5c33cc6953cca
|
oid sha256:707ec6660e60ced6e176c9d326ed8859f3365db0b5b9a2efb57e9d1e1bd2e899
|
||||||
size 43699
|
size 47290
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue