Skip to content

Commit

Permalink
[BWA-86] Debug Menu #4 (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrebispo5 authored Nov 18, 2024
1 parent 5f109b5 commit f3f51cf
Show file tree
Hide file tree
Showing 26 changed files with 1,358 additions and 7 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ android {
signingConfig = signingConfigs.getByName("debug")
isDebuggable = true
isMinifyEnabled = false
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
}

release {
Expand All @@ -77,6 +78,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
}
}
compileOptions {
Expand Down
34 changes: 30 additions & 4 deletions app/src/main/kotlin/com/bitwarden/authenticator/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.bitwarden.authenticator

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 @@ -10,12 +12,17 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.bitwarden.authenticator.data.platform.util.isSuspicious
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavScreen
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

/**
* Primary entry point for the application.
Expand All @@ -25,14 +32,15 @@ class MainActivity : AppCompatActivity() {

private val mainViewModel: MainViewModel by viewModels()

@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager

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

observeViewModelEvents()

if (savedInstanceState == null) {
mainViewModel.trySendAction(
MainAction.ReceiveFirstIntent(
Expand All @@ -43,11 +51,13 @@ class MainActivity : AppCompatActivity() {

setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()

val navController = rememberNavController()
observeViewModelEvents(navController)
AuthenticatorTheme(
theme = state.theme,
) {
RootNavScreen(
navController = navController,
onSplashScreenRemoved = { shouldShowSplashScreen = false },
onExitApplication = { finishAffinity() },
)
Expand All @@ -72,19 +82,35 @@ class MainActivity : AppCompatActivity() {
}
}

private fun observeViewModelEvents() {
private fun observeViewModelEvents(navController: NavHostController) {
mainViewModel
.eventFlow
.onEach { event ->
when (event) {
is MainEvent.ScreenCaptureSettingChange -> {
handleScreenCaptureSettingChange(event)
}

MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
}
}
.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 handleScreenCaptureSettingChange(event: MainEvent.ScreenCaptureSettingChange) {
if (event.isAllowed) {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/kotlin/com/bitwarden/authenticator/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package com.bitwarden.authenticator
import android.content.Intent
import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject

Expand All @@ -19,6 +21,7 @@ import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
settingsRepository: SettingsRepository,
configRepository: ServerConfigRepository,
) : BaseViewModel<MainState, MainEvent, MainAction>(
MainState(
theme = settingsRepository.appTheme,
Expand All @@ -37,16 +40,24 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed))
}
.launchIn(viewModelScope)
viewModelScope.launch {
configRepository.getServerConfig(forceRefresh = false)
}
}

override fun handleAction(action: MainAction) {
when (action) {
is MainAction.Internal.ThemeUpdate -> handleThemeUpdated(action)
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
}
}

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

private fun handleThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
mutableStateFlow.update { it.copy(theme = action.theme) }
}
Expand Down Expand Up @@ -95,6 +106,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 All @@ -114,6 +130,11 @@ sealed class MainAction {
*/
sealed class MainEvent {

/**
* Navigate to the debug menu.
*/
data object NavigateToDebugMenu : MainEvent()

/**
* Event indicating a change in the screen capture setting.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.bitwarden.authenticator.data.platform.manager

import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.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 @@ -9,6 +9,7 @@ import com.bitwarden.authenticator.data.platform.manager.BitwardenEncodingManage
import com.bitwarden.authenticator.data.platform.manager.BitwardenEncodingManagerImpl
import com.bitwarden.authenticator.data.platform.manager.CrashLogsManager
import com.bitwarden.authenticator.data.platform.manager.CrashLogsManagerImpl
import com.bitwarden.authenticator.data.platform.manager.DebugMenuFeatureFlagManagerImpl
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.manager.DispatcherManagerImpl
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
Expand All @@ -19,6 +20,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.DebugMenuRepository
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import dagger.Module
Expand Down Expand Up @@ -79,7 +81,19 @@ object PlatformManagerModule {

@Provides
@Singleton
fun provideFeatureFlagManager(
fun providesFeatureFlagManager(
debugMenuRepository: DebugMenuRepository,
serverConfigRepository: ServerConfigRepository,
): FeatureFlagManager = FeatureFlagManagerImpl(serverConfigRepository)
): FeatureFlagManager = if (debugMenuRepository.isDebugMenuEnabled) {
DebugMenuFeatureFlagManagerImpl(
debugMenuRepository = debugMenuRepository,
defaultFeatureFlagManager = FeatureFlagManagerImpl(
serverConfigRepository = serverConfigRepository,
),
)
} else {
FeatureFlagManagerImpl(
serverConfigRepository = serverConfigRepository,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.bitwarden.authenticator.data.platform.repository

import com.bitwarden.authenticator.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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.bitwarden.authenticator.data.platform.repository

import com.bitwarden.authenticator.BuildConfig
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.bitwarden.authenticator.data.platform.manager.getFlagValueOrDefault
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription

/**
* Default implementation of the [DebugMenuRepository]
*/
class DebugMenuRepositoryImpl(
private val featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
private val serverConfigRepository: ServerConfigRepository,
) : DebugMenuRepository {

private val mutableOverridesUpdatedFlow = bufferedMutableSharedFlow<Unit>(replay = 1)
override val featureFlagOverridesUpdatedFlow: Flow<Unit> = mutableOverridesUpdatedFlow
.onSubscription { emit(Unit) }

override val isDebugMenuEnabled: Boolean
get() = BuildConfig.HAS_DEBUG_MENU

override fun <T : Any> updateFeatureFlag(key: FlagKey<T>, value: T) {
featureFlagOverrideDiskSource.saveFeatureFlag(key = key, value = value)
mutableOverridesUpdatedFlow.tryEmit(Unit)
}

override fun <T : Any> getFeatureFlag(key: FlagKey<T>): T? =
featureFlagOverrideDiskSource.getFeatureFlag(
key = key,
)

override fun resetFeatureFlagOverrides() {
val currentServerConfig = serverConfigRepository.serverConfigStateFlow.value
FlagKey.activeFlags.forEach { flagKey ->
updateFeatureFlag(
flagKey,
currentServerConfig.getFlagValueOrDefault(flagKey),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.sdk.AuthenticatorSdkSource
import com.bitwarden.authenticator.data.platform.datasource.disk.ConfigDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
import com.bitwarden.authenticator.data.platform.datasource.network.service.ConfigService
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepositoryImpl
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepository
import com.bitwarden.authenticator.data.platform.repository.FeatureFlagRepositoryImpl
import com.bitwarden.authenticator.data.platform.repository.ServerConfigRepository
Expand Down Expand Up @@ -70,4 +73,14 @@ object PlatformRepositoryModule {
featureFlagDiskSource = featureFlagDiskSource,
dispatcherManager = dispatcherManager,
)

@Provides
@Singleton
fun provideDebugMenuRepository(
featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
serverConfigRepository: ServerConfigRepository,
): DebugMenuRepository = DebugMenuRepositoryImpl(
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
serverConfigRepository = serverConfigRepository,
)
}
Loading

0 comments on commit f3f51cf

Please sign in to comment.