BIT-1457: Setup autofill save request (#898)

This commit is contained in:
Lucas Kivi
2024-01-31 22:55:22 -06:00
committed by Álison Fernandes
parent 8bb754f85b
commit 81c78fc115
32 changed files with 1906 additions and 92 deletions

View File

@@ -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,
)
}
}

View File

@@ -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?
}

View File

@@ -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 ->

View File

@@ -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?
}

View File

@@ -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",
)

View File

@@ -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,
)
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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()
/**

View File

@@ -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,
)

View File

@@ -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,
)
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -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.

View File

@@ -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

View File

@@ -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,
)
}
}

View File

@@ -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,
)
}

View File

@@ -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()
}
/**