mirror of
https://github.com/bitwarden/android.git
synced 2026-06-05 04:06:34 -05:00
BIT-1457: Setup autofill save request (#898)
This commit is contained in:
committed by
Álison Fernandes
parent
8bb754f85b
commit
81c78fc115
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user