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>
|
</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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_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>
|
||||||
|
|
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