mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-11487: Initial accessibility service and processor for handling autofill (#3906)
This commit is contained in:
parent
f544ccc3ef
commit
4c1d55e9fe
11 changed files with 643 additions and 1 deletions
|
@ -21,6 +21,26 @@
|
|||
</intent-filter>
|
||||
</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 -->
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
|
|
|
@ -6,12 +6,15 @@ import android.os.Build
|
|||
import androidx.annotation.Keep
|
||||
import androidx.core.app.AppComponentFactory
|
||||
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.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
|
||||
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_CREDENTIAL_SERVICE_NAME =
|
||||
"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.
|
||||
*
|
||||
* Services currently being managed:
|
||||
* * [BitwardenAccessibilityService]
|
||||
* * [BitwardenAutofillService]
|
||||
* * [BitwardenAutofillTileService]
|
||||
* * [BitwardenFido2ProviderService]
|
||||
|
@ -44,6 +48,14 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
|
|||
className: String,
|
||||
intent: Intent?,
|
||||
): Service = when (className) {
|
||||
LEGACY_ACCESSIBILITY_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenAccessibilityService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
LEGACY_AUTOFILL_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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_trash">There are no items in the trash.</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="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>
|
||||
|
|
12
app/src/main/res/xml/accessibility_service.xml
Normal file
12
app/src/main/res/xml/accessibility_service.xml
Normal 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" />
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue