Skip to content

Commit

Permalink
PM-11487: Initial accessibility service and processor for handling au…
Browse files Browse the repository at this point in the history
…tofill (#3906)
  • Loading branch information
david-livefront authored Sep 12, 2024
1 parent f544ccc commit 4c1d55e
Show file tree
Hide file tree
Showing 11 changed files with 643 additions and 1 deletion.
20 changes: 20 additions & 0 deletions app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -43,7 +46,7 @@ object AccessibilityModule {

@Singleton
@Provides
fun providesAccessibilityInvokeManager(): AccessibilityAutofillManager =
fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager =
AccessibilityAutofillManagerImpl()

@Singleton
Expand All @@ -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(
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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?)
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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),
)
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/xml/accessibility_service.xml
Original file line number Diff line number Diff line change
@@ -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" />
Loading

0 comments on commit 4c1d55e

Please sign in to comment.