diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt index b68fdcbe46..8bda2a31b5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt @@ -187,9 +187,7 @@ class AutofillParserImpl( autofillViews } - val effectiveFocusedView = effectiveViews - .filterNot { it is AutofillView.Identity } - .firstFocusedOrNull() + val effectiveFocusedView = effectiveViews.effectiveFocusedViewOrNull() ?: return AutofillRequest.Unfillable // Choose the first focused partition of data for fulfillment. @@ -284,8 +282,11 @@ class AutofillParserImpl( is AutofillView.Login -> rule.category in loginCategories is AutofillView.Identity -> rule.category in IDENTITY_FILL_ASSIST_CATEGORIES is AutofillView.Unused -> { + val identityCoversRule = isIdentityAutofillEnabled && + rule.category in IDENTITY_FILL_ASSIST_CATEGORIES rule.category in loginCategories || - rule.category in CARD_FILL_ASSIST_CATEGORIES + rule.category in CARD_FILL_ASSIST_CATEGORIES || + identityCoversRule } } } @@ -352,6 +353,18 @@ private fun List.selectCandidateAutofillViews( private fun List.firstFocusedOrNull(): AutofillView? = firstOrNull { it.data.isFocused } ?: firstOrNull() +/** + * Returns the [AutofillView] that should win partition selection: a focused Login/Card view + * first, then any focused view (so a real Identity partition can still be built when Identity is + * the only classification available), then any Login/Card view, then any view at all. + */ +private fun List.effectiveFocusedViewOrNull(): AutofillView? { + val focusedNonIdentity = firstOrNull { it.data.isFocused && it !is AutofillView.Identity } + val focusedAny = firstOrNull { it.data.isFocused } + val nonIdentity = firstOrNull { it !is AutofillView.Identity } + return focusedNonIdentity ?: focusedAny ?: nonIdentity ?: firstOrNull() +} + /** * This helper function updates the [ViewNodeTraversalData] if necessary for missing password * fields that were marked invalid because they contained a specific `hint` or `idEntry`. If the diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt index fab8ccbc4d..2bb58508c7 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserTests.kt @@ -2074,13 +2074,12 @@ class AutofillParserTests { @Suppress("MaxLineLength") @Test - fun `parse should choose AutofillPartition Login when an Identity view is focused but a Login view is fillable elsewhere`() { + fun `parse should choose AutofillPartition Identity when an Identity view is focused even though a Login view is fillable in another window`() { // Setup: a focused field heuristics classify as Identity (e.g. "First name" on a signup // form) sits in one window, while a fillable, unfocused Login.Username field exists in - // another window on the same screen. Before Phase D's Identity partition exists, a - // focused Identity view must not force the whole request to Unfillable when a fillable - // Login/Card partition exists elsewhere -- it should be excluded from candidates exactly - // like Unused, falling through to the other fillable view. + // another window on the same screen. The Login view never enters the candidate pool -- + // selectCandidateAutofillViews only considers a window whose own views have a focused + // entry -- so the focused Identity view wins and builds a real Identity partition. val identityAutofillId: AutofillId = mockk() val identityViewNode: AssistStructure.ViewNode = mockk { every { this@mockk.autofillHints } returns emptyArray() @@ -2130,13 +2129,14 @@ class AutofillParserTests { // Test val actual = parser.parse(autofillAppInfo = autofillAppInfo, fillRequest = fillRequest) - // Verify: falls through to the fillable Login view instead of becoming Unfillable. + // Verify: the focused Identity view wins; the Login view in the other window is never a + // candidate. val expected = AutofillRequest.Fillable( ignoreAutofillIds = emptyList(), inlinePresentationSpecs = inlinePresentationSpecs, maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT, packageName = PACKAGE_NAME, - partition = AutofillPartition.Login(views = listOf(loginAutofillView)), + partition = AutofillPartition.Identity(views = listOf(identityAutofillView)), uri = URI, ) assertEquals(expected, actual)