mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 01:46:00 +03:00
BIT-1457: Setup autofill save request (#898)
This commit is contained in:
parent
8bb754f85b
commit
81c78fc115
32 changed files with 1906 additions and 92 deletions
|
@ -25,6 +25,15 @@ class BitwardenAutofillService : AutofillService() {
|
|||
*/
|
||||
@Inject
|
||||
lateinit var processor: AutofillProcessor
|
||||
/**
|
||||
* App information for the autofill feature.
|
||||
*/
|
||||
private val autofillAppInfo: AutofillAppInfo
|
||||
get() = AutofillAppInfo(
|
||||
context = applicationContext,
|
||||
packageName = packageName,
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
)
|
||||
|
||||
override fun onFillRequest(
|
||||
request: FillRequest,
|
||||
|
@ -32,11 +41,7 @@ class BitwardenAutofillService : AutofillService() {
|
|||
fillCallback: FillCallback,
|
||||
) {
|
||||
processor.processFillRequest(
|
||||
autofillAppInfo = AutofillAppInfo(
|
||||
context = applicationContext,
|
||||
packageName = packageName,
|
||||
sdkInt = Build.VERSION.SDK_INT,
|
||||
),
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
cancellationSignal = cancellationSignal,
|
||||
fillCallback = fillCallback,
|
||||
request = request,
|
||||
|
@ -47,6 +52,10 @@ class BitwardenAutofillService : AutofillService() {
|
|||
saverRequest: SaveRequest,
|
||||
saveCallback: SaveCallback,
|
||||
) {
|
||||
// TODO: add save request behavior (BIT-1299)
|
||||
processor.processSaveRequest(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
request = saverRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.service.autofill.FillResponse
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
|
||||
|
@ -14,5 +15,6 @@ interface FillResponseBuilder {
|
|||
fun build(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
filledData: FilledData,
|
||||
saveInfo: SaveInfo?,
|
||||
): FillResponse?
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.builder
|
|||
|
||||
import android.content.IntentSender
|
||||
import android.service.autofill.FillResponse
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
|
@ -18,10 +19,16 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
|||
override fun build(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
filledData: FilledData,
|
||||
saveInfo: SaveInfo?,
|
||||
): FillResponse? =
|
||||
if (filledData.fillableAutofillIds.isNotEmpty()) {
|
||||
val fillResponseBuilder = FillResponse.Builder()
|
||||
|
||||
saveInfo
|
||||
?.let { nonNullSaveInfo ->
|
||||
fillResponseBuilder.setSaveInfo(nonNullSaveInfo)
|
||||
}
|
||||
|
||||
filledData
|
||||
.filledPartitions
|
||||
.forEach { filledPartition ->
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
|
||||
/**
|
||||
* A builder for converting processed autofill request data into save info.
|
||||
*/
|
||||
interface SaveInfoBuilder {
|
||||
/**
|
||||
* Build a save info out the provided data. If that isn't possible, return null.
|
||||
*
|
||||
* @param autofillAppInfo App data that is required for building the [SaveInfo].
|
||||
* @param autofillPartition The portion of the processed [FillRequest] that will be filled.
|
||||
* @param fillRequest The [FillRequest] that initiated the autofill flow.
|
||||
* @param packageName The package name that was extracted from the [FillRequest].
|
||||
*/
|
||||
fun build(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillPartition: AutofillPartition,
|
||||
fillRequest: FillRequest,
|
||||
packageName: String?,
|
||||
): SaveInfo?
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
|
||||
/**
|
||||
* The primary implementation of [SaveInfoBuilder].This is used for converting autofill data into
|
||||
* a save info.
|
||||
*/
|
||||
class SaveInfoBuilderImpl(
|
||||
val settingsRepository: SettingsRepository,
|
||||
) : SaveInfoBuilder {
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun build(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillPartition: AutofillPartition,
|
||||
fillRequest: FillRequest,
|
||||
packageName: String?,
|
||||
): SaveInfo? {
|
||||
// Make sure that the save prompt is possible.
|
||||
val canPerformSaveRequest = autofillPartition.canPerformSaveRequest
|
||||
if (settingsRepository.isAutofillSavePromptDisabled || !canPerformSaveRequest) return null
|
||||
|
||||
// Docs state that password fields cannot be reliably saved
|
||||
// in Compat mode since they show as masked values.
|
||||
val isInCompatMode = if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.Q) {
|
||||
// Attempt to automatically establish compat request mode on Android 10+
|
||||
(fillRequest.flags or FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) == fillRequest.flags
|
||||
} else {
|
||||
COMPAT_BROWSERS.contains(packageName)
|
||||
}
|
||||
|
||||
// If login and compat mode, the password might be obfuscated,
|
||||
// in which case we should skip the save request.
|
||||
return if (autofillPartition is AutofillPartition.Login && isInCompatMode) {
|
||||
null
|
||||
} else {
|
||||
val saveInfoBuilder = SaveInfo
|
||||
.Builder(
|
||||
autofillPartition.saveType,
|
||||
autofillPartition.requiredSaveIds.toTypedArray(),
|
||||
)
|
||||
.setOptionalIds(autofillPartition.optionalSaveIds.toTypedArray())
|
||||
if (isInCompatMode) saveInfoBuilder.setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
|
||||
saveInfoBuilder.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These browsers function using the compatibility shim for the Autofill Framework.
|
||||
*
|
||||
* Ensure that these entries are sorted alphabetically and keep this list synchronized with the
|
||||
* values in /xml/autofill_service_configuration.xml and
|
||||
* /xml-v30/autofill_service_configuration.xml.
|
||||
*/
|
||||
private val COMPAT_BROWSERS: List<String> = listOf(
|
||||
"alook.browser",
|
||||
"alook.browser.google",
|
||||
"app.vanadium.browser",
|
||||
"com.amazon.cloud9",
|
||||
"com.android.browser",
|
||||
"com.android.chrome",
|
||||
"com.android.htmlviewer",
|
||||
"com.avast.android.secure.browser",
|
||||
"com.avg.android.secure.browser",
|
||||
"com.brave.browser",
|
||||
"com.brave.browser_beta",
|
||||
"com.brave.browser_default",
|
||||
"com.brave.browser_dev",
|
||||
"com.brave.browser_nightly",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.canary",
|
||||
"com.chrome.dev",
|
||||
"com.cookiegames.smartcookie",
|
||||
"com.cookiejarapps.android.smartcookieweb",
|
||||
"com.ecosia.android",
|
||||
"com.google.android.apps.chrome",
|
||||
"com.google.android.apps.chrome_dev",
|
||||
"com.google.android.captiveportallogin",
|
||||
"com.iode.firefox",
|
||||
"com.jamal2367.styx",
|
||||
"com.kiwibrowser.browser",
|
||||
"com.kiwibrowser.browser.dev",
|
||||
"com.lemurbrowser.exts",
|
||||
"com.microsoft.emmx",
|
||||
"com.microsoft.emmx.beta",
|
||||
"com.microsoft.emmx.canary",
|
||||
"com.microsoft.emmx.dev",
|
||||
"com.mmbox.browser",
|
||||
"com.mmbox.xbrowser",
|
||||
"com.mycompany.app.soulbrowser",
|
||||
"com.naver.whale",
|
||||
"com.neeva.app",
|
||||
"com.opera.browser",
|
||||
"com.opera.browser.beta",
|
||||
"com.opera.gx",
|
||||
"com.opera.mini.native",
|
||||
"com.opera.mini.native.beta",
|
||||
"com.opera.touch",
|
||||
"com.qflair.browserq",
|
||||
"com.qwant.liberty",
|
||||
"com.rainsee.create",
|
||||
"com.sec.android.app.sbrowser",
|
||||
"com.sec.android.app.sbrowser.beta",
|
||||
"com.stoutner.privacybrowser.free",
|
||||
"com.stoutner.privacybrowser.standard",
|
||||
"com.vivaldi.browser",
|
||||
"com.vivaldi.browser.snapshot",
|
||||
"com.vivaldi.browser.sopranos",
|
||||
"com.yandex.browser",
|
||||
"com.yjllq.internet",
|
||||
"com.yjllq.kito",
|
||||
"com.yujian.ResideMenuDemo",
|
||||
"com.z28j.feel",
|
||||
"idm.internet.download.manager",
|
||||
"idm.internet.download.manager.adm.lite",
|
||||
"idm.internet.download.manager.plus",
|
||||
"io.github.forkmaintainers.iceraven",
|
||||
"mark.via",
|
||||
"mark.via.gp",
|
||||
"net.dezor.browser",
|
||||
"net.slions.fulguris.full.download",
|
||||
"net.slions.fulguris.full.download.debug",
|
||||
"net.slions.fulguris.full.playstore",
|
||||
"net.slions.fulguris.full.playstore.debug",
|
||||
"org.adblockplus.browser",
|
||||
"org.adblockplus.browser.beta",
|
||||
"org.bromite.bromite",
|
||||
"org.bromite.chromium",
|
||||
"org.chromium.chrome",
|
||||
"org.codeaurora.swe.browser",
|
||||
"org.cromite.cromite",
|
||||
"org.gnu.icecat",
|
||||
"org.mozilla.fenix",
|
||||
"org.mozilla.fenix.nightly",
|
||||
"org.mozilla.fennec_aurora",
|
||||
"org.mozilla.fennec_fdroid",
|
||||
"org.mozilla.firefox",
|
||||
"org.mozilla.firefox_beta",
|
||||
"org.mozilla.reference.browser",
|
||||
"org.mozilla.rocket",
|
||||
"org.torproject.torbrowser",
|
||||
"org.torproject.torbrowser_alpha",
|
||||
"org.ungoogled.chromium.extensions.stable",
|
||||
"org.ungoogled.chromium.stable",
|
||||
"us.spotco.fennec_dos",
|
||||
)
|
|
@ -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.builder.SaveInfoBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.SaveInfoBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
|
@ -17,6 +19,7 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
|||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
|
@ -90,12 +93,18 @@ object AutofillModule {
|
|||
filledDataBuilder: FilledDataBuilder,
|
||||
fillResponseBuilder: FillResponseBuilder,
|
||||
parser: AutofillParser,
|
||||
policyManager: PolicyManager,
|
||||
saveInfoBuilder: SaveInfoBuilder,
|
||||
settingsRepository: SettingsRepository,
|
||||
): AutofillProcessor =
|
||||
AutofillProcessorImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
filledDataBuilder = filledDataBuilder,
|
||||
fillResponseBuilder = fillResponseBuilder,
|
||||
parser = parser,
|
||||
policyManager = policyManager,
|
||||
saveInfoBuilder = saveInfoBuilder,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
@ -107,4 +116,13 @@ object AutofillModule {
|
|||
|
||||
@Provides
|
||||
fun providesFillResponseBuilder(): FillResponseBuilder = FillResponseBuilderImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesSaveInfoBuilder(
|
||||
settingsRepository: SettingsRepository,
|
||||
): SaveInfoBuilder =
|
||||
SaveInfoBuilderImpl(
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,25 +1,75 @@
|
|||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.service.autofill.SaveInfo
|
||||
import android.view.autofill.AutofillId
|
||||
|
||||
/**
|
||||
* A partition of autofill data.
|
||||
*/
|
||||
sealed class AutofillPartition {
|
||||
|
||||
/**
|
||||
* [AutofillId]s that are optional for save requests. For example, with cards we require a
|
||||
* phone number too trigger the save request, other card data is optional.
|
||||
*/
|
||||
abstract val optionalSaveIds: List<AutofillId>
|
||||
|
||||
/**
|
||||
* [AutofillId]s that are required for save requests. If there are no required fields present,
|
||||
* then save requests aren't allowed.
|
||||
*/
|
||||
abstract val requiredSaveIds: List<AutofillId>
|
||||
|
||||
/**
|
||||
* The autofill save associated with this [AutofillPartition].
|
||||
*/
|
||||
abstract val saveType: Int
|
||||
|
||||
/**
|
||||
* The views that correspond to this partition.
|
||||
*/
|
||||
abstract val views: List<AutofillView>
|
||||
|
||||
/**
|
||||
* Whether it is possible to perform a save request with this [AutofillPartition].
|
||||
*/
|
||||
val canPerformSaveRequest: Boolean
|
||||
get() = requiredSaveIds.isNotEmpty()
|
||||
|
||||
/**
|
||||
* The credit card [AutofillPartition] data.
|
||||
*/
|
||||
data class Card(
|
||||
override val views: List<AutofillView.Card>,
|
||||
) : AutofillPartition()
|
||||
) : AutofillPartition() {
|
||||
override val optionalSaveIds: List<AutofillId>
|
||||
get() = views
|
||||
.filter { it !is AutofillView.Card.Number }
|
||||
.map { it.data.autofillId }
|
||||
override val requiredSaveIds: List<AutofillId>
|
||||
get() = views
|
||||
.filterIsInstance<AutofillView.Card.Number>()
|
||||
.map { it.data.autofillId }
|
||||
|
||||
override val saveType: Int
|
||||
get() = SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
||||
}
|
||||
|
||||
/**
|
||||
* The login [AutofillPartition] data.
|
||||
*/
|
||||
data class Login(
|
||||
override val views: List<AutofillView.Login>,
|
||||
) : AutofillPartition()
|
||||
) : AutofillPartition() {
|
||||
override val optionalSaveIds: List<AutofillId>
|
||||
get() = views
|
||||
.filter { it !is AutofillView.Login.Password }
|
||||
.map { it.data.autofillId }
|
||||
override val requiredSaveIds: List<AutofillId>
|
||||
get() = views
|
||||
.filterIsInstance<AutofillView.Login.Password>()
|
||||
.map { it.data.autofillId }
|
||||
override val saveType: Int
|
||||
get() = SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ sealed class AutofillRequest {
|
|||
val ignoreAutofillIds: List<AutofillId>,
|
||||
val inlinePresentationSpecs: List<InlinePresentationSpec>,
|
||||
val maxInlineSuggestionsCount: Int,
|
||||
val packageName: String?,
|
||||
val partition: AutofillPartition,
|
||||
val uri: String?,
|
||||
) : AutofillRequest()
|
||||
|
|
|
@ -12,10 +12,12 @@ sealed class AutofillView {
|
|||
*
|
||||
* @param autofillId The [AutofillId] associated with this view.
|
||||
* @param isFocused Whether the view is currently focused.
|
||||
* @param textValue A text value that represents the input present in the field.
|
||||
*/
|
||||
data class Data(
|
||||
val autofillId: AutofillId,
|
||||
val isFocused: Boolean,
|
||||
val textValue: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -29,10 +31,14 @@ sealed class AutofillView {
|
|||
sealed class Card : AutofillView() {
|
||||
|
||||
/**
|
||||
* The expiration month [AutofillView] for the [Card] data partition.
|
||||
* The expiration month [AutofillView] for the [Card] data partition. This implementation
|
||||
* also has its own [monthValue] because it can be present in lists, in which case there
|
||||
* is specialized logic for determining its [monthValue]. The [Data.textValue] is very
|
||||
* likely going to be a very different value.
|
||||
*/
|
||||
data class ExpirationMonth(
|
||||
override val data: Data,
|
||||
val monthValue: String?,
|
||||
) : Card()
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
|||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildPackageNameOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildUriOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getInlinePresentationSpecs
|
||||
import com.x8bit.bitwarden.data.autofill.util.getMaxInlineSuggestionsCount
|
||||
|
@ -78,9 +79,12 @@ class AutofillParserImpl(
|
|||
// Find the focused view.
|
||||
val focusedView = autofillViews.firstOrNull { it.data.isFocused }
|
||||
|
||||
val uri = traversalDataList.buildUriOrNull(
|
||||
val packageName = traversalDataList.buildPackageNameOrNull(
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
val uri = traversalDataList.buildUriOrNull(
|
||||
packageName = packageName,
|
||||
)
|
||||
|
||||
val blockListedURIs = settingsRepository.blockedAutofillUris + BLOCK_LISTED_URIS
|
||||
if (focusedView == null || blockListedURIs.contains(uri)) {
|
||||
|
@ -122,6 +126,7 @@ class AutofillParserImpl(
|
|||
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
maxInlineSuggestionsCount = maxInlineSuggestionsCount,
|
||||
packageName = packageName,
|
||||
partition = partition,
|
||||
uri = uri,
|
||||
)
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.autofill.processor
|
|||
import android.os.CancellationSignal
|
||||
import android.service.autofill.FillCallback
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveCallback
|
||||
import android.service.autofill.SaveRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
|
||||
/**
|
||||
|
@ -12,9 +14,10 @@ interface AutofillProcessor {
|
|||
/**
|
||||
* Process the autofill [FillRequest] and invoke the [fillCallback] with the result.
|
||||
*
|
||||
* @param autofillAppInfo app data that is required for the autofill [request] processing.
|
||||
* @param fillCallback the callback to invoke when the [request] has been processed.
|
||||
* @param request the request data from the OS that contains data about the autofill hierarchy.
|
||||
* @param autofillAppInfo App data that is required for the autofill [request] processing.
|
||||
* @param cancellationSignal A signal to listen to for cancellations.
|
||||
* @param fillCallback The callback to invoke when the [request] has been processed.
|
||||
* @param request The request data from the OS that contains data about the autofill hierarchy.
|
||||
*/
|
||||
fun processFillRequest(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
|
@ -22,4 +25,17 @@ interface AutofillProcessor {
|
|||
fillCallback: FillCallback,
|
||||
request: FillRequest,
|
||||
)
|
||||
|
||||
/**
|
||||
* Process the autofill [SaveRequest] and invoke the [saveCallback] with the result.
|
||||
*
|
||||
* @param autofillAppInfo App data that is required for the autofill [request] processing.
|
||||
* @param request The request data from the OS that contains data about the autofill hierarchy.
|
||||
* @param saveCallback The callback to invoke when the [request] has been processed.
|
||||
*/
|
||||
fun processSaveRequest(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
request: SaveRequest,
|
||||
saveCallback: SaveCallback,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,12 +3,20 @@ package com.x8bit.bitwarden.data.autofill.processor
|
|||
import android.os.CancellationSignal
|
||||
import android.service.autofill.FillCallback
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveCallback
|
||||
import android.service.autofill.SaveRequest
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.SaveInfoBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSavedItemIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -17,11 +25,15 @@ import kotlinx.coroutines.launch
|
|||
* The default implementation of [AutofillProcessor]. Its purpose is to handle autofill related
|
||||
* processing.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class AutofillProcessorImpl(
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val filledDataBuilder: FilledDataBuilder,
|
||||
private val fillResponseBuilder: FillResponseBuilder,
|
||||
private val parser: AutofillParser,
|
||||
private val saveInfoBuilder: SaveInfoBuilder,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) : AutofillProcessor {
|
||||
|
||||
/**
|
||||
|
@ -45,6 +57,47 @@ class AutofillProcessorImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun processSaveRequest(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
request: SaveRequest,
|
||||
saveCallback: SaveCallback,
|
||||
) {
|
||||
if (settingsRepository.isAutofillSavePromptDisabled) {
|
||||
saveCallback.onSuccess()
|
||||
return
|
||||
}
|
||||
|
||||
if (policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP).any()) {
|
||||
saveCallback.onSuccess()
|
||||
return
|
||||
}
|
||||
|
||||
request
|
||||
.fillContexts
|
||||
.lastOrNull()
|
||||
?.structure
|
||||
?.let { assistStructure ->
|
||||
val autofillRequest = parser.parse(
|
||||
assistStructure = assistStructure,
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
)
|
||||
|
||||
when (autofillRequest) {
|
||||
is AutofillRequest.Fillable -> {
|
||||
val intentSender = createAutofillSavedItemIntentSender(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillSaveItem = autofillRequest.toAutofillSaveItem(),
|
||||
)
|
||||
|
||||
saveCallback.onSuccess(intentSender)
|
||||
}
|
||||
|
||||
AutofillRequest.Unfillable -> saveCallback.onSuccess()
|
||||
}
|
||||
}
|
||||
?: saveCallback.onSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the [fillRequest] and invoke the [FillCallback] with the response.
|
||||
*/
|
||||
|
@ -65,11 +118,18 @@ class AutofillProcessorImpl(
|
|||
val filledData = filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
)
|
||||
val saveInfo = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillRequest.partition,
|
||||
fillRequest = fillRequest,
|
||||
packageName = autofillRequest.packageName,
|
||||
)
|
||||
|
||||
// Load the [filledData] into a [FillResponse].
|
||||
// Load the filledData and saveInfo into a FillResponse.
|
||||
val response = fillResponseBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
|
||||
fillCallback.onSuccess(response)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.autofill.AutofillValue
|
||||
|
||||
/**
|
||||
* Extract a month value from this [AutofillValue].
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun AutofillValue.extractMonthValue(
|
||||
autofillOptions: List<String>?,
|
||||
): String? =
|
||||
when {
|
||||
this.isList && autofillOptions?.size == 13 -> {
|
||||
this.listValue.toString()
|
||||
}
|
||||
|
||||
this.isList && autofillOptions?.size == 12 -> {
|
||||
(this.listValue + 1).toString()
|
||||
}
|
||||
|
||||
this.isText -> this.textValue.toString()
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a text value from this [AutofillValue].
|
||||
*/
|
||||
fun AutofillValue.extractTextValue(): String? = this
|
||||
.textValue
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.toString()
|
|
@ -76,13 +76,13 @@ fun createTotpCopyIntentSender(
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an [Intent] in order to start the cipher saving process during the autofill flow.
|
||||
* Creates an [IntentSender] in order to start the cipher saving process during the autofill flow.
|
||||
*/
|
||||
fun createAutofillSavedItemIntent(
|
||||
fun createAutofillSavedItemIntentSender(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillSaveItem: AutofillSaveItem,
|
||||
): Intent =
|
||||
Intent(
|
||||
): IntentSender {
|
||||
val intent = Intent(
|
||||
autofillAppInfo.context,
|
||||
MainActivity::class.java,
|
||||
)
|
||||
|
@ -91,6 +91,16 @@ fun createAutofillSavedItemIntent(
|
|||
putExtra(AUTOFILL_SAVE_ITEM_DATA_KEY, autofillSaveItem)
|
||||
}
|
||||
|
||||
return PendingIntent
|
||||
.getActivity(
|
||||
autofillAppInfo.context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
.intentSender
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [Intent] in order to specify that there is a successful selection during a manual
|
||||
* autofill process.
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
|
||||
/**
|
||||
* The text value representation of the expiration month from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.expirationMonthSaveValue: String?
|
||||
get() = this
|
||||
.views
|
||||
.firstOrNull { it is AutofillView.Card.ExpirationMonth && it.monthValue != null }
|
||||
?.data
|
||||
?.textValue
|
||||
|
||||
/**
|
||||
* The text value representation of the year from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.expirationYearSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.ExpirationYear }
|
||||
|
||||
/**
|
||||
* The text value representation of the card number from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.numberSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.Number }
|
||||
|
||||
/**
|
||||
* The text value representation of the security code from the [AutofillPartition.Card].
|
||||
*/
|
||||
val AutofillPartition.Card.securityCodeSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Card.SecurityCode }
|
||||
|
||||
/**
|
||||
* The text value representation of the password from the [AutofillPartition.Login].
|
||||
*/
|
||||
val AutofillPartition.Login.passwordSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Login.Password }
|
||||
|
||||
/**
|
||||
* The text value representation of the username from the [AutofillPartition.Login].
|
||||
*/
|
||||
val AutofillPartition.Login.usernameSaveValue: String?
|
||||
get() = this
|
||||
.extractNonNullTextValueOrNull { it is AutofillView.Login.Username }
|
||||
|
||||
/**
|
||||
* Search [AutofillPartition.views] for an [AutofillView] that matches [condition] and has a
|
||||
* non-null text value then return that text value.
|
||||
*/
|
||||
private fun AutofillPartition.extractNonNullTextValueOrNull(
|
||||
condition: (AutofillView) -> Boolean,
|
||||
): String? =
|
||||
this
|
||||
.views
|
||||
.firstOrNull { condition(it) && it.data.textValue != null }
|
||||
?.data
|
||||
?.textValue
|
|
@ -0,0 +1,35 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
|
||||
/**
|
||||
* Convert the [AutofillRequest.Fillable] to an [AutofillSaveItem].
|
||||
*/
|
||||
fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
when (this.partition) {
|
||||
is AutofillPartition.Card -> {
|
||||
AutofillSaveItem.Card(
|
||||
number = partition.numberSaveValue,
|
||||
expirationMonth = partition.expirationMonthSaveValue,
|
||||
expirationYear = partition.expirationYearSaveValue,
|
||||
securityCode = partition.securityCodeSaveValue,
|
||||
)
|
||||
}
|
||||
|
||||
is AutofillPartition.Login -> {
|
||||
// Skip the scheme for the save value.
|
||||
val uri = this
|
||||
.uri
|
||||
?.replace("https://", "")
|
||||
?.replace("http://", "")
|
||||
?.replace("androidapp://", "")
|
||||
|
||||
AutofillSaveItem.Login(
|
||||
username = partition.usernameSaveValue,
|
||||
password = partition.passwordSaveValue,
|
||||
uri = uri,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
|||
val autofillViewData = AutofillView.Data(
|
||||
autofillId = nonNullAutofillId,
|
||||
isFocused = isFocused,
|
||||
textValue = this.autofillValue?.extractTextValue(),
|
||||
)
|
||||
buildAutofillView(
|
||||
autofillViewData = autofillViewData,
|
||||
|
@ -97,8 +98,18 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
|||
supportedHint: String?,
|
||||
): AutofillView? = when {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
val autofillOptions = this
|
||||
.autofillOptions
|
||||
?.map { it.toString() }
|
||||
val monthValue = this
|
||||
.autofillValue
|
||||
?.extractMonthValue(
|
||||
autofillOptions = autofillOptions,
|
||||
)
|
||||
|
||||
AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = monthValue,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,16 +10,14 @@ import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
|||
private const val ANDROID_APP_SCHEME: String = "androidapp"
|
||||
|
||||
/**
|
||||
* Try and build a URI. The try progression looks like this:
|
||||
* 1. Try searching traversal data for website URIs.
|
||||
* 2. Try searching traversal data for package names, if one is found, convert it into a URI.
|
||||
* 3. Try extracting a package name from [assistStructure], if one is found, convert it into a URI.
|
||||
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
|
||||
* that fails, try converting [packageName] into an Android app URI.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun List<ViewNodeTraversalData>.buildUriOrNull(
|
||||
assistStructure: AssistStructure,
|
||||
packageName: String?,
|
||||
): String? {
|
||||
// Search list of [ViewNodeTraversalData] for a website URI.
|
||||
// Search list of ViewNodeTraversalData for a website URI.
|
||||
this
|
||||
.firstOrNull { it.website != null }
|
||||
?.website
|
||||
|
@ -27,26 +25,32 @@ fun List<ViewNodeTraversalData>.buildUriOrNull(
|
|||
return websiteUri
|
||||
}
|
||||
|
||||
// Search list of [ViewNodeTraversalData] for a valid package name.
|
||||
this
|
||||
// If the package name is available, build a URI out of that.
|
||||
return packageName
|
||||
?.let { nonNullPackageName ->
|
||||
buildUri(
|
||||
domain = nonNullPackageName,
|
||||
scheme = ANDROID_APP_SCHEME,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try and build a package name. First, try searching traversal data for package names. If that
|
||||
* fails, try extracting a package name from [assistStructure].
|
||||
*/
|
||||
fun List<ViewNodeTraversalData>.buildPackageNameOrNull(
|
||||
assistStructure: AssistStructure,
|
||||
): String? {
|
||||
// Search list of ViewNodeTraversalData for a valid package name.
|
||||
val traversalDataPackageName = this
|
||||
.firstOrNull { it.idPackage != null }
|
||||
?.idPackage
|
||||
?.let { packageName ->
|
||||
return buildUri(
|
||||
domain = packageName,
|
||||
scheme = ANDROID_APP_SCHEME,
|
||||
)
|
||||
}
|
||||
|
||||
// Try getting the package name from the [AssistStructure] as a last ditch effort.
|
||||
return assistStructure
|
||||
.buildPackageNameOrNull()
|
||||
?.let { packageName ->
|
||||
buildUri(
|
||||
domain = packageName,
|
||||
scheme = ANDROID_APP_SCHEME,
|
||||
)
|
||||
}
|
||||
// Try getting the package name from the AssistStructure as a last ditch effort.
|
||||
return traversalDataPackageName
|
||||
?: assistStructure
|
||||
.buildPackageNameOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:supportsInlineSuggestions="true">
|
||||
<!--
|
||||
These compatibility packages must be kept in sync with
|
||||
/xml/autofill_service_configuration.xml
|
||||
"Maintain alphabetical order for these compatibility packages and ensure synchronization with
|
||||
both /xml/autofill_service_configuration.xml and the COMPAT_BROWSERS list within
|
||||
SaveInfoBuilderImpl.kt."
|
||||
-->
|
||||
<compatibility-package
|
||||
android:name="alook.browser"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!--
|
||||
These compatibility packages must be kept in sync with
|
||||
/xml-v30/autofill_service_configuration.xml
|
||||
"Maintain alphabetical order for these compatibility packages and ensure synchronization with
|
||||
both /xml-v30/autofill_service_configuration.xml and the COMPAT_BROWSERS list within
|
||||
SaveInfoBuilderImpl.kt."
|
||||
-->
|
||||
<compatibility-package
|
||||
android:name="alook.browser"
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.IntentSender
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.FillResponse
|
||||
import android.service.autofill.SaveInfo
|
||||
import android.view.autofill.AutofillId
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
|
@ -68,6 +69,7 @@ class FillResponseBuilderTest {
|
|||
every { this@mockk.filledItems } returns listOf(mockk())
|
||||
every { this@mockk.autofillCipher } returns autofillCipherTotpDisabled
|
||||
}
|
||||
private val saveInfo: SaveInfo = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
|
@ -117,14 +119,16 @@ class FillResponseBuilderTest {
|
|||
val actual = fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should apply FilledPartitions with filledItems and ignore ignoreAutofillIds`() {
|
||||
fun `build should apply FilledPartitions with filledItems, ignore ignoreAutofillIds, and set valid SaveInfo`() {
|
||||
// Setup
|
||||
val ignoredAutofillIdOne: AutofillId = mockk()
|
||||
val ignoredAutofillIdTwo: AutofillId = mockk()
|
||||
|
@ -147,6 +151,116 @@ class FillResponseBuilderTest {
|
|||
data = AutofillView.Data(
|
||||
autofillId = mockk(),
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
uri = null,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
every {
|
||||
filledPartitionOne.buildDataset(
|
||||
authIntentSender = intentSender,
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
} returns dataset
|
||||
every {
|
||||
filledPartitionThree.buildDataset(
|
||||
authIntentSender = null,
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
} returns dataset
|
||||
every {
|
||||
filledPartitionFour.buildDataset(
|
||||
authIntentSender = null,
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
} returns dataset
|
||||
every {
|
||||
filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
} returns vaultItemDataSet
|
||||
mockBuilder<FillResponse.Builder> {
|
||||
it.addDataset(dataset)
|
||||
it.addDataset(vaultItemDataSet)
|
||||
}
|
||||
mockBuilder<FillResponse.Builder> {
|
||||
it.setIgnoredIds(
|
||||
ignoredAutofillIdOne,
|
||||
ignoredAutofillIdTwo,
|
||||
)
|
||||
}
|
||||
mockBuilder<FillResponse.Builder> {
|
||||
it.setSaveInfo(saveInfo)
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(fillResponse, actual)
|
||||
|
||||
verify(exactly = 1) {
|
||||
filledPartitionOne.buildDataset(
|
||||
authIntentSender = intentSender,
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
filledPartitionThree.buildDataset(
|
||||
authIntentSender = null,
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
filledPartitionFour.buildDataset(
|
||||
authIntentSender = null,
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
anyConstructed<FillResponse.Builder>().addDataset(vaultItemDataSet)
|
||||
anyConstructed<FillResponse.Builder>().setIgnoredIds(
|
||||
ignoredAutofillIdOne,
|
||||
ignoredAutofillIdTwo,
|
||||
)
|
||||
anyConstructed<FillResponse.Builder>().setSaveInfo(saveInfo)
|
||||
}
|
||||
verify(exactly = 3) {
|
||||
anyConstructed<FillResponse.Builder>().addDataset(dataset)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should apply FilledPartitions with filledItems, ignore ignoreAutofillIds, and skip valid SaveInfo`() {
|
||||
// Setup
|
||||
val ignoredAutofillIdOne: AutofillId = mockk()
|
||||
val ignoredAutofillIdTwo: AutofillId = mockk()
|
||||
val ignoreAutofillIds = listOf(
|
||||
ignoredAutofillIdOne,
|
||||
ignoredAutofillIdTwo,
|
||||
)
|
||||
val filledPartitions = listOf(
|
||||
filledPartitionOne,
|
||||
filledPartitionTwo,
|
||||
filledPartitionThree,
|
||||
filledPartitionFour,
|
||||
)
|
||||
val filledData = FilledData(
|
||||
filledPartitions = filledPartitions,
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
originalPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = mockk(),
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -193,6 +307,7 @@ class FillResponseBuilderTest {
|
|||
val actual = fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
|
|
|
@ -36,6 +36,7 @@ class FilledDataBuilderTest {
|
|||
private val autofillViewData = AutofillView.Data(
|
||||
autofillId = autofillId,
|
||||
isFocused = false,
|
||||
textValue = null,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
|
@ -83,6 +84,7 @@ class FilledDataBuilderTest {
|
|||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
packageName = null,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -153,6 +155,7 @@ class FilledDataBuilderTest {
|
|||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
packageName = null,
|
||||
partition = autofillPartition,
|
||||
uri = null,
|
||||
)
|
||||
|
@ -196,6 +199,7 @@ class FilledDataBuilderTest {
|
|||
)
|
||||
val autofillViewExpirationMonth = AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = null,
|
||||
)
|
||||
val autofillViewExpirationYear = AutofillView.Card.ExpirationYear(
|
||||
data = autofillViewData,
|
||||
|
@ -216,6 +220,7 @@ class FilledDataBuilderTest {
|
|||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
packageName = null,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -302,6 +307,7 @@ class FilledDataBuilderTest {
|
|||
inlinePresentationSpec,
|
||||
),
|
||||
maxInlineSuggestionsCount = 3,
|
||||
packageName = null,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.os.Build
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveInfo
|
||||
import android.view.autofill.AutofillId
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SaveInfoBuilderTest {
|
||||
private lateinit var saveInfoBuilder: SaveInfoBuilder
|
||||
|
||||
private val settingsRepository: SettingsRepository = mockk()
|
||||
|
||||
private val autofillAppInfo: AutofillAppInfo = mockk()
|
||||
private val fillRequest: FillRequest = mockk()
|
||||
private val autofillIdOptional: AutofillId = mockk()
|
||||
private val autofillViewDataOptional = AutofillView.Data(
|
||||
autofillId = autofillIdOptional,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
)
|
||||
private val autofillIdValid: AutofillId = mockk()
|
||||
private val autofillViewDataValid = AutofillView.Data(
|
||||
autofillId = autofillIdValid,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
)
|
||||
private val autofillPartitionCard: AutofillPartition.Card = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.Number(
|
||||
data = autofillViewDataValid,
|
||||
),
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillViewDataOptional,
|
||||
),
|
||||
),
|
||||
)
|
||||
private val autofillPartitionLogin: AutofillPartition.Login = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Password(
|
||||
data = autofillViewDataValid,
|
||||
),
|
||||
AutofillView.Login.Username(
|
||||
data = autofillViewDataOptional,
|
||||
),
|
||||
),
|
||||
)
|
||||
private val saveInfo: SaveInfo = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkConstructor(SaveInfo.Builder::class)
|
||||
saveInfoBuilder = SaveInfoBuilderImpl(
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
every { anyConstructed<SaveInfo.Builder>().build() } returns saveInfo
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkConstructor(SaveInfo.Builder::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build should return null if autofill disabled`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns true
|
||||
|
||||
// Test
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillPartitionCard,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build should return null if autofill enabled and can't perform autofill`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
|
||||
// Test
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = AUTOFILL_PARTITION_LOGIN_EMPTY,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should return null if autofill possible but flags indicate compat mode and is login`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every { fillRequest.flags } returns FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST
|
||||
every { autofillAppInfo.sdkInt } returns Build.VERSION_CODES.TIRAMISU
|
||||
|
||||
// Test
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillPartitionLogin,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should return null if autofill possible but package name is in compat list and is login`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every { autofillAppInfo.sdkInt } returns Build.VERSION_CODES.P
|
||||
|
||||
// Test
|
||||
COMPAT_BROWSERS
|
||||
.forEach { packageName ->
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillPartitionLogin,
|
||||
fillRequest = fillRequest,
|
||||
packageName = packageName,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should return SaveInfo with flag set if autofill possible, flags indicate compat mode, and is card`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every { fillRequest.flags } returns FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST
|
||||
every { autofillAppInfo.sdkInt } returns Build.VERSION_CODES.TIRAMISU
|
||||
mockBuilder<SaveInfo.Builder> {
|
||||
it.setOptionalIds(arrayOf(autofillIdOptional))
|
||||
}
|
||||
mockBuilder<SaveInfo.Builder> {
|
||||
it.setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillPartitionCard,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(saveInfo, actual)
|
||||
verify(exactly = 1) {
|
||||
anyConstructed<SaveInfo.Builder>().setOptionalIds(arrayOf(autofillIdOptional))
|
||||
anyConstructed<SaveInfo.Builder>().setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `build should return SaveInfo if autofill possible, packageName is not in compat list, and is login`() {
|
||||
// Setup
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every { autofillAppInfo.sdkInt } returns Build.VERSION_CODES.P
|
||||
mockBuilder<SaveInfo.Builder> {
|
||||
it.setOptionalIds(arrayOf(autofillIdOptional))
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillPartitionLogin,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(saveInfo, actual)
|
||||
verify(exactly = 1) {
|
||||
anyConstructed<SaveInfo.Builder>().setOptionalIds(arrayOf(autofillIdOptional))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val PACKAGE_NAME: String = "com.google"
|
||||
private val AUTOFILL_PARTITION_LOGIN_EMPTY: AutofillPartition.Login = AutofillPartition.Login(
|
||||
views = listOf(),
|
||||
)
|
||||
private val COMPAT_BROWSERS: List<String> = listOf(
|
||||
"alook.browser",
|
||||
"alook.browser.google",
|
||||
"app.vanadium.browser",
|
||||
"com.amazon.cloud9",
|
||||
"com.android.browser",
|
||||
"com.android.chrome",
|
||||
"com.android.htmlviewer",
|
||||
"com.avast.android.secure.browser",
|
||||
"com.avg.android.secure.browser",
|
||||
"com.brave.browser",
|
||||
"com.brave.browser_beta",
|
||||
"com.brave.browser_default",
|
||||
"com.brave.browser_dev",
|
||||
"com.brave.browser_nightly",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.canary",
|
||||
"com.chrome.dev",
|
||||
"com.cookiegames.smartcookie",
|
||||
"com.cookiejarapps.android.smartcookieweb",
|
||||
"com.ecosia.android",
|
||||
"com.google.android.apps.chrome",
|
||||
"com.google.android.apps.chrome_dev",
|
||||
"com.google.android.captiveportallogin",
|
||||
"com.iode.firefox",
|
||||
"com.jamal2367.styx",
|
||||
"com.kiwibrowser.browser",
|
||||
"com.kiwibrowser.browser.dev",
|
||||
"com.lemurbrowser.exts",
|
||||
"com.microsoft.emmx",
|
||||
"com.microsoft.emmx.beta",
|
||||
"com.microsoft.emmx.canary",
|
||||
"com.microsoft.emmx.dev",
|
||||
"com.mmbox.browser",
|
||||
"com.mmbox.xbrowser",
|
||||
"com.mycompany.app.soulbrowser",
|
||||
"com.naver.whale",
|
||||
"com.neeva.app",
|
||||
"com.opera.browser",
|
||||
"com.opera.browser.beta",
|
||||
"com.opera.gx",
|
||||
"com.opera.mini.native",
|
||||
"com.opera.mini.native.beta",
|
||||
"com.opera.touch",
|
||||
"com.qflair.browserq",
|
||||
"com.qwant.liberty",
|
||||
"com.rainsee.create",
|
||||
"com.sec.android.app.sbrowser",
|
||||
"com.sec.android.app.sbrowser.beta",
|
||||
"com.stoutner.privacybrowser.free",
|
||||
"com.stoutner.privacybrowser.standard",
|
||||
"com.vivaldi.browser",
|
||||
"com.vivaldi.browser.snapshot",
|
||||
"com.vivaldi.browser.sopranos",
|
||||
"com.yandex.browser",
|
||||
"com.yjllq.internet",
|
||||
"com.yjllq.kito",
|
||||
"com.yujian.ResideMenuDemo",
|
||||
"com.z28j.feel",
|
||||
"idm.internet.download.manager",
|
||||
"idm.internet.download.manager.adm.lite",
|
||||
"idm.internet.download.manager.plus",
|
||||
"io.github.forkmaintainers.iceraven",
|
||||
"mark.via",
|
||||
"mark.via.gp",
|
||||
"net.dezor.browser",
|
||||
"net.slions.fulguris.full.download",
|
||||
"net.slions.fulguris.full.download.debug",
|
||||
"net.slions.fulguris.full.playstore",
|
||||
"net.slions.fulguris.full.playstore.debug",
|
||||
"org.adblockplus.browser",
|
||||
"org.adblockplus.browser.beta",
|
||||
"org.bromite.bromite",
|
||||
"org.bromite.chromium",
|
||||
"org.chromium.chrome",
|
||||
"org.codeaurora.swe.browser",
|
||||
"org.cromite.cromite",
|
||||
"org.gnu.icecat",
|
||||
"org.mozilla.fenix",
|
||||
"org.mozilla.fenix.nightly",
|
||||
"org.mozilla.fennec_aurora",
|
||||
"org.mozilla.fennec_fdroid",
|
||||
"org.mozilla.firefox",
|
||||
"org.mozilla.firefox_beta",
|
||||
"org.mozilla.reference.browser",
|
||||
"org.mozilla.rocket",
|
||||
"org.torproject.torbrowser",
|
||||
"org.torproject.torbrowser_alpha",
|
||||
"org.ungoogled.chromium.extensions.stable",
|
||||
"org.ungoogled.chromium.stable",
|
||||
"us.spotco.fennec_dos",
|
||||
)
|
|
@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
|||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildPackageNameOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildUriOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getInlinePresentationSpecs
|
||||
import com.x8bit.bitwarden.data.autofill.util.getMaxInlineSuggestionsCount
|
||||
|
@ -115,7 +116,10 @@ class AutofillParserTests {
|
|||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
} returns 0
|
||||
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure) } returns URI
|
||||
every {
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
} returns PACKAGE_NAME
|
||||
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME) } returns URI
|
||||
parser = AutofillParserImpl(
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
|
@ -184,7 +188,9 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = parentAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val parentViewNode: AssistStructure.ViewNode = mockk {
|
||||
every { this@mockk.autofillHints } returns arrayOf(parentAutofillHint)
|
||||
|
@ -206,6 +212,7 @@ class AutofillParserTests {
|
|||
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||
partition = autofillPartition,
|
||||
packageName = PACKAGE_NAME,
|
||||
uri = URI,
|
||||
)
|
||||
every { assistStructure.windowNodeCount } returns 1
|
||||
|
@ -228,7 +235,8 @@ class AutofillParserTests {
|
|||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = true,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,12 +248,15 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = false,
|
||||
textValue = null,
|
||||
),
|
||||
)
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
|
@ -255,6 +266,7 @@ class AutofillParserTests {
|
|||
ignoreAutofillIds = emptyList(),
|
||||
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||
packageName = PACKAGE_NAME,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -278,7 +290,8 @@ class AutofillParserTests {
|
|||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = true,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,12 +303,15 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = false,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
)
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
|
@ -305,6 +321,7 @@ class AutofillParserTests {
|
|||
ignoreAutofillIds = emptyList(),
|
||||
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||
packageName = PACKAGE_NAME,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -328,7 +345,8 @@ class AutofillParserTests {
|
|||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = true,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,12 +358,15 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
)
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
|
@ -355,6 +376,7 @@ class AutofillParserTests {
|
|||
ignoreAutofillIds = emptyList(),
|
||||
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||
packageName = PACKAGE_NAME,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -378,7 +400,8 @@ class AutofillParserTests {
|
|||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = true,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -391,12 +414,15 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
)
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
|
@ -406,6 +432,7 @@ class AutofillParserTests {
|
|||
ignoreAutofillIds = emptyList(),
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
packageName = PACKAGE_NAME,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -429,7 +456,8 @@ class AutofillParserTests {
|
|||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -442,12 +470,15 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
)
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
|
@ -457,6 +488,7 @@ class AutofillParserTests {
|
|||
ignoreAutofillIds = emptyList(),
|
||||
inlinePresentationSpecs = emptyList(),
|
||||
maxInlineSuggestionsCount = 0,
|
||||
packageName = PACKAGE_NAME,
|
||||
partition = autofillPartition,
|
||||
uri = URI,
|
||||
)
|
||||
|
@ -480,7 +512,8 @@ class AutofillParserTests {
|
|||
autofillAppInfo = autofillAppInfo,
|
||||
isInlineAutofillEnabled = false,
|
||||
)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -492,12 +525,15 @@ class AutofillParserTests {
|
|||
data = AutofillView.Data(
|
||||
autofillId = cardAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
monthValue = null,
|
||||
)
|
||||
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = loginAutofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
)
|
||||
val remoteBlockList = listOf(
|
||||
|
@ -512,7 +548,7 @@ class AutofillParserTests {
|
|||
fun testBlockListedUri(blockListedUri: String) {
|
||||
// Setup
|
||||
every {
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
} returns blockListedUri
|
||||
|
||||
// Test
|
||||
|
@ -531,7 +567,8 @@ class AutofillParserTests {
|
|||
|
||||
// Verify all tests
|
||||
verify(exactly = BLOCK_LISTED_URIS.size + remoteBlockList.size) {
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -554,5 +591,6 @@ private val BLOCK_LISTED_URIS: List<String> = listOf(
|
|||
)
|
||||
private const val ID_PACKAGE: String = "com.x8bit.bitwarden"
|
||||
private const val MAX_INLINE_SUGGESTION_COUNT: Int = 42
|
||||
private const val PACKAGE_NAME: String = "com.google"
|
||||
private const val URI: String = "androidapp://com.google"
|
||||
private const val WEBSITE: String = "https://www.google.com"
|
||||
|
|
|
@ -1,23 +1,39 @@
|
|||
package com.x8bit.bitwarden.data.autofill.processor
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.IntentSender
|
||||
import android.os.CancellationSignal
|
||||
import android.service.autofill.FillCallback
|
||||
import android.service.autofill.FillContext
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.FillResponse
|
||||
import android.service.autofill.SaveCallback
|
||||
import android.service.autofill.SaveInfo
|
||||
import android.service.autofill.SaveRequest
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.SaveInfoBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSavedItemIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.confirmVerified
|
||||
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
|
@ -35,6 +51,9 @@ class AutofillProcessorTest {
|
|||
private val filledDataBuilder: FilledDataBuilder = mockk()
|
||||
private val fillResponseBuilder: FillResponseBuilder = mockk()
|
||||
private val parser: AutofillParser = mockk()
|
||||
private val policyManager: PolicyManager = mockk()
|
||||
private val saveInfoBuilder: SaveInfoBuilder = mockk()
|
||||
private val settingsRepository: SettingsRepository = mockk()
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
private val appInfo: AutofillAppInfo = AutofillAppInfo(
|
||||
|
@ -46,6 +65,8 @@ class AutofillProcessorTest {
|
|||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(::createAutofillSavedItemIntentSender)
|
||||
mockkStatic(AutofillRequest.Fillable::toAutofillSaveItem)
|
||||
every { dispatcherManager.unconfined } returns testDispatcher
|
||||
|
||||
autofillProcessor = AutofillProcessorImpl(
|
||||
|
@ -53,6 +74,9 @@ class AutofillProcessorTest {
|
|||
filledDataBuilder = filledDataBuilder,
|
||||
fillResponseBuilder = fillResponseBuilder,
|
||||
parser = parser,
|
||||
policyManager = policyManager,
|
||||
saveInfoBuilder = saveInfoBuilder,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -61,13 +85,8 @@ class AutofillProcessorTest {
|
|||
verify(exactly = 1) {
|
||||
dispatcherManager.unconfined
|
||||
}
|
||||
confirmVerified(
|
||||
cancellationSignal,
|
||||
dispatcherManager,
|
||||
filledDataBuilder,
|
||||
fillResponseBuilder,
|
||||
parser,
|
||||
)
|
||||
unmockkStatic(::createAutofillSavedItemIntentSender)
|
||||
unmockkStatic(AutofillRequest.Fillable::toAutofillSaveItem)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -117,7 +136,12 @@ class AutofillProcessorTest {
|
|||
isVaultLocked = false,
|
||||
)
|
||||
val fillResponse: FillResponse = mockk()
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk()
|
||||
val autofillPartition: AutofillPartition = mockk()
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk {
|
||||
every { packageName } returns PACKAGE_NAME
|
||||
every { partition } returns autofillPartition
|
||||
}
|
||||
val saveInfo: SaveInfo = mockk()
|
||||
coEvery {
|
||||
filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
|
@ -130,10 +154,19 @@ class AutofillProcessorTest {
|
|||
fillRequest = fillRequest,
|
||||
)
|
||||
} returns autofillRequest
|
||||
every {
|
||||
saveInfoBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
autofillPartition = autofillPartition,
|
||||
fillRequest = fillRequest,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
} returns saveInfo
|
||||
every {
|
||||
fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
} returns fillResponse
|
||||
every { fillCallback.onSuccess(fillResponse) } just runs
|
||||
|
@ -161,8 +194,198 @@ class AutofillProcessorTest {
|
|||
fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
fillCallback.onSuccess(fillResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processSaveRequest should invoke empty callback when autofill prompt disabled`() {
|
||||
// Setup
|
||||
val saveCallback: SaveCallback = mockk {
|
||||
every { onSuccess() } just runs
|
||||
}
|
||||
val saveRequest: SaveRequest = mockk()
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns true
|
||||
|
||||
// Test
|
||||
autofillProcessor.processSaveRequest(
|
||||
autofillAppInfo = appInfo,
|
||||
request = saveRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isAutofillSavePromptDisabled
|
||||
saveCallback.onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processSaveRequest should invoke empty callback when personal ownership applies`() {
|
||||
// Setup
|
||||
val saveCallback: SaveCallback = mockk {
|
||||
every { onSuccess() } just runs
|
||||
}
|
||||
val saveRequest: SaveRequest = mockk()
|
||||
val policies: List<SyncResponseJson.Policy> = listOf(mockk())
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every {
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
} returns policies
|
||||
|
||||
// Test
|
||||
autofillProcessor.processSaveRequest(
|
||||
autofillAppInfo = appInfo,
|
||||
request = saveRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isAutofillSavePromptDisabled
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
saveCallback.onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processSaveRequest should invoke empty callback when no fill contexts`() {
|
||||
// Setup
|
||||
val saveCallback: SaveCallback = mockk {
|
||||
every { onSuccess() } just runs
|
||||
}
|
||||
val saveRequest: SaveRequest = mockk {
|
||||
every { fillContexts } returns emptyList()
|
||||
}
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every {
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
} returns emptyList()
|
||||
|
||||
// Test
|
||||
autofillProcessor.processSaveRequest(
|
||||
autofillAppInfo = appInfo,
|
||||
request = saveRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isAutofillSavePromptDisabled
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
saveCallback.onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processSaveRequest should invoke intentSender callback when autofill enabled, has fill contexts, and parser returns Fillable`() {
|
||||
// Setup
|
||||
val intentSender: IntentSender = mockk()
|
||||
val saveCallback: SaveCallback = mockk {
|
||||
every { onSuccess(intentSender) } just runs
|
||||
}
|
||||
val assistStructure: AssistStructure = mockk()
|
||||
val fillContext: FillContext = mockk {
|
||||
every { structure } returns assistStructure
|
||||
}
|
||||
val saveRequest: SaveRequest = mockk {
|
||||
every { fillContexts } returns listOf(fillContext)
|
||||
}
|
||||
val autofillPartition: AutofillPartition = mockk()
|
||||
val autofillSaveItem: AutofillSaveItem = mockk()
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk {
|
||||
every { packageName } returns PACKAGE_NAME
|
||||
every { partition } returns autofillPartition
|
||||
every { toAutofillSaveItem() } returns autofillSaveItem
|
||||
}
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every {
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
} returns emptyList()
|
||||
every {
|
||||
parser.parse(
|
||||
autofillAppInfo = appInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
} returns autofillRequest
|
||||
every {
|
||||
createAutofillSavedItemIntentSender(
|
||||
autofillAppInfo = appInfo,
|
||||
autofillSaveItem = autofillSaveItem,
|
||||
)
|
||||
} returns intentSender
|
||||
|
||||
// Test
|
||||
autofillProcessor.processSaveRequest(
|
||||
autofillAppInfo = appInfo,
|
||||
request = saveRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isAutofillSavePromptDisabled
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
parser.parse(
|
||||
autofillAppInfo = appInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
createAutofillSavedItemIntentSender(
|
||||
autofillAppInfo = appInfo,
|
||||
autofillSaveItem = autofillSaveItem,
|
||||
)
|
||||
saveCallback.onSuccess(intentSender)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `processSaveRequest should invoke empty callback when autofill enabled, has fill contexts, and parser returns Unfillable`() {
|
||||
// Setup
|
||||
val saveCallback: SaveCallback = mockk {
|
||||
every { onSuccess() } just runs
|
||||
}
|
||||
val assistStructure: AssistStructure = mockk()
|
||||
val fillContext: FillContext = mockk {
|
||||
every { structure } returns assistStructure
|
||||
}
|
||||
val saveRequest: SaveRequest = mockk {
|
||||
every { fillContexts } returns listOf(fillContext)
|
||||
}
|
||||
val autofillRequest = AutofillRequest.Unfillable
|
||||
every { settingsRepository.isAutofillSavePromptDisabled } returns false
|
||||
every {
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
} returns emptyList()
|
||||
every {
|
||||
parser.parse(
|
||||
autofillAppInfo = appInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
} returns autofillRequest
|
||||
|
||||
// Test
|
||||
autofillProcessor.processSaveRequest(
|
||||
autofillAppInfo = appInfo,
|
||||
request = saveRequest,
|
||||
saveCallback = saveCallback,
|
||||
)
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.isAutofillSavePromptDisabled
|
||||
policyManager.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
parser.parse(
|
||||
autofillAppInfo = appInfo,
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
saveCallback.onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val PACKAGE_NAME: String = "com.google"
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
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 AutofillPartitionExtensionsTest {
|
||||
private val autofillDataEmptyText: AutofillView.Data = AutofillView.Data(
|
||||
autofillId = mockk(),
|
||||
isFocused = false,
|
||||
textValue = null,
|
||||
)
|
||||
private val autofillDataValidText: AutofillView.Data = AutofillView.Data(
|
||||
autofillId = mockk(),
|
||||
isFocused = false,
|
||||
textValue = TEXT_VALUE,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `expirationMonthSaveValue should return null when no month views present`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.Number(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.expirationMonthSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expirationMonthSaveValue should return null when has month view but no monthValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.ExpirationMonth(
|
||||
data = autofillDataValidText,
|
||||
monthValue = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.expirationMonthSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expirationMonthSaveValue should return text value when has month view has monthValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.ExpirationMonth(
|
||||
data = autofillDataValidText,
|
||||
monthValue = TEXT_VALUE,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.expirationMonthSaveValue
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expirationYearSaveValue should return null when no year views present`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.Number(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.expirationYearSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expirationYearSaveValue should return null when has year view but no textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillDataEmptyText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.expirationYearSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expirationYearSaveValue should return text value when has year view has textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.expirationYearSaveValue
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `numberSaveValue should return null when no number views present`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.numberSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `numberSaveValue should return null when has number view but no textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.Number(
|
||||
data = autofillDataEmptyText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.numberSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `numberSaveValue should return text value when has number view has textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.Number(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.numberSaveValue
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `securityCodeSaveValue should return null when no code views present`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.ExpirationYear(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.securityCodeSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `securityCodeSaveValue should return null when has code view but no textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillDataEmptyText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.securityCodeSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `securityCodeSaveValue should return text value when has code view has textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Card(
|
||||
views = listOf(
|
||||
AutofillView.Card.SecurityCode(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.securityCodeSaveValue
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordSaveValue should return null when no password views present`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Username(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.passwordSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordSaveValue should return null when has password view but no textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Password(
|
||||
data = autofillDataEmptyText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.passwordSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordSaveValue should return text value when has password view has textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Password(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.passwordSaveValue
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `usernameSaveValue should return null when no username views present`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Password(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.usernameSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `usernameSaveValue should return null when has username view but no textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Username(
|
||||
data = autofillDataEmptyText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.passwordSaveValue
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `usernameSaveValue should return text value when has username view has textValue`() {
|
||||
// Setup
|
||||
val autofillPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Username(
|
||||
data = autofillDataValidText,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillPartition.usernameSaveValue
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val TEXT_VALUE: String = "TEXT_VALUE"
|
|
@ -0,0 +1,97 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AutofillRequestExtensionsTest {
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(AUTOFILL_REQUEST_EXTENSIONS_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(AUTOFILL_REQUEST_EXTENSIONS_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillSaveItem should return AutofillSaveItem Card when card partition`() {
|
||||
// Setup
|
||||
val autofillPartition: AutofillPartition.Card = mockk {
|
||||
every { expirationMonthSaveValue } returns SAVE_VALUE_MONTH
|
||||
every { expirationYearSaveValue } returns SAVE_VALUE_YEAR
|
||||
every { numberSaveValue } returns SAVE_VALUE_NUMBER
|
||||
every { securityCodeSaveValue } returns SAVE_VALUE_CODE
|
||||
}
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk {
|
||||
every { partition } returns autofillPartition
|
||||
}
|
||||
val expected = AutofillSaveItem.Card(
|
||||
number = SAVE_VALUE_NUMBER,
|
||||
expirationMonth = SAVE_VALUE_MONTH,
|
||||
expirationYear = SAVE_VALUE_YEAR,
|
||||
securityCode = SAVE_VALUE_CODE,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillRequest.toAutofillSaveItem()
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toAutofillSaveItem should return AutofillSaveItem Login when card partition`() {
|
||||
RAW_URI_LIST
|
||||
.forEach { rawUri ->
|
||||
// Setup
|
||||
val autofillPartition: AutofillPartition.Login = mockk {
|
||||
every { usernameSaveValue } returns SAVE_VALUE_USERNAME
|
||||
every { passwordSaveValue } returns SAVE_VALUE_PASSWORD
|
||||
}
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk {
|
||||
every { partition } returns autofillPartition
|
||||
every { uri } returns rawUri
|
||||
}
|
||||
val expected = AutofillSaveItem.Login(
|
||||
username = SAVE_VALUE_USERNAME,
|
||||
password = SAVE_VALUE_PASSWORD,
|
||||
uri = FINAL_URI,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = autofillRequest.toAutofillSaveItem()
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val AUTOFILL_REQUEST_EXTENSIONS_PATH =
|
||||
"com.x8bit.bitwarden.data.autofill.util.AutofillPartitionExtensionsKt"
|
||||
|
||||
// CARD DATA
|
||||
private const val SAVE_VALUE_CODE: String = "SAVE_VALUE_CODE"
|
||||
private const val SAVE_VALUE_MONTH: String = "SAVE_VALUE_MONTH"
|
||||
private const val SAVE_VALUE_NUMBER: String = "SAVE_VALUE_NUMBER"
|
||||
private const val SAVE_VALUE_YEAR: String = "SAVE_VALUE_YEAR"
|
||||
|
||||
// LOGIN DATA
|
||||
private const val SAVE_VALUE_PASSWORD: String = "SAVE_VALUE_PASSWORD"
|
||||
private const val SAVE_VALUE_USERNAME: String = "SAVE_VALUE_USERNAME"
|
||||
private const val FINAL_URI: String = "URI"
|
||||
private val RAW_URI_LIST: List<String> = listOf(
|
||||
"androidapp://URI",
|
||||
"https://URI",
|
||||
"http://URI",
|
||||
)
|
|
@ -0,0 +1,108 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.view.autofill.AutofillValue
|
||||
import io.mockk.every
|
||||
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 AutofillValueExtensionsTest {
|
||||
@Test
|
||||
fun `extractMonthValue should return listValue when isList and options size 13`() {
|
||||
// Setup
|
||||
val autofillOptions = List(13) { "option-$it" }
|
||||
val autofillValue: AutofillValue = mockk {
|
||||
every { isList } returns true
|
||||
every { listValue } returns LIST_VALUE
|
||||
}
|
||||
val expected = LIST_VALUE.toString()
|
||||
|
||||
// Test
|
||||
val actual = autofillValue.extractMonthValue(autofillOptions)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractMonthValue should return listValue plus one when isList and options size 12`() {
|
||||
// Setup
|
||||
val autofillOptions = List(12) { "option-$it" }
|
||||
val autofillValue: AutofillValue = mockk {
|
||||
every { isList } returns true
|
||||
every { listValue } returns LIST_VALUE
|
||||
}
|
||||
val expected = (LIST_VALUE + 1).toString()
|
||||
|
||||
// Test
|
||||
val actual = autofillValue.extractMonthValue(autofillOptions)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractMonthValue should return textValue when isText`() {
|
||||
// Setup
|
||||
val autofillOptions = List(1) { "option-$it" }
|
||||
val autofillValue: AutofillValue = mockk {
|
||||
every { isList } returns false
|
||||
every { isText } returns true
|
||||
every { textValue } returns TEXT_VALUE
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = autofillValue.extractMonthValue(autofillOptions)
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractMonthValue should return null not list or text`() {
|
||||
// Setup
|
||||
val autofillOptions = List(1) { "option-$it" }
|
||||
val autofillValue: AutofillValue = mockk {
|
||||
every { isList } returns false
|
||||
every { isText } returns false
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = autofillValue.extractMonthValue(autofillOptions)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractTextValue should return textValue when not blank`() {
|
||||
// Setup
|
||||
val autofillValue: AutofillValue = mockk {
|
||||
every { textValue } returns TEXT_VALUE
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = autofillValue.extractTextValue()
|
||||
|
||||
// Verify
|
||||
assertEquals(TEXT_VALUE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractTextValue should return null when not blank`() {
|
||||
// Setup
|
||||
val autofillValue: AutofillValue = mockk {
|
||||
every { textValue } returns " "
|
||||
}
|
||||
|
||||
// Test
|
||||
val actual = autofillValue.extractTextValue()
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val LIST_VALUE: Int = 5
|
||||
private const val TEXT_VALUE: String = "TEXT_VALUE"
|
|
@ -20,6 +20,7 @@ class AutofillViewExtensionsTest {
|
|||
private val autofillViewData = AutofillView.Data(
|
||||
autofillId = autofillId,
|
||||
isFocused = false,
|
||||
textValue = null,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
|
|
|
@ -70,6 +70,7 @@ class FilledDataExtensionsTest {
|
|||
data = AutofillView.Data(
|
||||
autofillId = autofillId,
|
||||
isFocused = true,
|
||||
textValue = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.assist.AssistStructure
|
|||
import android.view.View
|
||||
import android.view.ViewStructure.HtmlInfo
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -23,10 +24,18 @@ class ViewNodeExtensionsTest {
|
|||
private val autofillViewData = AutofillView.Data(
|
||||
autofillId = expectedAutofillId,
|
||||
isFocused = expectedIsFocused,
|
||||
textValue = TEXT_VALUE,
|
||||
)
|
||||
private val testAutofillOptions = arrayOf(
|
||||
"option one",
|
||||
"option two",
|
||||
)
|
||||
private val testAutofillValue: AutofillValue = mockk()
|
||||
|
||||
private val viewNode: AssistStructure.ViewNode = mockk {
|
||||
every { this@mockk.autofillId } returns expectedAutofillId
|
||||
every { this@mockk.autofillOptions } returns testAutofillOptions
|
||||
every { this@mockk.autofillValue } returns testAutofillValue
|
||||
every { this@mockk.childCount } returns 0
|
||||
every { this@mockk.inputType } returns 1
|
||||
every { this@mockk.isFocused } returns expectedIsFocused
|
||||
|
@ -38,6 +47,14 @@ class ViewNodeExtensionsTest {
|
|||
mockkStatic(HtmlInfo::isPasswordField)
|
||||
mockkStatic(Int::isPasswordInputType)
|
||||
mockkStatic(Int::isUsernameInputType)
|
||||
mockkStatic(AutofillValue::extractMonthValue)
|
||||
mockkStatic(AutofillValue::extractTextValue)
|
||||
every {
|
||||
testAutofillValue.extractMonthValue(
|
||||
autofillOptions = testAutofillOptions.toList(),
|
||||
)
|
||||
} returns MONTH_VALUE
|
||||
every { testAutofillValue.extractTextValue() } returns TEXT_VALUE
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
@ -46,6 +63,8 @@ class ViewNodeExtensionsTest {
|
|||
unmockkStatic(HtmlInfo::isPasswordField)
|
||||
unmockkStatic(Int::isPasswordInputType)
|
||||
unmockkStatic(Int::isUsernameInputType)
|
||||
unmockkStatic(AutofillValue::extractMonthValue)
|
||||
unmockkStatic(AutofillValue::extractTextValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -54,6 +73,7 @@ class ViewNodeExtensionsTest {
|
|||
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH
|
||||
val expected = AutofillView.Card.ExpirationMonth(
|
||||
data = autofillViewData,
|
||||
monthValue = MONTH_VALUE,
|
||||
)
|
||||
every { viewNode.autofillHints } returns arrayOf(autofillHint)
|
||||
|
||||
|
@ -474,3 +494,5 @@ private val SUPPORTED_RAW_USERNAME_HINTS: List<String> = listOf(
|
|||
"phone",
|
||||
"username",
|
||||
)
|
||||
private const val MONTH_VALUE: String = "MONTH_VALUE"
|
||||
private const val TEXT_VALUE: String = "TEXT_VALUE"
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
|||
import io.mockk.every
|
||||
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 ViewNodeTraversalDataExtensionsTest {
|
||||
|
@ -17,6 +18,7 @@ class ViewNodeTraversalDataExtensionsTest {
|
|||
private val autofillViewData = AutofillView.Data(
|
||||
autofillId = mockk(),
|
||||
isFocused = false,
|
||||
textValue = null,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
@ -31,7 +33,7 @@ class ViewNodeTraversalDataExtensionsTest {
|
|||
|
||||
// Test
|
||||
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
|
||||
assistStructure = assistStructure,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
|
@ -39,27 +41,7 @@ class ViewNodeTraversalDataExtensionsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `buildUriOrNull should return idPackage URI when WEBSITE is null`() {
|
||||
// Setup
|
||||
val viewNodeTraversalData = ViewNodeTraversalData(
|
||||
autofillViews = emptyList(),
|
||||
idPackage = ID_PACKAGE,
|
||||
ignoreAutofillIds = emptyList(),
|
||||
website = null,
|
||||
)
|
||||
val expected = "androidapp://$ID_PACKAGE"
|
||||
|
||||
// Test
|
||||
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildUriOrNull should return title URI when website and idPackage are null`() {
|
||||
fun `buildUriOrNull should return package name URI when website is null`() {
|
||||
// Setup
|
||||
val viewNodeTraversalData = ViewNodeTraversalData(
|
||||
autofillViews = emptyList(),
|
||||
|
@ -67,20 +49,77 @@ class ViewNodeTraversalDataExtensionsTest {
|
|||
ignoreAutofillIds = emptyList(),
|
||||
website = null,
|
||||
)
|
||||
val expected = "androidapp://com.x8bit.bitwarden"
|
||||
every { windowNode.title } returns "com.x8bit.bitwarden/path.deeper.into.app"
|
||||
val expected = "androidapp://$PACKAGE_NAME"
|
||||
|
||||
// Test
|
||||
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
|
||||
assistStructure = assistStructure,
|
||||
packageName = PACKAGE_NAME,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ID_PACKAGE: String = "com.x8bit.bitwarden"
|
||||
private const val WEBSITE: String = "https://www.google.com"
|
||||
@Test
|
||||
fun `buildUriOrNull should return null when website and packageName are null`() {
|
||||
// Setup
|
||||
val viewNodeTraversalData = ViewNodeTraversalData(
|
||||
autofillViews = emptyList(),
|
||||
idPackage = null,
|
||||
ignoreAutofillIds = emptyList(),
|
||||
website = null,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
|
||||
packageName = null,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildPackageNameOrNull should return idPackage when available`() {
|
||||
// Setup
|
||||
val viewNodeTraversalData = ViewNodeTraversalData(
|
||||
autofillViews = emptyList(),
|
||||
idPackage = ID_PACKAGE,
|
||||
ignoreAutofillIds = emptyList(),
|
||||
website = null,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = listOf(viewNodeTraversalData).buildPackageNameOrNull(
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(ID_PACKAGE, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildPackageNameOrNull should return title URI when idPackage is null`() {
|
||||
// Setup
|
||||
val viewNodeTraversalData = ViewNodeTraversalData(
|
||||
autofillViews = emptyList(),
|
||||
idPackage = null,
|
||||
ignoreAutofillIds = emptyList(),
|
||||
website = null,
|
||||
)
|
||||
val expected = "com.x8bit.bitwarden"
|
||||
every { windowNode.title } returns "com.x8bit.bitwarden/path.deeper.into.app"
|
||||
|
||||
// Test
|
||||
val actual = listOf(viewNodeTraversalData).buildPackageNameOrNull(
|
||||
assistStructure = assistStructure,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val ID_PACKAGE: String = "com.x8bit.bitwarden"
|
||||
private const val PACKAGE_NAME: String = "com.google"
|
||||
private const val WEBSITE: String = "https://www.google.com"
|
||||
|
|
Loading…
Reference in a new issue