mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
Add logic to find missing username fields (#3440)
This commit is contained in:
parent
b2300328e1
commit
f6f28f6a58
5 changed files with 169 additions and 5 deletions
|
@ -86,4 +86,11 @@ sealed class AutofillView {
|
||||||
override val data: Data,
|
override val data: Data,
|
||||||
) : Login()
|
) : Login()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view that is an input field but does not correspond to any known autofill field.
|
||||||
|
*/
|
||||||
|
data class Unused(
|
||||||
|
override val data: Data,
|
||||||
|
) : AutofillView()
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,11 @@ class AutofillParserImpl(
|
||||||
views = autofillViews.filterIsInstance<AutofillView.Login>(),
|
views = autofillViews.filterIsInstance<AutofillView.Login>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AutofillView.Unused -> {
|
||||||
|
// The view is unfillable since the field is not meant to be used for autofill.
|
||||||
|
return AutofillRequest.Unfillable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Flatten the ignorable autofill ids.
|
// Flatten the ignorable autofill ids.
|
||||||
val ignoreAutofillIds = traversalDataList
|
val ignoreAutofillIds = traversalDataList
|
||||||
|
@ -139,7 +144,52 @@ class AutofillParserImpl(
|
||||||
private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
|
private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
|
||||||
(0 until windowNodeCount)
|
(0 until windowNodeCount)
|
||||||
.map { getWindowNodeAt(it) }
|
.map { getWindowNodeAt(it) }
|
||||||
.mapNotNull { windowNode -> windowNode.rootViewNode?.traverse() }
|
.mapNotNull { windowNode ->
|
||||||
|
windowNode
|
||||||
|
.rootViewNode
|
||||||
|
?.traverse()
|
||||||
|
?.updateForMissingUsernameFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper function updates the [ViewNodeTraversalData] if necessary for missing username
|
||||||
|
* fields that could have been missed. If the current `ViewNodeTraversalData` contains password
|
||||||
|
* fields but no username fields, we check to see if there are any unused fields directly above
|
||||||
|
* the password fields and we assume that those are the missing username fields.
|
||||||
|
*/
|
||||||
|
private fun ViewNodeTraversalData.updateForMissingUsernameFields(): ViewNodeTraversalData {
|
||||||
|
val passwordPositions = this.autofillViews.mapIndexedNotNull { index, autofillView ->
|
||||||
|
(autofillView as? AutofillView.Login.Password)?.let { index }
|
||||||
|
}
|
||||||
|
return if (passwordPositions.any() &&
|
||||||
|
this.autofillViews.none { it is AutofillView.Login.Username }
|
||||||
|
) {
|
||||||
|
val updatedAutofillViews = autofillViews.mapIndexed { index, autofillView ->
|
||||||
|
if (autofillView is AutofillView.Unused && passwordPositions.contains(index + 1)) {
|
||||||
|
AutofillView.Login.Username(data = autofillView.data)
|
||||||
|
} else {
|
||||||
|
autofillView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val previousUnusedIds = autofillViews
|
||||||
|
.filterIsInstance<AutofillView.Unused>()
|
||||||
|
.map { it.data.autofillId }
|
||||||
|
.toSet()
|
||||||
|
val currentUnusedIds = updatedAutofillViews
|
||||||
|
.filterIsInstance<AutofillView.Unused>()
|
||||||
|
.map { it.data.autofillId }
|
||||||
|
.toSet()
|
||||||
|
val unignoredAutofillIds = previousUnusedIds - currentUnusedIds
|
||||||
|
this.copy(
|
||||||
|
autofillViews = updatedAutofillViews,
|
||||||
|
ignoreAutofillIds = this.ignoreAutofillIds - unignoredAutofillIds,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// We already have username fields available or there are no password fields, so no need
|
||||||
|
// to search for them.
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
|
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
|
||||||
|
|
|
@ -112,7 +112,7 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||||
autofillOptions: List<String>,
|
autofillOptions: List<String>,
|
||||||
autofillViewData: AutofillView.Data,
|
autofillViewData: AutofillView.Data,
|
||||||
supportedHint: String?,
|
supportedHint: String?,
|
||||||
): AutofillView? = when {
|
): AutofillView = when {
|
||||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||||
val monthValue = this
|
val monthValue = this
|
||||||
.autofillValue
|
.autofillValue
|
||||||
|
@ -156,7 +156,11 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> {
|
||||||
|
AutofillView.Unused(
|
||||||
|
data = autofillViewData,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class AutofillParserTests {
|
class AutofillParserTests {
|
||||||
private lateinit var parser: AutofillParser
|
private lateinit var parser: AutofillParser
|
||||||
|
|
||||||
|
@ -377,6 +378,105 @@ class AutofillParserTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `parse should have Username and Password AutofillView when the Username field is not identifiable but directly above the Password field in the hierarchy`() {
|
||||||
|
// Setup
|
||||||
|
val hiddenUserNameViewNode: AssistStructure.ViewNode = mockk {
|
||||||
|
every { this@mockk.autofillHints } returns emptyArray()
|
||||||
|
every { this@mockk.autofillId } returns loginAutofillId
|
||||||
|
every { this@mockk.childCount } returns 0
|
||||||
|
every { this@mockk.idPackage } returns ID_PACKAGE
|
||||||
|
every { this@mockk.website } returns WEBSITE
|
||||||
|
}
|
||||||
|
val passwordAutofillId = mockk<AutofillId>()
|
||||||
|
val passwordViewNode: AssistStructure.ViewNode = mockk {
|
||||||
|
every { this@mockk.autofillHints } returns emptyArray()
|
||||||
|
every { this@mockk.autofillId } returns passwordAutofillId
|
||||||
|
every { this@mockk.childCount } returns 0
|
||||||
|
every { this@mockk.idPackage } returns ID_PACKAGE
|
||||||
|
every { this@mockk.website } returns WEBSITE
|
||||||
|
}
|
||||||
|
val rootAutofillId = mockk<AutofillId>()
|
||||||
|
val rootViewNode: AssistStructure.ViewNode = mockk {
|
||||||
|
every { this@mockk.autofillHints } returns emptyArray()
|
||||||
|
every { this@mockk.autofillId } returns rootAutofillId
|
||||||
|
every { this@mockk.childCount } returns 0
|
||||||
|
every { this@mockk.idPackage } returns ID_PACKAGE
|
||||||
|
every { this@mockk.website } returns WEBSITE
|
||||||
|
every { this@mockk.childCount } returns 2
|
||||||
|
every { this@mockk.getChildAt(0) } returns hiddenUserNameViewNode
|
||||||
|
every { this@mockk.getChildAt(1) } returns passwordViewNode
|
||||||
|
}
|
||||||
|
val windowNode: AssistStructure.WindowNode = mockk {
|
||||||
|
every { this@mockk.rootViewNode } returns rootViewNode
|
||||||
|
}
|
||||||
|
every { assistStructure.windowNodeCount } returns 1
|
||||||
|
every { assistStructure.getWindowNodeAt(0) } returns windowNode
|
||||||
|
val unusedAutofillView: AutofillView.Unused = AutofillView.Unused(
|
||||||
|
data = AutofillView.Data(
|
||||||
|
autofillId = loginAutofillId,
|
||||||
|
autofillOptions = emptyList(),
|
||||||
|
autofillType = AUTOFILL_TYPE,
|
||||||
|
isFocused = true,
|
||||||
|
textValue = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val loginUsernameAutofillView: AutofillView.Login = AutofillView.Login.Username(
|
||||||
|
data = AutofillView.Data(
|
||||||
|
autofillId = loginAutofillId,
|
||||||
|
autofillOptions = emptyList(),
|
||||||
|
autofillType = AUTOFILL_TYPE,
|
||||||
|
isFocused = true,
|
||||||
|
textValue = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val loginPasswordAutofillView: AutofillView.Login = AutofillView.Login.Password(
|
||||||
|
data = AutofillView.Data(
|
||||||
|
autofillId = passwordAutofillId,
|
||||||
|
autofillOptions = emptyList(),
|
||||||
|
autofillType = AUTOFILL_TYPE,
|
||||||
|
isFocused = false,
|
||||||
|
textValue = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val autofillPartition = AutofillPartition.Login(
|
||||||
|
views = listOf(loginUsernameAutofillView, loginPasswordAutofillView),
|
||||||
|
)
|
||||||
|
val expected = AutofillRequest.Fillable(
|
||||||
|
ignoreAutofillIds = listOf(rootAutofillId),
|
||||||
|
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||||
|
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||||
|
packageName = PACKAGE_NAME,
|
||||||
|
partition = autofillPartition,
|
||||||
|
uri = URI,
|
||||||
|
)
|
||||||
|
every { rootViewNode.toAutofillView() } returns null
|
||||||
|
every { hiddenUserNameViewNode.toAutofillView() } returns unusedAutofillView
|
||||||
|
every { passwordViewNode.toAutofillView() } returns loginPasswordAutofillView
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
isInlineAutofillEnabled = true,
|
||||||
|
)
|
||||||
|
fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
isInlineAutofillEnabled = true,
|
||||||
|
)
|
||||||
|
any<List<ViewNodeTraversalData>>().buildPackageNameOrNull(assistStructure)
|
||||||
|
any<List<ViewNodeTraversalData>>().buildUriOrNull(PACKAGE_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `parse should choose first focused AutofillView for partition when there are multiple`() {
|
fun `parse should choose first focused AutofillView for partition when there are multiple`() {
|
||||||
// Setup
|
// Setup
|
||||||
|
|
|
@ -230,15 +230,18 @@ class ViewNodeExtensionsTest {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `toAutofillView should return null when hint is not supported, is an inputField, and isn't a username or password`() {
|
fun `toAutofillView should return only unused field when hint is not supported, is an inputField, and isn't a username or password`() {
|
||||||
// Setup
|
// Setup
|
||||||
setupUnsupportedInputFieldViewNode()
|
setupUnsupportedInputFieldViewNode()
|
||||||
|
val expected = AutofillView.Unused(
|
||||||
|
data = autofillViewData,
|
||||||
|
)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = viewNode.toAutofillView()
|
val actual = viewNode.toAutofillView()
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertNull(actual)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue