mirror of
https://github.com/bitwarden/android.git
synced 2026-06-10 00:28:29 -05:00
[PM-38118] fix: Support Firefox updated toolbar in accessibility autofill (#6986)
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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`() {
|
||||
|
||||
Reference in New Issue
Block a user