Compare commits

...

1 Commits

Author SHA1 Message Date
David Perez
3cec5da78c 🍒 PM-38745: Feat: Update accessibility UI (#7038) 2026-06-08 15:15:58 -05:00
10 changed files with 522 additions and 107 deletions

View File

@@ -17,7 +17,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -26,8 +25,6 @@ import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.setHorizonOSAppLayout
import com.bitwarden.ui.platform.util.setupEdgeToEdge
@@ -39,6 +36,8 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.accessibilityDisclosureDestination
import com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure.navigateToAccessibilityDisclosure
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.cookieAcquisitionDestination
import com.x8bit.bitwarden.ui.platform.feature.cookieacquisition.navigateToCookieAcquisition
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
@@ -136,14 +135,6 @@ class MainActivity : AppCompatActivity() {
theme = state.theme,
dynamicColor = state.isDynamicColorsEnabled,
) {
MainActivityDialogs(
dialogState = state.dialogState,
onAccessibilityDisclaimerDismiss = {
mainViewModel.trySendAction(
MainAction.DismissAccessibilityDisclaimerDialog,
)
},
)
NavHost(
navController = navController,
startDestination = RootNavigationRoute,
@@ -163,6 +154,10 @@ class MainActivity : AppCompatActivity() {
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
accessibilityDisclosureDestination(
onDismiss = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
}
}
}
@@ -235,26 +230,6 @@ class MainActivity : AppCompatActivity() {
}
}
@Composable
private fun MainActivityDialogs(
dialogState: MainState.DialogState?,
onAccessibilityDisclaimerDismiss: () -> Unit,
) {
when (dialogState) {
MainState.DialogState.AccessibilityDisclosure -> {
BitwardenBasicDialog(
title = stringResource(id = BitwardenString.accessibility_service_disclosure),
message = stringResource(
id = BitwardenString.accessibility_disclosure_start_up_text,
),
onDismissRequest = onAccessibilityDisclaimerDismiss,
)
}
null -> Unit
}
}
@Composable
private fun SetupEventsEffect(navController: NavController) {
EventsEffect(viewModel = mainViewModel) { event ->
@@ -268,6 +243,10 @@ class MainActivity : AppCompatActivity() {
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
MainEvent.NavigateToCookieAcquisition -> navController.navigateToCookieAcquisition()
MainEvent.NavigateToAccessibilityDisclosure -> {
navController.navigateToAccessibilityDisclosure()
}
is MainEvent.UpdateAppLocale -> {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(event.localeName),

View File

@@ -100,7 +100,6 @@ class MainViewModel @Inject constructor(
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
hasResizeBeenRequested = false,
dialogState = null,
),
) {
private var specialCircumstance: SpecialCircumstance?
@@ -209,10 +208,6 @@ class MainViewModel @Inject constructor(
is MainAction.WebAuthnResult -> handleWebAuthnResult(action)
is MainAction.CookieAcquisitionResult -> handleCookieAcquisitionResult(action)
is MainAction.PremiumCheckoutResult -> handlePremiumCheckoutResult(action)
is MainAction.DismissAccessibilityDisclaimerDialog -> {
handleDismissAccessibilityDisclaimerDialog()
}
is MainAction.Internal -> handleInternalAction(action)
}
}
@@ -245,11 +240,8 @@ class MainViewModel @Inject constructor(
private fun handleHasShownAccessibilityDisclaimerUpdate(
action: MainAction.Internal.HasShownAccessibilityDisclaimerUpdate,
) {
mutableStateFlow.update {
it.copy(
dialogState = MainState.DialogState.AccessibilityDisclosure
.takeUnless { action.hasBeenShown },
)
if (!action.hasBeenShown) {
sendEvent(MainEvent.NavigateToAccessibilityDisclosure)
}
}
@@ -283,10 +275,6 @@ class MainViewModel @Inject constructor(
)
}
private fun handleDismissAccessibilityDisclaimerDialog() {
settingsRepository.accessibilityDisclaimerHasBeenShown()
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
@@ -568,25 +556,12 @@ data class MainState(
val isScreenCaptureAllowed: Boolean,
val isDynamicColorsEnabled: Boolean,
val hasResizeBeenRequested: Boolean,
val dialogState: DialogState?,
) : Parcelable {
/**
* Contains all feature flags that are available to the UI.
*/
val featureFlagsState: FeatureFlagsState
get() = FeatureFlagsState
/**
* Representation of all dialogs displayed from the [MainActivity].
*/
@Parcelize
sealed class DialogState : Parcelable {
/**
* Displays an accessibility disclosure to users explaining how we utilize the
* AccessibilityService.
*/
data object AccessibilityDisclosure : DialogState()
}
}
/**
@@ -648,11 +623,6 @@ sealed class MainAction {
*/
data class AppSpecificLanguageUpdate(val appLanguage: AppLanguage) : MainAction()
/**
* Received if the user dismisses the accessibility disclaimer dialog.
*/
data object DismissAccessibilityDisclaimerDialog : MainAction()
/**
* Actions for internal use by the ViewModel.
*/
@@ -747,6 +717,11 @@ sealed class MainEvent {
*/
data object NavigateToCookieAcquisition : MainEvent()
/**
* Navigate to the accessibility disclosure screen.
*/
data object NavigateToAccessibilityDisclosure : MainEvent()
/**
* Indicates that the app language has been updated.
*/

View File

@@ -0,0 +1,40 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
/**
* The type-safe route for the accessibility disclosure screen.
*/
@OmitFromCoverage
@Serializable
data object AccessibilityDisclosureRoute
/**
* Add the accessibility disclosure screen to the nav graph.
*/
fun NavGraphBuilder.accessibilityDisclosureDestination(
onDismiss: () -> Unit,
onSplashScreenRemoved: () -> Unit,
) {
composableWithSlideTransitions<AccessibilityDisclosureRoute> {
AccessibilityDisclosureScreen(onDismiss = onDismiss)
// If we are displaying the accessibility disclosure screen, then we can just hide
// the splash screen.
onSplashScreenRemoved()
}
}
/**
* Navigate to the accessibility disclosure screen.
*/
fun NavController.navigateToAccessibilityDisclosure() {
this.navigate(route = AccessibilityDisclosureRoute) {
launchSingleTop = true
}
}

View File

@@ -0,0 +1,158 @@
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.composition.LocalExitManager
import com.bitwarden.ui.platform.manager.exit.ExitManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Top-level composable for the Accessibility Disclosure screen.
*/
@Composable
fun AccessibilityDisclosureScreen(
onDismiss: () -> Unit,
viewModel: AccessibilityDisclosureViewModel = hiltViewModel(),
exitManager: ExitManager = LocalExitManager.current,
) {
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is AccessibilityDisclosureEvent.Dismiss -> onDismiss()
is AccessibilityDisclosureEvent.CloseApp -> exitManager.exitApplication()
}
}
BackHandler { viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick) }
BitwardenScaffold(
contentWindowInsets = ScaffoldDefaults
.contentWindowInsets
.union(WindowInsets.displayCutout)
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top),
) {
AccessibilityDisclosureContent(
onAcceptClick = {
viewModel.trySendAction(AccessibilityDisclosureAction.AcceptClicked)
},
onCloseAppClick = {
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
},
modifier = Modifier.fillMaxSize(),
)
}
}
@Composable
private fun AccessibilityDisclosureContent(
onAcceptClick: () -> Unit,
onCloseAppClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.verticalScroll(state = rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(height = 32.dp))
Image(
painter = rememberVectorPainter(id = BitwardenDrawable.ill_autofill),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.standardHorizontalMargin()
.size(size = 100.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(height = 24.dp))
Text(
text = stringResource(id = BitwardenString.accessibility_service_disclosure),
style = BitwardenTheme.typography.headlineSmall,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = stringResource(id = BitwardenString.accessibility_disclosure_start_up_text),
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenFilledButton(
label = stringResource(id = BitwardenString.accept),
onClick = onAcceptClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
BitwardenOutlinedButton(
label = stringResource(id = BitwardenString.close_app),
onClick = onCloseAppClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@Preview(showBackground = true)
@Composable
private fun AccessibilityDisclosureContent_preview() {
BitwardenTheme {
AccessibilityDisclosureContent(
onAcceptClick = {},
onCloseAppClick = {},
modifier = Modifier.fillMaxSize(),
)
}
}

View File

@@ -0,0 +1,74 @@
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
import android.os.Parcelable
import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/**
* ViewModel for the Accessibility Disclosure screen.
*/
@HiltViewModel
class AccessibilityDisclosureViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
) : BaseViewModel<
AccessibilityDisclosureState,
AccessibilityDisclosureEvent,
AccessibilityDisclosureAction,
>(
initialState = AccessibilityDisclosureState,
) {
override fun handleAction(action: AccessibilityDisclosureAction) {
when (action) {
AccessibilityDisclosureAction.AcceptClicked -> handleAcceptClicked()
AccessibilityDisclosureAction.CloseAppClick -> handleCloseAppClick()
}
}
private fun handleAcceptClicked() {
settingsRepository.accessibilityDisclaimerHasBeenShown()
sendEvent(AccessibilityDisclosureEvent.Dismiss)
}
private fun handleCloseAppClick() {
sendEvent(AccessibilityDisclosureEvent.CloseApp)
}
}
/**
* State for the Accessibility Disclosure screen.
*/
@Parcelize
data object AccessibilityDisclosureState : Parcelable
/**
* Events for the Accessibility Disclosure screen.
*/
sealed class AccessibilityDisclosureEvent {
/**
* Navigate back, dismissing the screen.
*/
data object Dismiss : AccessibilityDisclosureEvent()
/**
* Closes the app.
*/
data object CloseApp : AccessibilityDisclosureEvent()
}
/**
* Actions for the Accessibility Disclosure screen.
*/
sealed class AccessibilityDisclosureAction {
/**
* User clicked the accept button.
*/
data object AcceptClicked : AccessibilityDisclosureAction()
/**
* User clicked the close app button.
*/
data object CloseAppClick : AccessibilityDisclosureAction()
}

View File

@@ -4,8 +4,6 @@ import android.content.Intent
import android.net.Uri
import androidx.browser.auth.AuthTabIntent
import androidx.credentials.GetPublicKeyCredentialOption
import com.x8bit.bitwarden.data.billing.util.PremiumCheckoutCallbackResult
import com.x8bit.bitwarden.data.billing.util.getPremiumCheckoutCallbackResult
import androidx.credentials.provider.BiometricPromptResult
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
@@ -48,6 +46,8 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.billing.util.PremiumCheckoutCallbackResult
import com.x8bit.bitwarden.data.billing.util.getPremiumCheckoutCallbackResult
import com.x8bit.bitwarden.data.credentials.manager.CredentialProviderRequestManager
import com.x8bit.bitwarden.data.credentials.manager.model.CredentialProviderRequest
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
@@ -1338,7 +1338,6 @@ class MainViewModelTest : BaseViewModelTest() {
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
hasResizeBeenRequested = false,
dialogState = null,
)
viewModel.stateFlow.test {
assertEquals(
@@ -1355,48 +1354,22 @@ class MainViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `on HasShownAccessibilityDisclaimerUpdate with false should show accessibility dialog`() =
runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableHasShownAccessibilityDisclaimerFlow.value = false
assertEquals(
DEFAULT_STATE.copy(
dialogState = MainState.DialogState.AccessibilityDisclosure,
),
awaitItem(),
)
}
}
@Test
fun `on HasShownAccessibilityDisclaimerUpdate with true should clear accessibility dialog`() =
runTest {
mutableHasShownAccessibilityDisclaimerFlow.value = false
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
dialogState = MainState.DialogState.AccessibilityDisclosure,
),
awaitItem(),
)
mutableHasShownAccessibilityDisclaimerFlow.value = true
assertEquals(DEFAULT_STATE, awaitItem())
}
}
@Suppress("MaxLineLength")
@Test
fun `on DismissAccessibilityDisclaimerDialog should store that the accessibility disclaimer has been shown`() {
val viewModel = createViewModel()
viewModel.trySendAction(MainAction.DismissAccessibilityDisclaimerDialog)
verify(exactly = 1) {
settingsRepository.accessibilityDisclaimerHasBeenShown()
fun `on HasShownAccessibilityDisclaimerUpdate with false should show the accessibility disclosure`() =
runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
// We skip the first 2 events because they are the default appTheme and appLanguage
skipItems(2)
mutableHasShownAccessibilityDisclaimerFlow.value = false
assertEquals(MainEvent.NavigateToAccessibilityDisclosure, awaitItem())
mutableHasShownAccessibilityDisclaimerFlow.value = true
expectNoEvents()
}
}
}
private fun createViewModel(
initialSpecialCircumstance: SpecialCircumstance? = null,
@@ -1427,7 +1400,6 @@ private val DEFAULT_STATE: MainState = MainState(
isScreenCaptureAllowed = true,
isDynamicColorsEnabled = false,
hasResizeBeenRequested = false,
dialogState = null,
)
private val DEFAULT_FIRST_TIME_STATE = FirstTimeState(

View File

@@ -0,0 +1,85 @@
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class AccessibilityDisclosureScreenTest : BitwardenComposeTest() {
private var onDismissCalled = false
private val mutableEventFlow = bufferedMutableSharedFlow<AccessibilityDisclosureEvent>()
private val mutableStateFlow = MutableStateFlow(AccessibilityDisclosureState)
private val viewModel = mockk<AccessibilityDisclosureViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
private val exitManager = mockk<ExitManager> {
every { exitApplication() } just runs
}
@Before
fun setUp() {
setContent(exitManager = exitManager) {
AccessibilityDisclosureScreen(
onDismiss = { onDismissCalled = true },
viewModel = viewModel,
)
}
}
@Test
fun `accept button click should send AcceptClicked action`() {
composeTestRule
.onNodeWithText(text = "Accept")
.performScrollTo()
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(AccessibilityDisclosureAction.AcceptClicked)
}
}
@Test
fun `close app button click should send CloseAppClick action`() {
composeTestRule
.onNodeWithText(text = "Close app")
.performScrollTo()
.performClick()
verify(exactly = 1) {
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
}
}
@Test
fun `Dismiss event should call onDismiss`() {
mutableEventFlow.tryEmit(AccessibilityDisclosureEvent.Dismiss)
assertTrue(onDismissCalled)
}
@Test
fun `CloseApp event should exit the application`() {
mutableEventFlow.tryEmit(AccessibilityDisclosureEvent.CloseApp)
verify(exactly = 1) {
exitManager.exitApplication()
}
}
@Test
fun `system back should not dismiss the screen`() {
backDispatcher?.onBackPressed()
verify(exactly = 1) {
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
}
}
}

View File

@@ -0,0 +1,55 @@
package com.x8bit.bitwarden.ui.platform.feature.accessibilitydisclosure
import app.cash.turbine.test
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
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.Test
class AccessibilityDisclosureViewModelTest : BaseViewModelTest() {
private val settingsRepository: SettingsRepository = mockk {
every { accessibilityDisclaimerHasBeenShown() } just runs
}
@Test
fun `initial state should be correct`() {
val viewModel = createViewModel()
assertEquals(AccessibilityDisclosureState, viewModel.stateFlow.value)
}
@Test
fun `AcceptClicked should mark disclaimer as shown and emit Dismiss event`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AccessibilityDisclosureAction.AcceptClicked)
assertEquals(AccessibilityDisclosureEvent.Dismiss, awaitItem())
}
verify(exactly = 1) {
settingsRepository.accessibilityDisclaimerHasBeenShown()
}
}
@Test
fun `CloseAppClick should emit CloseApp event`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AccessibilityDisclosureAction.CloseAppClick)
assertEquals(AccessibilityDisclosureEvent.CloseApp, awaitItem())
}
verify(exactly = 0) {
settingsRepository.accessibilityDisclaimerHasBeenShown()
}
}
private fun createViewModel(): AccessibilityDisclosureViewModel =
AccessibilityDisclosureViewModel(
settingsRepository = settingsRepository,
)
}

View File

@@ -0,0 +1,76 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="201dp"
android:viewportWidth="200"
android:viewportHeight="201">
<path
android:name="secondary"
android:fillColor="#AAC3EF"
android:pathData="M0,42.67C0,33.46 7.46,26 16.67,26H183.33C192.54,26 200,33.46 200,42.67V159.33C200,168.54 192.54,176 183.33,176H16.67C7.46,176 0,168.54 0,159.33V42.67Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M183.33,30.17H16.67C9.76,30.17 4.17,35.76 4.17,42.67V159.33C4.17,166.24 9.76,171.83 16.67,171.83H183.33C190.24,171.83 195.83,166.24 195.83,159.33V42.67C195.83,35.76 190.24,30.17 183.33,30.17ZM16.67,26C7.46,26 0,33.46 0,42.67V159.33C0,168.54 7.46,176 16.67,176H183.33C192.54,176 200,168.54 200,159.33V42.67C200,33.46 192.54,26 183.33,26H16.67Z" />
<path
android:name="primary"
android:fillColor="#DBE5F6"
android:pathData="M18.75,57.25C18.75,54.95 20.62,53.08 22.92,53.08H177.08C179.38,53.08 181.25,54.95 181.25,57.25V82.25C181.25,84.55 179.38,86.42 177.08,86.42H22.92C20.62,86.42 18.75,84.55 18.75,82.25V57.25Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M177.08,57.25H22.92L22.92,82.25H177.08V57.25ZM22.92,53.08C20.62,53.08 18.75,54.95 18.75,57.25V82.25C18.75,84.55 20.62,86.42 22.92,86.42H177.08C179.38,86.42 181.25,84.55 181.25,82.25V57.25C181.25,54.95 179.38,53.08 177.08,53.08H22.92Z" />
<path
android:name="primary"
android:fillColor="#DBE5F6"
android:pathData="M18.75,119.75C18.75,117.45 20.62,115.58 22.92,115.58H177.08C179.38,115.58 181.25,117.45 181.25,119.75V144.75C181.25,147.05 179.38,148.92 177.08,148.92H22.92C20.62,148.92 18.75,147.05 18.75,144.75V119.75Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M177.08,119.75H22.92L22.92,144.75H177.08V119.75ZM22.92,115.58C20.62,115.58 18.75,117.45 18.75,119.75V144.75C18.75,147.05 20.62,148.92 22.92,148.92H177.08C179.38,148.92 181.25,147.05 181.25,144.75V119.75C181.25,117.45 179.38,115.58 177.08,115.58H22.92Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M35.43,125.11C36.58,125.11 37.51,126.04 37.51,127.19V130.11L40.24,129.21C41.34,128.86 42.51,129.45 42.87,130.55C43.23,131.64 42.63,132.82 41.54,133.17L38.77,134.08L40.5,136.51C41.17,137.45 40.96,138.75 40.02,139.42C39.08,140.09 37.78,139.87 37.11,138.93L35.43,136.57L33.74,138.93C33.07,139.87 31.77,140.09 30.83,139.42C29.9,138.75 29.68,137.45 30.35,136.51L32.08,134.08L29.31,133.17C28.22,132.82 27.62,131.64 27.98,130.55C28.34,129.45 29.51,128.86 30.61,129.21L33.34,130.11V127.19C33.34,126.04 34.28,125.11 35.43,125.11ZM54.18,125.11C55.33,125.11 56.26,126.04 56.26,127.19V130.11L58.99,129.21C60.09,128.86 61.26,129.45 61.62,130.55C61.98,131.64 61.38,132.82 60.29,133.17L57.52,134.08L59.25,136.51C59.92,137.45 59.71,138.75 58.77,139.42C57.83,140.09 56.53,139.87 55.86,138.93L54.18,136.57L52.49,138.93C51.82,139.87 50.52,140.09 49.58,139.42C48.65,138.75 48.43,137.45 49.1,136.51L50.83,134.08L48.06,133.17C46.97,132.82 46.37,131.64 46.73,130.55C47.09,129.45 48.26,128.86 49.36,129.21L52.09,130.11V127.19C52.09,126.04 53.03,125.11 54.18,125.11ZM72.93,125.11C74.08,125.11 75.01,126.04 75.01,127.19V130.11L77.74,129.21C78.84,128.86 80.01,129.45 80.37,130.55C80.73,131.64 80.13,132.82 79.04,133.17L76.27,134.08L78,136.51C78.67,137.45 78.46,138.75 77.52,139.42C76.58,140.09 75.28,139.87 74.61,138.93L72.93,136.57L71.24,138.93C70.57,139.87 69.27,140.09 68.33,139.42C67.4,138.75 67.18,137.45 67.85,136.51L69.58,134.08L66.81,133.17C65.72,132.82 65.12,131.64 65.48,130.55C65.84,129.45 67.01,128.86 68.11,129.21L70.84,130.11V127.19C70.84,126.04 71.78,125.11 72.93,125.11ZM91.68,125.11C92.83,125.11 93.76,126.04 93.76,127.19V130.11L96.49,129.21C97.59,128.86 98.76,129.45 99.12,130.55C99.48,131.64 98.88,132.82 97.79,133.17L95.02,134.08L96.75,136.51C97.42,137.45 97.21,138.75 96.27,139.42C95.33,140.09 94.03,139.87 93.36,138.93L91.68,136.57L89.99,138.93C89.32,139.87 88.02,140.09 87.08,139.42C86.15,138.75 85.93,137.45 86.6,136.51L88.33,134.08L85.56,133.17C84.47,132.82 83.87,131.64 84.23,130.55C84.59,129.45 85.76,128.86 86.86,129.21L89.59,130.11V127.19C89.59,126.04 90.53,125.11 91.68,125.11ZM110.43,125.11C111.58,125.11 112.51,126.04 112.51,127.19V130.11L115.24,129.21C116.34,128.86 117.51,129.45 117.87,130.55C118.23,131.64 117.63,132.82 116.54,133.17L113.77,134.08L115.5,136.51C116.17,137.45 115.96,138.75 115.02,139.42C114.08,140.09 112.78,139.87 112.11,138.93L110.43,136.57L108.74,138.93C108.07,139.87 106.77,140.09 105.83,139.42C104.9,138.75 104.68,137.45 105.35,136.51L107.08,134.08L104.31,133.17C103.22,132.82 102.62,131.64 102.98,130.55C103.34,129.45 104.51,128.86 105.61,129.21L108.34,130.11V127.19C108.34,126.04 109.28,125.11 110.43,125.11Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M29.17,69.75C29.17,68.6 30.1,67.67 31.25,67.67L114.58,67.67C115.73,67.67 116.67,68.6 116.67,69.75C116.67,70.9 115.73,71.83 114.58,71.83L31.25,71.83C30.1,71.83 29.17,70.9 29.17,69.75Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M170.22,62.03C171.04,62.84 171.04,64.16 170.22,64.97L157.72,77.47C156.91,78.29 155.59,78.29 154.78,77.47L148.53,71.22C147.71,70.41 147.71,69.09 148.53,68.28C149.34,67.46 150.66,67.46 151.47,68.28L156.25,73.05L167.28,62.03C168.09,61.21 169.41,61.21 170.22,62.03Z" />
<path
android:name="accent"
android:fillColor="#FFBF00"
android:pathData="M191.67,132.25C191.67,146.06 180.47,157.25 166.67,157.25C152.86,157.25 141.67,146.06 141.67,132.25C141.67,118.44 152.86,107.25 166.67,107.25C180.47,107.25 191.67,118.44 191.67,132.25Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M166.67,153.08C178.17,153.08 187.5,143.76 187.5,132.25C187.5,120.74 178.17,111.42 166.67,111.42C155.16,111.42 145.83,120.74 145.83,132.25C145.83,143.76 155.16,153.08 166.67,153.08ZM166.67,157.25C180.47,157.25 191.67,146.06 191.67,132.25C191.67,118.44 180.47,107.25 166.67,107.25C152.86,107.25 141.67,118.44 141.67,132.25C141.67,146.06 152.86,157.25 166.67,157.25Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M178.56,124.53C179.37,125.34 179.37,126.66 178.56,127.47L163.97,142.06C163.16,142.87 161.84,142.87 161.03,142.06L154.78,135.81C153.96,134.99 153.96,133.67 154.78,132.86C155.59,132.05 156.91,132.05 157.72,132.86L162.5,137.64L175.61,124.53C176.42,123.71 177.74,123.71 178.56,124.53Z" />
<path
android:name="accent"
android:fillColor="#FFBF00"
android:pathData="M191.67,69.75C191.67,83.56 180.47,94.75 166.67,94.75C152.86,94.75 141.67,83.56 141.67,69.75C141.67,55.94 152.86,44.75 166.67,44.75C180.47,44.75 191.67,55.94 191.67,69.75Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M166.67,90.58C178.17,90.58 187.5,81.26 187.5,69.75C187.5,58.24 178.17,48.92 166.67,48.92C155.16,48.92 145.83,58.24 145.83,69.75C145.83,81.26 155.16,90.58 166.67,90.58ZM166.67,94.75C180.47,94.75 191.67,83.56 191.67,69.75C191.67,55.94 180.47,44.75 166.67,44.75C152.86,44.75 141.67,55.94 141.67,69.75C141.67,83.56 152.86,94.75 166.67,94.75Z" />
<path
android:name="outline"
android:fillColor="#020F66"
android:fillType="evenOdd"
android:pathData="M178.56,62.03C179.37,62.84 179.37,64.16 178.56,64.97L163.97,79.56C163.16,80.37 161.84,80.37 161.03,79.56L154.78,73.31C153.96,72.49 153.96,71.17 154.78,70.36C155.59,69.55 156.91,69.55 157.72,70.36L162.5,75.14L175.61,62.03C176.42,61.21 177.74,61.21 178.56,62.03Z" />
</vector>

View File

@@ -78,6 +78,7 @@
<string name="bitwarden_autofill_service">Bitwarden Autofill Service</string>
<string name="change_master_password">Change master password</string>
<string name="close">Close</string>
<string name="close_app">Close app</string>
<string name="continue_text">Continue</string>
<string name="create_account">Create account</string>
<string name="create_an_account">Create an account</string>
@@ -615,7 +616,7 @@ select Add TOTP to store the key safely</string>
<string name="forwarded_email_description">Generate an email alias with an external forwarding service.</string>
<string name="accessibility_service_disclosure">Accessibility Service Disclosure</string>
<string name="accessibility_disclosure_text">Bitwarden uses the Accessibility Service to search for login fields in apps and websites, then establish the appropriate field IDs for entering a username &amp; password when a match for the app or site is found. We do not store any of the information presented to us by the service, nor do we make any attempt to control any on-screen elements beyond text entry of credentials.</string>
<string name="accessibility_disclosure_start_up_text">Bitwarden offers an optional autofill method that uses Androids Accessibility Service to detect login fields in apps and websites. If you choose to enable it, Bitwarden will identify the appropriate fields and enter your credentials when a match is found. We do not store any information observed by the service, nor do we control any on-screen elements beyond credential entry.</string>
<string name="accessibility_disclosure_start_up_text">Bitwarden offers an optional autofill method that uses Androids Accessibility Service to detect login fields in apps and websites. If you choose to enable it, Bitwarden will identify the appropriate fields and enter your credentials when a match is found. We do not store any information observed by the service, and we do not control any on-screen elements beyond credential entry.</string>
<string name="accept">Accept</string>
<string name="decline">Decline</string>
<string name="login_request_has_already_expired">Login request has already expired.</string>