From f2a7998bb031dff341dbfe2521e4f22ac9e39fc1 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Sat, 27 Jan 2024 13:48:39 -0600 Subject: [PATCH] Navigate to the Vault Listing screen from autofill (#810) --- .../java/com/x8bit/bitwarden/MainViewModel.kt | 12 ++++ .../autofill/model/AutofillSelectionData.kt | 25 +++++++++ .../data/autofill/util/AutofillIntentUtils.kt | 46 +++++++++++++++ .../autofill/util/FilledDataExtensions.kt | 15 +++-- .../manager/model/SpecialCircumstance.kt | 10 ++++ .../platform/feature/rootnav/RootNavScreen.kt | 15 ++++- .../feature/rootnav/RootNavViewModel.kt | 18 +++++- .../util/AutofillSelectionDataExtensions.kt | 13 +++++ .../vaultunlocked/VaultUnlockedNavigation.kt | 12 ++++ .../itemlisting/VaultItemListingNavigation.kt | 56 ++++++++++++++++++- .../itemlisting/VaultItemListingViewModel.kt | 45 ++++++++++----- .../com/x8bit/bitwarden/MainViewModelTest.kt | 53 +++++++++++++++++- .../feature/rootnav/RootNavScreenTest.kt | 13 +++++ .../feature/rootnav/RootNavViewModelTest.kt | 41 ++++++++++++++ .../AutofillSelectionDataExtensionsTest.kt | 22 ++++++++ .../VaultItemListingViewModelTest.kt | 5 ++ 16 files changed, 374 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index 4c477ae82..05c7b055c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -91,8 +92,19 @@ class MainViewModel @Inject constructor( intent: Intent, isFirstIntent: Boolean, ) { + val autofillSelectionData = intent.getAutofillSelectionDataOrNull() val shareData = intentManager.getShareDataFromIntent(intent) when { + autofillSelectionData != null -> { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AutofillSelection( + autofillSelectionData = autofillSelectionData, + // Allow users back into the already-running app when completing the + // autofill task when this is not the first intent. + shouldFinishWhenComplete = isFirstIntent, + ) + } + shareData != null -> { specialCircumstanceManager.specialCircumstance = SpecialCircumstance.ShareNewSend( 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 new file mode 100644 index 000000000..d639c5669 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.autofill.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Represents data for a manual autofill selection. + * + * @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 +class AutofillSelectionData( + val type: Type, + val uri: String?, +) : Parcelable { + + /** + * The type of selection the user must make. + */ + enum class Type { + CARD, + LOGIN, + } +} 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 new file mode 100644 index 000000000..73fe6995c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt @@ -0,0 +1,46 @@ +@file:OmitFromCoverage + +package com.x8bit.bitwarden.data.autofill.util + +import android.content.Context +import android.content.Intent +import android.os.Build +import com.x8bit.bitwarden.MainActivity +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage + +private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data" + +/** + * Creates an [Intent] in order to send the user to a manual selection process for autofill. + */ +fun createAutofillSelectionIntent( + context: Context, + type: AutofillSelectionData.Type, + uri: String?, +): Intent = + Intent( + context, + MainActivity::class.java, + ) + .apply { + putExtra( + AUTOFILL_SELECTION_DATA_KEY, + AutofillSelectionData( + type = type, + uri = uri, + ), + ) + } + +/** + * Checks if the given [Intent] contains data about an ongoing manual autofill selection process. + * The [AutofillSelectionData] will be returned when present. + */ +fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + this.getParcelableExtra(AUTOFILL_SELECTION_DATA_KEY, AutofillSelectionData::class.java) + } else { + this.getParcelableExtra(AUTOFILL_SELECTION_DATA_KEY) + } +} 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 f614a6276..b38ba8e66 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 @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.util import android.annotation.SuppressLint import android.app.PendingIntent -import android.content.Intent import android.os.Build import android.service.autofill.Dataset import android.service.autofill.Presentations @@ -11,8 +10,9 @@ import android.view.autofill.AutofillValue import android.widget.RemoteViews import android.widget.inline.InlinePresentationSpec import androidx.annotation.RequiresApi -import com.x8bit.bitwarden.MainActivity import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo +import com.x8bit.bitwarden.data.autofill.model.AutofillPartition +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.FilledData import com.x8bit.bitwarden.data.autofill.model.FilledItem import com.x8bit.bitwarden.ui.autofill.buildVaultItemAutofillRemoteViews @@ -31,11 +31,14 @@ val FilledData.fillableAutofillIds: List fun FilledData.buildVaultItemDataset( autofillAppInfo: AutofillAppInfo, ): Dataset { - val intent = Intent( - autofillAppInfo.context, - MainActivity::class.java, + val intent = createAutofillSelectionIntent( + context = autofillAppInfo.context, + type = when (this.originalPartition) { + is AutofillPartition.Card -> AutofillSelectionData.Type.CARD + is AutofillPartition.Login -> AutofillSelectionData.Type.LOGIN + }, + uri = this.uri, ) - // TODO: Add additional data to the Intent to be pulled out in the app (BIT-1296) val pendingIntent = PendingIntent .getActivity( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt index 38e22d019..28247d8cf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.platform.manager.model import android.os.Parcelable +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import kotlinx.parcelize.Parcelize @@ -17,4 +18,13 @@ sealed class SpecialCircumstance : Parcelable { val data: IntentManager.ShareData, val shouldFinishWhenComplete: Boolean, ) : SpecialCircumstance() + + /** + * The app was launched in order to allow the user to manually select data for autofill. + */ + @Parcelize + data class AutofillSelection( + val autofillSelectionData: AutofillSelectionData, + val shouldFinishWhenComplete: Boolean, + ) : SpecialCircumstance() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 09d088f54..4709a4be5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -17,6 +17,7 @@ import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination +import com.x8bit.bitwarden.ui.platform.feature.rootnav.util.toVaultItemListingType import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination @@ -26,6 +27,7 @@ import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import java.util.concurrent.atomic.AtomicReference @@ -76,7 +78,8 @@ fun RootNavScreen( RootNavState.Splash -> SPLASH_ROUTE RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE is RootNavState.VaultUnlocked, - RootNavState.VaultUnlockedForNewSend, + is RootNavState.VaultUnlockedForAutofillSelection, + is RootNavState.VaultUnlockedForNewSend, -> VAULT_UNLOCKED_GRAPH_ROUTE } val currentRoute = navController.currentDestination?.rootLevelRoute() @@ -102,7 +105,7 @@ fun RootNavScreen( restoreState = false } - when (state) { + when (val currentState = state) { RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions) RootNavState.Splash -> navController.navigateToSplash(rootNavOptions) RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions) @@ -114,6 +117,14 @@ fun RootNavScreen( navOptions = rootNavOptions, ) } + + is RootNavState.VaultUnlockedForAutofillSelection -> { + navController.navigateToVaultUnlockedGraph(rootNavOptions) + navController.navigateToVaultItemListingAsRoot( + vaultItemListingType = currentState.type.toVaultItemListingType(), + navOptions = rootNavOptions, + ) + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 90f91617a..a2547c53f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.ui.platform.base.BaseViewModel @@ -64,10 +65,15 @@ class RootNavViewModel @Inject constructor( userState.activeAccount.isVaultUnlocked -> { when (specialCircumstance) { + is SpecialCircumstance.AutofillSelection -> { + RootNavState.VaultUnlockedForAutofillSelection( + type = specialCircumstance.autofillSelectionData.type, + ) + } + is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend - null, - -> { + null -> { RootNavState.VaultUnlocked( activeUserId = userState.activeAccount.userId, ) @@ -111,6 +117,14 @@ sealed class RootNavState : Parcelable { val activeUserId: String, ) : RootNavState() + /** + * App should show a selection screen for autofill for an unlocked user. + */ + @Parcelize + data class VaultUnlockedForAutofillSelection( + val type: AutofillSelectionData.Type, + ) : RootNavState() + /** * App should show the new send screen for an unlocked user. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt new file mode 100644 index 000000000..89e5f2da7 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt @@ -0,0 +1,13 @@ +package com.x8bit.bitwarden.ui.platform.feature.rootnav.util + +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType + +/** + * Derives a [VaultItemListingType] from the given [AutofillSelectionData.Type]. + */ +fun AutofillSelectionData.Type.toVaultItemListingType(): VaultItemListingType = + when (this) { + AutofillSelectionData.Type.CARD -> VaultItemListingType.Card + AutofillSelectionData.Type.LOGIN -> VaultItemListingType.Login + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index b12f6c8f7..ce36228f3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -34,6 +34,7 @@ import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination +import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.vaultManualCodeEntryDestination import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.navigateToVaultMoveToOrganization @@ -62,6 +63,17 @@ fun NavGraphBuilder.vaultUnlockedGraph( startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE, route = VAULT_UNLOCKED_GRAPH_ROUTE, ) { + vaultItemListingDestinationAsRoot( + onNavigateBack = { navController.popBackStack() }, + onNavigateToVaultItemScreen = { navController.navigateToVaultItem(vaultItemId = it) }, + onNavigateToVaultAddItemScreen = { + navController.navigateToVaultAddEdit(VaultAddEditType.AddItem) + }, + onNavigateToSearchVault = { navController.navigateToSearch(searchType = it) }, + onNavigateToVaultEditItemScreen = { + navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it)) + }, + ) vaultUnlockedNavBarDestination( onNavigateToExportVault = { navController.navigateToExportVault() }, onNavigateToFolders = { navController.navigateToFolders() }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt index 4c370d6c6..76ec73ff0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavType import androidx.navigation.navArgument import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType @@ -21,11 +22,15 @@ private const val SEND_FILE: String = "send_file" private const val SEND_TEXT: String = "send_text" private const val TRASH: String = "trash" private const val VAULT_ITEM_LISTING_PREFIX: String = "vault_item_listing" +private const val VAULT_ITEM_LISTING_AS_ROOT_PREFIX: String = "vault_item_listing_as_root" private const val VAULT_ITEM_LISTING_TYPE: String = "vault_item_listing_type" private const val ID: String = "id" private const val VAULT_ITEM_LISTING_ROUTE: String = "$VAULT_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" + "?$ID={$ID}" +private const val VAULT_ITEM_LISTING_AS_ROOT_ROUTE: String = + "$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" + + "?$ID={$ID}" private const val SEND_ITEM_LISTING_PREFIX: String = "send_item_listing" private const val SEND_ITEM_LISTING_ROUTE: String = "$SEND_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" + @@ -70,6 +75,39 @@ fun NavGraphBuilder.vaultItemListingDestination( ) } +/** + * Add the [VaultItemListingScreen] to the nav graph. + */ +fun NavGraphBuilder.vaultItemListingDestinationAsRoot( + onNavigateBack: () -> Unit, + onNavigateToVaultItemScreen: (id: String) -> Unit, + onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit, + onNavigateToVaultAddItemScreen: () -> Unit, + onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, +) { + composableWithStayTransitions( + route = VAULT_ITEM_LISTING_AS_ROOT_ROUTE, + arguments = listOf( + navArgument( + name = VAULT_ITEM_LISTING_TYPE, + builder = { + type = NavType.StringType + }, + ), + ), + ) { + VaultItemListingScreen( + onNavigateBack = onNavigateBack, + onNavigateToVaultItem = onNavigateToVaultItemScreen, + onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen, + onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen, + onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) }, + onNavigateToAddSendItem = {}, + onNavigateToEditSendItem = {}, + ) + } +} + /** * Add the [VaultItemListingScreen] to the nav graph. */ @@ -110,7 +148,9 @@ private fun NavGraphBuilder.internalVaultItemListingDestination( arguments = listOf( navArgument( name = VAULT_ITEM_LISTING_TYPE, - builder = { type = NavType.StringType }, + builder = { + type = NavType.StringType + }, ), navArgument( name = ID, @@ -147,6 +187,20 @@ fun NavController.navigateToVaultItemListing( ) } +/** + * Navigate to the [VaultItemListingScreen] for vault. + */ +fun NavController.navigateToVaultItemListingAsRoot( + vaultItemListingType: VaultItemListingType, + navOptions: NavOptions? = null, +) { + navigate( + route = "$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/${vaultItemListingType.toTypeString()}" + + "?$ID=${vaultItemListingType.toIdOrNull()}", + navOptions = navOptions, + ) +} + /** * Navigate to the [VaultItemListingScreen] for sends. */ 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 fa00b1ae4..08ecb5729 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 @@ -4,7 +4,10 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager +import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.DataState @@ -44,7 +47,7 @@ import javax.inject.Inject * and launches [VaultItemListingEvent] for the [VaultItemListingScreen]. */ @HiltViewModel -@Suppress("MagicNumber", "TooManyFunctions") +@Suppress("MagicNumber", "TooManyFunctions", "LongParameterList") class VaultItemListingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val clock: Clock, @@ -52,19 +55,26 @@ class VaultItemListingViewModel @Inject constructor( private val vaultRepository: VaultRepository, private val environmentRepository: EnvironmentRepository, private val settingsRepository: SettingsRepository, + private val specialCircumstanceManager: SpecialCircumstanceManager, ) : BaseViewModel( - initialState = VaultItemListingState( - itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle) - .vaultItemListingType - .toItemListingType(), - viewState = VaultItemListingState.ViewState.Loading, - vaultFilterType = vaultRepository.vaultFilterType, - baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl, - baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, - isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, - isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, - dialogState = null, - ), + initialState = run { + val specialCircumstance = + specialCircumstanceManager.specialCircumstance as? SpecialCircumstance.AutofillSelection + VaultItemListingState( + itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle) + .vaultItemListingType + .toItemListingType(), + viewState = VaultItemListingState.ViewState.Loading, + vaultFilterType = vaultRepository.vaultFilterType, + baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl, + baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, + isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, + isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, + dialogState = null, + autofillSelectionData = specialCircumstance?.autofillSelectionData, + shouldFinishOnComplete = specialCircumstance?.shouldFinishWhenComplete ?: false, + ) + }, ) { init { @@ -521,7 +531,10 @@ data class VaultItemListingState( val baseIconUrl: String, val isIconLoadingDisabled: Boolean, val dialogState: DialogState?, + // Internal private val isPullToRefreshSettingEnabled: Boolean, + private val autofillSelectionData: AutofillSelectionData? = null, + private val shouldFinishOnComplete: Boolean = false, ) { /** @@ -530,6 +543,12 @@ data class VaultItemListingState( val isPullToRefreshEnabled: Boolean get() = isPullToRefreshSettingEnabled && viewState.isPullToRefreshEnabled + /** + * Whether or not this represents a listing screen for autofill. + */ + val isAutofill: Boolean + get() = autofillSelectionData != null + /** * Represents the current state of any dialogs on the screen. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index f72db5c8d..27b01c56e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -5,7 +5,8 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState -import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -107,7 +108,7 @@ class MainViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() val mockIntent = mockk() val shareData = mockk() - every { mockIntent.getCaptchaCallbackTokenResult() } returns null + every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData viewModel.trySendAction( @@ -124,13 +125,36 @@ class MainViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `on ReceiveFirstIntent with autofill data should set the special circumstance to AutofillSelection`() { + val viewModel = createViewModel() + val mockIntent = mockk() + val autofillSelectionData = mockk() + every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData + every { intentManager.getShareDataFromIntent(mockIntent) } returns null + + viewModel.trySendAction( + MainAction.ReceiveFirstIntent( + intent = mockIntent, + ), + ) + assertEquals( + SpecialCircumstance.AutofillSelection( + autofillSelectionData = autofillSelectionData, + shouldFinishWhenComplete = true, + ), + specialCircumstanceManager.specialCircumstance, + ) + } + @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with share data should set the special circumstance to ShareNewSend`() { val viewModel = createViewModel() val mockIntent = mockk() val shareData = mockk() - every { mockIntent.getCaptchaCallbackTokenResult() } returns null + every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData viewModel.trySendAction( @@ -147,6 +171,29 @@ class MainViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() { + val viewModel = createViewModel() + val mockIntent = mockk() + val autofillSelectionData = mockk() + every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData + every { intentManager.getShareDataFromIntent(mockIntent) } returns null + + viewModel.trySendAction( + MainAction.ReceiveNewIntent( + intent = mockIntent, + ), + ) + assertEquals( + SpecialCircumstance.AutofillSelection( + autofillSelectionData = autofillSelectionData, + shouldFinishWhenComplete = false, + ), + specialCircumstanceManager.specialCircumstance, + ) + } + @Suppress("MaxLineLength") @Test fun `changes in the allowed screen capture value should result in emissions of ScreenCaptureSettingChange `() = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index da65e6bae..f779a080d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav import androidx.navigation.navOptions +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.FakeNavHostController import io.mockk.every @@ -98,5 +99,17 @@ class RootNavScreenTest : BaseComposeTest() { navOptions = expectedNavOptions, ) } + + // Make sure navigating to vault unlocked for autofill works as expected: + rootNavStateFlow.value = + RootNavState.VaultUnlockedForAutofillSelection( + type = AutofillSelectionData.Type.LOGIN, + ) + composeTestRule.runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "vault_item_listing_as_root/login", + navOptions = expectedNavOptions, + ) + } } } 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 f99fe6964..c3ccbd43c 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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -145,6 +146,46 @@ class RootNavViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + 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, + uri = "uri", + ) + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AutofillSelection( + autofillSelectionData = autofillSelectionData, + shouldFinishWhenComplete = true, + ) + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + isBiometricsEnabled = false, + organizations = emptyList(), + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.VaultUnlockedForAutofillSelection( + type = AutofillSelectionData.Type.LOGIN, + ), + viewModel.stateFlow.value, + ) + } + @Test fun `when the active user has a locked vault the nav state should be VaultLocked`() { mutableUserStateFlow.tryEmit( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt new file mode 100644 index 000000000..30881e342 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt @@ -0,0 +1,22 @@ +package com.x8bit.bitwarden.ui.platform.feature.rootnav.util + +import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData +import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class AutofillSelectionDataExtensionsTest { + @Test + fun `toVaultItemListingType should return the correct result for each type`() { + mapOf( + AutofillSelectionData.Type.CARD to VaultItemListingType.Card, + AutofillSelectionData.Type.LOGIN to VaultItemListingType.Login, + ) + .forEach { (type, expected) -> + assertEquals( + expected, + type.toVaultItemListingType(), + ) + } + } +} 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 3c403302e..e6459c398 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 @@ -4,6 +4,7 @@ import android.net.Uri import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -76,6 +77,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow } + private val specialCircumstanceManager = SpecialCircumstanceManagerImpl() private val initialState = createVaultItemListingState() private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType( vaultItemListingType = VaultItemListingType.Login, @@ -941,6 +943,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { vaultRepository = vaultRepository, environmentRepository = environmentRepository, settingsRepository = settingsRepository, + specialCircumstanceManager = specialCircumstanceManager, ) @Suppress("MaxLineLength") @@ -957,5 +960,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, isPullToRefreshSettingEnabled = false, dialogState = null, + autofillSelectionData = null, + shouldFinishOnComplete = false, ) }