PM-11487: Initial accessibility service and processor for handling autofill (#3906)

This commit is contained in:
David Perez 2024-09-12 12:17:25 -05:00 committed by GitHub
parent f544ccc3ef
commit 4c1d55e9fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 643 additions and 1 deletions

View file

@ -21,6 +21,26 @@
</intent-filter> </intent-filter>
</service> </service>
<!--
The AccessibilityService name below refers to the legacy Xamarin app's service name. This
must always match in order for the app to properly query if it is providing accessibility
services.
-->
<!--suppress AndroidDomInspection -->
<service
android:name="com.x8bit.bitwarden.Accessibility.AccessibilityService"
android:exported="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service" />
</service>
<!-- Disable Crashlytics for debug builds --> <!-- Disable Crashlytics for debug builds -->
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"

View file

@ -6,12 +6,15 @@ import android.os.Build
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.app.AppComponentFactory import androidx.core.app.AppComponentFactory
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService
private const val LEGACY_ACCESSIBILITY_SERVICE_NAME =
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
private const val LEGACY_AUTOFILL_SERVICE_NAME = "com.x8bit.bitwarden.Autofill.AutofillService" private const val LEGACY_AUTOFILL_SERVICE_NAME = "com.x8bit.bitwarden.Autofill.AutofillService"
private const val LEGACY_CREDENTIAL_SERVICE_NAME = private const val LEGACY_CREDENTIAL_SERVICE_NAME =
"com.x8bit.bitwarden.Autofill.CredentialProviderService" "com.x8bit.bitwarden.Autofill.CredentialProviderService"
@ -33,6 +36,7 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
* the legacy Xamarin app service name but the service name in this app is different. * the legacy Xamarin app service name but the service name in this app is different.
* *
* Services currently being managed: * Services currently being managed:
* * [BitwardenAccessibilityService]
* * [BitwardenAutofillService] * * [BitwardenAutofillService]
* * [BitwardenAutofillTileService] * * [BitwardenAutofillTileService]
* * [BitwardenFido2ProviderService] * * [BitwardenFido2ProviderService]
@ -44,6 +48,14 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
className: String, className: String,
intent: Intent?, intent: Intent?,
): Service = when (className) { ): Service = when (className) {
LEGACY_ACCESSIBILITY_SERVICE_NAME -> {
super.instantiateServiceCompat(
cl,
BitwardenAccessibilityService::class.java.name,
intent,
)
}
LEGACY_AUTOFILL_SERVICE_NAME -> { LEGACY_AUTOFILL_SERVICE_NAME -> {
super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent) super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent)
} }

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.autofill.accessibility
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* The [AccessibilityService] implementation for the app. This is not used in the traditional
* way, we use the [BitwardenAutofillTileService] to invoke this service in order to provide an
* autofill fallback mechanism.
*/
@Keep
@OmitFromCoverage
@AndroidEntryPoint
class BitwardenAccessibilityService : AccessibilityService() {
@Inject
lateinit var processor: BitwardenAccessibilityProcessor
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (rootInActiveWindow?.packageName != event.packageName) return
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = rootInActiveWindow)
}
override fun onInterrupt() = Unit
}

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.PowerManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
@ -12,6 +13,8 @@ import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNa
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParserImpl import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParserImpl
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessorImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import dagger.Module import dagger.Module
@ -43,7 +46,7 @@ object AccessibilityModule {
@Singleton @Singleton
@Provides @Provides
fun providesAccessibilityInvokeManager(): AccessibilityAutofillManager = fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager =
AccessibilityAutofillManagerImpl() AccessibilityAutofillManagerImpl()
@Singleton @Singleton
@ -55,6 +58,23 @@ object AccessibilityModule {
fun providesAccessibilitySelectionManager(): AccessibilitySelectionManager = fun providesAccessibilitySelectionManager(): AccessibilitySelectionManager =
AccessibilitySelectionManagerImpl() AccessibilitySelectionManagerImpl()
@Singleton
@Provides
fun providesBitwardenAccessibilityProcessor(
@ApplicationContext context: Context,
accessibilityParser: AccessibilityParser,
accessibilityAutofillManager: AccessibilityAutofillManager,
launcherPackageNameManager: LauncherPackageNameManager,
powerManager: PowerManager,
): BitwardenAccessibilityProcessor =
BitwardenAccessibilityProcessorImpl(
context = context,
accessibilityParser = accessibilityParser,
accessibilityAutofillManager = accessibilityAutofillManager,
launcherPackageNameManager = launcherPackageNameManager,
powerManager = powerManager,
)
@Singleton @Singleton
@Provides @Provides
fun providesLauncherPackageNameManager( fun providesLauncherPackageNameManager(
@ -71,4 +91,10 @@ object AccessibilityModule {
fun providesPackageManager( fun providesPackageManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
): PackageManager = context.packageManager ): PackageManager = context.packageManager
@Singleton
@Provides
fun providesPowerManager(
@ApplicationContext context: Context,
): PowerManager = context.getSystemService(PowerManager::class.java)
} }

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.view.accessibility.AccessibilityNodeInfo
/**
* A class to handle accessibility event processing. This only includes fill requests.
*/
interface BitwardenAccessibilityProcessor {
/**
* Processes the [AccessibilityNodeInfo] for autofill options.
*/
fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?)
}

View file

@ -0,0 +1,86 @@
package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.content.Context
import android.os.PowerManager
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser
import com.x8bit.bitwarden.data.autofill.accessibility.util.fillTextField
import com.x8bit.bitwarden.data.autofill.accessibility.util.shouldSkipPackage
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionIntent
/**
* The default implementation of the [BitwardenAccessibilityProcessor].
*/
class BitwardenAccessibilityProcessorImpl(
private val context: Context,
private val accessibilityParser: AccessibilityParser,
private val accessibilityAutofillManager: AccessibilityAutofillManager,
private val launcherPackageNameManager: LauncherPackageNameManager,
private val powerManager: PowerManager,
) : BitwardenAccessibilityProcessor {
override fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) {
val rootNode = rootAccessibilityNodeInfo ?: return
// Ignore the event when the phone is inactive
if (!powerManager.isInteractive) return
// We skip if the package is not supported
if (rootNode.shouldSkipPackage) return
// We skip any package that is a launcher
if (launcherPackageNameManager.launcherPackages.any { it == rootNode.packageName }) return
// Only process the event if the tile was clicked
val accessibilityAction = accessibilityAutofillManager.accessibilityAction ?: return
accessibilityAutofillManager.accessibilityAction = null
when (accessibilityAction) {
is AccessibilityAction.AttemptFill -> {
handleAttemptFill(rootNode = rootNode, attemptFill = accessibilityAction)
}
AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = rootNode)
}
}
private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
accessibilityParser
.parseForUriOrPackageName(rootNode = rootNode)
?.let { uri ->
context.startActivity(
createAutofillSelectionIntent(
context = context,
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
type = AutofillSelectionData.Type.LOGIN,
uri = uri.toString(),
),
)
}
?: run {
Toast
.makeText(
context,
R.string.autofill_tile_uri_not_found,
Toast.LENGTH_LONG,
)
.show()
}
}
private fun handleAttemptFill(
rootNode: AccessibilityNodeInfo,
attemptFill: AccessibilityAction.AttemptFill,
) {
val loginView = attemptFill.cipherView.login ?: return
val fields = accessibilityParser.parseForFillableFields(rootNode = rootNode)
fields.usernameFields.forEach { usernameField ->
usernameField.fillTextField(value = loginView.username)
}
fields.passwordFields.forEach { passwordField ->
passwordField.fillTextField(value = loginView.password)
}
}
}

View file

@ -0,0 +1,52 @@
package com.x8bit.bitwarden.data.autofill.accessibility.util
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.os.bundleOf
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
private const val PACKAGE_NAME_BITWARDEN_PREFIX: String = "com.x8bit.bitwarden"
private const val PACKAGE_NAME_SYSTEM_UI: String = "com.android.systemui"
private const val PACKAGE_NAME_LAUNCHER_PARTIAL: String = "launcher"
private val PACKAGE_NAME_BLOCK_LIST: List<String> = listOf(
"com.google.android.googlequicksearchbox",
"com.google.android.apps.nexuslauncher",
"com.google.android.launcher",
"com.computer.desktop.ui.launcher",
"com.launcher.notelauncher",
"com.anddoes.launcher",
"com.actionlauncher.playstore",
"ch.deletescape.lawnchair.plah",
"com.microsoft.launcher",
"com.teslacoilsw.launcher",
"com.teslacoilsw.launcher.prime",
"is.shortcut",
"me.craftsapp.nlauncher",
"com.ss.squarehome2",
"com.treydev.pns",
)
/**
* Returns true if the event is for an unsupported package.
*/
val AccessibilityNodeInfo.shouldSkipPackage: Boolean
get() {
val packageName = this.packageName.takeUnless { it.isNullOrBlank() } ?: return true
if (packageName == PACKAGE_NAME_SYSTEM_UI) return true
if (packageName.startsWith(prefix = PACKAGE_NAME_BITWARDEN_PREFIX)) return true
if (packageName.contains(other = PACKAGE_NAME_LAUNCHER_PARTIAL, ignoreCase = true)) {
return true
}
if (PACKAGE_NAME_BLOCK_LIST.contains(packageName)) return true
return false
}
/**
* Fills the [AccessibilityNodeInfo] text field with the [value] provided.
*/
@OmitFromCoverage
fun AccessibilityNodeInfo.fillTextField(value: String?) {
performAction(
AccessibilityNodeInfo.ACTION_SET_TEXT,
bundleOf(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE to value),
)
}

View file

@ -329,6 +329,7 @@ Scanning will happen automatically.</string>
<string name="no_items_folder">There are no items in this folder.</string> <string name="no_items_folder">There are no items in this folder.</string>
<string name="no_items_trash">There are no items in the trash.</string> <string name="no_items_trash">There are no items in the trash.</string>
<string name="autofill_accessibility_service">Auto-fill Accessibility Service</string> <string name="autofill_accessibility_service">Auto-fill Accessibility Service</string>
<string name="autofill_accessibility_summary">Assist with filling username and password fields in other apps and on the web.</string>
<string name="autofill_service_description">The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device.</string> <string name="autofill_service_description">The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device.</string>
<string name="bitwarden_autofill_service_description">Use the Bitwarden auto-fill service to fill login information into other apps.</string> <string name="bitwarden_autofill_service_description">Use the Bitwarden auto-fill service to fill login information into other apps.</string>
<string name="bitwarden_autofill_service_open_autofill_settings">Open Autofill Settings</string> <string name="bitwarden_autofill_service_open_autofill_settings">Open Autofill Settings</string>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true"
android:description="@string/autofill_service_description"
android:isAccessibilityTool="false"
android:notificationTimeout="100"
android:summary="@string/autofill_accessibility_summary"
tools:ignore="UnusedAttribute" />

View file

@ -0,0 +1,315 @@
package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.content.Context
import android.net.Uri
import android.os.PowerManager
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.LoginView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser
import com.x8bit.bitwarden.data.autofill.accessibility.util.fillTextField
import com.x8bit.bitwarden.data.autofill.accessibility.util.shouldSkipPackage
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionIntent
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class BitwardenAccessibilityProcessorTest {
private val context: Context = mockk {
every { startActivity(any()) } just runs
}
private val accessibilityParser: AccessibilityParser = mockk()
private val accessibilityAutofillManager: AccessibilityAutofillManager = mockk()
private val launcherPackageNameManager: LauncherPackageNameManager = mockk()
private val powerManager: PowerManager = mockk()
private val bitwardenAccessibilityProcessor: BitwardenAccessibilityProcessor =
BitwardenAccessibilityProcessorImpl(
context = context,
accessibilityParser = accessibilityParser,
accessibilityAutofillManager = accessibilityAutofillManager,
launcherPackageNameManager = launcherPackageNameManager,
powerManager = powerManager,
)
@BeforeEach
fun setup() {
mockkStatic(AccessibilityNodeInfo::shouldSkipPackage)
mockkStatic(::createAutofillSelectionIntent)
mockkStatic(Toast::class)
every {
Toast
.makeText(context, R.string.autofill_tile_uri_not_found, Toast.LENGTH_LONG)
.show()
} just runs
}
@AfterEach
fun tearDown() {
unmockkStatic(AccessibilityNodeInfo::shouldSkipPackage)
unmockkStatic(::createAutofillSelectionIntent)
unmockkStatic(Toast::class)
}
@Test
fun `processAccessibilityEvent with null rootNode should return`() {
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = null,
)
verify(exactly = 0) {
powerManager.isInteractive
}
}
@Test
fun `processAccessibilityEvent with powerManager not interactive should return`() {
val rootNode = mockk<AccessibilityNodeInfo>()
every { powerManager.isInteractive } returns false
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
}
}
@Test
fun `processAccessibilityEvent with skippable package should return`() {
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns true
}
every { powerManager.isInteractive } returns true
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
}
}
@Test
fun `processAccessibilityEvent with launcher package should return`() {
val testPackageName = "com.google.android.launcher"
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns false
every { packageName } returns testPackageName
}
every { launcherPackageNameManager.launcherPackages } returns listOf(testPackageName)
every { powerManager.isInteractive } returns true
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
launcherPackageNameManager.launcherPackages
}
}
@Test
fun `processAccessibilityEvent without accessibility action should return`() {
val testPackageName = "com.android.chrome"
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns false
every { packageName } returns testPackageName
}
every { launcherPackageNameManager.launcherPackages } returns emptyList()
every { accessibilityAutofillManager.accessibilityAction } returns null
every { powerManager.isInteractive } returns true
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
launcherPackageNameManager.launcherPackages
accessibilityAutofillManager.accessibilityAction
}
}
@Test
fun `processAccessibilityEvent with AttemptParseUri and a invalid uri should show a toast`() {
val testPackageName = "com.android.chrome"
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns false
every { packageName } returns testPackageName
}
every { powerManager.isInteractive } returns true
every { launcherPackageNameManager.launcherPackages } returns emptyList()
every {
accessibilityAutofillManager.accessibilityAction
} returns AccessibilityAction.AttemptParseUri
every { accessibilityAutofillManager.accessibilityAction = null } just runs
every { accessibilityParser.parseForUriOrPackageName(rootNode = rootNode) } returns null
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
launcherPackageNameManager.launcherPackages
accessibilityAutofillManager.accessibilityAction
accessibilityAutofillManager.accessibilityAction = null
accessibilityParser.parseForUriOrPackageName(rootNode = rootNode)
Toast
.makeText(context, R.string.autofill_tile_uri_not_found, Toast.LENGTH_LONG)
.show()
}
}
@Suppress("MaxLineLength")
@Test
fun `processAccessibilityEvent with AttemptParseUri and a valid uri should start the main activity`() {
val testPackageName = "com.android.chrome"
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns false
every { packageName } returns testPackageName
}
every { powerManager.isInteractive } returns true
every { launcherPackageNameManager.launcherPackages } returns emptyList()
every {
accessibilityAutofillManager.accessibilityAction
} returns AccessibilityAction.AttemptParseUri
every { accessibilityAutofillManager.accessibilityAction = null } just runs
every {
createAutofillSelectionIntent(
context = context,
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
type = AutofillSelectionData.Type.LOGIN,
uri = any(),
)
} returns mockk()
every { accessibilityParser.parseForUriOrPackageName(rootNode = rootNode) } returns mockk()
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
launcherPackageNameManager.launcherPackages
accessibilityAutofillManager.accessibilityAction
accessibilityAutofillManager.accessibilityAction = null
accessibilityParser.parseForUriOrPackageName(rootNode = rootNode)
createAutofillSelectionIntent(
context = context,
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
type = AutofillSelectionData.Type.LOGIN,
uri = any(),
)
context.startActivity(any())
}
}
@Test
fun `processAccessibilityEvent with AttemptFill and no login data should return`() {
val testPackageName = "com.android.chrome"
val cipherView = mockk<CipherView> {
every { login } returns null
}
val uri = mockk<Uri>()
val attemptFill = AccessibilityAction.AttemptFill(cipherView = cipherView, uri = uri)
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns false
every { packageName } returns testPackageName
}
every { powerManager.isInteractive } returns true
every { launcherPackageNameManager.launcherPackages } returns emptyList()
every { accessibilityAutofillManager.accessibilityAction } returns attemptFill
every { accessibilityAutofillManager.accessibilityAction = null } just runs
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
launcherPackageNameManager.launcherPackages
accessibilityAutofillManager.accessibilityAction
accessibilityAutofillManager.accessibilityAction = null
cipherView.login
}
}
@Test
fun `processAccessibilityEvent with AttemptFill and valid login data should fill the data`() {
val testPackageName = "com.android.chrome"
val testUsername = "testUsername"
val testPassword = "testPassword1234"
val loginView = mockk<LoginView> {
every { username } returns testUsername
every { password } returns testPassword
}
val cipherView = mockk<CipherView> {
every { login } returns loginView
}
val mockUsernameField = mockk<AccessibilityNodeInfo> {
every { fillTextField(testUsername) } just runs
}
val mockPasswordField = mockk<AccessibilityNodeInfo> {
every { fillTextField(testPassword) } just runs
}
val fillableFields = FillableFields(
usernameFields = listOf(mockUsernameField),
passwordFields = listOf(mockPasswordField),
)
val uri = mockk<Uri>()
val attemptFill = AccessibilityAction.AttemptFill(cipherView = cipherView, uri = uri)
val rootNode = mockk<AccessibilityNodeInfo> {
every { shouldSkipPackage } returns false
every { packageName } returns testPackageName
}
every { powerManager.isInteractive } returns true
every { launcherPackageNameManager.launcherPackages } returns emptyList()
every { accessibilityAutofillManager.accessibilityAction } returns attemptFill
every { accessibilityAutofillManager.accessibilityAction = null } just runs
every {
accessibilityParser.parseForFillableFields(rootNode = rootNode)
} returns fillableFields
bitwardenAccessibilityProcessor.processAccessibilityEvent(
rootAccessibilityNodeInfo = rootNode,
)
verify(exactly = 1) {
powerManager.isInteractive
rootNode.shouldSkipPackage
launcherPackageNameManager.launcherPackages
accessibilityAutofillManager.accessibilityAction
accessibilityAutofillManager.accessibilityAction = null
cipherView.login
accessibilityParser.parseForFillableFields(rootNode = rootNode)
mockUsernameField.fillTextField(testUsername)
mockPasswordField.fillTextField(testPassword)
}
}
}

View file

@ -0,0 +1,75 @@
package com.x8bit.bitwarden.data.autofill.accessibility.util
import android.view.accessibility.AccessibilityNodeInfo
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class AccessibilityNodeInfoExtensionsTest {
@Test
fun `shouldSkipPackage when packageName is null should return true`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns null
}
assertTrue(accessibilityNodeInfo.shouldSkipPackage)
}
@Test
fun `shouldSkipPackage when packageName is blank should return true`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns ""
}
assertTrue(accessibilityNodeInfo.shouldSkipPackage)
}
@Test
fun `shouldSkipPackage when packageName is system UI package should return true`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns "com.android.systemui"
}
assertTrue(accessibilityNodeInfo.shouldSkipPackage)
}
@Suppress("MaxLineLength")
@Test
fun `shouldSkipPackage when packageName is prefixed with bitwarden package should return true`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns "com.x8bit.bitwarden.beta"
}
assertTrue(accessibilityNodeInfo.shouldSkipPackage)
}
@Test
fun `shouldSkipPackage when packageName contains with 'launcher' should return true`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns "com.android.launcher.foo"
}
assertTrue(accessibilityNodeInfo.shouldSkipPackage)
}
@Test
fun `shouldSkipPackage when packageName is blocked package should return true`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns "com.google.android.googlequicksearchbox"
}
assertTrue(accessibilityNodeInfo.shouldSkipPackage)
}
@Test
fun `shouldSkipPackage when packageName is valid should return false`() {
val accessibilityNodeInfo = mockk<AccessibilityNodeInfo> {
every { packageName } returns "com.another.app"
}
assertFalse(accessibilityNodeInfo.shouldSkipPackage)
}
}