Skip to content

Commit

Permalink
[BWA-86] Debug Menu #3 - feature flag service (#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrebispo5 authored Nov 18, 2024
1 parent a0cc8a8 commit 5f109b5
Show file tree
Hide file tree
Showing 19 changed files with 575 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.model.DataState
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
Expand Down Expand Up @@ -156,7 +156,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(

@OptIn(ExperimentalCoroutinesApi::class)
override val sharedCodesStateFlow: StateFlow<SharedVerificationCodesState> by lazy {
if (!featureFlagManager.getFeatureFlag(LocalFeatureFlag.PasswordManagerSync)) {
if (!featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)) {
MutableStateFlow(SharedVerificationCodesState.FeatureNotEnabled)
} else {
authenticatorBridgeManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import com.bitwarden.authenticator.data.authenticator.repository.util.SymmetricKeyStorageProviderImpl
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticatorbridge.factory.AuthenticatorBridgeFactory
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
Expand Down Expand Up @@ -40,7 +40,7 @@ object AuthenticatorBridgeModule {
symmetricKeyStorageProvider: SymmetricKeyStorageProvider,
featureFlagManager: FeatureFlagManager,
): AuthenticatorBridgeManager =
if (featureFlagManager.getFeatureFlag(LocalFeatureFlag.PasswordManagerSync)) {
if (featureFlagManager.getFeatureFlag(FlagKey.PasswordManagerSync)) {
factory.getAuthenticatorBridgeManager(
connectionType = BuildConfig.AUTHENTICATOR_BRIDGE_CONNECTION_TYPE,
symmetricKeyStorageProvider = symmetricKeyStorageProvider,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bitwarden.authenticator.data.platform.datasource.disk

import com.bitwarden.authenticator.data.platform.manager.model.FlagKey

/**
* Disk data source for saved feature flag overrides.
*/
interface FeatureFlagOverrideDiskSource {

/**
* Save a feature flag [FlagKey] to disk.
*/
fun <T : Any> saveFeatureFlag(key: FlagKey<T>, value: T)

/**
* Get a feature flag value based on the associated [FlagKey] from disk.
*/
fun <T : Any> getFeatureFlag(key: FlagKey<T>): T?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.bitwarden.authenticator.data.platform.datasource.disk

import android.content.SharedPreferences
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey

/**
* Default implementation of the [FeatureFlagOverrideDiskSource]
*/
class FeatureFlagOverrideDiskSourceImpl(
sharedPreferences: SharedPreferences,
) : FeatureFlagOverrideDiskSource, BaseDiskSource(sharedPreferences) {

override fun <T : Any> saveFeatureFlag(key: FlagKey<T>, value: T) {
when (key.defaultValue) {
is Boolean -> putBoolean(key.keyName, value as Boolean)
is String -> putString(key.keyName, value as String)
is Int -> putInt(key.keyName, value as Int)
else -> Unit
}
}

@Suppress("UNCHECKED_CAST")
override fun <T : Any> getFeatureFlag(key: FlagKey<T>): T? {
return try {
when (key.defaultValue) {
is Boolean -> getBoolean(key.keyName) as? T
is String -> getString(key.keyName) as? T
is Int -> getInt(key.keyName) as? T
else -> null
}
} catch (castException: ClassCastException) {
null
} catch (numberFormatException: NumberFormatException) {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSourc
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSourceImpl
import dagger.Module
Expand Down Expand Up @@ -50,4 +52,12 @@ object PlatformDiskModule {
sharedPreferences = sharedPreferences,
json = json,
)

@Provides
@Singleton
fun provideFeatureFlagOverrideDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
): FeatureFlagOverrideDiskSource = FeatureFlagOverrideDiskSourceImpl(
sharedPreferences = sharedPreferences,
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.bitwarden.authenticator.data.platform.manager

import com.bitwarden.authenticator.data.platform.manager.model.FeatureFlag
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import kotlinx.coroutines.flow.Flow

/**
Expand All @@ -12,13 +12,23 @@ interface FeatureFlagManager {
* Returns a flow emitting the value of flag [key] which is of generic type [T].
* If the value of the flag cannot be retrieved, the default value of [key] will be returned
*/
fun <T : Any> getFeatureFlagFlow(key: FeatureFlag<T>): Flow<T>
fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T>

/**
* Get value for feature flag with [key] and returns it as generic type [T].
* If no value is found the given [key] its default value will be returned.
* Cached flags can be invalidated with [forceRefresh]
*/
suspend fun <T : Any> getFeatureFlag(
key: FlagKey<T>,
forceRefresh: Boolean,
): T

/**
* Gets the value for feature flag with [key] and returns it as generic type [T].
* If no value is found the given [key] its [FeatureFlag.defaultValue] will be returned.
* If no value is found the given [key] its [FlagKey.defaultValue] will be returned.
*/
fun <T : Any> getFeatureFlag(
key: FeatureFlag<T>,
key: FlagKey<T>,
): T
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
package com.bitwarden.authenticator.data.platform.manager

import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.manager.model.FeatureFlag
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* Primary implementation of [FeatureFlagManager].
*/
class FeatureFlagManagerImpl(
private val featureFlagRepository: FeatureFlagRepository,
private val serverConfigRepository: ServerConfigRepository,
) : FeatureFlagManager {

override fun <T : Any> getFeatureFlagFlow(key: FeatureFlag<T>): Flow<T> =
featureFlagRepository
.featureFlagConfigStateFlow
override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> =
serverConfigRepository
.serverConfigStateFlow
.map { serverConfig ->
serverConfig.getFlagValueOrDefault(key = key)
}

override fun <T : Any> getFeatureFlag(key: FeatureFlag<T>): T =
featureFlagRepository
.featureFlagConfigStateFlow
override suspend fun <T : Any> getFeatureFlag(
key: FlagKey<T>,
forceRefresh: Boolean,
): T =
serverConfigRepository
.getServerConfig(forceRefresh = forceRefresh)
.getFlagValueOrDefault(key = key)

override fun <T : Any> getFeatureFlag(key: FlagKey<T>): T =
serverConfigRepository
.serverConfigStateFlow
.value
.getFlagValueOrDefault(key = key)
}

private fun <T : Any> FeatureFlagsConfiguration?.getFlagValueOrDefault(key: FeatureFlag<T>): T {
/**
* Extract the value of a [FlagKey] from the [ServerConfig]. If there is an issue with retrieving
* or if the value is null, the default value will be returned.
*/
fun <T : Any> ServerConfig?.getFlagValueOrDefault(key: FlagKey<T>): T {
val defaultValue = key.defaultValue
if (!key.isRemotelyConfigured) return key.defaultValue
return this
?.featureFlags
?.get(key.name)
?.serverData
?.featureStates
?.get(key.keyName)
?.let {
try {
// Suppressed since we are checking the type before doing the cast
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClip
import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManagerImpl
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -80,6 +80,6 @@ object PlatformManagerModule {
@Provides
@Singleton
fun provideFeatureFlagManager(
featureFlagRepository: FeatureFlagRepository,
): FeatureFlagManager = FeatureFlagManagerImpl(featureFlagRepository)
serverConfigRepository: ServerConfigRepository,
): FeatureFlagManager = FeatureFlagManagerImpl(serverConfigRepository)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.bitwarden.authenticator.data.platform.manager.model

/**
* Class to hold feature flag keys.
*/
sealed class FlagKey<out T : Any> {
/**
* The string value of the given key. This must match the network value.
*/
abstract val keyName: String

/**
* The value to be used if the flags value cannot be determined or is not remotely configured.
*/
abstract val defaultValue: T

/**
* Indicates if the flag should respect the network value or not.
*/
abstract val isRemotelyConfigured: Boolean

@Suppress("UndocumentedPublicClass")
companion object {
/**
* List of all flag keys to consider
*/
val activeFlags: List<FlagKey<*>> by lazy {
listOf(
BitwardenAuthenticationEnabled,
PasswordManagerSync,
)
}
}

/**
* Indicates the state of Bitwarden authentication.
*/
data object BitwardenAuthenticationEnabled : FlagKey<Boolean>() {
override val keyName: String = "bitwarden-authentication-enabled"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = false
}

/**
* Indicates whether syncing with the main Bitwarden password manager app should be enabled..
*/
data object PasswordManagerSync : FlagKey<Boolean>() {
override val keyName: String = "enable-password-manager-sync-android"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}

/**
* Data object holding the key for a [Boolean] flag to be used in tests.
*/
data object DummyBoolean : FlagKey<Boolean>() {
override val keyName: String = "dummy-boolean"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}

/**
* Data object holding the key for an [Int] flag to be used in tests.
*/
data class DummyInt(
override val isRemotelyConfigured: Boolean = true,
) : FlagKey<Int>() {
override val keyName: String = "dummy-int"
override val defaultValue: Int = Int.MIN_VALUE
}

/**
* Data object holding the key for a [String] flag to be used in tests.
*/
data object DummyString : FlagKey<String>() {
override val keyName: String = "dummy-string"
override val defaultValue: String = "defaultValue"
override val isRemotelyConfigured: Boolean = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.model.FeatureFlagsConfiguration
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.manager.model.LocalFeatureFlag
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -36,8 +36,8 @@ class FeatureFlagRepositoryImpl(
private fun initLocalFeatureFlagsConfiguration(): FeatureFlagsConfiguration {
val config = FeatureFlagsConfiguration(
mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(
LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue,
FlagKey.BitwardenAuthenticationEnabled.keyName to JsonPrimitive(
FlagKey.BitwardenAuthenticationEnabled.defaultValue,
),
),
)
Expand Down
Loading

0 comments on commit 5f109b5

Please sign in to comment.