From 1a12a91a74243accc8f7b10bdb5a0aaff36cb86c Mon Sep 17 00:00:00 2001
From: Ramsey Smith <142836716+ramsey-livefront@users.noreply.github.com>
Date: Thu, 4 Apr 2024 19:36:59 -0600
Subject: [PATCH] BIT-1469: Autofill talkback (#1226)
---
.../ui/autofill/BitwardenRemoteViews.kt | 14 ++
.../ui/autofill/util/AutofillUtils.kt | 41 ++++++
.../util/InlinePresentationSpecExtensions.kt | 13 ++
.../main/res/values/strings_non_localized.xml | 1 +
.../ui/autofill/BitwardenRemoteViewsTest.kt | 21 +++
.../ui/autofill/util/AutofillUtilsTest.kt | 121 ++++++++++++++++++
.../InlinePresentationSpecExtensionsTest.kt | 44 ++++++-
7 files changed, 254 insertions(+), 1 deletion(-)
create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtils.kt
create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtilsTest.kt
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViews.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViews.kt
index 135a376f6..8d30a279a 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViews.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViews.kt
@@ -6,6 +6,7 @@ import androidx.annotation.DrawableRes
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
+import com.x8bit.bitwarden.ui.autofill.util.getAutofillSuggestionContentDescription
import com.x8bit.bitwarden.ui.autofill.util.isSystemDarkMode
/**
@@ -21,6 +22,10 @@ fun buildAutofillRemoteViews(
subtitle = autofillCipher.subtitle,
iconRes = autofillCipher.iconRes,
shouldTintIcon = true,
+ autofillContentDescription = getAutofillSuggestionContentDescription(
+ autofillCipher = autofillCipher,
+ autofillAppInfo = autofillAppInfo,
+ ),
)
/**
@@ -42,10 +47,13 @@ fun buildVaultItemAutofillRemoteViews(
},
iconRes = R.drawable.icon,
shouldTintIcon = false,
+ autofillContentDescription = null,
)
+@Suppress("LongParameterList")
private fun buildAutofillRemoteViews(
autofillAppInfo: AutofillAppInfo,
+ autofillContentDescription: String?,
name: String,
subtitle: String,
@DrawableRes iconRes: Int,
@@ -56,6 +64,12 @@ private fun buildAutofillRemoteViews(
R.layout.autofill_remote_view,
)
.apply {
+ autofillContentDescription?.let {
+ setContentDescription(
+ R.id.container,
+ it,
+ )
+ }
setTextViewText(
R.id.title,
name,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtils.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtils.kt
new file mode 100644
index 000000000..df37afe3c
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtils.kt
@@ -0,0 +1,41 @@
+package com.x8bit.bitwarden.ui.autofill.util
+
+import androidx.core.content.ContextCompat
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
+import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
+
+/**
+ * Creates content description for an autofill suggestion given an [AutofillCipher] and
+ * [AutofillAppInfo].
+ */
+fun getAutofillSuggestionContentDescription(
+ autofillCipher: AutofillCipher,
+ autofillAppInfo: AutofillAppInfo,
+): String =
+ String.format(
+ "%s, %s, %s, %s",
+ ContextCompat.getString(autofillAppInfo.context, R.string.autofill_suggestion),
+ getAutofillSuggestionCipherType(
+ autofillCipher = autofillCipher,
+ autofillAppInfo = autofillAppInfo,
+ ),
+ autofillCipher.name,
+ autofillCipher.subtitle,
+ )
+
+private fun getAutofillSuggestionCipherType(
+ autofillCipher: AutofillCipher,
+ autofillAppInfo: AutofillAppInfo,
+): String =
+ when (autofillCipher) {
+ is AutofillCipher.Card -> ContextCompat.getString(
+ autofillAppInfo.context,
+ R.string.type_card,
+ )
+
+ is AutofillCipher.Login -> ContextCompat.getString(
+ autofillAppInfo.context,
+ R.string.type_login,
+ )
+ }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensions.kt
index 6817282f9..c3735615f 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensions.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensions.kt
@@ -35,6 +35,7 @@ fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
PendingIntent.FLAG_IMMUTABLE,
),
autofillAppInfo = autofillAppInfo,
+ autofillCipher = autofillCipher,
title = autofillCipher.name,
subtitle = autofillCipher.subtitle,
iconRes = autofillCipher.iconRes,
@@ -54,6 +55,7 @@ fun InlinePresentationSpec.createVaultItemInlinePresentationOrNull(
createInlinePresentationOrNull(
pendingIntent = pendingIntent,
autofillAppInfo = autofillAppInfo,
+ autofillCipher = null,
title = autofillAppInfo.context.getString(R.string.app_name),
subtitle = if (isLocked) {
autofillAppInfo.context.getString(R.string.vault_is_locked)
@@ -70,6 +72,7 @@ fun InlinePresentationSpec.createVaultItemInlinePresentationOrNull(
private fun InlinePresentationSpec.createInlinePresentationOrNull(
pendingIntent: PendingIntent,
autofillAppInfo: AutofillAppInfo,
+ autofillCipher: AutofillCipher?,
title: String,
subtitle: String,
@DrawableRes iconRes: Int,
@@ -96,6 +99,16 @@ private fun InlinePresentationSpec.createInlinePresentationOrNull(
}
val slice = InlineSuggestionUi
.newContentBuilder(pendingIntent)
+ .also { contentBuilder ->
+ autofillCipher?.let {
+ contentBuilder.setContentDescription(
+ getAutofillSuggestionContentDescription(
+ autofillAppInfo = autofillAppInfo,
+ autofillCipher = it,
+ ),
+ )
+ }
+ }
.setTitle(title)
.setSubtitle(subtitle)
.setStartIcon(icon)
diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml
index 01220718c..88e54f210 100644
--- a/app/src/main/res/values/strings_non_localized.xml
+++ b/app/src/main/res/values/strings_non_localized.xml
@@ -14,4 +14,5 @@
Give Feedback
Password Protected
This password will be used to export and import this file
+ Autofill suggestion
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViewsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViewsTest.kt
index 29ef80a69..83c59f6aa 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViewsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/BitwardenRemoteViewsTest.kt
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.autofill
import android.content.Context
import android.widget.RemoteViews
+import androidx.core.content.ContextCompat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
@@ -45,12 +46,14 @@ class BitwardenRemoteViewsTest {
@BeforeEach
fun setup() {
+ mockkStatic(ContextCompat::getString)
mockkStatic(Context::isSystemDarkMode)
mockkConstructor(RemoteViews::class)
}
@AfterEach
fun teardown() {
+ unmockkStatic(ContextCompat::getString)
unmockkStatic(Context::isSystemDarkMode)
unmockkConstructor(RemoteViews::class)
}
@@ -59,6 +62,15 @@ class BitwardenRemoteViewsTest {
fun `buildAutofillRemoteViews should set values and light mode colors when not night mode`() {
// Setup
every { testContext.isSystemDarkMode } returns false
+ every {
+ ContextCompat.getString(testContext, R.string.autofill_suggestion)
+ } returns "Autofill suggestion"
+ every {
+ ContextCompat.getString(testContext, R.string.type_card)
+ } returns "Card"
+ every {
+ ContextCompat.getString(testContext, R.string.type_login)
+ } returns "Login"
prepareRemoteViews(
name = NAME,
subtitle = SUBTITLE,
@@ -123,6 +135,15 @@ class BitwardenRemoteViewsTest {
fun `buildAutofillRemoteViews should set values and dark mode colors when night mode`() {
// Setup
every { testContext.isSystemDarkMode } returns true
+ every {
+ ContextCompat.getString(testContext, R.string.autofill_suggestion)
+ } returns "Autofill suggestion"
+ every {
+ ContextCompat.getString(testContext, R.string.type_card)
+ } returns "Card"
+ every {
+ ContextCompat.getString(testContext, R.string.type_login)
+ } returns "Login"
prepareRemoteViews(
name = NAME,
subtitle = SUBTITLE,
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtilsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtilsTest.kt
new file mode 100644
index 000000000..9450c447b
--- /dev/null
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/AutofillUtilsTest.kt
@@ -0,0 +1,121 @@
+package com.x8bit.bitwarden.ui.autofill.util
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
+import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class AutofillUtilsTest {
+ private val context: Context = mockk()
+
+ @BeforeEach
+ fun setup() {
+ mockkStatic(ContextCompat::getString)
+ every {
+ ContextCompat.getString(context, R.string.autofill_suggestion)
+ } returns "Autofill suggestion"
+ every {
+ ContextCompat.getString(context, R.string.type_card)
+ } returns "Card"
+ every {
+ ContextCompat.getString(context, R.string.type_login)
+ } returns "Login"
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkStatic(
+ ContextCompat::getString,
+ )
+ }
+
+ @Test
+ fun `getAutofillSuggestionContentDescription should return correct content description`() {
+ listOf(
+ Triple(
+ first = AutofillCipher.Card(
+ cardholderName = "John",
+ cipherId = null,
+ code = "code",
+ expirationMonth = "expirationMonth",
+ expirationYear = "expirationYear",
+ name = "Cipher One",
+ number = "number",
+ subtitle = "Subtitle",
+ ),
+ second = AutofillAppInfo(
+ context = context,
+ packageName = "com.x8bit.bitwarden",
+ sdkInt = 34,
+ ),
+ third = "Autofill suggestion, Card, Cipher One, Subtitle",
+ ),
+ Triple(
+ first = AutofillCipher.Card(
+ cardholderName = "John",
+ cipherId = null,
+ code = "code",
+ expirationMonth = "expirationMonth",
+ expirationYear = "expirationYear",
+ name = "Capital One",
+ number = "number",
+ subtitle = "JohnCardName",
+ ),
+ second = AutofillAppInfo(
+ context = context,
+ packageName = "com.x8bit.bitwarden",
+ sdkInt = 34,
+ ),
+ third = "Autofill suggestion, Card, Capital One, JohnCardName",
+ ),
+ Triple(
+ first = AutofillCipher.Login(
+ cipherId = null,
+ isTotpEnabled = false,
+ name = "Cipher One",
+ password = "password",
+ username = "username",
+ subtitle = "Subtitle",
+ ),
+ second = AutofillAppInfo(
+ context = context,
+ packageName = "com.x8bit.bitwarden",
+ sdkInt = 34,
+ ),
+ third = "Autofill suggestion, Login, Cipher One, Subtitle",
+ ),
+ Triple(
+ first = AutofillCipher.Login(
+ cipherId = null,
+ isTotpEnabled = false,
+ name = "Amazon",
+ password = "password",
+ username = "username",
+ subtitle = "AmazonSubtitle",
+ ),
+ second = AutofillAppInfo(
+ context = context,
+ packageName = "com.x8bit.bitwarden",
+ sdkInt = 34,
+ ),
+ third = "Autofill suggestion, Login, Amazon, AmazonSubtitle",
+ ),
+ )
+ .forEach {
+ val result = getAutofillSuggestionContentDescription(
+ autofillCipher = it.first,
+ autofillAppInfo = it.second,
+ )
+ assertEquals(it.third, result)
+ }
+ }
+}
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensionsTest.kt
index 69c2fcae6..910dc17f4 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensionsTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/autofill/util/InlinePresentationSpecExtensionsTest.kt
@@ -10,6 +10,7 @@ import android.os.Bundle
import android.widget.inline.InlinePresentationSpec
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
+import androidx.core.content.ContextCompat
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
@@ -36,6 +37,7 @@ class InlinePresentationSpecExtensionsTest {
@BeforeEach
fun setup() {
+ mockkStatic(ContextCompat::getString)
mockkStatic(Context::isSystemDarkMode)
mockkStatic(Icon::class)
mockkStatic(InlineSuggestionUi::class)
@@ -45,7 +47,8 @@ class InlinePresentationSpecExtensionsTest {
@AfterEach
fun teardown() {
- mockkStatic(Context::isSystemDarkMode)
+ unmockkStatic(ContextCompat::getString)
+ unmockkStatic(Context::isSystemDarkMode)
unmockkStatic(Icon::class)
unmockkStatic(InlineSuggestionUi::class)
unmockkStatic(PendingIntent::getService)
@@ -103,6 +106,7 @@ class InlinePresentationSpecExtensionsTest {
icon = icon,
pendingIntent = pendingIntent,
isSystemDarkMode = true,
+ cipherType = CARD,
)
// Test
@@ -141,6 +145,15 @@ class InlinePresentationSpecExtensionsTest {
@Test
fun `createCipherInlinePresentationOrNull should return presentation with login icon when login cipher and compatible`() {
// Setup
+ every {
+ ContextCompat.getString(testContext, R.string.autofill_suggestion)
+ } returns AUTOFILL_SUGGESTION
+ every {
+ ContextCompat.getString(testContext, R.string.type_card)
+ } returns CARD
+ every {
+ ContextCompat.getString(testContext, R.string.type_login)
+ } returns LOGIN
val icon: Icon = mockk()
val iconRes = R.drawable.ic_login_item
val autofillCipher: AutofillCipher.Login = mockk {
@@ -297,7 +310,17 @@ class InlinePresentationSpecExtensionsTest {
icon: Icon,
pendingIntent: PendingIntent,
isSystemDarkMode: Boolean,
+ cipherType: String = LOGIN,
) {
+ every {
+ ContextCompat.getString(testContext, R.string.autofill_suggestion)
+ } returns AUTOFILL_SUGGESTION
+ every {
+ ContextCompat.getString(testContext, R.string.type_card)
+ } returns CARD
+ every {
+ ContextCompat.getString(testContext, R.string.type_login)
+ } returns LOGIN
val slice: Slice = mockk()
every {
UiVersions.getVersions(testStyle)
@@ -320,6 +343,19 @@ class InlinePresentationSpecExtensionsTest {
)
.setTint(ICON_TINT)
} returns icon
+ every {
+ InlineSuggestionUi
+ .newContentBuilder(pendingIntent)
+ .setContentDescription(
+ createMockContentDescription(cipherType),
+ )
+ .setTitle(AUTOFILL_CIPHER_NAME)
+ .setSubtitle(AUTOFILL_CIPHER_SUBTITLE)
+ .setStartIcon(icon)
+ .build()
+ .slice
+ } returns slice
+
every {
InlineSuggestionUi
.newContentBuilder(pendingIntent)
@@ -371,6 +407,12 @@ class InlinePresentationSpecExtensionsTest {
}
}
+private fun createMockContentDescription(cipherType: String): String =
+ "${AUTOFILL_SUGGESTION}, $cipherType, ${AUTOFILL_CIPHER_NAME}, ${AUTOFILL_CIPHER_SUBTITLE}"
+
+private const val AUTOFILL_SUGGESTION = "Autofill suggestion"
+private const val CARD = "Card"
+private const val LOGIN = "Login"
private const val APP_NAME = "Bitwarden"
private const val AUTOFILL_CIPHER_NAME = "Cipher1"
private const val AUTOFILL_CIPHER_SUBTITLE = "Subtitle"