mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-11488: Add a switch to the autofill settings UI for enabling the accessibility service (#3911)
This commit is contained in:
parent
190ba792a1
commit
3ecf1382b2
20 changed files with 543 additions and 3 deletions
|
@ -15,6 +15,7 @@ import androidx.core.os.LocaleListCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||||
|
@ -38,6 +39,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val mainViewModel: MainViewModel by viewModels()
|
private val mainViewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var accessibilityActivityManager: AccessibilityActivityManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var autofillActivityManager: AutofillActivityManager
|
lateinit var autofillActivityManager: AutofillActivityManager
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAuto
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManagerImpl
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManagerImpl
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl
|
||||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||||
|
@ -49,6 +51,11 @@ object AccessibilityModule {
|
||||||
fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager =
|
fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager =
|
||||||
AccessibilityAutofillManagerImpl()
|
AccessibilityAutofillManagerImpl()
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager =
|
||||||
|
AccessibilityEnabledManagerImpl()
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
fun providesAccessibilityParser(): AccessibilityParser = AccessibilityParserImpl()
|
fun providesAccessibilityParser(): AccessibilityParser = AccessibilityParserImpl()
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.components.ActivityComponent
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides dependencies within the accessibility package scoped to the activity.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
@InstallIn(ActivityComponent::class)
|
||||||
|
object ActivityAccessibilityModule {
|
||||||
|
@ActivityScoped
|
||||||
|
@Provides
|
||||||
|
fun providesAccessibilityActivityManager(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||||
|
appForegroundManager: AppForegroundManager,
|
||||||
|
lifecycleScope: LifecycleCoroutineScope,
|
||||||
|
): AccessibilityActivityManager =
|
||||||
|
AccessibilityActivityManagerImpl(
|
||||||
|
context = context,
|
||||||
|
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||||
|
appForegroundManager = appForegroundManager,
|
||||||
|
lifecycleScope = lifecycleScope,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper for dealing with accessibility configuration that must be scoped to a specific
|
||||||
|
* [Activity]. In particular, this should be injected into an [Activity] to ensure that the
|
||||||
|
* [AccessibilityEnabledManager] reports correct values.
|
||||||
|
*/
|
||||||
|
interface AccessibilityActivityManager
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accessibility service name as it is known in the AndroidManifest.
|
||||||
|
*
|
||||||
|
* Note: This is not the name of the actual service but the name defined in the Android Manifest
|
||||||
|
* which matches the service in the Xamarin app.
|
||||||
|
*/
|
||||||
|
private const val LEGACY_ACCESSIBILITY_SERVICE_NAME =
|
||||||
|
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of the [AccessibilityActivityManager].
|
||||||
|
*/
|
||||||
|
class AccessibilityActivityManagerImpl(
|
||||||
|
private val context: Context,
|
||||||
|
private val accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||||
|
appForegroundManager: AppForegroundManager,
|
||||||
|
lifecycleScope: LifecycleCoroutineScope,
|
||||||
|
) : AccessibilityActivityManager {
|
||||||
|
private val isAccessibilityServiceEnabled: Boolean
|
||||||
|
get() {
|
||||||
|
val appContext = context.applicationContext
|
||||||
|
val accessibilityService = appContext
|
||||||
|
.packageName
|
||||||
|
?.let { "$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME" }
|
||||||
|
?: return false
|
||||||
|
return Settings
|
||||||
|
.Secure
|
||||||
|
.getString(
|
||||||
|
appContext.contentResolver,
|
||||||
|
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
|
||||||
|
)
|
||||||
|
?.contains(accessibilityService)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
appForegroundManager
|
||||||
|
.appForegroundStateFlow
|
||||||
|
.onEach {
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabled = isAccessibilityServiceEnabled
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for values specifying whether or not the accessibility service is enabled.
|
||||||
|
*/
|
||||||
|
interface AccessibilityEnabledManager {
|
||||||
|
/**
|
||||||
|
* Whether or not the accessibility service should be considered enabled.
|
||||||
|
*
|
||||||
|
* Note that changing this does not enable or disable autofill; it is only an indicator that
|
||||||
|
* this has occurred elsewhere.
|
||||||
|
*/
|
||||||
|
var isAccessibilityEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits updates that track [isAccessibilityEnabled] values.
|
||||||
|
*/
|
||||||
|
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of [AccessibilityEnabledManager].
|
||||||
|
*/
|
||||||
|
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager {
|
||||||
|
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
|
||||||
|
|
||||||
|
override var isAccessibilityEnabled: Boolean
|
||||||
|
get() = mutableIsAccessibilityEnabledStateFlow.value
|
||||||
|
set(value) {
|
||||||
|
mutableIsAccessibilityEnabledStateFlow.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||||
|
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
|
||||||
|
}
|
|
@ -133,6 +133,15 @@ interface SettingsRepository {
|
||||||
*/
|
*/
|
||||||
var blockedAutofillUris: List<String>
|
var blockedAutofillUris: List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits updates whenever there is a change in the app's status for accessibility-based
|
||||||
|
* autofill.
|
||||||
|
*
|
||||||
|
* Note that the correct value is only populated upon subscription so calling [StateFlow.value]
|
||||||
|
* may result in an out-of-date value.
|
||||||
|
*/
|
||||||
|
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits updates whenever there is a change in the app's status for supporting autofill.
|
* Emits updates whenever there is a change in the app's status for supporting autofill.
|
||||||
*
|
*
|
||||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||||
|
@ -49,6 +50,7 @@ class SettingsRepositoryImpl(
|
||||||
private val settingsDiskSource: SettingsDiskSource,
|
private val settingsDiskSource: SettingsDiskSource,
|
||||||
private val vaultSdkSource: VaultSdkSource,
|
private val vaultSdkSource: VaultSdkSource,
|
||||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||||
|
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||||
policyManager: PolicyManager,
|
policyManager: PolicyManager,
|
||||||
dispatcherManager: DispatcherManager,
|
dispatcherManager: DispatcherManager,
|
||||||
) : SettingsRepository {
|
) : SettingsRepository {
|
||||||
|
@ -293,6 +295,9 @@ class SettingsRepositoryImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean> =
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabledStateFlow
|
||||||
|
|
||||||
override val isAutofillEnabledStateFlow: StateFlow<Boolean> =
|
override val isAutofillEnabledStateFlow: StateFlow<Boolean> =
|
||||||
autofillEnabledManager.isAutofillEnabledStateFlow
|
autofillEnabledManager.isAutofillEnabledStateFlow
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository.di
|
||||||
|
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||||
|
@ -71,6 +72,7 @@ object PlatformRepositoryModule {
|
||||||
settingsDiskSource: SettingsDiskSource,
|
settingsDiskSource: SettingsDiskSource,
|
||||||
vaultSdkSource: VaultSdkSource,
|
vaultSdkSource: VaultSdkSource,
|
||||||
encryptionManager: BiometricsEncryptionManager,
|
encryptionManager: BiometricsEncryptionManager,
|
||||||
|
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||||
dispatcherManager: DispatcherManager,
|
dispatcherManager: DispatcherManager,
|
||||||
policyManager: PolicyManager,
|
policyManager: PolicyManager,
|
||||||
): SettingsRepository =
|
): SettingsRepository =
|
||||||
|
@ -81,6 +83,7 @@ object PlatformRepositoryModule {
|
||||||
settingsDiskSource = settingsDiskSource,
|
settingsDiskSource = settingsDiskSource,
|
||||||
vaultSdkSource = vaultSdkSource,
|
vaultSdkSource = vaultSdkSource,
|
||||||
biometricsEncryptionManager = encryptionManager,
|
biometricsEncryptionManager = encryptionManager,
|
||||||
|
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||||
dispatcherManager = dispatcherManager,
|
dispatcherManager = dispatcherManager,
|
||||||
policyManager = policyManager,
|
policyManager = policyManager,
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.x8bit.bitwarden.BuildConfig
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
@ -36,6 +37,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
|
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
|
||||||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
|
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
|
||||||
|
@ -67,6 +69,10 @@ fun AutoFillScreen(
|
||||||
when (event) {
|
when (event) {
|
||||||
AutoFillEvent.NavigateBack -> onNavigateBack.invoke()
|
AutoFillEvent.NavigateBack -> onNavigateBack.invoke()
|
||||||
|
|
||||||
|
AutoFillEvent.NavigateToAccessibilitySettings -> {
|
||||||
|
intentManager.startSystemAccessibilitySettingsActivity()
|
||||||
|
}
|
||||||
|
|
||||||
AutoFillEvent.NavigateToAutofillSettings -> {
|
AutoFillEvent.NavigateToAutofillSettings -> {
|
||||||
val isSuccess = intentManager.startSystemAutofillSettingsActivity()
|
val isSuccess = intentManager.startSystemAutofillSettingsActivity()
|
||||||
|
|
||||||
|
@ -171,6 +177,15 @@ fun AutoFillScreen(
|
||||||
withDivider = false,
|
withDivider = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AccessibilityAutofillSwitch(
|
||||||
|
isAccessibilityAutoFillEnabled = state.isAccessibilityAutofillEnabled,
|
||||||
|
onCheckedChange = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick) }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
BitwardenListHeaderText(
|
BitwardenListHeaderText(
|
||||||
label = stringResource(id = R.string.additional_options),
|
label = stringResource(id = R.string.additional_options),
|
||||||
|
@ -226,6 +241,45 @@ fun AutoFillScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccessibilityAutofillSwitch(
|
||||||
|
isAccessibilityAutoFillEnabled: Boolean,
|
||||||
|
onCheckedChange: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
// TODO: This should be visible in all variants once the feature is complete (PM-12014)
|
||||||
|
if (!BuildConfig.DEBUG) return
|
||||||
|
var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) }
|
||||||
|
BitwardenWideSwitch(
|
||||||
|
label = stringResource(id = R.string.accessibility),
|
||||||
|
description = stringResource(id = R.string.accessibility_description5),
|
||||||
|
isChecked = isAccessibilityAutoFillEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (isAccessibilityAutoFillEnabled) {
|
||||||
|
onCheckedChange()
|
||||||
|
} else {
|
||||||
|
shouldShowDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.testTag(tag = "AccessibilityAutofillSwitch"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (shouldShowDialog) {
|
||||||
|
BitwardenTwoButtonDialog(
|
||||||
|
title = stringResource(id = R.string.accessibility_service_disclosure),
|
||||||
|
message = stringResource(id = R.string.accessibility_disclosure_text),
|
||||||
|
confirmButtonText = stringResource(id = R.string.accept),
|
||||||
|
dismissButtonText = stringResource(id = R.string.decline),
|
||||||
|
onConfirmClick = {
|
||||||
|
onCheckedChange()
|
||||||
|
shouldShowDialog = false
|
||||||
|
},
|
||||||
|
onDismissClick = { shouldShowDialog = false },
|
||||||
|
onDismissRequest = { shouldShowDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultUriMatchTypeRow(
|
private fun DefaultUriMatchTypeRow(
|
||||||
selectedUriMatchType: UriMatchType,
|
selectedUriMatchType: UriMatchType,
|
||||||
|
|
|
@ -31,6 +31,9 @@ class AutoFillViewModel @Inject constructor(
|
||||||
initialState = savedStateHandle[KEY_STATE]
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
?: AutoFillState(
|
?: AutoFillState(
|
||||||
isAskToAddLoginEnabled = !settingsRepository.isAutofillSavePromptDisabled,
|
isAskToAddLoginEnabled = !settingsRepository.isAutofillSavePromptDisabled,
|
||||||
|
isAccessibilityAutofillEnabled = settingsRepository
|
||||||
|
.isAccessibilityEnabledStateFlow
|
||||||
|
.value,
|
||||||
isAutoFillServicesEnabled = settingsRepository.isAutofillEnabledStateFlow.value,
|
isAutoFillServicesEnabled = settingsRepository.isAutofillEnabledStateFlow.value,
|
||||||
isCopyTotpAutomaticallyEnabled = !settingsRepository.isAutoCopyTotpDisabled,
|
isCopyTotpAutomaticallyEnabled = !settingsRepository.isAutoCopyTotpDisabled,
|
||||||
isUseInlineAutoFillEnabled = settingsRepository.isInlineAutofillEnabled,
|
isUseInlineAutoFillEnabled = settingsRepository.isInlineAutofillEnabled,
|
||||||
|
@ -45,6 +48,15 @@ class AutoFillViewModel @Inject constructor(
|
||||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
settingsRepository
|
||||||
|
.isAccessibilityEnabledStateFlow
|
||||||
|
.map {
|
||||||
|
AutoFillAction.Internal.AccessibilityEnabledUpdateReceive(
|
||||||
|
isAccessibilityEnabled = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
settingsRepository
|
settingsRepository
|
||||||
.isAutofillEnabledStateFlow
|
.isAutofillEnabledStateFlow
|
||||||
.map {
|
.map {
|
||||||
|
@ -54,17 +66,28 @@ class AutoFillViewModel @Inject constructor(
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleAction(action: AutoFillAction): Unit = when (action) {
|
override fun handleAction(action: AutoFillAction) = when (action) {
|
||||||
is AutoFillAction.AskToAddLoginClick -> handleAskToAddLoginClick(action)
|
is AutoFillAction.AskToAddLoginClick -> handleAskToAddLoginClick(action)
|
||||||
is AutoFillAction.AutoFillServicesClick -> handleAutoFillServicesClick(action)
|
is AutoFillAction.AutoFillServicesClick -> handleAutoFillServicesClick(action)
|
||||||
AutoFillAction.BackClick -> handleBackClick()
|
AutoFillAction.BackClick -> handleBackClick()
|
||||||
is AutoFillAction.CopyTotpAutomaticallyClick -> handleCopyTotpAutomaticallyClick(action)
|
is AutoFillAction.CopyTotpAutomaticallyClick -> handleCopyTotpAutomaticallyClick(action)
|
||||||
is AutoFillAction.DefaultUriMatchTypeSelect -> handleDefaultUriMatchTypeSelect(action)
|
is AutoFillAction.DefaultUriMatchTypeSelect -> handleDefaultUriMatchTypeSelect(action)
|
||||||
AutoFillAction.BlockAutoFillClick -> handleBlockAutoFillClick()
|
AutoFillAction.BlockAutoFillClick -> handleBlockAutoFillClick()
|
||||||
|
AutoFillAction.UseAccessibilityAutofillClick -> handleUseAccessibilityAutofillClick()
|
||||||
is AutoFillAction.UseInlineAutofillClick -> handleUseInlineAutofillClick(action)
|
is AutoFillAction.UseInlineAutofillClick -> handleUseInlineAutofillClick(action)
|
||||||
AutoFillAction.PasskeyManagementClick -> handlePasskeyManagementClick()
|
AutoFillAction.PasskeyManagementClick -> handlePasskeyManagementClick()
|
||||||
is AutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
|
is AutoFillAction.Internal -> handleInternalAction(action)
|
||||||
handleAutofillEnabledUpdateReceive(action)
|
}
|
||||||
|
|
||||||
|
private fun handleInternalAction(action: AutoFillAction.Internal) {
|
||||||
|
when (action) {
|
||||||
|
is AutoFillAction.Internal.AccessibilityEnabledUpdateReceive -> {
|
||||||
|
handleAccessibilityEnabledUpdateReceive(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
|
||||||
|
handleAutofillEnabledUpdateReceive(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +115,10 @@ class AutoFillViewModel @Inject constructor(
|
||||||
mutableStateFlow.update { it.copy(isCopyTotpAutomaticallyEnabled = action.isEnabled) }
|
mutableStateFlow.update { it.copy(isCopyTotpAutomaticallyEnabled = action.isEnabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUseAccessibilityAutofillClick() {
|
||||||
|
sendEvent(AutoFillEvent.NavigateToAccessibilitySettings)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleUseInlineAutofillClick(action: AutoFillAction.UseInlineAutofillClick) {
|
private fun handleUseInlineAutofillClick(action: AutoFillAction.UseInlineAutofillClick) {
|
||||||
settingsRepository.isInlineAutofillEnabled = action.isEnabled
|
settingsRepository.isInlineAutofillEnabled = action.isEnabled
|
||||||
mutableStateFlow.update { it.copy(isUseInlineAutoFillEnabled = action.isEnabled) }
|
mutableStateFlow.update { it.copy(isUseInlineAutoFillEnabled = action.isEnabled) }
|
||||||
|
@ -108,6 +135,14 @@ class AutoFillViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleAccessibilityEnabledUpdateReceive(
|
||||||
|
action: AutoFillAction.Internal.AccessibilityEnabledUpdateReceive,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(isAccessibilityAutofillEnabled = action.isAccessibilityEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleAutofillEnabledUpdateReceive(
|
private fun handleAutofillEnabledUpdateReceive(
|
||||||
action: AutoFillAction.Internal.AutofillEnabledUpdateReceive,
|
action: AutoFillAction.Internal.AutofillEnabledUpdateReceive,
|
||||||
) {
|
) {
|
||||||
|
@ -127,6 +162,7 @@ class AutoFillViewModel @Inject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AutoFillState(
|
data class AutoFillState(
|
||||||
val isAskToAddLoginEnabled: Boolean,
|
val isAskToAddLoginEnabled: Boolean,
|
||||||
|
val isAccessibilityAutofillEnabled: Boolean,
|
||||||
val isAutoFillServicesEnabled: Boolean,
|
val isAutoFillServicesEnabled: Boolean,
|
||||||
val isCopyTotpAutomaticallyEnabled: Boolean,
|
val isCopyTotpAutomaticallyEnabled: Boolean,
|
||||||
val isUseInlineAutoFillEnabled: Boolean,
|
val isUseInlineAutoFillEnabled: Boolean,
|
||||||
|
@ -152,6 +188,11 @@ sealed class AutoFillEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : AutoFillEvent()
|
data object NavigateBack : AutoFillEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the system accessibility settings selection screen.
|
||||||
|
*/
|
||||||
|
data object NavigateToAccessibilitySettings : AutoFillEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to the system autofill settings selection screen.
|
* Navigates to the system autofill settings selection screen.
|
||||||
*/
|
*/
|
||||||
|
@ -217,6 +258,11 @@ sealed class AutoFillAction {
|
||||||
*/
|
*/
|
||||||
data object BlockAutoFillClick : AutoFillAction()
|
data object BlockAutoFillClick : AutoFillAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked use accessibility autofill switch.
|
||||||
|
*/
|
||||||
|
data object UseAccessibilityAutofillClick : AutoFillAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User clicked use inline autofill button.
|
* User clicked use inline autofill button.
|
||||||
*/
|
*/
|
||||||
|
@ -233,6 +279,12 @@ sealed class AutoFillAction {
|
||||||
* Internal actions.
|
* Internal actions.
|
||||||
*/
|
*/
|
||||||
sealed class Internal : AutoFillAction() {
|
sealed class Internal : AutoFillAction() {
|
||||||
|
/**
|
||||||
|
* An update for changes in the [isAccessibilityEnabled] value.
|
||||||
|
*/
|
||||||
|
data class AccessibilityEnabledUpdateReceive(
|
||||||
|
val isAccessibilityEnabled: Boolean,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An update for changes in the [isAutofillEnabled] value.
|
* An update for changes in the [isAutofillEnabled] value.
|
||||||
|
|
|
@ -27,6 +27,11 @@ interface IntentManager {
|
||||||
*/
|
*/
|
||||||
fun startCustomTabsActivity(uri: Uri)
|
fun startCustomTabsActivity(uri: Uri)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to start the system accessibility settings activity.
|
||||||
|
*/
|
||||||
|
fun startSystemAccessibilitySettingsActivity()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to start the system autofill settings activity. The return value indicates whether
|
* Attempts to start the system autofill settings activity. The return value indicates whether
|
||||||
* or not this was successful.
|
* or not this was successful.
|
||||||
|
|
|
@ -103,6 +103,10 @@ class IntentManagerImpl(
|
||||||
.launchUrl(context, uri)
|
.launchUrl(context, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startSystemAccessibilitySettingsActivity() {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||||
|
}
|
||||||
|
|
||||||
override fun startSystemAutofillSettingsActivity(): Boolean =
|
override fun startSystemAutofillSettingsActivity(): Boolean =
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
||||||
|
|
|
@ -542,6 +542,7 @@ Scanning will happen automatically.</string>
|
||||||
<string name="accessibility_description2">Use the Bitwarden Accessibility Service to auto-fill your logins across apps and the web. (Requires Draw-Over to be turned on as well)</string>
|
<string name="accessibility_description2">Use the Bitwarden Accessibility Service to auto-fill your logins across apps and the web. (Requires Draw-Over to be turned on as well)</string>
|
||||||
<string name="accessibility_description3">Use the Bitwarden Accessibility Service to use the Autofill Quick-Action Tile, and/or show a popup using Draw-Over (if turned on).</string>
|
<string name="accessibility_description3">Use the Bitwarden Accessibility Service to use the Autofill Quick-Action Tile, and/or show a popup using Draw-Over (if turned on).</string>
|
||||||
<string name="accessibility_description4">Required to use the Autofill Quick-Action Tile, or to augment the Autofill Service by using Draw-Over (if turned on).</string>
|
<string name="accessibility_description4">Required to use the Autofill Quick-Action Tile, or to augment the Autofill Service by using Draw-Over (if turned on).</string>
|
||||||
|
<string name="accessibility_description5">Required to use the Autofill Quick-Action Tile.</string>
|
||||||
<string name="draw_over">Use draw-over</string>
|
<string name="draw_over">Use draw-over</string>
|
||||||
<string name="draw_over_description">Allows the Bitwarden Accessibility Service to display a popup when login fields are selected.</string>
|
<string name="draw_over_description">Allows the Bitwarden Accessibility Service to display a popup when login fields are selected.</string>
|
||||||
<string name="draw_over_description2">If turned on, the Bitwarden Accessibility Service will display a popup when login fields are selected to assist with auto-filling your logins.</string>
|
<string name="draw_over_description2">If turned on, the Bitwarden Accessibility Service will display a popup when login fields are selected to assist with auto-filling your logins.</string>
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class AccessibilityActivityManagerTest {
|
||||||
|
private val context: Context = mockk {
|
||||||
|
every { applicationContext } returns this
|
||||||
|
every { packageName } returns "com.x8bit.bitwarden"
|
||||||
|
every { contentResolver } returns mockk()
|
||||||
|
}
|
||||||
|
private val accessibilityEnabledManager: AccessibilityEnabledManager =
|
||||||
|
AccessibilityEnabledManagerImpl()
|
||||||
|
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
|
||||||
|
private val appForegroundManager: AppForegroundManager = mockk {
|
||||||
|
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
|
||||||
|
}
|
||||||
|
private val lifecycleScope = mockk<LifecycleCoroutineScope> {
|
||||||
|
every { coroutineContext } returns UnconfinedTestDispatcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will construct an instance here just to hook the various dependencies together internally
|
||||||
|
@Suppress("unused")
|
||||||
|
private val autofillActivityManager: AccessibilityActivityManager =
|
||||||
|
AccessibilityActivityManagerImpl(
|
||||||
|
context = context,
|
||||||
|
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||||
|
appForegroundManager = appForegroundManager,
|
||||||
|
lifecycleScope = lifecycleScope,
|
||||||
|
)
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic(Settings.Secure::getString)
|
||||||
|
mockkSettingsSecureGetString(value = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic(Settings.Secure::getString)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `changes in app foreground status should update the AutofillEnabledManager as necessary`() =
|
||||||
|
runTest {
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test {
|
||||||
|
assertFalse(awaitItem())
|
||||||
|
|
||||||
|
// An update is received when both the accessibility state and foreground state
|
||||||
|
// change
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
mockkSettingsSecureGetString(
|
||||||
|
value = "com.x8bit.bitwarden/com.x8bit.bitwarden.Accessibility.AccessibilityService",
|
||||||
|
)
|
||||||
|
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
|
||||||
|
assertTrue(awaitItem())
|
||||||
|
|
||||||
|
// An update is not received when only the foreground state changes
|
||||||
|
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
// An update is not received when only the accessibility state changes
|
||||||
|
mockkSettingsSecureGetString(value = "com.x8bit.bitwarden/AccessibilityService")
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
// An update is received after both states have changed
|
||||||
|
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
|
||||||
|
assertFalse(awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mockkSettingsSecureGetString(value: String?) {
|
||||||
|
every {
|
||||||
|
Settings.Secure.getString(any(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
|
||||||
|
} returns value
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AccessibilityEnabledManagerTest {
|
||||||
|
|
||||||
|
private val accessibilityEnabledManager: AccessibilityEnabledManager =
|
||||||
|
AccessibilityEnabledManagerImpl()
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `isAccessibilityEnabledStateFlow should emit whenever isAccessibilityEnabled is set to a unique value`() =
|
||||||
|
runTest {
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabledStateFlow.test {
|
||||||
|
assertFalse(awaitItem())
|
||||||
|
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabled = true
|
||||||
|
assertTrue(awaitItem())
|
||||||
|
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabled = true
|
||||||
|
expectNoEvents()
|
||||||
|
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabled = false
|
||||||
|
assertFalse(awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||||
|
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||||
|
@ -51,6 +53,8 @@ class SettingsRepositoryTest {
|
||||||
every { disableAutofillServices() } just runs
|
every { disableAutofillServices() } just runs
|
||||||
}
|
}
|
||||||
private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl()
|
private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl()
|
||||||
|
private val accessibilityEnabledManager: AccessibilityEnabledManager =
|
||||||
|
AccessibilityEnabledManagerImpl()
|
||||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||||
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
||||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||||
|
@ -69,6 +73,7 @@ class SettingsRepositoryTest {
|
||||||
settingsDiskSource = fakeSettingsDiskSource,
|
settingsDiskSource = fakeSettingsDiskSource,
|
||||||
vaultSdkSource = vaultSdkSource,
|
vaultSdkSource = vaultSdkSource,
|
||||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||||
|
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||||
dispatcherManager = FakeDispatcherManager(),
|
dispatcherManager = FakeDispatcherManager(),
|
||||||
policyManager = policyManager,
|
policyManager = policyManager,
|
||||||
)
|
)
|
||||||
|
@ -677,6 +682,21 @@ class SettingsRepositoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `isAccessibilityEnabledStateFlow should emit whenever the accessibilityEnabledManager does`() =
|
||||||
|
runTest {
|
||||||
|
settingsRepository.isAccessibilityEnabledStateFlow.test {
|
||||||
|
assertFalse(awaitItem())
|
||||||
|
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabled = true
|
||||||
|
assertTrue(awaitItem())
|
||||||
|
|
||||||
|
accessibilityEnabledManager.isAccessibilityEnabled = false
|
||||||
|
assertFalse(awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isAutofillEnabledStateFlow should emit whenever the AutofillEnabledManager does`() =
|
fun `isAutofillEnabledStateFlow should emit whenever the AutofillEnabledManager does`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|
|
@ -45,6 +45,7 @@ class AutoFillScreenTest : BaseComposeTest() {
|
||||||
private val intentManager: IntentManager = mockk {
|
private val intentManager: IntentManager = mockk {
|
||||||
every { startSystemAutofillSettingsActivity() } answers { isSystemSettingsRequestSuccess }
|
every { startSystemAutofillSettingsActivity() } answers { isSystemSettingsRequestSuccess }
|
||||||
every { startCredentialManagerSettings(any()) } just runs
|
every { startCredentialManagerSettings(any()) } just runs
|
||||||
|
every { startSystemAccessibilitySettingsActivity() } just runs
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
@ -59,6 +60,15 @@ class AutoFillScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateToAccessibilitySettings should attempt to navigate to system settings`() {
|
||||||
|
mutableEventFlow.tryEmit(AutoFillEvent.NavigateToAccessibilitySettings)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
intentManager.startSystemAccessibilitySettingsActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on NavigateToAutofillSettings should attempt to navigate to system settings and not show the fallback dialog when result is a success`() {
|
fun `on NavigateToAutofillSettings should attempt to navigate to system settings and not show the fallback dialog when result is a success`() {
|
||||||
|
@ -106,6 +116,71 @@ class AutoFillScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.assertNoDialogExists()
|
composeTestRule.assertNoDialogExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on use accessibility click with accessibility already enabled should emit UseAccessibilityAutofillClick`() {
|
||||||
|
mutableStateFlow.update { it.copy(isAccessibilityAutofillEnabled = true) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Use accessibility")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on use accessibility click with accessibility already disabled should display disclosure dialog and declining closes the dialog`() {
|
||||||
|
mutableStateFlow.update { it.copy(isAccessibilityAutofillEnabled = false) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Use accessibility")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Accessibility Service Disclosure")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Decline")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on use accessibility click with accessibility already disabled should display disclosure dialog and accepting closes the dialog and emits UseAccessibilityAutofillClick`() {
|
||||||
|
mutableStateFlow.update { it.copy(isAccessibilityAutofillEnabled = false) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Use accessibility")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Accessibility Service Disclosure")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(text = "Accept")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performClick()
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on autofill settings fallback dialog Ok click should dismiss the dialog`() {
|
fun `on autofill settings fallback dialog Ok click should dismiss the dialog`() {
|
||||||
isSystemSettingsRequestSuccess = false
|
isSystemSettingsRequestSuccess = false
|
||||||
|
@ -391,6 +466,7 @@ class AutoFillScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
private val DEFAULT_STATE: AutoFillState = AutoFillState(
|
private val DEFAULT_STATE: AutoFillState = AutoFillState(
|
||||||
isAskToAddLoginEnabled = false,
|
isAskToAddLoginEnabled = false,
|
||||||
|
isAccessibilityAutofillEnabled = false,
|
||||||
isAutoFillServicesEnabled = false,
|
isAutoFillServicesEnabled = false,
|
||||||
isCopyTotpAutomaticallyEnabled = false,
|
isCopyTotpAutomaticallyEnabled = false,
|
||||||
isUseInlineAutoFillEnabled = false,
|
isUseInlineAutoFillEnabled = false,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class AutoFillViewModelTest : BaseViewModelTest() {
|
class AutoFillViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(false)
|
||||||
private val mutableIsAutofillEnabledStateFlow = MutableStateFlow(false)
|
private val mutableIsAutofillEnabledStateFlow = MutableStateFlow(false)
|
||||||
private val settingsRepository: SettingsRepository = mockk {
|
private val settingsRepository: SettingsRepository = mockk {
|
||||||
every { isInlineAutofillEnabled } returns true
|
every { isInlineAutofillEnabled } returns true
|
||||||
|
@ -33,6 +34,7 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
||||||
every { isAutofillSavePromptDisabled = any() } just runs
|
every { isAutofillSavePromptDisabled = any() } just runs
|
||||||
every { defaultUriMatchType } returns UriMatchType.DOMAIN
|
every { defaultUriMatchType } returns UriMatchType.DOMAIN
|
||||||
every { defaultUriMatchType = any() } just runs
|
every { defaultUriMatchType = any() } just runs
|
||||||
|
every { isAccessibilityEnabledStateFlow } returns mutableIsAccessibilityEnabledStateFlow
|
||||||
every { isAutofillEnabledStateFlow } returns mutableIsAutofillEnabledStateFlow
|
every { isAutofillEnabledStateFlow } returns mutableIsAutofillEnabledStateFlow
|
||||||
every { disableAutofill() } just runs
|
every { disableAutofill() } just runs
|
||||||
}
|
}
|
||||||
|
@ -103,6 +105,26 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `changes in accessibility enabled status should update the state`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
|
||||||
|
mutableIsAccessibilityEnabledStateFlow.value = true
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(isAccessibilityAutofillEnabled = true),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
mutableIsAccessibilityEnabledStateFlow.value = false
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(isAccessibilityAutofillEnabled = false),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `changes in autofill enabled status should update the state`() {
|
fun `changes in autofill enabled status should update the state`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -148,6 +170,16 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on UseAccessibilityAutofillClick should emit NavigateToAccessibilitySettings`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(AutoFillAction.UseAccessibilityAutofillClick)
|
||||||
|
assertEquals(AutoFillEvent.NavigateToAccessibilitySettings, awaitItem())
|
||||||
|
}
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on AutoFillServicesClick with true should emit NavigateToAutofillSettings`() = runTest {
|
fun `on AutoFillServicesClick with true should emit NavigateToAutofillSettings`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -244,6 +276,7 @@ class AutoFillViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val DEFAULT_STATE: AutoFillState = AutoFillState(
|
private val DEFAULT_STATE: AutoFillState = AutoFillState(
|
||||||
isAskToAddLoginEnabled = false,
|
isAskToAddLoginEnabled = false,
|
||||||
|
isAccessibilityAutofillEnabled = false,
|
||||||
isAutoFillServicesEnabled = false,
|
isAutoFillServicesEnabled = false,
|
||||||
isCopyTotpAutomaticallyEnabled = false,
|
isCopyTotpAutomaticallyEnabled = false,
|
||||||
isUseInlineAutoFillEnabled = true,
|
isUseInlineAutoFillEnabled = true,
|
||||||
|
|
Loading…
Reference in a new issue