mirror of
https://github.com/bitwarden/android.git
synced 2026-04-27 11:28:41 -05:00
@@ -63,6 +63,7 @@ android {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
|
||||
}
|
||||
|
||||
release {
|
||||
@@ -77,6 +78,7 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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(
|
||||
@@ -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() },
|
||||
)
|
||||
@@ -72,7 +82,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
private fun observeViewModelEvents(navController: NavHostController) {
|
||||
mainViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
@@ -80,11 +90,27 @@ class MainActivity : AppCompatActivity() {
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -10,6 +11,7 @@ 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
|
||||
|
||||
@@ -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,
|
||||
@@ -37,6 +40,9 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.ScreenCaptureSettingChange(isAllowed))
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
viewModelScope.launch {
|
||||
configRepository.getServerConfig(forceRefresh = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
@@ -44,9 +50,14 @@ class MainViewModel @Inject constructor(
|
||||
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) }
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -70,4 +73,14 @@ object PlatformRepositoryModule {
|
||||
featureFlagDiskSource = featureFlagDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDebugMenuRepository(
|
||||
featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
): DebugMenuRepository = DebugMenuRepositoryImpl(
|
||||
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bitwarden.authenticator.ui.platform.base.util
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DividerDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -8,10 +9,13 @@ import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.data.platform.annotation.OmitFromCoverage
|
||||
import com.bitwarden.authenticator.ui.platform.util.isPortrait
|
||||
|
||||
/**
|
||||
* This is a [Modifier] extension for drawing a divider at the bottom of the composable.
|
||||
@@ -57,3 +61,17 @@ fun Modifier.mirrorIfRtl(): Modifier =
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a [Modifier] extension for ensuring that the content uses the standard horizontal margin.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Stable
|
||||
@Composable
|
||||
fun Modifier.standardHorizontalMargin(
|
||||
portrait: Dp = 16.dp,
|
||||
landscape: Dp = 48.dp,
|
||||
): Modifier {
|
||||
val config = LocalConfiguration.current
|
||||
return this.padding(horizontal = if (config.isPortrait) portrait else landscape)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bitwarden.authenticator.ui.platform.components.divider
|
||||
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* A divider line.
|
||||
*
|
||||
* @param modifier The [Modifier] to be applied to this divider.
|
||||
* @param thickness The thickness of this divider. Using [Dp.Hairline] will produce a single pixel
|
||||
* divider regardless of screen density.
|
||||
* @param color The color of this divider.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenHorizontalDivider(
|
||||
modifier: Modifier = Modifier,
|
||||
thickness: Dp = 1.dp,
|
||||
color: Color = MaterialTheme.colorScheme.outline,
|
||||
) {
|
||||
HorizontalDivider(
|
||||
modifier = modifier,
|
||||
thickness = thickness,
|
||||
color = color,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.composableWithPushTransitions
|
||||
|
||||
private const val DEBUG_MENU = "debug_menu"
|
||||
|
||||
/**
|
||||
* Navigate to the setup unlock screen.
|
||||
*/
|
||||
fun NavController.navigateToDebugMenuScreen() {
|
||||
this.navigate(DEBUG_MENU) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup unlock screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupDebugMenuDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = DEBUG_MENU,
|
||||
) {
|
||||
DebugMenuScreen(onNavigateBack = onNavigateBack)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.authenticator.R
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.bitwarden.authenticator.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.authenticator.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.components.ListItemContent
|
||||
import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
|
||||
|
||||
/**
|
||||
* Top level screen for the debug menu.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun DebugMenuScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: DebugMenuViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
DebugMenuEvent.NavigateBack -> onNavigateBack()
|
||||
}
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(R.string.debug_menu),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(R.drawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(DebugMenuAction.NavigateBack)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(innerPadding),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FeatureFlagContent(
|
||||
featureFlagMap = state.featureFlags,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ key, value ->
|
||||
viewModel.trySendAction(DebugMenuAction.UpdateFeatureFlag(key, value))
|
||||
}
|
||||
},
|
||||
onResetValues = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureFlagContent(
|
||||
featureFlagMap: Map<FlagKey<Any>, Any>,
|
||||
onValueChange: (key: FlagKey<Any>, value: Any) -> Unit,
|
||||
onResetValues: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(R.string.feature_flags),
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
featureFlagMap.forEach { featureFlag ->
|
||||
featureFlag.key.ListItemContent(
|
||||
currentValue = featureFlag.value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
BitwardenHorizontalDivider()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.reset_values),
|
||||
onClick = onResetValues,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FeatureFlagContent_preview() {
|
||||
AuthenticatorTheme {
|
||||
FeatureFlagContent(
|
||||
featureFlagMap = mapOf(
|
||||
FlagKey.BitwardenAuthenticationEnabled to true,
|
||||
FlagKey.PasswordManagerSync to false,
|
||||
),
|
||||
onValueChange = { _, _ -> },
|
||||
onResetValues = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* ViewModel for the [DebugMenuScreen]
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DebugMenuViewModel @Inject constructor(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val debugMenuRepository: DebugMenuRepository,
|
||||
) : BaseViewModel<DebugMenuState, DebugMenuEvent, DebugMenuAction>(
|
||||
initialState = DebugMenuState(featureFlags = emptyMap()),
|
||||
) {
|
||||
|
||||
private var featureFlagResetJob: Job? = null
|
||||
|
||||
init {
|
||||
combine(
|
||||
flows = FlagKey.activeFlags.map { flagKey ->
|
||||
featureFlagManager.getFeatureFlagFlow(flagKey).map { flagKey to it }
|
||||
},
|
||||
) { DebugMenuAction.Internal.UpdateFeatureFlagMap(it.toMap()) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: DebugMenuAction) {
|
||||
when (action) {
|
||||
is DebugMenuAction.UpdateFeatureFlag<*> -> handleUpdateFeatureFlag(action)
|
||||
is DebugMenuAction.Internal.UpdateFeatureFlagMap -> handleUpdateFeatureFlagMap(action)
|
||||
DebugMenuAction.NavigateBack -> handleNavigateBack()
|
||||
DebugMenuAction.ResetFeatureFlagValues -> handleResetFeatureFlagValues()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResetFeatureFlagValues() {
|
||||
featureFlagResetJob?.cancel()
|
||||
featureFlagResetJob = viewModelScope.launch {
|
||||
debugMenuRepository.resetFeatureFlagOverrides()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavigateBack() {
|
||||
sendEvent(DebugMenuEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleUpdateFeatureFlagMap(action: DebugMenuAction.Internal.UpdateFeatureFlagMap) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(featureFlags = action.newMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateFeatureFlag(action: DebugMenuAction.UpdateFeatureFlag<*>) {
|
||||
debugMenuRepository.updateFeatureFlag(action.flagKey, action.newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State for the [DebugMenuViewModel]
|
||||
*/
|
||||
data class DebugMenuState(
|
||||
val featureFlags: Map<FlagKey<Any>, Any>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Models event for the [DebugMenuViewModel] to send to the UI.
|
||||
*/
|
||||
sealed class DebugMenuEvent {
|
||||
/**
|
||||
* Navigates back to previous screen.
|
||||
*/
|
||||
data object NavigateBack : DebugMenuEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models action for the [DebugMenuViewModel] to handle.
|
||||
*/
|
||||
sealed class DebugMenuAction {
|
||||
|
||||
/**
|
||||
* Updates a feature flag for the given [FlagKey] to the given [newValue].
|
||||
*/
|
||||
data class UpdateFeatureFlag<T : Any>(val flagKey: FlagKey<T>, val newValue: T) :
|
||||
DebugMenuAction()
|
||||
|
||||
/**
|
||||
* The user has clicked "back" button.
|
||||
*/
|
||||
data object NavigateBack : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* The user has clicked "reset" button for the feature flag section.
|
||||
*/
|
||||
data object ResetFeatureFlagValues : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* Internal actions not triggered from the UI.
|
||||
*/
|
||||
sealed class Internal : DebugMenuAction() {
|
||||
/**
|
||||
* Update the feature flag map with the new value.
|
||||
*/
|
||||
data class UpdateFeatureFlagMap(val newMap: Map<FlagKey<Any>, Any>) : Internal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bitwarden.authenticator.R
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import com.bitwarden.authenticator.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
|
||||
/**
|
||||
* Creates a list item for a [FlagKey].
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
currentValue: T,
|
||||
onValueChange: (key: FlagKey<T>, value: T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) = when (val flagKey = this) {
|
||||
FlagKey.DummyBoolean,
|
||||
is FlagKey.DummyInt,
|
||||
FlagKey.DummyString,
|
||||
-> Unit
|
||||
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.PasswordManagerSync,
|
||||
-> BooleanFlagItem(
|
||||
label = flagKey.getDisplayLabel(),
|
||||
key = flagKey as FlagKey<Boolean>,
|
||||
currentValue = currentValue as Boolean,
|
||||
onValueChange = onValueChange as (FlagKey<Boolean>, Boolean) -> Unit,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The UI layout for a boolean backed flag key.
|
||||
*/
|
||||
@Composable
|
||||
private fun BooleanFlagItem(
|
||||
label: String,
|
||||
key: FlagKey<Boolean>,
|
||||
currentValue: Boolean,
|
||||
onValueChange: (key: FlagKey<Boolean>, value: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenWideSwitch(
|
||||
label = label,
|
||||
isChecked = currentValue,
|
||||
onCheckedChange = {
|
||||
onValueChange(key, it)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.DummyBoolean,
|
||||
is FlagKey.DummyInt,
|
||||
FlagKey.DummyString,
|
||||
-> this.keyName
|
||||
|
||||
FlagKey.BitwardenAuthenticationEnabled ->
|
||||
stringResource(R.string.bitwarden_authentication_enabled)
|
||||
|
||||
FlagKey.PasswordManagerSync -> stringResource(R.string.password_manager_sync)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.di
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugLaunchManagerImpl
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
/**
|
||||
* Provides dependencies for the debug menu.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class DebugMenuModule {
|
||||
|
||||
@Provides
|
||||
fun provideDebugMenuLaunchManager(
|
||||
debugMenuRepository: DebugMenuRepository,
|
||||
): DebugMenuLaunchManager = DebugLaunchManagerImpl(debugMenuRepository = debugMenuRepository)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager
|
||||
|
||||
import android.view.InputEvent
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
|
||||
|
||||
private const val TAP_TIME_THRESHOLD_MILLIS = 500
|
||||
private const val POINTERS_REQUIRED = 3
|
||||
|
||||
/**
|
||||
* Default implementation of the [DebugMenuLaunchManager]
|
||||
*/
|
||||
class DebugLaunchManagerImpl(
|
||||
private val debugMenuRepository: DebugMenuRepository,
|
||||
) : DebugMenuLaunchManager {
|
||||
|
||||
private val tapEventQueue: ArrayDeque<Long> = ArrayDeque()
|
||||
|
||||
override fun actionOnInputEvent(
|
||||
event: InputEvent,
|
||||
action: () -> Unit,
|
||||
): Boolean {
|
||||
val shouldTakeAction = when (event) {
|
||||
is KeyEvent -> event.debugTrigger()
|
||||
is MotionEvent -> shouldHandleMotionEvent(event)
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (shouldTakeAction) {
|
||||
action()
|
||||
}
|
||||
|
||||
return shouldTakeAction
|
||||
}
|
||||
|
||||
private fun shouldHandleMotionEvent(event: MotionEvent): Boolean {
|
||||
if (!event.debugTrigger()) return false
|
||||
// Pop old tap events until we have ones within our threshold
|
||||
while (
|
||||
tapEventQueue
|
||||
.firstOrNull()
|
||||
?.let { event.eventTime - it >= TAP_TIME_THRESHOLD_MILLIS } == true
|
||||
) {
|
||||
tapEventQueue.removeFirst()
|
||||
}
|
||||
|
||||
// Add this tap event
|
||||
tapEventQueue.add(event.eventTime)
|
||||
return event.eventTime - tapEventQueue.first() < TAP_TIME_THRESHOLD_MILLIS &&
|
||||
tapEventQueue.size >= POINTERS_REQUIRED
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the equivalent of the entry of `shift` + `~` on a US keyboard.
|
||||
*/
|
||||
private fun KeyEvent.debugTrigger(): Boolean =
|
||||
action == KeyEvent.ACTION_DOWN &&
|
||||
keyCode == KeyEvent.KEYCODE_GRAVE &&
|
||||
isShiftPressed &&
|
||||
debugMenuRepository.isDebugMenuEnabled
|
||||
|
||||
private fun MotionEvent.debugTrigger(): Boolean =
|
||||
action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_DOWN &&
|
||||
pointerCount == POINTERS_REQUIRED &&
|
||||
debugMenuRepository.isDebugMenuEnabled
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager
|
||||
|
||||
import android.view.InputEvent
|
||||
|
||||
/**
|
||||
* Manager for abstracting the logic of launching debug menu.
|
||||
*/
|
||||
interface DebugMenuLaunchManager {
|
||||
|
||||
/**
|
||||
* Defines an interface to action on specific input events.
|
||||
* @param event the input event to evaluate
|
||||
* @param action the action to perform if the event matches
|
||||
*
|
||||
* @return true if the action was performed, false otherwise.
|
||||
*/
|
||||
fun actionOnInputEvent(event: InputEvent, action: () -> Unit): Boolean
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import com.bitwarden.authenticator.ui.auth.unlock.unlockDestination
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.AUTHENTICATOR_GRAPH_ROUTE
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.authenticatorGraph
|
||||
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.navigateToAuthenticatorGraph
|
||||
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.setupDebugMenuDestination
|
||||
import com.bitwarden.authenticator.ui.platform.feature.splash.SPLASH_ROUTE
|
||||
import com.bitwarden.authenticator.ui.platform.feature.splash.navigateToSplash
|
||||
import com.bitwarden.authenticator.ui.platform.feature.splash.splashDestination
|
||||
@@ -80,6 +81,11 @@ fun RootNavScreen(
|
||||
viewModel.trySendAction(RootNavAction.Internal.AppUnlocked)
|
||||
},
|
||||
)
|
||||
setupDebugMenuDestination(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
)
|
||||
authenticatorGraph(
|
||||
navController = navController,
|
||||
onNavigateBack = onExitApplication,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.bitwarden.authenticator.ui.platform.util
|
||||
|
||||
import android.content.res.Configuration
|
||||
import com.bitwarden.authenticator.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* A helper method to indicate if the current UI configuration is portrait or not.
|
||||
*/
|
||||
val Configuration.isPortrait: Boolean
|
||||
get() = when (this.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> false
|
||||
else -> true
|
||||
}
|
||||
@@ -6,4 +6,12 @@
|
||||
<string name="import_format_label_2fas_json">2FAS (no password)</string>
|
||||
<string name="import_format_label_lastpass_json">LastPass (.json)</string>
|
||||
<string name="import_format_label_aegis_json">Aegis (.json)</string>
|
||||
|
||||
<!-- Debug Menu -->
|
||||
<string name="feature_flags">Feature Flags:</string>
|
||||
<string name="debug_menu">Debug Menu</string>
|
||||
<string name="reset_values">Reset values</string>
|
||||
<string name="bitwarden_authentication_enabled">Bitwarden authentication enabled</string>
|
||||
<string name="password_manager_sync">Password manager sync</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.bitwarden.authenticator
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.bitwarden.authenticator.data.platform.repository.util.FakeServerConfigRepository
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -20,11 +23,15 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
every { appThemeStateFlow } returns mutableAppThemeFlow
|
||||
every { isScreenCaptureAllowedStateFlow } returns mutableScreenCaptureAllowedFlow
|
||||
}
|
||||
private val fakeServerConfigRepository = FakeServerConfigRepository()
|
||||
private lateinit var mainViewModel: MainViewModel
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mainViewModel = MainViewModel(settingsRepository)
|
||||
mainViewModel = MainViewModel(
|
||||
settingsRepository,
|
||||
fakeServerConfigRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,4 +59,14 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||
settingsRepository.appThemeStateFlow
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send NavigateToDebugMenu action when OpenDebugMenu action is sent`() = runTest {
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
|
||||
mainViewModel.eventFlow.test {
|
||||
awaitItem() // ignore first event
|
||||
assertEquals(MainEvent.NavigateToDebugMenu, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.bitwarden.authenticator.data.platform.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
|
||||
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DebugMenuFeatureFlagManagerTest {
|
||||
|
||||
private val mockFeatureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||
every { getFeatureFlag<Boolean>(any()) } returns true
|
||||
}
|
||||
|
||||
private val mutableOverridesUpdateFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mockDebugMenuRepository = mockk<DebugMenuRepository>(relaxed = true) {
|
||||
every { updateFeatureFlag(any(), any()) } just runs
|
||||
every { featureFlagOverridesUpdatedFlow } returns mutableOverridesUpdateFlow
|
||||
}
|
||||
|
||||
private val debugMenuFeatureFlagManager = DebugMenuFeatureFlagManagerImpl(
|
||||
defaultFeatureFlagManager = mockFeatureFlagManager,
|
||||
debugMenuRepository = mockDebugMenuRepository,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `If value exists in repository return that value for requested FlagKey`() {
|
||||
val flagKey = FlagKey.DummyBoolean
|
||||
val expectedValue = true
|
||||
every { mockDebugMenuRepository.getFeatureFlag(flagKey) } returns expectedValue
|
||||
|
||||
assertTrue(debugMenuFeatureFlagManager.getFeatureFlag(flagKey))
|
||||
|
||||
verify(exactly = 0) { mockFeatureFlagManager.getFeatureFlag(flagKey) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `If value does not exist in repository return that value from the default manager`() {
|
||||
val flagKey = FlagKey.DummyBoolean
|
||||
|
||||
every { mockDebugMenuRepository.getFeatureFlag(flagKey) } returns null
|
||||
|
||||
assertTrue(debugMenuFeatureFlagManager.getFeatureFlag(flagKey))
|
||||
|
||||
verify(exactly = 1) { mockFeatureFlagManager.getFeatureFlag(flagKey) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `get feature flag with force refresh will call the default manager to use as the fallback value`() =
|
||||
runTest {
|
||||
val flagKey = FlagKey.DummyBoolean
|
||||
val expectedValue = true
|
||||
|
||||
coEvery {
|
||||
mockFeatureFlagManager.getFeatureFlag(key = flagKey, forceRefresh = true)
|
||||
} returns expectedValue
|
||||
every { mockDebugMenuRepository.getFeatureFlag(flagKey) } returns null
|
||||
|
||||
assertTrue(
|
||||
debugMenuFeatureFlagManager.getFeatureFlag(
|
||||
key = flagKey,
|
||||
forceRefresh = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
mockFeatureFlagManager.getFeatureFlag(
|
||||
key = flagKey,
|
||||
forceRefresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when repository update flow emits, the feature flag flow will refresh to the value`() =
|
||||
runTest {
|
||||
val flagKey = FlagKey.DummyBoolean
|
||||
every { mockDebugMenuRepository.getFeatureFlag(flagKey) } returns true
|
||||
|
||||
debugMenuFeatureFlagManager
|
||||
.getFeatureFlagFlow(flagKey)
|
||||
.test {
|
||||
mutableOverridesUpdateFlow.emit(Unit)
|
||||
assertEquals(true, awaitItem())
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when repository update flow emits the flow will refresh to the value from default manager if repo returns null`() =
|
||||
runTest {
|
||||
val flagKey = FlagKey.DummyBoolean
|
||||
every { mockDebugMenuRepository.getFeatureFlag(flagKey) } returns null
|
||||
|
||||
debugMenuFeatureFlagManager
|
||||
.getFeatureFlagFlow(flagKey)
|
||||
.test {
|
||||
mutableOverridesUpdateFlow.emit(Unit)
|
||||
assertEquals(true, awaitItem())
|
||||
cancel()
|
||||
}
|
||||
verify(exactly = 1) { mockFeatureFlagManager.getFeatureFlag(flagKey) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.bitwarden.authenticator.data.platform.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.bitwarden.authenticator.data.platform.datasource.disk.model.ServerConfig
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.model.ConfigResponseJson
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DebugMenuRepositoryTest {
|
||||
private val mockFeatureFlagOverrideDiskSource =
|
||||
mockk<FeatureFlagOverrideDiskSource> {
|
||||
every { getFeatureFlag(FlagKey.DummyBoolean) } returns true
|
||||
every { getFeatureFlag(FlagKey.DummyString) } returns TEST_STRING_VALUE
|
||||
every { getFeatureFlag(FlagKey.DummyInt()) } returns TEST_INT_VALUE
|
||||
every { saveFeatureFlag(any(), any()) } just io.mockk.runs
|
||||
}
|
||||
private val mutableServerConfigStateFlow =
|
||||
MutableStateFlow<ServerConfig?>(
|
||||
null,
|
||||
)
|
||||
private val mockServerConfigRepository =
|
||||
mockk<ServerConfigRepository> {
|
||||
every { serverConfigStateFlow } returns mutableServerConfigStateFlow
|
||||
}
|
||||
|
||||
private val debugMenuRepository =
|
||||
DebugMenuRepositoryImpl(
|
||||
featureFlagOverrideDiskSource = mockFeatureFlagOverrideDiskSource,
|
||||
serverConfigRepository = mockServerConfigRepository,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `updateFeatureFlag should save the feature flag to disk`() {
|
||||
debugMenuRepository.updateFeatureFlag(
|
||||
FlagKey.DummyBoolean,
|
||||
true,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||
FlagKey.DummyBoolean,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateFeatureFlag should cause the feature flag overrides updated flow to emit`() =
|
||||
runTest {
|
||||
debugMenuRepository.updateFeatureFlag(
|
||||
FlagKey.DummyBoolean,
|
||||
true,
|
||||
)
|
||||
debugMenuRepository.featureFlagOverridesUpdatedFlow.test {
|
||||
awaitItem() // initial value on subscription
|
||||
awaitItem()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFeatureFlag should return the feature flag boolean value from disk`() {
|
||||
Assertions.assertTrue(debugMenuRepository.getFeatureFlag(FlagKey.DummyBoolean)!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFeatureFlag should return the feature flag string value from disk`() {
|
||||
Assertions.assertEquals(
|
||||
TEST_STRING_VALUE,
|
||||
debugMenuRepository.getFeatureFlag(FlagKey.DummyString)!!,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFeatureFlag should return the feature flag int value from disk`() {
|
||||
Assertions.assertEquals(
|
||||
TEST_INT_VALUE,
|
||||
debugMenuRepository.getFeatureFlag(FlagKey.DummyInt())!!,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFeatureFlag should return null if the feature flag does not exist in disk`() {
|
||||
every { mockFeatureFlagOverrideDiskSource.getFeatureFlag<Boolean>(any()) } returns null
|
||||
Assertions.assertNull(debugMenuRepository.getFeatureFlag(FlagKey.DummyBoolean))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `resetFeatureFlagOverrides should reset flags to default values if they don't exist in server config`() =
|
||||
runTest {
|
||||
debugMenuRepository.resetFeatureFlagOverrides()
|
||||
verify(exactly = 1) {
|
||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||
FlagKey.PasswordManagerSync,
|
||||
FlagKey.PasswordManagerSync.defaultValue,
|
||||
)
|
||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.BitwardenAuthenticationEnabled.defaultValue,
|
||||
)
|
||||
}
|
||||
debugMenuRepository.featureFlagOverridesUpdatedFlow.test {
|
||||
awaitItem() // initial value on subscription
|
||||
awaitItem()
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `resetFeatureFlagOverrides should save all feature flags to values from the server config if remote configured is on`() =
|
||||
runTest {
|
||||
val mockServerData =
|
||||
mockk<ConfigResponseJson>(
|
||||
relaxed = true,
|
||||
) {
|
||||
every { featureStates } returns mapOf(
|
||||
FlagKey.PasswordManagerSync.keyName to JsonPrimitive(
|
||||
true,
|
||||
),
|
||||
FlagKey.BitwardenAuthenticationEnabled.keyName to JsonPrimitive(
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
val mockServerConfig =
|
||||
mockk<ServerConfig>(
|
||||
relaxed = true,
|
||||
) {
|
||||
every { serverData } returns mockServerData
|
||||
}
|
||||
mutableServerConfigStateFlow.value = mockServerConfig
|
||||
|
||||
debugMenuRepository.resetFeatureFlagOverrides()
|
||||
|
||||
Assertions.assertTrue(FlagKey.PasswordManagerSync.isRemotelyConfigured)
|
||||
Assertions.assertFalse(FlagKey.BitwardenAuthenticationEnabled.isRemotelyConfigured)
|
||||
verify(exactly = 1) {
|
||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||
FlagKey.PasswordManagerSync,
|
||||
true,
|
||||
)
|
||||
mockFeatureFlagOverrideDiskSource.saveFeatureFlag(
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
debugMenuRepository.featureFlagOverridesUpdatedFlow.test {
|
||||
awaitItem() // initial value on subscription
|
||||
awaitItem()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TEST_STRING_VALUE = "test"
|
||||
private const val TEST_INT_VALUE = 100
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.printToLog
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class DebugMenuScreenTest : BaseComposeTest() {
|
||||
private var onNavigateBackCalled = false
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<DebugMenuEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DebugMenuState(featureFlags = emptyMap()))
|
||||
private val viewModel = mockk<DebugMenuViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
DebugMenuScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onNavigateBack should set onNavigateBackCalled to true`() {
|
||||
mutableEventFlow.tryEmit(DebugMenuEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onNavigateBack should send action to viewModel`() {
|
||||
composeTestRule
|
||||
.onRoot()
|
||||
.printToLog("djf")
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Back")
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(DebugMenuAction.NavigateBack) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `feature flag content should not display if the state is empty`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Password manager sync", ignoreCase = true)
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `feature flag content should display if the state is not empty`() {
|
||||
mutableStateFlow.tryEmit(
|
||||
DebugMenuState(
|
||||
featureFlags = mapOf(
|
||||
FlagKey.PasswordManagerSync to true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Password manager sync", ignoreCase = true)
|
||||
.assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `boolean feature flag content should send action when clicked`() {
|
||||
mutableStateFlow.tryEmit(
|
||||
DebugMenuState(
|
||||
featureFlags = mapOf(
|
||||
FlagKey.PasswordManagerSync to true,
|
||||
),
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNodeWithText("Password manager sync", ignoreCase = true)
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.UpdateFeatureFlag(
|
||||
FlagKey.PasswordManagerSync,
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset feature flag values should send action when clicked`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Reset Values", ignoreCase = true)
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.authenticator.data.platform.manager.FeatureFlagManager
|
||||
import com.bitwarden.authenticator.data.platform.manager.model.FlagKey
|
||||
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mockFeatureFlagManager = mockk<FeatureFlagManager>(relaxed = true) {
|
||||
every { getFeatureFlagFlow<Boolean>(any()) } returns flowOf(true)
|
||||
}
|
||||
|
||||
private val mockDebugMenuRepository = mockk<DebugMenuRepository>(relaxed = true) {
|
||||
coEvery { resetFeatureFlagOverrides() } just runs
|
||||
every { updateFeatureFlag<Boolean>(any(), any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(viewModel.stateFlow.value, DEFAULT_STATE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleUpdateFeatureFlag should update the feature flag`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(viewModel.stateFlow.value, DEFAULT_STATE)
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.Internal.UpdateFeatureFlagMap(UPDATED_MAP_VALUE),
|
||||
)
|
||||
assertEquals(viewModel.stateFlow.value, DebugMenuState(UPDATED_MAP_VALUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleResetFeatureFlagValues should reset the feature flag values`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues)
|
||||
coVerify(exactly = 1) { mockDebugMenuRepository.resetFeatureFlagOverrides() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleNavigateBack should send NavigateBack event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(DebugMenuAction.NavigateBack)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(DebugMenuEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleUpdateFeatureFlag should update the feature flag via the repository`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.UpdateFeatureFlag(FlagKey.PasswordManagerSync, false),
|
||||
)
|
||||
verify { mockDebugMenuRepository.updateFeatureFlag(FlagKey.PasswordManagerSync, false) }
|
||||
}
|
||||
|
||||
private fun createViewModel(): DebugMenuViewModel = DebugMenuViewModel(
|
||||
featureFlagManager = mockFeatureFlagManager,
|
||||
debugMenuRepository = mockDebugMenuRepository,
|
||||
)
|
||||
}
|
||||
|
||||
private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||
FlagKey.BitwardenAuthenticationEnabled to true,
|
||||
FlagKey.PasswordManagerSync to true,
|
||||
)
|
||||
|
||||
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||
FlagKey.BitwardenAuthenticationEnabled to false,
|
||||
FlagKey.PasswordManagerSync to false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE = DebugMenuState(
|
||||
featureFlags = DEFAULT_MAP_VALUE,
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager
|
||||
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DebugLaunchManagerTest {
|
||||
|
||||
private val mockDebugMenuRepository = mockk<DebugMenuRepository>(relaxed = true) {
|
||||
every { isDebugMenuEnabled } returns true
|
||||
}
|
||||
|
||||
private val mockKeyEvent = mockk<KeyEvent>(relaxed = true) {
|
||||
every { action } returns KeyEvent.ACTION_DOWN
|
||||
every { keyCode } returns KeyEvent.KEYCODE_GRAVE
|
||||
every { isShiftPressed } returns true
|
||||
}
|
||||
|
||||
private val mockMotionEvent = mockk<MotionEvent>(relaxed = true) {
|
||||
every { action and MotionEvent.ACTION_MASK } returns MotionEvent.ACTION_POINTER_DOWN
|
||||
every { pointerCount } returns 3
|
||||
}
|
||||
|
||||
private var actionHasBeenCalled = false
|
||||
private val action: () -> Unit = { actionHasBeenCalled = true }
|
||||
|
||||
private val debugLaunchManager =
|
||||
DebugLaunchManagerImpl(debugMenuRepository = mockDebugMenuRepository)
|
||||
|
||||
@Test
|
||||
fun `actionOnInputEvent should return true when KeyEvent is debug trigger`() {
|
||||
assertFalse(actionHasBeenCalled)
|
||||
val result = debugLaunchManager.actionOnInputEvent(event = mockKeyEvent, action = action)
|
||||
assertTrue(result)
|
||||
assertTrue(actionHasBeenCalled)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `actionOnInputEvent should return true when TouchEvent is debug trigger done 3 times in a row`() {
|
||||
assertFalse(actionHasBeenCalled)
|
||||
debugLaunchManager.actionOnInputEvent(event = mockMotionEvent, action = action)
|
||||
debugLaunchManager.actionOnInputEvent(event = mockMotionEvent, action = action)
|
||||
val result = debugLaunchManager.actionOnInputEvent(event = mockMotionEvent, action = action)
|
||||
assertTrue(result)
|
||||
assertTrue(actionHasBeenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `actionOnInputEvent should return false when debug menu is not enabled`() {
|
||||
every { mockDebugMenuRepository.isDebugMenuEnabled } returns false
|
||||
assertFalse(actionHasBeenCalled)
|
||||
val result = debugLaunchManager.actionOnInputEvent(event = mockKeyEvent, action = action)
|
||||
assertFalse(result)
|
||||
assertFalse(actionHasBeenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `actionOnInputEvent should return false when key event is not debug trigger`() {
|
||||
assertFalse(actionHasBeenCalled)
|
||||
val result = debugLaunchManager
|
||||
.actionOnInputEvent(
|
||||
event = mockKeyEvent.apply {
|
||||
every { action } returns KeyEvent.ACTION_UP
|
||||
},
|
||||
action = action,
|
||||
)
|
||||
assertFalse(result)
|
||||
assertFalse(actionHasBeenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `actionOnInputEvent should return false when touch event is not debug trigger`() {
|
||||
assertFalse(actionHasBeenCalled)
|
||||
debugLaunchManager.actionOnInputEvent(event = mockMotionEvent, action = action)
|
||||
debugLaunchManager.actionOnInputEvent(event = mockMotionEvent, action = action)
|
||||
val result = debugLaunchManager.actionOnInputEvent(
|
||||
event = mockMotionEvent.apply {
|
||||
every { pointerCount } returns 100
|
||||
},
|
||||
action = action,
|
||||
)
|
||||
assertFalse(result)
|
||||
assertFalse(actionHasBeenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if touch action input takes place too slow should return false`() {
|
||||
val eventTimeMillis = 100L
|
||||
assertFalse(actionHasBeenCalled)
|
||||
debugLaunchManager.actionOnInputEvent(event = mockMotionEvent, action = action)
|
||||
debugLaunchManager.actionOnInputEvent(event = mockMotionEvent.apply {
|
||||
every { eventTime } returns eventTimeMillis
|
||||
}, action = action)
|
||||
val result = debugLaunchManager.actionOnInputEvent(
|
||||
event = mockMotionEvent.apply {
|
||||
every { eventTime } returns eventTimeMillis + 501
|
||||
},
|
||||
action = action,
|
||||
)
|
||||
assertFalse(result)
|
||||
assertFalse(actionHasBeenCalled)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user