BIT-1469: Autofill talkback (#1226)

This commit is contained in:
Ramsey Smith 2024-04-04 19:36:59 -06:00 committed by Álison Fernandes
parent 8f3f87a333
commit 1a12a91a74
7 changed files with 254 additions and 1 deletions

View file

@ -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,

View file

@ -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,
)
}

View file

@ -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)

View file

@ -14,4 +14,5 @@
<string name="give_feedback" translatable="false">Give Feedback</string>
<string name="password_protected" translatable="false">Password Protected</string>
<string name="password_used_to_export" translatable="false">This password will be used to export and import this file</string>
<string name="autofill_suggestion" translatable="false">Autofill suggestion</string>
</resources>

View file

@ -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,

View file

@ -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)
}
}
}

View file

@ -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"