[PM-38118] fix: Support Firefox updated toolbar in accessibility autofill (#6986)

This commit is contained in:
aj-rosado
2026-05-29 14:35:50 +01:00
committed by GitHub
parent e7e2c26bef
commit 124ce37bc3
4 changed files with 245 additions and 34 deletions

View File

@@ -6,6 +6,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.model
data class Browser(
val packageName: String,
val possibleUrlFieldIds: List<String>,
val possibleUrlSemanticIds: List<String> = emptyList(),
val urlExtractor: (String) -> String? = { it },
) {
constructor(
@@ -15,6 +16,7 @@ data class Browser(
) : this(
packageName = packageName,
possibleUrlFieldIds = listOf(urlFieldId),
possibleUrlSemanticIds = emptyList(),
urlExtractor = urlExtractor,
)
}

View File

@@ -43,22 +43,34 @@ class AccessibilityParserImpl(
return browser
.possibleUrlFieldIds
.flatMap { viewId ->
rootNode
.findAccessibilityNodeInfosByViewId("$packageName:id/$viewId")
.map { accessibilityNodeInfo ->
browser
.urlExtractor(accessibilityNodeInfo.text.toString())
?.trim()
?.let { rawUrl ->
if (rawUrl.contains(other = ".") && !rawUrl.hasHttpProtocol()) {
"https://$rawUrl"
} else {
rawUrl
}
}
rootNode.findAccessibilityNodeInfosByViewId("$packageName:id/$viewId")
}
.ifEmpty {
browser
.possibleUrlSemanticIds
.flatMap { semanticId ->
// Semantic IDs are exposed as viewIdResourceName via testTagsAsResourceId
// and cannot be found via findAccessibilityNodeInfosByViewId on Firefox.
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(rootNode) {
it.viewIdResourceName == semanticId
}
}
}
.firstNotNullOfOrNull { node ->
val urlText = node.text?.toString()?.takeIf { it.isNotEmpty() }
?: node.contentDescription?.toString()?.takeIf { it.isNotEmpty() }
?: return@firstNotNullOfOrNull null
browser
.urlExtractor(urlText)
?.trim()
?.let { rawUrl ->
if (rawUrl.contains(other = ".") && !rawUrl.hasHttpProtocol()) {
"https://$rawUrl"
} else {
rawUrl
}
}
}
.firstOrNull()
?.toUriOrNull()
}
}

View File

@@ -2,6 +2,15 @@ package com.x8bit.bitwarden.data.autofill.accessibility.util
import com.x8bit.bitwarden.data.autofill.accessibility.model.Browser
/**
* URL extractor for Mozilla browsers whose toolbar exposes the URL via [contentDescription]
* rather than [text]. The content description format is " $url. Search or enter address".
* Falls back to [text] for builds where the URL is still exposed via [text].
*/
private val mozillaUrlExtractor: (String) -> String? = { text ->
text.trim().split(" ").firstOrNull()?.trimEnd('.')?.takeIf { it.isNotEmpty() }
}
/**
* Determines if the [String] receiver is a package name for a supported browser and returns that
* [Browser] if it is a match.
@@ -36,14 +45,21 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
Browser(packageName = "com.cookiegames.smartcookie", urlFieldId = "search"),
Browser(
packageName = "com.cookiejarapps.android.smartcookieweb",
urlFieldId = "mozac_browser_toolbar_url_view",
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view"),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "com.duckduckgo.mobile.android", urlFieldId = "omnibarTextInput"),
Browser(packageName = "com.ecosia.android", urlFieldId = "url_bar"),
Browser(packageName = "com.google.android.apps.chrome", urlFieldId = "url_bar"),
Browser(packageName = "com.google.android.apps.chrome_dev", urlFieldId = "url_bar"),
// "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId
Browser(packageName = "com.iode.firefox", urlFieldId = "mozac_browser_toolbar_url_view"),
Browser(
packageName = "com.iode.firefox",
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view"),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "com.jamal2367.styx", urlFieldId = "search"),
Browser(packageName = "com.kiwibrowser.browser", urlFieldId = "url_bar"),
Browser(packageName = "com.kiwibrowser.browser.dev", urlFieldId = "url_bar"),
@@ -67,7 +83,12 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
Browser(
packageName = "com.qwant.liberty",
// 2nd = Legacy (before v4)
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "com.rainsee.create", urlFieldId = "search_box"),
Browser(packageName = "com.sec.android.app.sbrowser", urlFieldId = "location_bar_edit_text"),
@@ -102,7 +123,9 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
Browser(packageName = "idm.internet.download.manager.plus", urlFieldId = "search"),
Browser(
packageName = "io.github.forkmaintainers.iceraven",
urlFieldId = "mozac_browser_toolbar_url_view",
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view"),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "mark.via", urlFieldId = "am,an"),
Browser(packageName = "mark.via.gp", urlFieldId = "as"),
@@ -129,78 +152,155 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
Browser(
packageName = "org.gnu.icecat",
// 2nd = Anticipation
possibleUrlFieldIds = listOf("url_bar_title", "mozac_browser_toolbar_url_view"),
possibleUrlFieldIds = listOf(
"url_bar_title",
"mozac_browser_toolbar_url_view",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.ironfoxoss.ironfox",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.ironfoxoss.ironfox.nightly",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.fenix",
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view"),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "org.mozilla.fenix", urlFieldId = "mozac_browser_toolbar_url_view"),
// [DEPRECATED ENTRY]
Browser(
packageName = "org.mozilla.fenix.nightly",
urlFieldId = "mozac_browser_toolbar_url_view",
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
// [DEPRECATED ENTRY]
Browser(
packageName = "org.mozilla.fennec_aurora",
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
// 2nd = Legacy
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.fennec_fdroid",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.firefox",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.firefox_beta",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.focus",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"display_url",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.focus.beta",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"display_url",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.focus.nightly",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"display_url",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.klar",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"display_url",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.mozilla.reference.browser",
urlFieldId = "mozac_browser_toolbar_url_view",
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view"),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "org.mozilla.rocket", urlFieldId = "display_url"),
Browser(
packageName = "org.torproject.torbrowser",
// 2nd = Legacy (before v10.0.3)
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(
packageName = "org.torproject.torbrowser_alpha",
// 2nd = Legacy (before v10.0a8)
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
possibleUrlFieldIds = listOf(
"mozac_browser_toolbar_url_view",
"url_bar_title",
),
possibleUrlSemanticIds = listOf("ADDRESSBAR_URL_BOX"),
urlExtractor = mozillaUrlExtractor,
),
Browser(packageName = "org.ungoogled.chromium.extensions.stable", urlFieldId = "url_bar"),
Browser(packageName = "org.ungoogled.chromium.stable", urlFieldId = "url_bar"),

View File

@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.autofill.accessibility.model.Browser
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
@@ -156,6 +157,102 @@ class AccessibilityParserTest {
assertNull(result)
}
@Suppress("MaxLineLength")
@Test
fun `parseForUriOrPackageName should return null when URL bar node has no text or content description`() {
val testBrowser = Browser(packageName = "com.android.chrome", urlFieldId = "url_bar")
val emptyNode = mockk<AccessibilityNodeInfo> {
every { text } returns null
every { contentDescription } returns null
}
val rootNode = mockk<AccessibilityNodeInfo> {
every { packageName } returns testBrowser.packageName
every {
findAccessibilityNodeInfosByViewId(
"${testBrowser.packageName}:id/${testBrowser.possibleUrlFieldIds.first()}",
)
} returns listOf(emptyNode)
}
val result = accessibilityParser.parseForUriOrPackageName(rootNode = rootNode)
assertNull(result)
}
@Test
fun `parseForUriOrPackageName should not use semantic lookup when standard lookup succeeds`() {
val firefoxPackage = "org.mozilla.firefox"
val url = "https://www.reddit.com"
val urlNode = mockk<AccessibilityNodeInfo> {
every { text } returns url
}
val rootNode = mockk<AccessibilityNodeInfo> {
every { packageName } returns firefoxPackage
every { findAccessibilityNodeInfosByViewId(any()) } returns emptyList()
every {
findAccessibilityNodeInfosByViewId(
"$firefoxPackage:id/mozac_browser_toolbar_url_view",
)
} returns listOf(urlNode)
}
val result = accessibilityParser.parseForUriOrPackageName(rootNode = rootNode)
assertEquals(url.toUri(), result)
verify(exactly = 0) {
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
rootNode = any(),
predicate = any(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `parseForUriOrPackageName should return null when package is a supported browser with semantic ids and no URL bar is found`() {
val firefoxPackage = "org.mozilla.firefox"
val rootNode = mockk<AccessibilityNodeInfo> {
every { packageName } returns firefoxPackage
every { findAccessibilityNodeInfosByViewId(any()) } returns emptyList()
}
every {
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
rootNode = rootNode,
predicate = any(),
)
} returns emptyList()
val result = accessibilityParser.parseForUriOrPackageName(rootNode = rootNode)
assertNull(result)
}
@Suppress("MaxLineLength")
@Test
fun `parseForUriOrPackageName should return the site url from content description when URL bar is found via semantic id`() {
val firefoxPackage = "org.mozilla.firefox"
val contentDesc = " www.reddit.com. Search or enter address"
val urlNode = mockk<AccessibilityNodeInfo> {
every { text } returns null
every { contentDescription } returns contentDesc
}
val rootNode = mockk<AccessibilityNodeInfo> {
every { packageName } returns firefoxPackage
every { findAccessibilityNodeInfosByViewId(any()) } returns emptyList()
}
every {
accessibilityNodeInfoManager.findAccessibilityNodeInfoList(
rootNode = rootNode,
predicate = any(),
)
} returns listOf(urlNode)
val expectedResult = "https://www.reddit.com".toUri()
val result = accessibilityParser.parseForUriOrPackageName(rootNode = rootNode)
assertEquals(expectedResult, result)
}
@Suppress("MaxLineLength")
@Test
fun `parseForUriOrPackageName should return the site url un-augmented with https protocol as a URI when package is a supported browser and URL is found`() {