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

View file

@ -27,7 +27,10 @@ class FilledDataBuilderTest {
val autofillId: AutofillId = mockk()
val autofillView = AutofillView.Login.Username(
autofillId = autofillId,
idPackage = null,
isFocused = false,
webDomain = null,
webScheme = null,
)
val autofillPartition = AutofillPartition.Login(
views = listOf(autofillView),
@ -36,6 +39,7 @@ class FilledDataBuilderTest {
val autofillRequest = AutofillRequest.Fillable(
ignoreAutofillIds = ignoreAutofillIds,
partition = autofillPartition,
uri = URI,
)
val filledItem = FilledItem(
autofillId = autofillId,
@ -67,7 +71,10 @@ class FilledDataBuilderTest {
val autofillId: AutofillId = mockk()
val autofillView = AutofillView.Card.Number(
autofillId = autofillId,
idPackage = null,
isFocused = false,
webDomain = null,
webScheme = null,
)
val autofillPartition = AutofillPartition.Card(
views = listOf(autofillView),
@ -76,6 +83,7 @@ class FilledDataBuilderTest {
val autofillRequest = AutofillRequest.Fillable(
ignoreAutofillIds = ignoreAutofillIds,
partition = autofillPartition,
uri = URI,
)
val filledItem = FilledItem(
autofillId = autofillId,
@ -100,4 +108,8 @@ class FilledDataBuilderTest {
// Verify
assertEquals(expected, actual)
}
companion object {
private const val URI: String = "androidapp://com.x8bit.bitwarden"
}
}

View file

@ -6,11 +6,14 @@ 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
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
@ -44,12 +47,15 @@ class AutofillParserTests {
@BeforeEach
fun setup() {
mockkStatic(AssistStructure.ViewNode::toAutofillView)
mockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure) } returns URI
parser = AutofillParserImpl()
}
@AfterEach
fun teardown() {
unmockkStatic(AssistStructure.ViewNode::toAutofillView)
unmockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
}
@Test
@ -82,6 +88,9 @@ class AutofillParserTests {
val parentAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth(
autofillId = parentAutofillId,
isFocused = true,
idPackage = null,
webDomain = null,
webScheme = null,
)
val parentViewNode: AssistStructure.ViewNode = mockk {
every { this@mockk.autofillHints } returns arrayOf(parentAutofillHint)
@ -99,6 +108,7 @@ class AutofillParserTests {
val expected = AutofillRequest.Fillable(
ignoreAutofillIds = listOf(childAutofillId),
partition = autofillPartition,
uri = URI,
)
every { assistStructure.windowNodeCount } returns 1
every { assistStructure.getWindowNodeAt(0) } returns windowNode
@ -108,6 +118,9 @@ class AutofillParserTests {
// Verify
assertEquals(expected, actual)
verify(exactly = 1) {
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
}
}
@Test
@ -117,10 +130,16 @@ class AutofillParserTests {
val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth(
autofillId = cardAutofillId,
isFocused = true,
idPackage = null,
webDomain = null,
webScheme = null,
)
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
autofillId = loginAutofillId,
isFocused = false,
idPackage = null,
webDomain = null,
webScheme = null,
)
val autofillPartition = AutofillPartition.Card(
views = listOf(cardAutofillView),
@ -128,6 +147,7 @@ class AutofillParserTests {
val expected = AutofillRequest.Fillable(
ignoreAutofillIds = emptyList(),
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
@ -137,6 +157,9 @@ class AutofillParserTests {
// Verify
assertEquals(expected, actual)
verify(exactly = 1) {
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
}
}
@Test
@ -146,10 +169,16 @@ class AutofillParserTests {
val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth(
autofillId = cardAutofillId,
isFocused = false,
idPackage = null,
webDomain = null,
webScheme = null,
)
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
autofillId = loginAutofillId,
isFocused = true,
idPackage = null,
webDomain = null,
webScheme = null,
)
val autofillPartition = AutofillPartition.Login(
views = listOf(loginAutofillView),
@ -157,6 +186,7 @@ class AutofillParserTests {
val expected = AutofillRequest.Fillable(
ignoreAutofillIds = emptyList(),
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
@ -166,6 +196,9 @@ class AutofillParserTests {
// Verify
assertEquals(expected, actual)
verify(exactly = 1) {
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
}
}
@Test
@ -175,10 +208,16 @@ class AutofillParserTests {
val cardAutofillView: AutofillView.Card = AutofillView.Card.ExpirationMonth(
autofillId = cardAutofillId,
isFocused = true,
idPackage = null,
webDomain = null,
webScheme = null,
)
val loginAutofillView: AutofillView.Login = AutofillView.Login.Username(
autofillId = loginAutofillId,
isFocused = true,
idPackage = null,
webDomain = null,
webScheme = null,
)
val autofillPartition = AutofillPartition.Card(
views = listOf(cardAutofillView),
@ -186,6 +225,7 @@ class AutofillParserTests {
val expected = AutofillRequest.Fillable(
ignoreAutofillIds = emptyList(),
partition = autofillPartition,
uri = URI,
)
every { cardViewNode.toAutofillView() } returns cardAutofillView
every { loginViewNode.toAutofillView() } returns loginAutofillView
@ -195,6 +235,9 @@ class AutofillParserTests {
// Verify
assertEquals(expected, actual)
verify(exactly = 1) {
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
}
}
/**
@ -206,4 +249,8 @@ class AutofillParserTests {
every { assistStructure.getWindowNodeAt(0) } returns cardWindowNode
every { assistStructure.getWindowNodeAt(1) } returns loginWindowNode
}
companion object {
private const val URI: String = "androidapp://com.x8bit.bitwarden"
}
}

View file

@ -16,7 +16,10 @@ class ViewNodeExtensionsTest {
private val viewNode: AssistStructure.ViewNode = mockk {
every { this@mockk.autofillId } returns expectedAutofillId
every { this@mockk.childCount } returns 0
every { this@mockk.idPackage } returns ID_PACKAGE
every { this@mockk.isFocused } returns expectedIsFocused
every { this@mockk.webDomain } returns WEB_DOMAIN
every { this@mockk.webScheme } returns WEB_SCHEME
}
@Test
@ -25,7 +28,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH
val expected = AutofillView.Card.ExpirationMonth(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -42,7 +48,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR
val expected = AutofillView.Card.ExpirationYear(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -59,7 +68,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_NUMBER
val expected = AutofillView.Card.Number(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -76,7 +88,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE
val expected = AutofillView.Card.SecurityCode(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -93,7 +108,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_EMAIL_ADDRESS
val expected = AutofillView.Login.EmailAddress(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -110,7 +128,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_PASSWORD
val expected = AutofillView.Login.Password(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -127,7 +148,10 @@ class ViewNodeExtensionsTest {
val autofillHint = View.AUTOFILL_HINT_USERNAME
val expected = AutofillView.Login.Username(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHint)
@ -158,7 +182,10 @@ class ViewNodeExtensionsTest {
val autofillHintTwo = View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR
val expected = AutofillView.Card.ExpirationYear(
autofillId = expectedAutofillId,
idPackage = ID_PACKAGE,
isFocused = expectedIsFocused,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
every { viewNode.autofillHints } returns arrayOf(autofillHintOne, autofillHintTwo)
@ -168,4 +195,10 @@ class ViewNodeExtensionsTest {
// Verify
assertEquals(expected, actual)
}
companion object {
private const val ID_PACKAGE: String = "ID_PACKAGE"
private const val WEB_DOMAIN: String = "WEB_DOMAIN"
private const val WEB_SCHEME: String = "WEB_SCHEME"
}
}

View file

@ -0,0 +1,270 @@
package com.x8bit.bitwarden.data.autofill.util
import android.app.assist.AssistStructure
import com.x8bit.bitwarden.data.autofill.model.AutofillView
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 {
private val windowNode: AssistStructure.WindowNode = mockk()
private val assistStructure: AssistStructure = mockk {
every { this@mockk.windowNodeCount } returns 1
every { this@mockk.getWindowNodeAt(0) } returns windowNode
}
@Test
fun `buildUriOrNull should return URI when contains valid domain and scheme`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = null,
isFocused = false,
webDomain = WEB_DOMAIN,
webScheme = WEB_SCHEME,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
val expected = "$WEB_SCHEME://$WEB_DOMAIN"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return URI with default scheme when domain valid and scheme null`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = null,
isFocused = false,
webDomain = WEB_DOMAIN,
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
val expected = "https://$WEB_DOMAIN"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `buildUriOrNull should return URI with default scheme when domain valid and scheme empty`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = null,
isFocused = false,
webDomain = WEB_DOMAIN,
webScheme = "",
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
val expected = "https://$WEB_DOMAIN"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return idPackage URI when domain is null`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = ID_PACKAGE,
isFocused = false,
webDomain = null,
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
val expected = "androidapp://$ID_PACKAGE"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return idPackage URI when domain is empty`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = ID_PACKAGE,
isFocused = false,
webDomain = null,
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
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 domain and idPackage are null`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = null,
isFocused = false,
webDomain = null,
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
val expected = "androidapp://com.x8bit.bitwarden"
every { windowNode.title } returns "com.x8bit.bitwarden/path.deeper.into.app"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return title URI when domain and idPackage are empty`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = "",
isFocused = false,
webDomain = "",
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
val expected = "androidapp://com.x8bit.bitwarden"
every { windowNode.title } returns "com.x8bit.bitwarden/path.deeper.into.app"
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertEquals(expected, actual)
}
@Test
fun `buildUriOrNull should return null when title, domain, and idPackage are null`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = null,
isFocused = false,
webDomain = null,
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
every { windowNode.title } returns null
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertNull(actual)
}
@Test
fun `buildUriOrNull should return null when title, domain, and idPackage are empty`() {
// Setup
val autofillView = AutofillView.Card.Number(
autofillId = mockk(),
idPackage = "",
isFocused = false,
webDomain = "",
webScheme = null,
)
val viewNodeTraversalData = ViewNodeTraversalData(
autofillViews = listOf(
autofillView,
),
ignoreAutofillIds = emptyList(),
)
every { windowNode.title } returns ""
// Test
val actual = listOf(viewNodeTraversalData).buildUriOrNull(
assistStructure = assistStructure,
)
// Verify
assertNull(actual)
}
companion object {
private const val ID_PACKAGE: String = "com.x8bit.bitwarden"
private const val WEB_DOMAIN: String = "www.google.com"
private const val WEB_SCHEME: String = "https"
}
}