diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index f55d7cf5d..fd566975b 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -21,6 +21,26 @@ + + + + + + + + + { + super.instantiateServiceCompat( + cl, + BitwardenAccessibilityService::class.java.name, + intent, + ) + } + LEGACY_AUTOFILL_SERVICE_NAME -> { super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt new file mode 100644 index 000000000..f41b85a0b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/BitwardenAccessibilityService.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt index 0fa7744e4..b22c1ee50 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/di/AccessibilityModule.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di import android.content.Context 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.AccessibilityAutofillManagerImpl 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.parser.AccessibilityParser 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.platform.manager.dispatcher.DispatcherManager import dagger.Module @@ -43,7 +46,7 @@ object AccessibilityModule { @Singleton @Provides - fun providesAccessibilityInvokeManager(): AccessibilityAutofillManager = + fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager = AccessibilityAutofillManagerImpl() @Singleton @@ -55,6 +58,23 @@ object AccessibilityModule { fun providesAccessibilitySelectionManager(): AccessibilitySelectionManager = 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 @Provides fun providesLauncherPackageNameManager( @@ -71,4 +91,10 @@ object AccessibilityModule { fun providesPackageManager( @ApplicationContext context: Context, ): PackageManager = context.packageManager + + @Singleton + @Provides + fun providesPowerManager( + @ApplicationContext context: Context, + ): PowerManager = context.getSystemService(PowerManager::class.java) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt new file mode 100644 index 000000000..a75227804 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessor.kt @@ -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?) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt new file mode 100644 index 000000000..5cf625e7b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorImpl.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/AccessibilityNodeInfoExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/AccessibilityNodeInfoExtensions.kt new file mode 100644 index 000000000..be3a8d33a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/accessibility/util/AccessibilityNodeInfoExtensions.kt @@ -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 = 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), + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da70b383e..4cb470c79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -329,6 +329,7 @@ Scanning will happen automatically. There are no items in this folder. There are no items in the trash. Auto-fill Accessibility Service + Assist with filling username and password fields in other apps and on the web. The Bitwarden auto-fill service uses the Android Autofill Framework to assist in filling login information into other apps on your device. Use the Bitwarden auto-fill service to fill login information into other apps. Open Autofill Settings diff --git a/app/src/main/res/xml/accessibility_service.xml b/app/src/main/res/xml/accessibility_service.xml new file mode 100644 index 000000000..8ab097fbc --- /dev/null +++ b/app/src/main/res/xml/accessibility_service.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt new file mode 100644 index 000000000..4841bd9b4 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/processor/BitwardenAccessibilityProcessorTest.kt @@ -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() + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + every { login } returns null + } + val uri = mockk() + val attemptFill = AccessibilityAction.AttemptFill(cipherView = cipherView, uri = uri) + val rootNode = mockk { + 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 { + every { username } returns testUsername + every { password } returns testPassword + } + val cipherView = mockk { + every { login } returns loginView + } + val mockUsernameField = mockk { + every { fillTextField(testUsername) } just runs + } + val mockPasswordField = mockk { + every { fillTextField(testPassword) } just runs + } + val fillableFields = FillableFields( + usernameFields = listOf(mockUsernameField), + passwordFields = listOf(mockPasswordField), + ) + val uri = mockk() + val attemptFill = AccessibilityAction.AttemptFill(cipherView = cipherView, uri = uri) + val rootNode = mockk { + 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) + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/util/AccessibilityNodeInfoExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/util/AccessibilityNodeInfoExtensionsTest.kt new file mode 100644 index 000000000..eac42fd6a --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/accessibility/util/AccessibilityNodeInfoExtensionsTest.kt @@ -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 { + every { packageName } returns null + } + + assertTrue(accessibilityNodeInfo.shouldSkipPackage) + } + + @Test + fun `shouldSkipPackage when packageName is blank should return true`() { + val accessibilityNodeInfo = mockk { + every { packageName } returns "" + } + + assertTrue(accessibilityNodeInfo.shouldSkipPackage) + } + + @Test + fun `shouldSkipPackage when packageName is system UI package should return true`() { + val accessibilityNodeInfo = mockk { + 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 { + every { packageName } returns "com.x8bit.bitwarden.beta" + } + + assertTrue(accessibilityNodeInfo.shouldSkipPackage) + } + + @Test + fun `shouldSkipPackage when packageName contains with 'launcher' should return true`() { + val accessibilityNodeInfo = mockk { + every { packageName } returns "com.android.launcher.foo" + } + + assertTrue(accessibilityNodeInfo.shouldSkipPackage) + } + + @Test + fun `shouldSkipPackage when packageName is blocked package should return true`() { + val accessibilityNodeInfo = mockk { + every { packageName } returns "com.google.android.googlequicksearchbox" + } + + assertTrue(accessibilityNodeInfo.shouldSkipPackage) + } + + @Test + fun `shouldSkipPackage when packageName is valid should return false`() { + val accessibilityNodeInfo = mockk { + every { packageName } returns "com.another.app" + } + + assertFalse(accessibilityNodeInfo.shouldSkipPackage) + } +}