Add URI generation algorithm to autofill parsing (#582)

This commit is contained in:
Lucas Kivi
2024-01-12 12:45:56 -06:00
committed by Álison Fernandes
parent e9e538db59
commit 197feea56a
10 changed files with 549 additions and 12 deletions

View File

@@ -13,6 +13,7 @@ sealed class AutofillRequest {
data class Fillable(
val ignoreAutofillIds: List<AutofillId>,
val partition: AutofillPartition,
val uri: String?,
) : AutofillRequest()
/**

View File

@@ -11,11 +11,26 @@ sealed class AutofillView {
*/
abstract val autofillId: AutofillId
/**
* The package id for this view, if there is one. (ex: "com.x8bit.bitwarden")
*/
abstract val idPackage: String?
/**
* Whether the view is currently focused.
*/
abstract val isFocused: Boolean
/**
* The web domain for this view, if there is one. (ex: "m.facebook.com")
*/
abstract val webDomain: String?
/**
* The web scheme for this view, if there is one. (ex: "https")
*/
abstract val webScheme: String?
/**
* A view that corresponds to the card data partition for autofill fields.
*/
@@ -26,7 +41,10 @@ sealed class AutofillView {
*/
data class ExpirationMonth(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Card()
/**
@@ -34,7 +52,10 @@ sealed class AutofillView {
*/
data class ExpirationYear(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Card()
/**
@@ -42,7 +63,10 @@ sealed class AutofillView {
*/
data class Number(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Card()
/**
@@ -50,7 +74,10 @@ sealed class AutofillView {
*/
data class SecurityCode(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Card()
}
@@ -64,7 +91,10 @@ sealed class AutofillView {
*/
data class EmailAddress(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Login()
/**
@@ -72,7 +102,10 @@ sealed class AutofillView {
*/
data class Password(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Login()
/**
@@ -80,7 +113,10 @@ sealed class AutofillView {
*/
data class Username(
override val autofillId: AutofillId,
override val idPackage: String?,
override val isFocused: Boolean,
override val webDomain: String?,
override val webScheme: String?,
) : Login()
}
}

View File

@@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.autofill.model
import android.view.autofill.AutofillId
/**
* A convenience data structure for view node traversal.
*/
data class ViewNodeTraversalData(
val autofillViews: List<AutofillView>,
val ignoreAutofillIds: List<AutofillId>,
)

View File

@@ -5,6 +5,8 @@ import android.view.autofill.AutofillId
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.buildUriOrNull
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
/**
@@ -14,9 +16,9 @@ import com.x8bit.bitwarden.data.autofill.util.toAutofillView
class AutofillParserImpl : AutofillParser {
override fun parse(assistStructure: AssistStructure): AutofillRequest {
// Parse the `assistStructure` into internal models.
val traversalData = assistStructure.traverse()
val traversalDataList = assistStructure.traverse()
// Flatten the autofill views for processing.
val autofillViews = traversalData
val autofillViews = traversalDataList
.map { it.autofillViews }
.flatten()
@@ -25,6 +27,10 @@ class AutofillParserImpl : AutofillParser {
.firstOrNull { it.isFocused }
?: return AutofillRequest.Unfillable
val uri = traversalDataList.buildUriOrNull(
assistStructure = assistStructure,
)
// Choose the first focused partition of data for fulfillment.
val partition = when (focusedView) {
is AutofillView.Card -> {
@@ -40,13 +46,14 @@ class AutofillParserImpl : AutofillParser {
}
}
// Flatten the ignorable autofill ids.
val ignoreAutofillIds = traversalData
val ignoreAutofillIds = traversalDataList
.map { it.ignoreAutofillIds }
.flatten()
return AutofillRequest.Fillable(
ignoreAutofillIds = ignoreAutofillIds,
partition = partition,
uri = uri,
)
}
}
@@ -92,11 +99,3 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
ignoreAutofillIds = mutableIgnoreAutofillIdList,
)
}
/**
* A convenience data structure for view node traversal.
*/
private data class ViewNodeTraversalData(
val autofillViews: List<AutofillView>,
val ignoreAutofillIds: List<AutofillId>,
)

View File

@@ -18,8 +18,11 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = autofillId
?.let { supportedHint ->
buildAutofillView(
autofillId = nonNullAutofillId,
idPackage = idPackage,
isFocused = isFocused,
hint = supportedHint,
webDomain = webDomain,
webScheme = webScheme,
)
}
}
@@ -27,58 +30,82 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = autofillId
/**
* Convert the data into an [AutofillView] if the [hint] is supported.
*/
@Suppress("LongMethod")
@Suppress("LongMethod", "LongParameterList")
private fun buildAutofillView(
autofillId: AutofillId,
idPackage: String?,
isFocused: Boolean,
hint: String,
webDomain: String?,
webScheme: String?,
): AutofillView? = when (hint) {
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
AutofillView.Card.ExpirationMonth(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
AutofillView.Card.ExpirationYear(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
AutofillView.Card.Number(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
AutofillView.Card.SecurityCode(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}
View.AUTOFILL_HINT_EMAIL_ADDRESS -> {
AutofillView.Login.EmailAddress(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}
View.AUTOFILL_HINT_PASSWORD -> {
AutofillView.Login.Password(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}
View.AUTOFILL_HINT_USERNAME -> {
AutofillView.Login.Username(
autofillId = autofillId,
idPackage = idPackage,
isFocused = isFocused,
webDomain = webDomain,
webScheme = webScheme,
)
}

View File

@@ -0,0 +1,101 @@
package com.x8bit.bitwarden.data.autofill.util
import android.app.assist.AssistStructure
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"
/**
* The default web URI scheme.
*/
private const val DEFAULT_SCHEME: String = "https"
/**
* 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.
*/
@Suppress("ReturnCount")
fun List<ViewNodeTraversalData>.buildUriOrNull(
assistStructure: AssistStructure,
): String? {
// Search list of [ViewNodeTraversalData] for a website URI.
buildWebsiteUriOrNull()
?.let { websiteUri ->
return websiteUri
}
// Search list of [ViewNodeTraversalData] for a valid package name.
buildPackageNameOrNull()
?.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,
)
}
}
/**
* Combine [domain] and [scheme] into a URI.
*/
private fun buildUri(
domain: String,
scheme: String,
): String = "$scheme://$domain"
/**
* Attempt to extract the package name from the title of the [AssistStructure].
*
* As an example, the title might look like this: com.facebook.katana/com.facebook.bloks.facebook...
* Then this function would return: com.facebook.katana
*/
private fun AssistStructure.buildPackageNameOrNull(): String? = if (windowNodeCount > 0) {
getWindowNodeAt(0)
.title
?.toString()
?.orNullIfBlank()
?.split('/')
?.firstOrNull()
} else {
null
}
/**
* Search each [ViewNodeTraversalData.autofillViews] list for a valid package id. If one is found
* return it and terminate the search.
*/
private fun List<ViewNodeTraversalData>.buildPackageNameOrNull(): String? =
flatMap { it.autofillViews }
.firstOrNull { !it.idPackage.isNullOrEmpty() }
?.idPackage
/**
* Search each [ViewNodeTraversalData.autofillViews] list for a valid web domain. If one is found,
* combine it with its scheme and return it.
*/
private fun List<ViewNodeTraversalData>.buildWebsiteUriOrNull(): String? =
flatMap { it.autofillViews }
.firstOrNull { !it.webDomain.isNullOrEmpty() }
?.let { autofillView ->
val webDomain = requireNotNull(autofillView.webDomain)
val webScheme = autofillView.webScheme.orNullIfBlank() ?: DEFAULT_SCHEME
buildUri(
domain = webDomain,
scheme = webScheme,
)
}