mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Send autofill selections back to autofill flow (#829)
This commit is contained in:
parent
b199a67b7d
commit
b3fa33a02c
22 changed files with 823 additions and 13 deletions
|
@ -12,6 +12,7 @@ import androidx.core.os.LocaleListCompat
|
|||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
@ -28,6 +29,9 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
|
@ -79,6 +83,10 @@ class MainActivity : AppCompatActivity() {
|
|||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAutofill -> {
|
||||
handleCompleteAutofill(event)
|
||||
}
|
||||
|
||||
is MainEvent.ScreenCaptureSettingChange -> {
|
||||
handleScreenCaptureSettingChange(event)
|
||||
}
|
||||
|
@ -87,6 +95,13 @@ class MainActivity : AppCompatActivity() {
|
|||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) {
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleScreenCaptureSettingChange(event: MainEvent.ScreenCaptureSettingChange) {
|
||||
if (event.isAllowed) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.content.Intent
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
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
|
||||
|
@ -25,6 +27,7 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
private val autofillSelectionManager: AutofillSelectionManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
|
@ -49,6 +52,11 @@ class MainViewModel @Inject constructor(
|
|||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
autofillSelectionManager
|
||||
.autofillSelectionFlow
|
||||
.onEach { trySendAction(MainAction.Internal.AutofillSelectionReceive(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.appThemeStateFlow
|
||||
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
|
||||
|
@ -64,12 +72,22 @@ class MainViewModel @Inject constructor(
|
|||
|
||||
override fun handleAction(action: MainAction) {
|
||||
when (action) {
|
||||
is MainAction.Internal.AutofillSelectionReceive -> {
|
||||
handleAutofillSelectionReceive(action)
|
||||
}
|
||||
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAutofillSelectionReceive(
|
||||
action: MainAction.Internal.AutofillSelectionReceive,
|
||||
) {
|
||||
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
|
||||
mutableStateFlow.update { it.copy(theme = action.theme) }
|
||||
}
|
||||
|
@ -144,6 +162,13 @@ sealed class MainAction {
|
|||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
sealed class Internal : MainAction() {
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
data class AutofillSelectionReceive(
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the app theme has changed.
|
||||
*/
|
||||
|
@ -157,6 +182,11 @@ 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 autofill and that the
|
||||
* process is ready to complete.
|
||||
*/
|
||||
data class CompleteAutofill(val cipherView: CipherView) : MainEvent()
|
||||
|
||||
/**
|
||||
* Event indicating a change in the screen capture setting.
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package com.x8bit.bitwarden.data.autofill.di
|
||||
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityRetainedComponent
|
||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||
|
||||
/**
|
||||
* Provides dependencies in the autofill package that must be scoped to a retained Activity. These
|
||||
* are for dependencies that must operate independently in different application tasks that contain
|
||||
* unique [MainActivity] instances.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ActivityRetainedComponent::class)
|
||||
object ActivityAutofillModule {
|
||||
|
||||
@ActivityRetainedScoped
|
||||
@Provides
|
||||
fun provideAutofillSelectionManager(): AutofillSelectionManager =
|
||||
AutofillSelectionManagerImpl()
|
||||
}
|
|
@ -7,6 +7,8 @@ import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
|
|||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
|
@ -36,6 +38,17 @@ object AutofillModule {
|
|||
@ApplicationContext context: Context,
|
||||
): AutofillManager = context.getSystemService(AutofillManager::class.java)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
autofillParser: AutofillParser,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): AutofillCompletionManager =
|
||||
AutofillCompletionManagerImpl(
|
||||
autofillParser = autofillParser,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
fun providesAutofillParser(
|
||||
settingsRepository: SettingsRepository,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.app.Activity
|
||||
import com.bitwarden.core.CipherView
|
||||
|
||||
/**
|
||||
* A manager for completing the autofill process after the user has made a selection.
|
||||
*/
|
||||
interface AutofillCompletionManager {
|
||||
|
||||
/**
|
||||
* Completes the autofill flow originating with the given [activity] using the selected
|
||||
* [cipherView].
|
||||
*/
|
||||
fun completeAutofill(
|
||||
activity: Activity,
|
||||
cipherView: CipherView,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionResultIntent
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillAssistStructureOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Primary implementation of [AutofillCompletionManager].
|
||||
*/
|
||||
class AutofillCompletionManagerImpl(
|
||||
private val autofillParser: AutofillParser,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
private val filledDataBuilderProvider: (CipherView) -> FilledDataBuilder =
|
||||
{ createSingleItemFilledDataBuilder(cipherView = it) },
|
||||
) : AutofillCompletionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
|
||||
override fun completeAutofill(
|
||||
activity: Activity,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val autofillAppInfo = activity.toAutofillAppInfo()
|
||||
val assistStructure = activity
|
||||
.intent
|
||||
?.getAutofillAssistStructureOrNull()
|
||||
?: run {
|
||||
activity.cancelAndFinish()
|
||||
return
|
||||
}
|
||||
|
||||
val autofillRequest = autofillParser
|
||||
.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
if (autofillRequest !is AutofillRequest.Fillable) {
|
||||
activity.cancelAndFinish()
|
||||
return
|
||||
}
|
||||
|
||||
val fillDataBuilder = filledDataBuilderProvider(cipherView)
|
||||
// We'll launch a coroutine here but this code will technically run synchronously given
|
||||
// how we've constructed a single-item AutofillCipherProvider.
|
||||
mainScope.launch {
|
||||
val dataset = fillDataBuilder
|
||||
.build(autofillRequest)
|
||||
.filledPartitions
|
||||
.firstOrNull()
|
||||
?.buildDataset(autofillAppInfo = autofillAppInfo)
|
||||
?: run {
|
||||
activity.cancelAndFinish()
|
||||
return@launch
|
||||
}
|
||||
val resultIntent = createAutofillSelectionResultIntent(dataset)
|
||||
activity.setResultAndFinish(resultIntent = resultIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleItemFilledDataBuilder(
|
||||
cipherView: CipherView,
|
||||
): FilledDataBuilder =
|
||||
FilledDataBuilderImpl(
|
||||
autofillCipherProvider = cipherView.toAutofillCipherProvider(),
|
||||
)
|
||||
|
||||
private fun Activity.cancelAndFinish() {
|
||||
this.setResult(Activity.RESULT_CANCELED)
|
||||
this.finish()
|
||||
}
|
||||
|
||||
private fun Activity.setResultAndFinish(resultIntent: Intent) {
|
||||
this.setResult(Activity.RESULT_OK, resultIntent)
|
||||
this.finish()
|
||||
}
|
||||
|
||||
private fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
object : AutofillCipherProvider {
|
||||
override suspend fun isVaultLocked(): Boolean = true
|
||||
|
||||
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
|
||||
val card = this@toAutofillCipherProvider.card ?: return emptyList()
|
||||
return listOf(
|
||||
AutofillCipher.Card(
|
||||
name = name,
|
||||
subtitle = subtitle.orEmpty(),
|
||||
cardholderName = card.cardholderName.orEmpty(),
|
||||
code = card.code.orEmpty(),
|
||||
expirationMonth = card.expMonth.orEmpty(),
|
||||
expirationYear = card.expYear.orEmpty(),
|
||||
number = card.number.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getLoginAutofillCiphers(
|
||||
uri: String,
|
||||
): List<AutofillCipher.Login> {
|
||||
val login = this@toAutofillCipherProvider.login ?: return emptyList()
|
||||
return listOf(
|
||||
AutofillCipher.Login(
|
||||
name = name,
|
||||
password = login.password.orEmpty(),
|
||||
subtitle = subtitle.orEmpty(),
|
||||
username = login.username.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import com.bitwarden.core.CipherView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Tracks the selection of a [CipherView] during the autofill flow within the app.
|
||||
*/
|
||||
interface AutofillSelectionManager {
|
||||
|
||||
/**
|
||||
* Emits a [CipherView] as a result of calls to [emitAutofillSelection].
|
||||
*/
|
||||
val autofillSelectionFlow: Flow<CipherView>
|
||||
|
||||
/**
|
||||
* Triggers an emission via [autofillSelectionFlow].
|
||||
*/
|
||||
fun emitAutofillSelection(cipherView: CipherView)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import com.bitwarden.core.CipherView
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
||||
/**
|
||||
* Primary implementation of [AutofillSelectionManager].
|
||||
*/
|
||||
class AutofillSelectionManagerImpl : AutofillSelectionManager {
|
||||
private val autofillSelectionChannel = Channel<CipherView>(capacity = Int.MAX_VALUE)
|
||||
|
||||
override val autofillSelectionFlow: Flow<CipherView> =
|
||||
autofillSelectionChannel.receiveAsFlow()
|
||||
|
||||
override fun emitAutofillSelection(cipherView: CipherView) {
|
||||
autofillSelectionChannel.trySend(cipherView)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.autofill.parser
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.service.autofill.FillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
|
@ -19,4 +20,15 @@ interface AutofillParser {
|
|||
autofillAppInfo: AutofillAppInfo,
|
||||
fillRequest: FillRequest,
|
||||
): AutofillRequest
|
||||
|
||||
/**
|
||||
* Parse the useful information from [assistStructure] into an [AutofillRequest].
|
||||
*
|
||||
* @param autofillAppInfo Provides app context that is required to properly parse the request.
|
||||
* @param assistStructure The key data from the original request that needs parsing.
|
||||
*/
|
||||
fun parse(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
assistStructure: AssistStructure,
|
||||
): AutofillRequest
|
||||
}
|
||||
|
|
|
@ -40,13 +40,23 @@ class AutofillParserImpl(
|
|||
}
|
||||
?: AutofillRequest.Unfillable
|
||||
|
||||
override fun parse(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
assistStructure: AssistStructure,
|
||||
): AutofillRequest =
|
||||
parseInternal(
|
||||
assistStructure = assistStructure,
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
fillRequest = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Parse the [AssistStructure] into an [AutofillRequest].
|
||||
*/
|
||||
private fun parseInternal(
|
||||
assistStructure: AssistStructure,
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
fillRequest: FillRequest,
|
||||
fillRequest: FillRequest?,
|
||||
): AutofillRequest {
|
||||
// Parse the `assistStructure` into internal models.
|
||||
val traversalDataList = assistStructure.traverse()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Build an [AutofillAppInfo] from the given [Activity].
|
||||
*/
|
||||
fun Activity.toAutofillAppInfo(): AutofillAppInfo =
|
||||
AutofillAppInfo(
|
||||
context = this.applicationContext,
|
||||
packageName = this.packageName,
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
)
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
|
||||
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
|
||||
|
||||
|
@ -33,14 +36,28 @@ fun createAutofillSelectionIntent(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [Intent] in order to specify that there is a successful selection during a manual
|
||||
* autofill process.
|
||||
*/
|
||||
fun createAutofillSelectionResultIntent(
|
||||
dataset: Dataset,
|
||||
): Intent =
|
||||
Intent()
|
||||
.apply {
|
||||
putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given [Intent] contains an [AssistStructure] related to an ongoing manual autofill
|
||||
* selection process.
|
||||
*/
|
||||
fun Intent.getAutofillAssistStructureOrNull(): AssistStructure? =
|
||||
this.getSafeParcelableExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
|
||||
this.getSafeParcelableExtra(AUTOFILL_SELECTION_DATA_KEY)
|
||||
|
|
|
@ -10,11 +10,14 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
|||
* Extract the list of [InlinePresentationSpec]s. If it fails, return an empty list.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FillRequest.getInlinePresentationSpecs(
|
||||
fun FillRequest?.getInlinePresentationSpecs(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
isInlineAutofillEnabled: Boolean,
|
||||
): List<InlinePresentationSpec> =
|
||||
if (isInlineAutofillEnabled && autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
if (this != null &&
|
||||
isInlineAutofillEnabled &&
|
||||
autofillAppInfo.sdkInt >= Build.VERSION_CODES.R
|
||||
) {
|
||||
inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
|
@ -25,11 +28,14 @@ fun FillRequest.getInlinePresentationSpecs(
|
|||
* return 0.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FillRequest.getMaxInlineSuggestionsCount(
|
||||
fun FillRequest?.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
isInlineAutofillEnabled: Boolean,
|
||||
): Int =
|
||||
if (isInlineAutofillEnabled && autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
if (this != null &&
|
||||
isInlineAutofillEnabled &&
|
||||
autofillAppInfo.sdkInt >= Build.VERSION_CODES.R
|
||||
) {
|
||||
inlineSuggestionsRequest?.maxSuggestionCount ?: 0
|
||||
} else {
|
||||
0
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* A means of retrieving a [Parcelable] from an [Intent] using the given [name] in a manner that
|
||||
* is safe across SDK versions.
|
||||
*/
|
||||
inline fun <reified T> Intent.getSafeParcelableExtra(name: String): T? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(
|
||||
name,
|
||||
T::class.java,
|
||||
)
|
||||
} else {
|
||||
getParcelableExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE)
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
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
|
||||
|
@ -55,6 +56,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
private val vaultRepository: VaultRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val autofillSelectionManager: AutofillSelectionManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
|
||||
initialState = run {
|
||||
|
@ -180,6 +182,12 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleItemClick(action: VaultItemListingsAction.ItemClick) {
|
||||
if (state.isAutofill) {
|
||||
val cipherView = getCipherViewOrNull(action.id) ?: return
|
||||
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView)
|
||||
return
|
||||
}
|
||||
|
||||
val event = when (state.itemListingType) {
|
||||
is VaultItemListingState.ItemListingType.Vault -> {
|
||||
VaultItemListingEvent.NavigateToVaultItem(id = action.id)
|
||||
|
@ -518,6 +526,14 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCipherViewOrNull(cipherId: String) =
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.value
|
||||
.data
|
||||
?.cipherViewList
|
||||
?.firstOrNull { it.id == cipherId }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,8 +3,11 @@ package com.x8bit.bitwarden
|
|||
import android.content.Intent
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
|
@ -25,6 +28,7 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl()
|
||||
private val mutableAppThemeFlow = MutableStateFlow(AppTheme.DEFAULT)
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val mutableScreenCaptureAllowedFlow = MutableStateFlow(true)
|
||||
|
@ -59,6 +63,22 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `autofill selection updates should emit CompleteAutofill events`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val cipherView = mockk<CipherView>()
|
||||
viewModel.eventFlow.test {
|
||||
// Ignore initial screen capture event
|
||||
awaitItem()
|
||||
|
||||
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView)
|
||||
assertEquals(
|
||||
MainEvent.CompleteAutofill(cipherView = cipherView),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SpecialCircumstance updates should update the SavedStateHandle`() {
|
||||
createViewModel()
|
||||
|
@ -217,6 +237,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
private fun createViewModel(
|
||||
initialSpecialCircumstance: SpecialCircumstance? = null,
|
||||
) = MainViewModel(
|
||||
autofillSelectionManager = autofillSelectionManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
settingsRepository = settingsRepository,
|
||||
intentManager = intentManager,
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Intent
|
||||
import android.service.autofill.Dataset
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionResultIntent
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillAssistStructureOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillAppInfo
|
||||
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 AutofillCompletionManagerTest {
|
||||
private val activity: Activity = mockk {
|
||||
every { finish() } just runs
|
||||
every { setResult(any()) } just runs
|
||||
every { setResult(any(), any()) } just runs
|
||||
}
|
||||
private val assistStructure: AssistStructure = mockk()
|
||||
private val autofillAppInfo: AutofillAppInfo = mockk()
|
||||
private val autofillParser: AutofillParser = mockk()
|
||||
private val cipherView: CipherView = mockk()
|
||||
private val dataset: Dataset = mockk()
|
||||
private val dispatcherManager = FakeDispatcherManager()
|
||||
private val fillableRequest: AutofillRequest.Fillable = mockk()
|
||||
private val filledDataBuilder: FilledDataBuilder = mockk()
|
||||
private val filledPartition: FilledPartition = mockk()
|
||||
private val mockIntent: Intent = mockk()
|
||||
private val resultIntent: Intent = mockk()
|
||||
|
||||
private val autofillCompletionManager: AutofillCompletionManager =
|
||||
AutofillCompletionManagerImpl(
|
||||
autofillParser = autofillParser,
|
||||
dispatcherManager = dispatcherManager,
|
||||
filledDataBuilderProvider = { filledDataBuilder },
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
dispatcherManager.setMain(dispatcherManager.unconfined)
|
||||
mockkStatic(::createAutofillSelectionResultIntent)
|
||||
mockkStatic(Activity::toAutofillAppInfo)
|
||||
mockkStatic(FilledPartition::buildDataset)
|
||||
mockkStatic(Intent::getAutofillAssistStructureOrNull)
|
||||
every { activity.toAutofillAppInfo() } returns autofillAppInfo
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
dispatcherManager.resetMain()
|
||||
unmockkStatic(::createAutofillSelectionResultIntent)
|
||||
unmockkStatic(Activity::toAutofillAppInfo)
|
||||
unmockkStatic(FilledPartition::buildDataset)
|
||||
unmockkStatic(Intent::getAutofillAssistStructureOrNull)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeAutofill when there is no Intent present should cancel and finish the Activity`() {
|
||||
every { activity.intent } returns null
|
||||
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
verify {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
activity.finish()
|
||||
}
|
||||
verify {
|
||||
activity.intent
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeAutofill when there is no AssistStructure present should cancel and finish the Activity`() {
|
||||
every { activity.intent } returns mockIntent
|
||||
every { mockIntent.getAutofillAssistStructureOrNull() } returns null
|
||||
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
verify {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
activity.finish()
|
||||
}
|
||||
verify {
|
||||
activity.intent
|
||||
mockIntent.getAutofillAssistStructureOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeAutofill when the request is not fillable should cancel and finish the Activity`() {
|
||||
every { activity.intent } returns mockIntent
|
||||
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
|
||||
every {
|
||||
autofillParser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
} returns AutofillRequest.Unfillable
|
||||
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
verify {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
activity.finish()
|
||||
}
|
||||
verify {
|
||||
activity.intent
|
||||
mockIntent.getAutofillAssistStructureOrNull()
|
||||
autofillParser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeAutofill when there are no filled partitions should cancel and finish the Activity`() {
|
||||
val filledData: FilledData = mockk {
|
||||
every { filledPartitions } returns emptyList()
|
||||
}
|
||||
every { activity.intent } returns mockIntent
|
||||
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
|
||||
every {
|
||||
autofillParser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
} returns fillableRequest
|
||||
coEvery {
|
||||
filledDataBuilder.build(autofillRequest = fillableRequest)
|
||||
} returns filledData
|
||||
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
verify {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
activity.finish()
|
||||
}
|
||||
verify {
|
||||
activity.intent
|
||||
mockIntent.getAutofillAssistStructureOrNull()
|
||||
autofillParser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
filledDataBuilder.build(autofillRequest = fillableRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `completeAutofill when there is a filled partition should build a dataset, place it in a result Intent, and finish the Activity`() {
|
||||
val filledData: FilledData = mockk {
|
||||
every { filledPartitions } returns listOf(filledPartition)
|
||||
}
|
||||
every { activity.intent } returns mockIntent
|
||||
every { mockIntent.getAutofillAssistStructureOrNull() } returns assistStructure
|
||||
every {
|
||||
autofillParser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
} returns fillableRequest
|
||||
coEvery {
|
||||
filledDataBuilder.build(autofillRequest = fillableRequest)
|
||||
} returns filledData
|
||||
every { filledPartition.buildDataset(autofillAppInfo = autofillAppInfo) } returns dataset
|
||||
every { createAutofillSelectionResultIntent(dataset = dataset) } returns resultIntent
|
||||
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
|
||||
verify {
|
||||
activity.setResult(Activity.RESULT_OK, resultIntent)
|
||||
activity.finish()
|
||||
}
|
||||
verify {
|
||||
activity.intent
|
||||
mockIntent.getAutofillAssistStructureOrNull()
|
||||
autofillParser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
filledPartition.buildDataset(autofillAppInfo = autofillAppInfo)
|
||||
createAutofillSelectionResultIntent(dataset = dataset)
|
||||
}
|
||||
coVerify {
|
||||
filledDataBuilder.build(autofillRequest = fillableRequest)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.CipherView
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AutofillSelectionManagerTest {
|
||||
private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl()
|
||||
|
||||
@Test
|
||||
fun `autofillSelectionFlow should emit whenever emitAutofillSelection is called`() =
|
||||
runTest {
|
||||
autofillSelectionManager.autofillSelectionFlow.test {
|
||||
expectNoEvents()
|
||||
|
||||
val cipherView1 = mockk<CipherView>()
|
||||
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView1)
|
||||
|
||||
assertEquals(cipherView1, awaitItem())
|
||||
|
||||
val cipherView2 = mockk<CipherView>()
|
||||
autofillSelectionManager.emitAutofillSelection(cipherView = cipherView2)
|
||||
|
||||
assertEquals(cipherView2, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -90,6 +90,12 @@ class AutofillParserTests {
|
|||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
} returns emptyList()
|
||||
every {
|
||||
null.getInlinePresentationSpecs(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
} returns emptyList()
|
||||
every {
|
||||
fillRequest.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
|
@ -102,6 +108,12 @@ class AutofillParserTests {
|
|||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
} returns 0
|
||||
every {
|
||||
null.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
} returns 0
|
||||
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure) } returns URI
|
||||
parser = AutofillParserImpl(
|
||||
settingsRepository = settingsRepository,
|
||||
|
@ -420,6 +432,57 @@ class AutofillParserTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parse should return empty inline suggestions when parsing an AssistStructure directly`() {
|
||||
// Setup
|
||||
mockIsInlineAutofillEnabled = false
|
||||
setupAssistStructureWithAllAutofillViewTypes()
|
||||
val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth(
|
||||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = true,
|
||||
),
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = true,
|
||||
),
|
||||
)
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(cardAutofillView),
|
||||
)
|
||||
val expected = AutofillRequest.Fillable(
|
||||
ignoreAutofillIds = emptyList(),
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
every { cardViewNode.toAutofillView() } returns cardAutofillView
|
||||
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
||||
|
||||
// Test
|
||||
val actual = parser.parse(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
verify(exactly = 1) {
|
||||
null.getInlinePresentationSpecs(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
null.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup [assistStructure] to return window nodes with each [AutofillView] type (card and login)
|
||||
* so we can test how different window node configurations produce different partitions.
|
||||
|
|
|
@ -19,6 +19,22 @@ class FillRequestExtensionsTest {
|
|||
every { this@mockk.inlineSuggestionsRequest } returns expectedInlineSuggestionsRequest
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getInlinePresentationSpecs should return empty list when request is null`() {
|
||||
// Setup
|
||||
val autofillAppInfo: AutofillAppInfo = mockk()
|
||||
val expected: List<InlinePresentationSpec> = emptyList()
|
||||
|
||||
// Test
|
||||
val actual = null.getInlinePresentationSpecs(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getInlinePresentationSpecs should return empty list when disabled`() {
|
||||
// Setup
|
||||
|
@ -75,6 +91,22 @@ class FillRequestExtensionsTest {
|
|||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMaxInlineSuggestionsCount should return 0 when request is null`() {
|
||||
// Setup
|
||||
val autofillAppInfo: AutofillAppInfo = mockk()
|
||||
val expected = 0
|
||||
|
||||
// Test
|
||||
val actual = null.getMaxInlineSuggestionsCount(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMaxInlineSuggestionsCount should return 0 when disabled`() {
|
||||
// Setup
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SpecialCircumstanceManagerTest {
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager =
|
||||
SpecialCircumstanceManagerImpl()
|
||||
|
||||
@Test
|
||||
fun `specialCircumstanceStateFlow should emit whenever the SpecialCircumstance is updated`() =
|
||||
runTest {
|
||||
specialCircumstanceManager.specialCircumstanceStateFlow.test {
|
||||
assertNull(awaitItem())
|
||||
|
||||
val specialCircumstance1 = mockk<SpecialCircumstance>()
|
||||
specialCircumstanceManager.specialCircumstance = specialCircumstance1
|
||||
|
||||
assertEquals(specialCircumstance1, awaitItem())
|
||||
|
||||
val specialCircumstance2 = mockk<SpecialCircumstance>()
|
||||
specialCircumstanceManager.specialCircumstance = specialCircumstance2
|
||||
|
||||
assertEquals(specialCircumstance2, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,11 @@ import android.net.Uri
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
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
|
||||
|
@ -49,6 +52,8 @@ import java.time.ZoneOffset
|
|||
@Suppress("LargeClass")
|
||||
class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl()
|
||||
|
||||
private val clock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
|
@ -150,6 +155,35 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemClick for vault item when autofill should post to the AutofillSelectionManager`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val cipherView = createMockCipherView(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSelection(
|
||||
autofillSelectionData = mockk(),
|
||||
shouldFinishWhenComplete = true,
|
||||
)
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
autofillSelectionManager.autofillSelectionFlow.test {
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick(id = "mockId-1"))
|
||||
assertEquals(
|
||||
cipherView,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -943,6 +977,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
vaultRepository = vaultRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
autofillSelectionManager = autofillSelectionManager,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue