mirror of
https://github.com/bitwarden/android.git
synced 2026-06-07 23:58:03 -05:00
Add URI generation algorithm to autofill parsing (#582)
This commit is contained in:
committed by
Álison Fernandes
parent
e9e538db59
commit
197feea56a
@@ -13,6 +13,7 @@ sealed class AutofillRequest {
|
||||
data class Fillable(
|
||||
val ignoreAutofillIds: List<AutofillId>,
|
||||
val partition: AutofillPartition,
|
||||
val uri: String?,
|
||||
) : AutofillRequest()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user