From 6521848a8dd1c94989d53e503dc88426c0fa9f01 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 11 Sep 2024 11:53:30 -0500 Subject: [PATCH] PM-11485: Add routing for accessibility autofill (#3895) --- .../java/com/x8bit/bitwarden/MainActivity.kt | 17 ++ .../java/com/x8bit/bitwarden/MainViewModel.kt | 36 ++- .../accessibility/di/AccessibilityModule.kt | 32 +++ .../manager/AccessibilityAutofillManager.kt | 4 +- .../AccessibilityAutofillManagerImpl.kt | 4 +- .../manager/AccessibilityCompletionManager.kt | 16 ++ .../AccessibilityCompletionManagerImpl.kt | 53 +++++ .../manager/AccessibilitySelectionManager.kt | 19 ++ .../AccessibilitySelectionManagerImpl.kt | 22 ++ .../model/AccessibilityAction.kt | 24 ++ .../accessibility/util/UriExtensions.kt | 16 ++ .../autofill/model/AutofillSelectionData.kt | 10 + .../data/autofill/util/AutofillIntentUtils.kt | 13 +- .../autofill/util/FilledDataExtensions.kt | 1 + .../tiles/BitwardenAutofillTileService.kt | 3 +- .../itemlisting/VaultItemListingViewModel.kt | 22 +- .../com/x8bit/bitwarden/MainViewModelTest.kt | 19 ++ .../AccessibilityAutofillManagerTest.kt | 25 +++ .../AccessibilityCompletionManagerTest.kt | 210 ++++++++++++++++++ .../AccessibilitySelectionManagerTest.kt | 32 +++ .../AccessibilityAutofillManagerTest.kt | 21 -- .../util/SpecialCircumstanceExtensionsTest.kt | 1 + .../feature/rootnav/RootNavViewModelTest.kt | 1 + .../feature/search/SearchViewModelTest.kt | 1 + .../addedit/VaultAddEditViewModelTest.kt | 1 + .../AutofillSelectionDataExtensionsTest.kt | 2 + .../itemlisting/VaultItemListingScreenTest.kt | 1 + .../VaultItemListingViewModelTest.kt | 50 +++++ .../VaultItemListingDataExtensionsTest.kt | 3 + 29 files changed, 624 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/AccessibilityAction.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/UriExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerTest.kt delete mode 100644 app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AccessibilityAutofillManagerTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt b/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt index 8db178338..175487965 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainActivity.kt @@ -15,6 +15,7 @@ import androidx.core.os.LocaleListCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage @@ -43,6 +44,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var autofillCompletionManager: AutofillCompletionManager + @Inject + lateinit var accessibilityCompletionManager: AccessibilityCompletionManager + @Inject lateinit var settingsRepository: SettingsRepository @@ -74,6 +78,10 @@ class MainActivity : AppCompatActivity() { val navController = rememberNavController() EventsEffect(viewModel = mainViewModel) { event -> when (event) { + is MainEvent.CompleteAccessibilityAutofill -> { + handleCompleteAccessibilityAutofill(event) + } + is MainEvent.CompleteAutofill -> handleCompleteAutofill(event) MainEvent.Recreate -> handleRecreate() MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen() @@ -130,6 +138,15 @@ class MainActivity : AppCompatActivity() { mainViewModel.trySendAction(MainAction.OpenDebugMenu) } + private fun handleCompleteAccessibilityAutofill( + event: MainEvent.CompleteAccessibilityAutofill, + ) { + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = this, + cipherView = event.cipherView, + ) + } + private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) { autofillCompletionManager.completeAutofill( activity = this, diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index d76672d05..adb4d0303 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull @@ -53,13 +54,14 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance" @Suppress("LongParameterList", "TooManyFunctions") @HiltViewModel class MainViewModel @Inject constructor( + accessibilitySelectionManager: AccessibilitySelectionManager, autofillSelectionManager: AutofillSelectionManager, private val specialCircumstanceManager: SpecialCircumstanceManager, private val garbageCollectionManager: GarbageCollectionManager, private val fido2CredentialManager: Fido2CredentialManager, private val intentManager: IntentManager, settingsRepository: SettingsRepository, - private val vaultRepository: VaultRepository, + vaultRepository: VaultRepository, private val authRepository: AuthRepository, private val environmentRepository: EnvironmentRepository, private val savedStateHandle: SavedStateHandle, @@ -85,6 +87,12 @@ class MainViewModel @Inject constructor( .onEach { specialCircumstance = it } .launchIn(viewModelScope) + accessibilitySelectionManager + .accessibilitySelectionFlow + .map { MainAction.Internal.AccessibilitySelectionReceive(it) } + .onEach(::sendAction) + .launchIn(viewModelScope) + autofillSelectionManager .autofillSelectionFlow .onEach { trySendAction(MainAction.Internal.AutofillSelectionReceive(it)) } @@ -151,6 +159,10 @@ class MainViewModel @Inject constructor( override fun handleAction(action: MainAction) { when (action) { + is MainAction.Internal.AccessibilitySelectionReceive -> { + handleAccessibilitySelectionReceive(action) + } + is MainAction.Internal.AutofillSelectionReceive -> { handleAutofillSelectionReceive(action) } @@ -169,6 +181,12 @@ class MainViewModel @Inject constructor( sendEvent(MainEvent.NavigateToDebugMenu) } + private fun handleAccessibilitySelectionReceive( + action: MainAction.Internal.AccessibilitySelectionReceive, + ) { + sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView)) + } + private fun handleAutofillSelectionReceive( action: MainAction.Internal.AutofillSelectionReceive, ) { @@ -332,11 +350,13 @@ class MainViewModel @Inject constructor( ), ) } + EmailTokenResult.Expired -> { specialCircumstanceManager.specialCircumstance = SpecialCircumstance .RegistrationEvent .ExpiredRegistrationLink } + EmailTokenResult.Success -> { if (authRepository.activeUserId != null) { authRepository.hasPendingAccountAddition = true @@ -384,6 +404,14 @@ sealed class MainAction { * Actions for internal use by the ViewModel. */ sealed class Internal : MainAction() { + /** + * Indicates the user has manually selected the given [cipherView] for accessibility + * autofill. + */ + data class AccessibilitySelectionReceive( + val cipherView: CipherView, + ) : Internal() + /** * Indicates the user has manually selected the given [cipherView] for autofill. */ @@ -421,6 +449,12 @@ sealed class MainAction { * Represents events that are emitted by the [MainViewModel]. */ sealed class MainEvent { + /** + * Event indicating that the user has chosen the given [cipherView] for accessibility autofill + * and that the process is ready to complete. + */ + data class CompleteAccessibilityAutofill(val cipherView: CipherView) : MainEvent() + /** * Event indicating that the user has chosen the given [cipherView] for autofill and that the * process is ready to complete. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt index 79fcf8132..0fa7744e4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt @@ -1,15 +1,23 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di +import android.content.Context import android.content.pm.PackageManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager 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.AccessibilityCompletionManagerImpl +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.LauncherPackageNameManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParserImpl +import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import java.time.Clock import javax.inject.Singleton @@ -20,6 +28,19 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object AccessibilityModule { + @Singleton + @Provides + fun providesAccessibilityCompletionManager( + accessibilityAutofillManager: AccessibilityAutofillManager, + totpManager: AutofillTotpManager, + dispatcherManager: DispatcherManager, + ): AccessibilityCompletionManager = + AccessibilityCompletionManagerImpl( + accessibilityAutofillManager = accessibilityAutofillManager, + totpManager = totpManager, + dispatcherManager = dispatcherManager, + ) + @Singleton @Provides fun providesAccessibilityInvokeManager(): AccessibilityAutofillManager = @@ -29,6 +50,11 @@ object AccessibilityModule { @Provides fun providesAccessibilityParser(): AccessibilityParser = AccessibilityParserImpl() + @Singleton + @Provides + fun providesAccessibilitySelectionManager(): AccessibilitySelectionManager = + AccessibilitySelectionManagerImpl() + @Singleton @Provides fun providesLauncherPackageNameManager( @@ -39,4 +65,10 @@ object AccessibilityModule { clockProvider = { clock }, packageManager = packageManager, ) + + @Singleton + @Provides + fun providesPackageManager( + @ApplicationContext context: Context, + ): PackageManager = context.packageManager } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManager.kt index dac46d71a..1e4189839 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManager.kt @@ -1,5 +1,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager +import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction + /** * A relay manager used to notify the accessibility service to attempt an autofill. */ @@ -8,5 +10,5 @@ interface AccessibilityAutofillManager { * Indicates that the Autofill tile has been clicked and we attempt an accessibility-based * autofill. */ - var isAccessibilityTileClicked: Boolean + var accessibilityAction: AccessibilityAction? } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerImpl.kt index 52ee8fee4..5714bcb8e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerImpl.kt @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager +import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction + /** * The default implementation for the [AccessibilityAutofillManager]. */ class AccessibilityAutofillManagerImpl : AccessibilityAutofillManager { - override var isAccessibilityTileClicked: Boolean = false + override var accessibilityAction: AccessibilityAction? = null } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManager.kt new file mode 100644 index 000000000..feb3f9927 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManager.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import android.app.Activity +import com.bitwarden.vault.CipherView + +/** + * A manager for completing the accessibility-based autofill process after the user has made a + * selection. + */ +interface AccessibilityCompletionManager { + /** + * Completes the accessibility-based autofill flow originating with the given [activity] using + * the selected [cipherView]. + */ + fun completeAccessibilityAutofill(activity: Activity, cipherView: CipherView) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerImpl.kt new file mode 100644 index 000000000..3fa8dff8d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerImpl.kt @@ -0,0 +1,53 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import android.app.Activity +import com.bitwarden.vault.CipherView +import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction +import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull +import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull +import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Default implementation for the [AccessibilityCompletionManager]. + */ +class AccessibilityCompletionManagerImpl( + private val accessibilityAutofillManager: AccessibilityAutofillManager, + private val totpManager: AutofillTotpManager, + dispatcherManager: DispatcherManager, +) : AccessibilityCompletionManager { + private val mainScope = CoroutineScope(dispatcherManager.main) + + override fun completeAccessibilityAutofill(activity: Activity, cipherView: CipherView) { + val autofillSelectionData = activity + .intent + ?.getAutofillSelectionDataOrNull() + ?: run { + activity.finish() + return + } + if (autofillSelectionData.framework != AutofillSelectionData.Framework.ACCESSIBILITY) { + activity.finish() + return + } + val uri = autofillSelectionData + .uri + ?.toUriOrNull() + ?: run { + activity.finish() + return + } + + accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptFill( + cipherView = cipherView, + uri = uri, + ) + mainScope.launch { + totpManager.tryCopyTotpToClipboard(cipherView = cipherView) + activity.finish() + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManager.kt new file mode 100644 index 000000000..c884aa5a8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManager.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import com.bitwarden.vault.CipherView +import kotlinx.coroutines.flow.Flow + +/** + * A manager class used to handle the accessibility autofill selections. + */ +interface AccessibilitySelectionManager { + /** + * Emits a [CipherView] as a result of calls to [emitAccessibilitySelection]. + */ + val accessibilitySelectionFlow: Flow + + /** + * Triggers an emission via [accessibilitySelectionFlow]. + */ + fun emitAccessibilitySelection(cipherView: CipherView) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerImpl.kt new file mode 100644 index 000000000..90bab8b14 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerImpl.kt @@ -0,0 +1,22 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import com.bitwarden.vault.CipherView +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow + +/** + * The default implementation of the [AccessibilitySelectionManager]. + */ +class AccessibilitySelectionManagerImpl : AccessibilitySelectionManager { + private val accessibilitySelectionChannel: Channel = Channel( + capacity = Int.MAX_VALUE, + ) + + override val accessibilitySelectionFlow: Flow = + accessibilitySelectionChannel.receiveAsFlow() + + override fun emitAccessibilitySelection(cipherView: CipherView) { + accessibilitySelectionChannel.trySend(cipherView) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/AccessibilityAction.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/AccessibilityAction.kt new file mode 100644 index 000000000..e7d1c7336 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/model/AccessibilityAction.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.model + +import android.net.Uri +import com.bitwarden.vault.CipherView + +/** + *Represents an action to be taken by the accessibility service. + */ +sealed class AccessibilityAction { + /** + * Indicates that the accessibility service should attempt to scan the currently foregrounded + * application for a [Uri]. + */ + data object AttemptParseUri : AccessibilityAction() + + /** + * Indicates that the accessibility service should attempt to scan the currently foregrounded + * application for a fields to fill. + */ + data class AttemptFill( + val cipherView: CipherView, + val uri: Uri, + ) : AccessibilityAction() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/UriExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/UriExtensions.kt new file mode 100644 index 000000000..0b8aa25c8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/UriExtensions.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.util + +import android.net.Uri +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import java.net.URISyntaxException + +/** + * Attempts to parse a [Uri] from a string and returns null if an error occurs. + */ +@OmitFromCoverage +fun String.toUriOrNull(): Uri? = + try { + Uri.parse(this) + } catch (e: URISyntaxException) { + null + } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt index ac31788d1..0327ebf24 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt @@ -6,11 +6,13 @@ import kotlinx.parcelize.Parcelize /** * Represents data for a manual autofill selection. * + * @property framework The framework being used to fulfill this autofill request. * @property type The type of autofill selection that must be made. * @property uri A URI representing the location where data should be filled (if available). */ @Parcelize data class AutofillSelectionData( + val framework: Framework, val type: Type, val uri: String?, ) : Parcelable { @@ -22,4 +24,12 @@ data class AutofillSelectionData( CARD, LOGIN, } + + /** + * The type of framework to use with this autofill. + */ + enum class Framework { + ACCESSIBILITY, + AUTOFILL, + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt index ce3140acb..795789f5f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt @@ -30,18 +30,21 @@ private const val AUTOFILL_BUNDLE_KEY = "autofill-bundle-key" */ fun createAutofillSelectionIntent( context: Context, + framework: AutofillSelectionData.Framework, type: AutofillSelectionData.Type, uri: String?, ): Intent = - Intent( - context, - MainActivity::class.java, - ) + Intent(context, MainActivity::class.java) .apply { + setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) putExtra( AUTOFILL_BUNDLE_KEY, bundleOf( - AUTOFILL_SELECTION_DATA_KEY to AutofillSelectionData(type = type, uri = uri), + AUTOFILL_SELECTION_DATA_KEY to AutofillSelectionData( + framework = framework, + type = type, + uri = uri, + ), ), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt index ab6b4d302..2eef7b9a2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt @@ -34,6 +34,7 @@ fun FilledData.buildVaultItemDataset( ): Dataset { val intent = createAutofillSelectionIntent( context = autofillAppInfo.context, + framework = AutofillSelectionData.Framework.AUTOFILL, type = when (this.originalPartition) { is AutofillPartition.Card -> AutofillSelectionData.Type.CARD is AutofillPartition.Login -> AutofillSelectionData.Type.LOGIN diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt b/app/src/main/java/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt index 229392f69..875a0a495 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/tiles/BitwardenAutofillTileService.kt @@ -8,6 +8,7 @@ import android.service.quicksettings.TileService import androidx.annotation.Keep import com.x8bit.bitwarden.AccessibilityActivity import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager +import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow import dagger.hilt.android.AndroidEntryPoint @@ -33,7 +34,7 @@ class BitwardenAutofillTileService : TileService() { @SuppressLint("StartActivityAndCollapseDeprecated") private fun launchAutofill() { - accessibilityAutofillManager.isAccessibilityTileClicked = true + accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptParseUri val intent = Intent(applicationContext, AccessibilityActivity::class.java) if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) { @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 6ddab13af..5ba396c72 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -10,6 +10,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult @@ -29,6 +30,7 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.platform.manager.util.toFido2AssertionRequestOrNull import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository @@ -92,6 +94,7 @@ class VaultItemListingViewModel @Inject constructor( private val vaultRepository: VaultRepository, private val environmentRepository: EnvironmentRepository, private val settingsRepository: SettingsRepository, + private val accessibilitySelectionManager: AccessibilitySelectionManager, private val autofillSelectionManager: AutofillSelectionManager, private val cipherMatchingManager: CipherMatchingManager, private val specialCircumstanceManager: SpecialCircumstanceManager, @@ -104,7 +107,6 @@ class VaultItemListingViewModel @Inject constructor( val activeAccountSummary = userState.toActiveAccountSummary() val accountSummaries = userState.toAccountSummaries() val specialCircumstance = specialCircumstanceManager.specialCircumstance - val autofillSelectionData = specialCircumstance as? SpecialCircumstance.AutofillSelection val fido2CreationData = specialCircumstance as? SpecialCircumstance.Fido2Save val fido2AssertionData = specialCircumstance as? SpecialCircumstance.Fido2Assertion val fido2GetCredentialsData = @@ -127,7 +129,7 @@ class VaultItemListingViewModel @Inject constructor( policyDisablesSend = policyManager .getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) .any(), - autofillSelectionData = autofillSelectionData?.autofillSelectionData, + autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull(), hasMasterPassword = userState.activeAccount.hasMasterPassword, fido2CredentialRequest = fido2CreationData?.fido2CredentialRequest, fido2CredentialAssertionRequest = fido2AssertionData?.fido2AssertionRequest, @@ -547,9 +549,19 @@ class VaultItemListingViewModel @Inject constructor( } private fun handleItemClick(action: VaultItemListingsAction.ItemClick) { - if (state.isAutofill) { - val cipherView = getCipherViewOrNull(action.id) ?: return - autofillSelectionManager.emitAutofillSelection(cipherView = cipherView) + state.autofillSelectionData?.let { autofillSelectionData -> + val cipherView = getCipherViewOrNull(cipherId = action.id) ?: return + when (autofillSelectionData.framework) { + AutofillSelectionData.Framework.ACCESSIBILITY -> { + accessibilitySelectionManager.emitAccessibilitySelection( + cipherView = cipherView, + ) + } + + AutofillSelectionData.Framework.AUTOFILL -> { + autofillSelectionManager.emitAutofillSelection(cipherView = cipherView) + } + } return } diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index 3f14b2d67..6cb201409 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -11,6 +11,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest @@ -70,6 +72,8 @@ import java.time.ZoneOffset class MainViewModelTest : BaseViewModelTest() { private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl() + private val accessibilitySelectionManager: AccessibilitySelectionManager = + AccessibilitySelectionManagerImpl() private val mutableUserStateFlow = MutableStateFlow(null) private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT) private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true) @@ -221,6 +225,20 @@ class MainViewModelTest : BaseViewModelTest() { } } + @Test + fun `accessibility selection updates should emit CompleteAccessibilityAutofill events`() = + runTest { + val viewModel = createViewModel() + val cipherView = mockk() + viewModel.eventFlow.test { + accessibilitySelectionManager.emitAccessibilitySelection(cipherView = cipherView) + assertEquals( + MainEvent.CompleteAccessibilityAutofill(cipherView = cipherView), + awaitItem(), + ) + } + } + @Test fun `autofill selection updates should emit CompleteAutofill events`() = runTest { val viewModel = createViewModel() @@ -964,6 +982,7 @@ class MainViewModelTest : BaseViewModelTest() { private fun createViewModel( initialSpecialCircumstance: SpecialCircumstance? = null, ) = MainViewModel( + accessibilitySelectionManager = accessibilitySelectionManager, autofillSelectionManager = autofillSelectionManager, specialCircumstanceManager = specialCircumstanceManager, garbageCollectionManager = garbageCollectionManager, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerTest.kt new file mode 100644 index 000000000..cccfc4a2d --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityAutofillManagerTest.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class AccessibilityAutofillManagerTest { + private val accessibilityAutofillManager: AccessibilityAutofillManager = + AccessibilityAutofillManagerImpl() + + @Test + fun `isAccessibilityTileClicked should simply hold the state it is provided`() { + val attemptParseUri = AccessibilityAction.AttemptParseUri + val attemptFill = AccessibilityAction.AttemptFill(cipherView = mockk(), uri = mockk()) + assertNull(accessibilityAutofillManager.accessibilityAction) + accessibilityAutofillManager.accessibilityAction = attemptParseUri + assertEquals(attemptParseUri, accessibilityAutofillManager.accessibilityAction) + accessibilityAutofillManager.accessibilityAction = attemptFill + assertEquals(attemptFill, accessibilityAutofillManager.accessibilityAction) + accessibilityAutofillManager.accessibilityAction = null + assertNull(accessibilityAutofillManager.accessibilityAction) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerTest.kt new file mode 100644 index 000000000..46669b560 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilityCompletionManagerTest.kt @@ -0,0 +1,210 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import com.bitwarden.vault.CipherView +import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction +import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull +import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull +import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.unmockkStatic +import io.mockk.verify +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class AccessibilityCompletionManagerTest { + + private val activity: Activity = mockk { + every { finish() } just runs + } + private val accessibilityAutofillManager: AccessibilityAutofillManager = mockk() + private val totpManager: AutofillTotpManager = mockk() + private val fakeDispatcherManager: FakeDispatcherManager = FakeDispatcherManager() + + private val accessibilityCompletionManager: AccessibilityCompletionManager = + AccessibilityCompletionManagerImpl( + accessibilityAutofillManager = accessibilityAutofillManager, + totpManager = totpManager, + dispatcherManager = fakeDispatcherManager, + ) + + @BeforeEach + fun setup() { + fakeDispatcherManager.setMain(fakeDispatcherManager.unconfined) + mockkStatic( + Intent::getAutofillSelectionDataOrNull, + String::toUriOrNull, + ) + } + + @AfterEach + fun tearDown() { + fakeDispatcherManager.resetMain() + unmockkStatic( + Intent::getAutofillSelectionDataOrNull, + String::toUriOrNull, + ) + } + + @Test + fun `completeAccessibilityAutofill when there is no Intent should finish the Activity`() { + every { activity.intent } returns null + + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = activity, + cipherView = mockk(), + ) + + verify(exactly = 1) { + activity.intent + activity.finish() + } + } + + @Suppress("MaxLineLength") + @Test + fun `completeAccessibilityAutofill when there is no AutofillSelectionData should finish the Activity`() { + val mockIntent: Intent = mockk() + every { activity.intent } returns mockIntent + every { mockIntent.getAutofillSelectionDataOrNull() } returns null + + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = activity, + cipherView = mockk(), + ) + + verify(exactly = 1) { + activity.intent + mockIntent.getAutofillSelectionDataOrNull() + activity.finish() + } + } + + @Suppress("MaxLineLength") + @Test + fun `completeAccessibilityAutofill when the AutofillSelectionData is for the incorrect framework should finish the Activity`() { + val mockIntent: Intent = mockk() + every { activity.intent } returns mockIntent + val selectionData = AutofillSelectionData( + framework = AutofillSelectionData.Framework.AUTOFILL, + type = AutofillSelectionData.Type.LOGIN, + uri = "", + ) + every { mockIntent.getAutofillSelectionDataOrNull() } returns selectionData + + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = activity, + cipherView = mockk(), + ) + + verify(exactly = 1) { + activity.intent + mockIntent.getAutofillSelectionDataOrNull() + activity.finish() + } + } + + @Suppress("MaxLineLength") + @Test + fun `completeAccessibilityAutofill when the AutofillSelectionData is missing uri should finish the Activity`() { + val mockIntent: Intent = mockk() + every { activity.intent } returns mockIntent + val selectionData = AutofillSelectionData( + framework = AutofillSelectionData.Framework.ACCESSIBILITY, + type = AutofillSelectionData.Type.LOGIN, + uri = null, + ) + every { mockIntent.getAutofillSelectionDataOrNull() } returns selectionData + + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = activity, + cipherView = mockk(), + ) + + verify(exactly = 1) { + activity.intent + mockIntent.getAutofillSelectionDataOrNull() + activity.finish() + } + } + + @Suppress("MaxLineLength") + @Test + fun `completeAccessibilityAutofill when the AutofillSelectionData contains an invalid uri should finish the Activity`() { + val mockIntent: Intent = mockk() + every { activity.intent } returns mockIntent + val stringUri = "invalid uri" + every { stringUri.toUriOrNull() } returns null + + val selectionData = AutofillSelectionData( + framework = AutofillSelectionData.Framework.ACCESSIBILITY, + type = AutofillSelectionData.Type.LOGIN, + uri = stringUri, + ) + every { mockIntent.getAutofillSelectionDataOrNull() } returns selectionData + + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = activity, + cipherView = mockk(), + ) + + verify(exactly = 1) { + activity.intent + mockIntent.getAutofillSelectionDataOrNull() + activity.finish() + } + } + + @Suppress("MaxLineLength") + @Test + fun `completeAccessibilityAutofill when the AutofillSelectionData is correct should set the accessibility action, copy the totp and finish the activity`() { + val cipherView: CipherView = mockk() + val mockIntent: Intent = mockk() + every { activity.intent } returns mockIntent + val stringUri = "androidapp://com.x8bit.bitwarden" + val uri: Uri = mockk() + every { stringUri.toUriOrNull() } returns uri + val selectionData = AutofillSelectionData( + framework = AutofillSelectionData.Framework.ACCESSIBILITY, + type = AutofillSelectionData.Type.LOGIN, + uri = stringUri, + ) + every { mockIntent.getAutofillSelectionDataOrNull() } returns selectionData + every { + accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptFill( + cipherView = cipherView, + uri = uri, + ) + } just runs + coEvery { totpManager.tryCopyTotpToClipboard(cipherView = cipherView) } just runs + + accessibilityCompletionManager.completeAccessibilityAutofill( + activity = activity, + cipherView = cipherView, + ) + + verify(exactly = 1) { + activity.intent + mockIntent.getAutofillSelectionDataOrNull() + accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptFill( + cipherView = cipherView, + uri = uri, + ) + activity.finish() + } + coVerify(exactly = 1) { + totpManager.tryCopyTotpToClipboard(cipherView = cipherView) + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerTest.kt new file mode 100644 index 000000000..a78c3bf5e --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/manager/AccessibilitySelectionManagerTest.kt @@ -0,0 +1,32 @@ +package com.x8bit.bitwarden.data.autofill.accessibility.manager + +import app.cash.turbine.test +import com.bitwarden.vault.CipherView +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class AccessibilitySelectionManagerTest { + + private val accessibilitySelectionManager: AccessibilitySelectionManager = + AccessibilitySelectionManagerImpl() + + @Test + fun `autofillSelectionFlow should emit whenever emitAutofillSelection is called`() = + runTest { + accessibilitySelectionManager.accessibilitySelectionFlow.test { + expectNoEvents() + + val cipherView1 = mockk() + accessibilitySelectionManager.emitAccessibilitySelection(cipherView = cipherView1) + + assertEquals(cipherView1, awaitItem()) + + val cipherView2 = mockk() + accessibilitySelectionManager.emitAccessibilitySelection(cipherView = cipherView2) + + assertEquals(cipherView2, awaitItem()) + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AccessibilityAutofillManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AccessibilityAutofillManagerTest.kt deleted file mode 100644 index 25cab4865..000000000 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/manager/AccessibilityAutofillManagerTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.x8bit.bitwarden.data.autofill.manager - -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager -import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class AccessibilityAutofillManagerTest { - private val accessibilityAutofillManager: AccessibilityAutofillManager = - AccessibilityAutofillManagerImpl() - - @Test - fun `isAccessibilityTileClicked should simply hold the state it is provided`() { - assertFalse(accessibilityAutofillManager.isAccessibilityTileClicked) - accessibilityAutofillManager.isAccessibilityTileClicked = true - assertTrue(accessibilityAutofillManager.isAccessibilityTileClicked) - accessibilityAutofillManager.isAccessibilityTileClicked = false - assertFalse(accessibilityAutofillManager.isAccessibilityTileClicked) - } -} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt index 71f8b1d48..22fa1e7ee 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt @@ -63,6 +63,7 @@ class SpecialCircumstanceExtensionsTest { fun `toAutofillSelectionDataOrNull should return a non-null value for AutofillSelection`() { val autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "uri", ) assertEquals( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index f7665ab78..c27ea5ac4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -489,6 +489,7 @@ class RootNavViewModelTest : BaseViewModelTest() { fun `when the active user has an unlocked vault but there is an AutofillSelection special circumstance the nav state should be VaultUnlockedForAutofillSelection`() { val autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "uri", ) specialCircumstanceManager.specialCircumstance = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index 9cec3c0f3..8591ea0e0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -1435,6 +1435,7 @@ private const val CIPHER_ID = "mockId-1" private val AUTOFILL_SELECTION_DATA = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = AUTOFILL_URI, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 05cf531ae..1f1472f00 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -263,6 +263,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { fun `initial add state should be correct when autofill selection`() = runTest { val autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "https://www.test.com", ) specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AutofillSelection( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt index 8377c8f04..4b0f6d9c1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt @@ -32,6 +32,7 @@ class AutofillSelectionDataExtensionsTest { ), AutofillSelectionData( type = AutofillSelectionData.Type.CARD, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = null, ) .toDefaultAddTypeContent(isIndividualVaultDisabled = false), @@ -60,6 +61,7 @@ class AutofillSelectionDataExtensionsTest { ), AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "https://www.test.com", ) .toDefaultAddTypeContent(isIndividualVaultDisabled = true), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index fb89f2ad2..c5f3f2ef5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -2041,6 +2041,7 @@ private val ACCOUNT_SUMMARIES = listOf( private val AUTOFILL_SELECTION_DATA = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "https:://www.test.com", ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 62ef30a7a..404314759 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -13,6 +13,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager +import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest @@ -93,6 +95,8 @@ import java.time.ZoneOffset @Suppress("LargeClass") class VaultItemListingViewModelTest : BaseViewModelTest() { + private val accessibilitySelectionManager: AccessibilitySelectionManager = + AccessibilitySelectionManagerImpl() private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl() private var mockFilteredCiphers: List? = null @@ -309,6 +313,49 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `ItemClick for vault item when accessibility autofill should post to the accessibilitySelectionManager`() = + runTest { + setupMockUri() + val cipherView = createMockCipherView( + number = 1, + fido2Credentials = createMockSdkFido2CredentialList(number = 1), + ) + coEvery { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView), + ) + } returns DecryptFido2CredentialAutofillViewResult.Error + specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AutofillSelection( + autofillSelectionData = AutofillSelectionData( + type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.ACCESSIBILITY, + uri = "https://www.test.com", + ), + shouldFinishWhenComplete = true, + ) + mutableVaultDataStateFlow.value = DataState.Loaded( + data = VaultData( + cipherViewList = listOf(cipherView), + folderViewList = emptyList(), + collectionViewList = emptyList(), + sendViewList = emptyList(), + ), + ) + val viewModel = createVaultItemListingViewModel() + + accessibilitySelectionManager.accessibilitySelectionFlow.test { + viewModel.trySendAction(VaultItemListingsAction.ItemClick(id = "mockId-1")) + assertEquals(cipherView, awaitItem()) + } + coVerify(exactly = 1) { + vaultRepository.getDecryptedFido2CredentialAutofillViews( + cipherViewList = listOf(cipherView), + ) + } + } + @Test fun `ItemClick for vault item when autofill should post to the AutofillSelectionManager`() = runTest { @@ -326,6 +373,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { SpecialCircumstance.AutofillSelection( autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "https://www.test.com", ), shouldFinishWhenComplete = true, @@ -1248,6 +1296,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { val autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "https://www.test.com", ) specialCircumstanceManager.specialCircumstance = @@ -3707,6 +3756,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { vaultRepository = vaultRepository, environmentRepository = environmentRepository, settingsRepository = settingsRepository, + accessibilitySelectionManager = accessibilitySelectionManager, autofillSelectionManager = autofillSelectionManager, cipherMatchingManager = cipherMatchingManager, specialCircumstanceManager = specialCircumstanceManager, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt index 52bab1b16..9f508ee23 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt @@ -488,6 +488,7 @@ class VaultItemListingDataExtensionsTest { isIconLoadingDisabled = false, autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = null, ), fido2CreationData = null, @@ -572,6 +573,7 @@ class VaultItemListingDataExtensionsTest { isIconLoadingDisabled = false, autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = null, ), fido2CreationData = null, @@ -694,6 +696,7 @@ class VaultItemListingDataExtensionsTest { isIconLoadingDisabled = false, autofillSelectionData = AutofillSelectionData( type = AutofillSelectionData.Type.LOGIN, + framework = AutofillSelectionData.Framework.AUTOFILL, uri = "https://www.test.com", ), fido2CreationData = null,