Skip to content

Commit

Permalink
PM-11224 Add menu to update feature flags with overridden values in r…
Browse files Browse the repository at this point in the history
…eal time (#3838)
  • Loading branch information
dseverns-livefront authored Aug 29, 2024
1 parent 2a057bb commit 3c39d8b
Show file tree
Hide file tree
Showing 29 changed files with 1,451 additions and 17 deletions.
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ android {
signingConfig = signingConfigs.getByName("debug")
isDebuggable = true
isMinifyEnabled = false

buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
}

// Beta and Release variants are identical except beta has a different package name
Expand All @@ -72,6 +74,8 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)

buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
}
release {
isDebuggable = false
Expand All @@ -80,6 +84,8 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)

buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
}
}

Expand Down
45 changes: 30 additions & 15 deletions app/src/main/java/com/x8bit/bitwarden/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.x8bit.bitwarden

import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.viewModels
Expand All @@ -11,17 +13,18 @@ import androidx.compose.runtime.getValue
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

/**
Expand All @@ -42,13 +45,14 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var settingsRepository: SettingsRepository

@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager

override fun onCreate(savedInstanceState: Bundle?) {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
super.onCreate(savedInstanceState)

observeViewModelEvents()

if (savedInstanceState == null) {
mainViewModel.trySendAction(
MainAction.ReceiveFirstIntent(
Expand All @@ -66,11 +70,20 @@ class MainActivity : AppCompatActivity() {
}
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
EventsEffect(viewModel = mainViewModel) { event ->
when (event) {
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },
navController = navController,
)
}
}
Expand All @@ -93,16 +106,18 @@ class MainActivity : AppCompatActivity() {
currentFocus?.clearFocus()
}

private fun observeViewModelEvents() {
mainViewModel
.eventFlow
.onEach { event ->
when (event) {
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
}
}
.launchIn(lifecycleScope)
override fun dispatchTouchEvent(event: MotionEvent): Boolean = debugLaunchManager
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
.takeIf { it }
?: super.dispatchTouchEvent(event)

override fun dispatchKeyEvent(event: KeyEvent): Boolean = debugLaunchManager
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
.takeIf { it }
?: super.dispatchKeyEvent(event)

private fun sendOpenDebugMenuEvent() {
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
}

private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) {
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,14 @@ class MainViewModel @Inject constructor(
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
}
}

private fun handleOpenDebugMenu() {
sendEvent(MainEvent.NavigateToDebugMenu)
}

private fun handleAutofillSelectionReceive(
action: MainAction.Internal.AutofillSelectionReceive,
) {
Expand Down Expand Up @@ -315,6 +320,11 @@ sealed class MainAction {
*/
data class ReceiveNewIntent(val intent: Intent) : MainAction()

/**
* Receive event to open the debug menu.
*/
data object OpenDebugMenu : MainAction()

/**
* Actions for internal use by the ViewModel.
*/
Expand Down Expand Up @@ -366,4 +376,9 @@ sealed class MainEvent {
* Event indicating that the UI should recreate itself.
*/
data object Recreate : MainEvent()

/**
* Navigate to the debug menu.
*/
data object NavigateToDebugMenu : MainEvent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.platform.datasource.disk

import com.x8bit.bitwarden.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.x8bit.bitwarden.data.platform.datasource.disk

import android.content.SharedPreferences
import com.x8bit.bitwarden.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 @@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
Expand Down Expand Up @@ -149,4 +151,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
@@ -0,0 +1,37 @@
package com.x8bit.bitwarden.data.platform.manager

import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
* The [FeatureFlagManager] implementation for the debug menu. This manager uses the
* values returned from the [debugMenuRepository] if they are available. otherwise it will use
* the default [FeatureFlagManager].
*/
class DebugMenuFeatureFlagManagerImpl(
private val defaultFeatureFlagManager: FeatureFlagManager,
private val debugMenuRepository: DebugMenuRepository,
) : FeatureFlagManager by defaultFeatureFlagManager {

override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> {
return debugMenuRepository.featureFlagOverridesUpdatedFlow.map { _ ->
debugMenuRepository
.getFeatureFlag(key)
?: defaultFeatureFlagManager.getFeatureFlag(key = key)
}
}

override suspend fun <T : Any> getFeatureFlag(key: FlagKey<T>, forceRefresh: Boolean): T {
return debugMenuRepository
.getFeatureFlag(key)
?: defaultFeatureFlagManager.getFeatureFlag(key = key, forceRefresh = forceRefresh)
}

override fun <T : Any> getFeatureFlag(key: FlagKey<T>): T {
return debugMenuRepository
.getFeatureFlag(key)
?: defaultFeatureFlagManager.getFeatureFlag(key = key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ class FeatureFlagManagerImpl(
.getFlagValueOrDefault(key = key)
}

private fun <T : Any> ServerConfig?.getFlagValueOrDefault(key: FlagKey<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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManagerImpl
import com.x8bit.bitwarden.data.platform.manager.DebugMenuFeatureFlagManagerImpl
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
Expand All @@ -48,6 +49,7 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
Expand Down Expand Up @@ -141,11 +143,20 @@ object PlatformManagerModule {
@Provides
@Singleton
fun providesFeatureFlagManager(
debugMenuRepository: DebugMenuRepository,
serverConfigRepository: ServerConfigRepository,
): FeatureFlagManager =
): FeatureFlagManager = if (debugMenuRepository.isDebugMenuEnabled) {
DebugMenuFeatureFlagManagerImpl(
debugMenuRepository = debugMenuRepository,
defaultFeatureFlagManager = FeatureFlagManagerImpl(
serverConfigRepository = serverConfigRepository,
),
)
} else {
FeatureFlagManagerImpl(
serverConfigRepository = serverConfigRepository,
)
}

@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ sealed class FlagKey<out T : Any> {
*/
abstract val isRemotelyConfigured: Boolean

companion object {
/**
* List of all flag keys to consider
*/
val activeFlags: List<FlagKey<*>> by lazy {
listOf(
EmailVerification,
OnboardingFlow,
OnboardingCarousel,
)
}
}

/**
* Data object holding the key for Email Verification feature.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.platform.repository

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

/**
* Repository for accessing data required or associated with the debug menu.
*/
interface DebugMenuRepository {

/**
* Value to determine if the debug menu is enabled.
*/
val isDebugMenuEnabled: Boolean

/**
* Observable flow for when any of the feature flag overrides have been updated.
*/
val featureFlagOverridesUpdatedFlow: Flow<Unit>

/**
* Update a feature flag which matches the given [key] to the given [value].
*/
fun <T : Any> updateFeatureFlag(key: FlagKey<T>, value: T)

/**
* Get a feature flag value based on the associated [FlagKey].
*/
fun <T : Any> getFeatureFlag(key: FlagKey<T>): T?

/**
* Reset all feature flag overrides to their default values or values from the network.
*/
fun resetFeatureFlagOverrides()
}
Loading

0 comments on commit 3c39d8b

Please sign in to comment.