mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-1469: Autofill talkback (#1226)
This commit is contained in:
parent
8f3f87a333
commit
1a12a91a74
7 changed files with 254 additions and 1 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue