mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1462: Add Vault suggestion to autofill (#699)
This commit is contained in:
parent
a760127711
commit
2f918650a1
16 changed files with 1122 additions and 218 deletions
|
@ -26,7 +26,7 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstancePerTask"
|
||||
android:launchMode="@integer/launchModeAPIlevel"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.service.autofill.FillResponse
|
|||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.fillableAutofillIds
|
||||
|
||||
/**
|
||||
* The default implementation for [FillResponseBuilder]. This is a component for compiling fulfilled
|
||||
|
@ -14,7 +16,7 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
|||
autofillAppInfo: AutofillAppInfo,
|
||||
filledData: FilledData,
|
||||
): FillResponse? =
|
||||
if (filledData.filledPartitions.any { it.filledItems.isNotEmpty() }) {
|
||||
if (filledData.fillableAutofillIds.isNotEmpty()) {
|
||||
val fillResponseBuilder = FillResponse.Builder()
|
||||
|
||||
filledData
|
||||
|
@ -35,9 +37,14 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: add vault item dataset (BIT-1296)
|
||||
|
||||
fillResponseBuilder
|
||||
// Add the Vault Item
|
||||
.addDataset(
|
||||
filledData
|
||||
.buildVaultItemDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
),
|
||||
)
|
||||
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
||||
.build()
|
||||
} else {
|
||||
|
|
|
@ -19,6 +19,7 @@ class FilledDataBuilderImpl(
|
|||
) : FilledDataBuilder {
|
||||
override suspend fun build(autofillRequest: AutofillRequest.Fillable): FilledData {
|
||||
// TODO: determine whether or not the vault is locked (BIT-1296)
|
||||
val isVaultLocked = false
|
||||
|
||||
// Subtract one to make sure there is space for the vault item.
|
||||
val maxCipherInlineSuggestionsCount = autofillRequest.maxInlineSuggestionsCount - 1
|
||||
|
@ -78,7 +79,10 @@ class FilledDataBuilderImpl(
|
|||
return FilledData(
|
||||
filledPartitions = filledPartitions,
|
||||
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
||||
originalPartition = autofillRequest.partition,
|
||||
uri = autofillRequest.uri,
|
||||
vaultItemInlinePresentationSpec = vaultItemInlinePresentationSpec,
|
||||
isVaultLocked = isVaultLocked,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,5 +10,8 @@ import android.widget.inline.InlinePresentationSpec
|
|||
data class FilledData(
|
||||
val filledPartitions: List<FilledPartition>,
|
||||
val ignoreAutofillIds: List<AutofillId>,
|
||||
val originalPartition: AutofillPartition,
|
||||
val uri: String?,
|
||||
val vaultItemInlinePresentationSpec: InlinePresentationSpec?,
|
||||
val isVaultLocked: Boolean,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Presentations
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
import com.x8bit.bitwarden.ui.autofill.buildVaultItemAutofillRemoteViews
|
||||
import com.x8bit.bitwarden.ui.autofill.util.createVaultItemInlinePresentationOrNull
|
||||
|
||||
/**
|
||||
* Returns all the possible [AutofillId]s that were potentially fillable for the given [FilledData].
|
||||
*/
|
||||
val FilledData.fillableAutofillIds: List<AutofillId>
|
||||
get() = this.originalPartition.views.map { it.data.autofillId }
|
||||
|
||||
/**
|
||||
* Builds a [Dataset] for the Vault item.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FilledData.buildVaultItemDataset(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
): Dataset {
|
||||
val intent = Intent(
|
||||
autofillAppInfo.context,
|
||||
MainActivity::class.java,
|
||||
)
|
||||
// TODO: Add additional data to the Intent to be pulled out in the app (BIT-1296)
|
||||
|
||||
val pendingIntent = PendingIntent
|
||||
.getActivity(
|
||||
autofillAppInfo.context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
|
||||
val remoteViewsForOverlay = buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = this.isVaultLocked,
|
||||
)
|
||||
|
||||
val filledItems = this
|
||||
.fillableAutofillIds
|
||||
.map {
|
||||
FilledItem(
|
||||
autofillId = it,
|
||||
// A placeholder value must be used for now, but this are temporary. It will get
|
||||
// reset when a real value is chosen by the user in the "authentication activity"
|
||||
// that is launched as part of the PendingIntent.
|
||||
value = AutofillValue.forText("PLACEHOLDER"),
|
||||
)
|
||||
}
|
||||
val inlinePresentationSpec = this.vaultItemInlinePresentationSpec
|
||||
return Dataset.Builder()
|
||||
.setAuthentication(pendingIntent.intentSender)
|
||||
.apply {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||
addVaultItemDataPostTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
remoteViews = remoteViewsForOverlay,
|
||||
filledItems = filledItems,
|
||||
inlinePresentationSpec = inlinePresentationSpec,
|
||||
isLocked = isVaultLocked,
|
||||
)
|
||||
} else {
|
||||
addVaultItemDataPreTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
remoteViews = remoteViewsForOverlay,
|
||||
filledItems = filledItems,
|
||||
inlinePresentationSpec = inlinePresentationSpec,
|
||||
isLocked = isVaultLocked,
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Vault data to the given [Dataset.Builder] for post-Tiramisu versions.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private fun Dataset.Builder.addVaultItemDataPostTiramisu(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
pendingIntent: PendingIntent,
|
||||
remoteViews: RemoteViews,
|
||||
filledItems: List<FilledItem>,
|
||||
inlinePresentationSpec: InlinePresentationSpec?,
|
||||
isLocked: Boolean,
|
||||
): Dataset.Builder {
|
||||
val presentationBuilder = Presentations.Builder()
|
||||
inlinePresentationSpec
|
||||
?.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
isLocked = isLocked,
|
||||
)
|
||||
?.let { inlinePresentation ->
|
||||
presentationBuilder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
val presentation = presentationBuilder
|
||||
.setMenuPresentation(remoteViews)
|
||||
.build()
|
||||
filledItems
|
||||
.forEach {
|
||||
it.applyToDatasetPostTiramisu(
|
||||
datasetBuilder = this,
|
||||
presentations = presentation,
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Vault data to the given [Dataset.Builder] for pre-Tiramisu versions.
|
||||
*/
|
||||
@Suppress("DEPRECATION", "LongParameterList")
|
||||
@SuppressLint("NewApi")
|
||||
private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
pendingIntent: PendingIntent,
|
||||
remoteViews: RemoteViews,
|
||||
filledItems: List<FilledItem>,
|
||||
inlinePresentationSpec: InlinePresentationSpec?,
|
||||
isLocked: Boolean,
|
||||
): Dataset.Builder {
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||
inlinePresentationSpec
|
||||
?.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
isLocked = isLocked,
|
||||
)
|
||||
?.let { inlinePresentation ->
|
||||
this.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
filledItems
|
||||
.forEach {
|
||||
it.applyToDatasetPreTiramisu(
|
||||
datasetBuilder = this,
|
||||
remoteViews = remoteViews,
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from an initial pending intent flag (ex: [PendingIntent.FLAG_CANCEL_CURRENT], derives
|
||||
* a new flag with the correct mutability determined by [isMutable].
|
||||
*/
|
||||
private fun Int.toPendingIntentMutabilityFlag(): Int =
|
||||
// Mutable flag was added on API level 31
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
this or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
this
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.autofill
|
|||
|
||||
import android.content.Context
|
||||
import android.widget.RemoteViews
|
||||
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
|
||||
|
@ -13,6 +14,42 @@ import com.x8bit.bitwarden.ui.autofill.util.isSystemDarkMode
|
|||
fun buildAutofillRemoteViews(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillCipher: AutofillCipher,
|
||||
): RemoteViews =
|
||||
buildAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
name = autofillCipher.name,
|
||||
subtitle = autofillCipher.subtitle,
|
||||
iconRes = autofillCipher.iconRes,
|
||||
shouldTintIcon = true,
|
||||
)
|
||||
|
||||
/**
|
||||
* Build [RemoteViews] to represent the Vault Item suggestion (for opening or unlocking the vault).
|
||||
*/
|
||||
fun buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
isLocked: Boolean,
|
||||
): RemoteViews =
|
||||
buildAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
name = autofillAppInfo.context.getString(R.string.app_name),
|
||||
subtitle = autofillAppInfo.context.run {
|
||||
if (isLocked) {
|
||||
getString(R.string.vault_is_locked)
|
||||
} else {
|
||||
getString(R.string.go_to_my_vault)
|
||||
}
|
||||
},
|
||||
iconRes = R.drawable.icon,
|
||||
shouldTintIcon = false,
|
||||
)
|
||||
|
||||
private fun buildAutofillRemoteViews(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
name: String,
|
||||
subtitle: String,
|
||||
@DrawableRes iconRes: Int,
|
||||
shouldTintIcon: Boolean,
|
||||
): RemoteViews =
|
||||
RemoteViews(
|
||||
autofillAppInfo.packageName,
|
||||
|
@ -21,27 +58,21 @@ fun buildAutofillRemoteViews(
|
|||
.apply {
|
||||
setTextViewText(
|
||||
R.id.title,
|
||||
autofillCipher.name,
|
||||
name,
|
||||
)
|
||||
setTextViewText(
|
||||
R.id.subtitle,
|
||||
autofillCipher.subtitle,
|
||||
subtitle,
|
||||
)
|
||||
setImageViewResource(
|
||||
R.id.icon,
|
||||
autofillCipher.iconRes,
|
||||
iconRes,
|
||||
)
|
||||
|
||||
setInt(
|
||||
R.id.container,
|
||||
"setBackgroundColor",
|
||||
autofillAppInfo.context.surface,
|
||||
)
|
||||
setInt(
|
||||
R.id.icon,
|
||||
"setColorFilter",
|
||||
autofillAppInfo.context.onSurface,
|
||||
)
|
||||
setInt(
|
||||
R.id.title,
|
||||
"setTextColor",
|
||||
|
@ -52,6 +83,13 @@ fun buildAutofillRemoteViews(
|
|||
"setTextColor",
|
||||
autofillAppInfo.context.onSurfaceVariant,
|
||||
)
|
||||
if (shouldTintIcon) {
|
||||
setInt(
|
||||
R.id.icon,
|
||||
"setColorFilter",
|
||||
autofillAppInfo.context.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val Context.onSurface: Int
|
||||
|
|
|
@ -3,10 +3,12 @@ package com.x8bit.bitwarden.ui.autofill.util
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.autofill.InlinePresentation
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
|
@ -19,10 +21,59 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
|||
* it fails, return null.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillCipher: AutofillCipher,
|
||||
): InlinePresentation? =
|
||||
createInlinePresentationOrNull(
|
||||
pendingIntent = PendingIntent.getService(
|
||||
autofillAppInfo.context,
|
||||
0,
|
||||
Intent(),
|
||||
PendingIntent.FLAG_ONE_SHOT or
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
),
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
title = autofillCipher.name,
|
||||
subtitle = autofillCipher.subtitle,
|
||||
iconRes = autofillCipher.iconRes,
|
||||
shouldTintIcon = true,
|
||||
)
|
||||
|
||||
/**
|
||||
* Try creating an [InlinePresentation] for the Vault item with this [InlinePresentationSpec]. If
|
||||
* it fails, return null.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun InlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
pendingIntent: PendingIntent,
|
||||
isLocked: Boolean,
|
||||
): InlinePresentation? =
|
||||
createInlinePresentationOrNull(
|
||||
pendingIntent = pendingIntent,
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
title = autofillAppInfo.context.getString(R.string.app_name),
|
||||
subtitle = if (isLocked) {
|
||||
autofillAppInfo.context.getString(R.string.vault_is_locked)
|
||||
} else {
|
||||
autofillAppInfo.context.getString(R.string.my_vault)
|
||||
},
|
||||
iconRes = R.drawable.icon,
|
||||
shouldTintIcon = false,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun InlinePresentationSpec.createInlinePresentationOrNull(
|
||||
pendingIntent: PendingIntent,
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
@DrawableRes iconRes: Int,
|
||||
shouldTintIcon: Boolean,
|
||||
): InlinePresentation? {
|
||||
val isInlineCompatible = UiVersions
|
||||
.getVersions(style)
|
||||
|
@ -30,24 +81,23 @@ fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
|
|||
|
||||
if (!isInlineCompatible) return null
|
||||
|
||||
val pendingIntent = PendingIntent.getService(
|
||||
autofillAppInfo.context,
|
||||
0,
|
||||
Intent(),
|
||||
PendingIntent.FLAG_ONE_SHOT or
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val icon = Icon
|
||||
.createWithResource(
|
||||
autofillAppInfo.context,
|
||||
autofillCipher.iconRes,
|
||||
iconRes,
|
||||
)
|
||||
.setTint(autofillAppInfo.contentColor)
|
||||
.run {
|
||||
if (shouldTintIcon) {
|
||||
setTint(autofillAppInfo.contentColor)
|
||||
} else {
|
||||
// Remove tinting
|
||||
setTintBlendMode(BlendMode.DST)
|
||||
}
|
||||
}
|
||||
val slice = InlineSuggestionUi
|
||||
.newContentBuilder(pendingIntent)
|
||||
.setTitle(autofillCipher.name)
|
||||
.setSubtitle(autofillCipher.subtitle)
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
|
|
30
app/src/main/res/drawable/icon.xml
Normal file
30
app/src/main/res/drawable/icon.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="1024dp"
|
||||
android:height="1024dp"
|
||||
android:viewportHeight="1024"
|
||||
android:viewportWidth="1024">
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:pathData="M1024,864c0,88.4 -71.6,160 -160,160H160C71.6,1024 0,952.4 0,864V160C0,71.6 71.6,0 160,0h704c88.4,0 160,71.6 160,160V864z" />
|
||||
<path android:pathData="M803.2,195.8l668.8,668.8l-376,694.4l-792,-792l208,120l280,-280z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="922.23486"
|
||||
android:endY="1353.2649"
|
||||
android:startX="96.33417"
|
||||
android:startY="389.33688"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#3F000000"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M802.24,194.52c-5.92,-5.92 -12.96,-8.96 -20.96,-8.96L242.72,185.56c-8.16,0 -15.04,3.04 -20.96,8.96C215.84,200.44 212.8,207.32 212.8,215.48l0,359.04c0,26.72 5.28,53.44 15.68,79.68c10.4,26.4 23.36,49.76 38.88,70.08c15.36,20.48 33.76,40.32 55.2,59.52c21.28,19.36 41.12,35.36 59.2,48.16c18.08,12.8 36.96,24.8 56.64,36.16c19.68,11.36 33.6,19.04 41.92,23.2c8.32,4 14.88,7.2 19.84,9.28c3.68,1.92 7.84,2.88 12.16,2.88c4.32,0 8.48,-0.96 12.16,-2.88c4.96,-2.24 11.68,-5.28 19.84,-9.28c8.32,-4 22.24,-11.84 41.92,-23.2c19.68,-11.36 38.56,-23.52 56.64,-36.16c18.08,-12.8 37.76,-28.8 59.2,-48.16c21.28,-19.36 39.68,-39.2 55.2,-59.52c15.36,-20.48 28.32,-43.84 38.72,-70.08c10.4,-26.4 15.68,-52.96 15.68,-79.68L811.68,215.48C811.2,207.32 808.16,200.44 802.24,194.52zM732.8,577.88C732.8,707.8 512,819.8 512,819.8L512,262.36l220.8,0C732.8,262.36 732.8,447.96 732.8,577.88z" />
|
||||
</vector>
|
5
app/src/main/res/values-v30/manifest.xml
Normal file
5
app/src/main/res/values-v30/manifest.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<resources>
|
||||
<!-- standard -->
|
||||
<integer name="launchModeAPIlevel">0</integer>
|
||||
</resources>
|
5
app/src/main/res/values/manifest.xml
Normal file
5
app/src/main/res/values/manifest.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<resources>
|
||||
<!-- singleTask -->
|
||||
<integer name="launchModeAPIlevel">2</integer>
|
||||
</resources>
|
|
@ -5,15 +5,19 @@ import android.service.autofill.Dataset
|
|||
import android.service.autofill.FillResponse
|
||||
import android.view.autofill.AutofillId
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
@ -26,6 +30,7 @@ class FillResponseBuilderTest {
|
|||
|
||||
private val context: Context = mockk()
|
||||
private val dataset: Dataset = mockk()
|
||||
private val vaultItemDataSet: Dataset = mockk()
|
||||
private val fillResponse: FillResponse = mockk()
|
||||
private val appInfo: AutofillAppInfo = AutofillAppInfo(
|
||||
context = context,
|
||||
|
@ -42,6 +47,7 @@ class FillResponseBuilderTest {
|
|||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkConstructor(FillResponse.Builder::class)
|
||||
mockkStatic(FilledData::buildVaultItemDataset)
|
||||
mockkStatic(FilledPartition::buildDataset)
|
||||
every { anyConstructed<FillResponse.Builder>().build() } returns fillResponse
|
||||
|
||||
|
@ -51,28 +57,12 @@ class FillResponseBuilderTest {
|
|||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkConstructor(FillResponse.Builder::class)
|
||||
mockkStatic(FilledPartition::buildDataset)
|
||||
unmockkStatic(FilledData::buildVaultItemDataset)
|
||||
unmockkStatic(FilledPartition::buildDataset)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build should return null when filledPartitions is empty`() {
|
||||
// Test
|
||||
val filledData = FilledData(
|
||||
filledPartitions = emptyList(),
|
||||
ignoreAutofillIds = emptyList(),
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
)
|
||||
val actual = fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
filledData = filledData,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `build should return null when filledPartitions contains no views`() {
|
||||
fun `build should return null when original partition contains no views`() {
|
||||
// Test
|
||||
val filledPartitions = FilledPartition(
|
||||
autofillCipher = mockk(),
|
||||
|
@ -84,7 +74,12 @@ class FillResponseBuilderTest {
|
|||
filledPartitions,
|
||||
),
|
||||
ignoreAutofillIds = emptyList(),
|
||||
originalPartition = AutofillPartition.Login(
|
||||
views = emptyList(),
|
||||
),
|
||||
uri = null,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
val actual = fillResponseBuilder.build(
|
||||
autofillAppInfo = appInfo,
|
||||
|
@ -111,14 +106,37 @@ class FillResponseBuilderTest {
|
|||
val filledData = FilledData(
|
||||
filledPartitions = filledPartitions,
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
originalPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = mockk(),
|
||||
idPackage = null,
|
||||
isFocused = true,
|
||||
webDomain = null,
|
||||
webScheme = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
uri = null,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
every {
|
||||
filledPartitionOne.buildDataset(
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
} returns dataset
|
||||
mockBuilder<FillResponse.Builder> { it.addDataset(dataset) }
|
||||
every {
|
||||
filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
} returns vaultItemDataSet
|
||||
mockBuilder<FillResponse.Builder> {
|
||||
it.addDataset(dataset)
|
||||
it.addDataset(vaultItemDataSet)
|
||||
}
|
||||
mockBuilder<FillResponse.Builder> {
|
||||
it.setIgnoredIds(
|
||||
ignoredAutofillIdOne,
|
||||
|
@ -139,7 +157,11 @@ class FillResponseBuilderTest {
|
|||
filledPartitionOne.buildDataset(
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = appInfo,
|
||||
)
|
||||
anyConstructed<FillResponse.Builder>().addDataset(dataset)
|
||||
anyConstructed<FillResponse.Builder>().addDataset(vaultItemDataSet)
|
||||
anyConstructed<FillResponse.Builder>().setIgnoredIds(
|
||||
ignoredAutofillIdOne,
|
||||
ignoredAutofillIdTwo,
|
||||
|
|
|
@ -106,7 +106,10 @@ class FilledDataBuilderTest {
|
|||
filledPartition,
|
||||
),
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
originalPartition = autofillPartition,
|
||||
uri = URI,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
coEvery {
|
||||
autofillCipherProvider.getLoginAutofillCiphers(
|
||||
|
@ -167,7 +170,10 @@ class FilledDataBuilderTest {
|
|||
val expected = FilledData(
|
||||
filledPartitions = emptyList(),
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
originalPartition = autofillPartition,
|
||||
uri = null,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
|
||||
// Test
|
||||
|
@ -242,7 +248,10 @@ class FilledDataBuilderTest {
|
|||
filledPartition,
|
||||
),
|
||||
ignoreAutofillIds = ignoreAutofillIds,
|
||||
originalPartition = autofillPartition,
|
||||
uri = URI,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
coEvery { autofillCipherProvider.getCardAutofillCiphers() } returns listOf(autofillCipher)
|
||||
every { autofillViewCode.buildFilledItemOrNull(code) } returns filledItemCode
|
||||
|
@ -337,7 +346,10 @@ class FilledDataBuilderTest {
|
|||
filledPartitionThree,
|
||||
),
|
||||
ignoreAutofillIds = emptyList(),
|
||||
originalPartition = autofillPartition,
|
||||
uri = URI,
|
||||
vaultItemInlinePresentationSpec = inlinePresentationSpec,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
coEvery {
|
||||
autofillCipherProvider.getLoginAutofillCiphers(
|
||||
|
|
|
@ -111,7 +111,10 @@ class AutofillProcessorTest {
|
|||
val filledData = FilledData(
|
||||
filledPartitions = listOf(mockk()),
|
||||
ignoreAutofillIds = emptyList(),
|
||||
originalPartition = mockk(),
|
||||
uri = null,
|
||||
vaultItemInlinePresentationSpec = null,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
val fillResponse: FillResponse = mockk()
|
||||
val autofillRequest: AutofillRequest.Fillable = mockk()
|
||||
|
|
|
@ -0,0 +1,303 @@
|
|||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import android.content.res.Resources
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.InlinePresentation
|
||||
import android.service.autofill.Presentations
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||
import com.x8bit.bitwarden.ui.autofill.buildVaultItemAutofillRemoteViews
|
||||
import com.x8bit.bitwarden.ui.autofill.util.createVaultItemInlinePresentationOrNull
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
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 FilledDataExtensionsTest {
|
||||
private val autofillCipher: AutofillCipher = mockk {
|
||||
every { this@mockk.name } returns CIPHER_NAME
|
||||
}
|
||||
private val autofillId: AutofillId = mockk()
|
||||
private val autofillValue: AutofillValue = mockk()
|
||||
private val res: Resources = mockk()
|
||||
private val context: Context = mockk {
|
||||
every { this@mockk.resources } returns res
|
||||
}
|
||||
private val dataset: Dataset = mockk()
|
||||
private val filledItem: FilledItem = mockk() {
|
||||
every { autofillId } returns mockk()
|
||||
}
|
||||
private val filledItemPlaceholder = FilledItem(
|
||||
autofillId = autofillId,
|
||||
value = autofillValue,
|
||||
)
|
||||
private val inlinePresentationSpec: InlinePresentationSpec = mockk()
|
||||
private val filledPartition = FilledPartition(
|
||||
autofillCipher = autofillCipher,
|
||||
filledItems = listOf(
|
||||
filledItem,
|
||||
),
|
||||
inlinePresentationSpec = inlinePresentationSpec,
|
||||
)
|
||||
private val filledData = FilledData(
|
||||
filledPartitions = listOf(filledPartition),
|
||||
ignoreAutofillIds = emptyList(),
|
||||
originalPartition = AutofillPartition.Login(
|
||||
views = listOf(
|
||||
AutofillView.Login.Username(
|
||||
data = AutofillView.Data(
|
||||
autofillId = autofillId,
|
||||
idPackage = null,
|
||||
isFocused = true,
|
||||
webDomain = null,
|
||||
webScheme = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
uri = "uri",
|
||||
vaultItemInlinePresentationSpec = inlinePresentationSpec,
|
||||
isVaultLocked = false,
|
||||
)
|
||||
private val mockIntentSender: IntentSender = mockk()
|
||||
private val pendingIntent: PendingIntent = mockk() {
|
||||
every { intentSender } returns mockIntentSender
|
||||
}
|
||||
private val presentations: Presentations = mockk()
|
||||
private val remoteViews: RemoteViews = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkConstructor(Dataset.Builder::class)
|
||||
mockkConstructor(Presentations.Builder::class)
|
||||
mockkStatic(::buildVaultItemAutofillRemoteViews)
|
||||
mockkStatic(AutofillValue::forText)
|
||||
mockkStatic(FilledItem::applyToDatasetPostTiramisu)
|
||||
mockkStatic(FilledItem::applyToDatasetPreTiramisu)
|
||||
mockkStatic(InlinePresentationSpec::createVaultItemInlinePresentationOrNull)
|
||||
mockkStatic(PendingIntent::class)
|
||||
every { anyConstructed<Dataset.Builder>().build() } returns dataset
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkConstructor(Dataset.Builder::class)
|
||||
unmockkConstructor(Presentations.Builder::class)
|
||||
unmockkStatic(::buildVaultItemAutofillRemoteViews)
|
||||
unmockkStatic(AutofillValue::forText)
|
||||
unmockkStatic(FilledItem::applyToDatasetPostTiramisu)
|
||||
unmockkStatic(FilledItem::applyToDatasetPreTiramisu)
|
||||
unmockkStatic(InlinePresentationSpec::createVaultItemInlinePresentationOrNull)
|
||||
unmockkStatic(PendingIntent::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fillableAutofillIds should return a list derived from the original partition`() {
|
||||
assertEquals(
|
||||
listOf(autofillId),
|
||||
filledData.fillableAutofillIds,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildVaultItemDataset should applyToDatasetPostTiramisu when sdkInt is at least 33`() {
|
||||
// Setup
|
||||
val autofillAppInfo = AutofillAppInfo(
|
||||
context = context,
|
||||
packageName = PACKAGE_NAME,
|
||||
sdkInt = 34,
|
||||
)
|
||||
val inlinePresentation: InlinePresentation = mockk()
|
||||
every { AutofillValue.forText(any()) } returns autofillValue
|
||||
every { PendingIntent.getActivity(any(), any(), any(), any()) } returns pendingIntent
|
||||
every {
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
} returns remoteViews
|
||||
every {
|
||||
inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = any(),
|
||||
isLocked = false,
|
||||
)
|
||||
} returns inlinePresentation
|
||||
mockBuilder<Dataset.Builder> { it.setAuthentication(mockIntentSender) }
|
||||
mockBuilder<Presentations.Builder> { it.setInlinePresentation(inlinePresentation) }
|
||||
mockBuilder<Presentations.Builder> { it.setMenuPresentation(remoteViews) }
|
||||
every {
|
||||
filledItemPlaceholder.applyToDatasetPostTiramisu(
|
||||
datasetBuilder = any(),
|
||||
presentations = presentations,
|
||||
)
|
||||
} just runs
|
||||
every { anyConstructed<Presentations.Builder>().build() } returns presentations
|
||||
|
||||
// Test
|
||||
val actual = filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(dataset, actual)
|
||||
verify(exactly = 1) {
|
||||
AutofillValue.forText("PLACEHOLDER")
|
||||
PendingIntent.getActivity(any(), any(), any(), any())
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = any(),
|
||||
isLocked = false,
|
||||
)
|
||||
anyConstructed<Dataset.Builder>().setAuthentication(mockIntentSender)
|
||||
anyConstructed<Presentations.Builder>().setInlinePresentation(inlinePresentation)
|
||||
anyConstructed<Presentations.Builder>().setMenuPresentation(remoteViews)
|
||||
anyConstructed<Presentations.Builder>().build()
|
||||
filledItemPlaceholder.applyToDatasetPostTiramisu(
|
||||
datasetBuilder = any(),
|
||||
presentations = presentations,
|
||||
)
|
||||
anyConstructed<Dataset.Builder>().build()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `buildVaultItemDataset should skip inline and applyToDatasetPreTiramisu when sdkInt is less than 30`() {
|
||||
// Setup
|
||||
val autofillAppInfo = AutofillAppInfo(
|
||||
context = context,
|
||||
packageName = PACKAGE_NAME,
|
||||
sdkInt = 18,
|
||||
)
|
||||
every { AutofillValue.forText(any()) } returns autofillValue
|
||||
every { PendingIntent.getActivity(any(), any(), any(), any()) } returns pendingIntent
|
||||
every {
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
} returns remoteViews
|
||||
mockBuilder<Dataset.Builder> { it.setAuthentication(mockIntentSender) }
|
||||
every {
|
||||
filledItemPlaceholder.applyToDatasetPostTiramisu(
|
||||
datasetBuilder = any(),
|
||||
presentations = presentations,
|
||||
)
|
||||
} just runs
|
||||
|
||||
// Test
|
||||
val actual = filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(dataset, actual)
|
||||
verify(exactly = 1) {
|
||||
AutofillValue.forText("PLACEHOLDER")
|
||||
PendingIntent.getActivity(any(), any(), any(), any())
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
filledItemPlaceholder.applyToDatasetPreTiramisu(
|
||||
datasetBuilder = any(),
|
||||
remoteViews = remoteViews,
|
||||
)
|
||||
anyConstructed<Dataset.Builder>().build()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("Deprecation", "MaxLineLength")
|
||||
@Test
|
||||
fun `buildVaultItemDataset should skip inline and applyToDatasetPreTiramisu when sdkInt is less than 33 but more than 29`() {
|
||||
// Setup
|
||||
val autofillAppInfo = AutofillAppInfo(
|
||||
context = context,
|
||||
packageName = PACKAGE_NAME,
|
||||
sdkInt = 30,
|
||||
)
|
||||
val inlinePresentation: InlinePresentation = mockk()
|
||||
every { AutofillValue.forText(any()) } returns autofillValue
|
||||
every { PendingIntent.getActivity(any(), any(), any(), any()) } returns pendingIntent
|
||||
every {
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
} returns remoteViews
|
||||
every {
|
||||
inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = any(),
|
||||
isLocked = false,
|
||||
)
|
||||
} returns inlinePresentation
|
||||
mockBuilder<Dataset.Builder> { it.setAuthentication(mockIntentSender) }
|
||||
mockBuilder<Dataset.Builder> { it.setInlinePresentation(inlinePresentation) }
|
||||
every {
|
||||
filledItemPlaceholder.applyToDatasetPostTiramisu(
|
||||
datasetBuilder = any(),
|
||||
presentations = presentations,
|
||||
)
|
||||
} just runs
|
||||
|
||||
// Test
|
||||
val actual = filledData.buildVaultItemDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertEquals(dataset, actual)
|
||||
verify(exactly = 1) {
|
||||
AutofillValue.forText("PLACEHOLDER")
|
||||
PendingIntent.getActivity(any(), any(), any(), any())
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = any(),
|
||||
isLocked = false,
|
||||
)
|
||||
anyConstructed<Dataset.Builder>().setInlinePresentation(inlinePresentation)
|
||||
filledItemPlaceholder.applyToDatasetPreTiramisu(
|
||||
datasetBuilder = any(),
|
||||
remoteViews = remoteViews,
|
||||
)
|
||||
anyConstructed<Dataset.Builder>().build()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CIPHER_NAME: String = "Autofill Cipher"
|
||||
private const val PACKAGE_NAME: String = "com.x8bit.bitwarden"
|
||||
}
|
||||
}
|
|
@ -29,6 +29,9 @@ class BitwardenRemoteViewsTest {
|
|||
every { this@mockk.getColor(R.color.on_surface) } returns ON_SURFACE_COLOR
|
||||
every { this@mockk.getColor(R.color.on_surface_variant) } returns ON_SURFACE_VARIANT_COLOR
|
||||
every { this@mockk.getColor(R.color.surface) } returns SURFACE_COLOR
|
||||
every { this@mockk.getString(R.string.app_name) } returns APP_NAME
|
||||
every { this@mockk.getString(R.string.go_to_my_vault) } returns GO_TO_MY_VAULT
|
||||
every { this@mockk.getString(R.string.vault_is_locked) } returns VAULT_IS_LOCKED
|
||||
}
|
||||
private val autofillAppInfo: AutofillAppInfo = mockk {
|
||||
every { this@mockk.context } returns testContext
|
||||
|
@ -56,59 +59,11 @@ class BitwardenRemoteViewsTest {
|
|||
fun `buildAutofillRemoteViews should set values and light mode colors when not night mode`() {
|
||||
// Setup
|
||||
every { testContext.isSystemDarkMode } returns false
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.title,
|
||||
NAME,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.subtitle,
|
||||
SUBTITLE,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setImageViewResource(
|
||||
R.id.icon,
|
||||
ICON_RES,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.container,
|
||||
"setBackgroundColor",
|
||||
SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.icon,
|
||||
"setColorFilter",
|
||||
ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.title,
|
||||
"setTextColor",
|
||||
ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.subtitle,
|
||||
"setTextColor",
|
||||
ON_SURFACE_VARIANT_COLOR,
|
||||
)
|
||||
} just runs
|
||||
prepareRemoteViews(
|
||||
name = NAME,
|
||||
subtitle = SUBTITLE,
|
||||
iconRes = ICON_RES,
|
||||
)
|
||||
|
||||
// Test
|
||||
buildAutofillRemoteViews(
|
||||
|
@ -168,59 +123,11 @@ class BitwardenRemoteViewsTest {
|
|||
fun `buildAutofillRemoteViews should set values and dark mode colors when night mode`() {
|
||||
// Setup
|
||||
every { testContext.isSystemDarkMode } returns true
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.title,
|
||||
NAME,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.subtitle,
|
||||
SUBTITLE,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setImageViewResource(
|
||||
R.id.icon,
|
||||
ICON_RES,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.container,
|
||||
"setBackgroundColor",
|
||||
DARK_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.icon,
|
||||
"setColorFilter",
|
||||
DARK_ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.title,
|
||||
"setTextColor",
|
||||
DARK_ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.subtitle,
|
||||
"setTextColor",
|
||||
DARK_ON_SURFACE_VARIANT_COLOR,
|
||||
)
|
||||
} just runs
|
||||
prepareRemoteViews(
|
||||
name = NAME,
|
||||
subtitle = SUBTITLE,
|
||||
iconRes = ICON_RES,
|
||||
)
|
||||
|
||||
// Test
|
||||
buildAutofillRemoteViews(
|
||||
|
@ -275,11 +182,197 @@ class BitwardenRemoteViewsTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildVaultItemAutofillRemoteViews should set values properly when vault is locked`() {
|
||||
// Setup
|
||||
every { testContext.isSystemDarkMode } returns false
|
||||
prepareRemoteViews(
|
||||
name = APP_NAME,
|
||||
subtitle = VAULT_IS_LOCKED,
|
||||
iconRes = R.drawable.icon,
|
||||
)
|
||||
|
||||
// Test
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = true,
|
||||
)
|
||||
|
||||
// Note: impossible to do a useful test of the returned RemoteViews due to mockking
|
||||
// constraints of the [RemoteViews] constructor. Our best bet is to make sure the correct
|
||||
// operations are performed on the constructed [RemoteViews].
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.title,
|
||||
APP_NAME,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.subtitle,
|
||||
VAULT_IS_LOCKED,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setImageViewResource(
|
||||
R.id.icon,
|
||||
R.drawable.icon,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.container,
|
||||
"setBackgroundColor",
|
||||
SURFACE_COLOR,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.title,
|
||||
"setTextColor",
|
||||
ON_SURFACE_COLOR,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.subtitle,
|
||||
"setTextColor",
|
||||
ON_SURFACE_VARIANT_COLOR,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildVaultItemAutofillRemoteViews should set values properly when vault is unlocked`() {
|
||||
// Setup
|
||||
every { testContext.isSystemDarkMode } returns true
|
||||
prepareRemoteViews(
|
||||
name = APP_NAME,
|
||||
subtitle = GO_TO_MY_VAULT,
|
||||
iconRes = R.drawable.icon,
|
||||
)
|
||||
|
||||
// Test
|
||||
buildVaultItemAutofillRemoteViews(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
isLocked = false,
|
||||
)
|
||||
|
||||
// Note: impossible to do a useful test of the returned RemoteViews due to mockking
|
||||
// constraints of the [RemoteViews] constructor. Our best bet is to make sure the correct
|
||||
// operations are performed on the constructed [RemoteViews].
|
||||
|
||||
// Verify
|
||||
verify(exactly = 1) {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.title,
|
||||
APP_NAME,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.subtitle,
|
||||
GO_TO_MY_VAULT,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setImageViewResource(
|
||||
R.id.icon,
|
||||
R.drawable.icon,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.container,
|
||||
"setBackgroundColor",
|
||||
DARK_SURFACE_COLOR,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.title,
|
||||
"setTextColor",
|
||||
DARK_ON_SURFACE_COLOR,
|
||||
)
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.subtitle,
|
||||
"setTextColor",
|
||||
DARK_ON_SURFACE_VARIANT_COLOR,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareRemoteViews(
|
||||
name: String,
|
||||
subtitle: String,
|
||||
iconRes: Int,
|
||||
) {
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.title,
|
||||
name,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setTextViewText(
|
||||
R.id.subtitle,
|
||||
subtitle,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setImageViewResource(
|
||||
R.id.icon,
|
||||
iconRes,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.container,
|
||||
"setBackgroundColor",
|
||||
DARK_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.icon,
|
||||
"setColorFilter",
|
||||
DARK_ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.icon,
|
||||
"setColorFilter",
|
||||
ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.title,
|
||||
"setTextColor",
|
||||
DARK_ON_SURFACE_COLOR,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
anyConstructed<RemoteViews>()
|
||||
.setInt(
|
||||
R.id.subtitle,
|
||||
"setTextColor",
|
||||
DARK_ON_SURFACE_VARIANT_COLOR,
|
||||
)
|
||||
} just runs
|
||||
}
|
||||
}
|
||||
|
||||
private const val APP_NAME = "Bitwarden"
|
||||
private const val DARK_ON_SURFACE_COLOR: Int = 321
|
||||
private const val DARK_ON_SURFACE_VARIANT_COLOR: Int = 654
|
||||
private const val DARK_SURFACE_COLOR: Int = 987
|
||||
private const val GO_TO_MY_VAULT = "Go to my vault"
|
||||
private const val ICON_RES: Int = 41421421
|
||||
private const val NAME: String = "NAME"
|
||||
private const val ON_SURFACE_COLOR: Int = 123
|
||||
|
@ -287,3 +380,4 @@ private const val ON_SURFACE_VARIANT_COLOR: Int = 456
|
|||
private const val PACKAGE_NAME: String = "com.x8bit.bitwarden"
|
||||
private const val SUBTITLE: String = "SUBTITLE"
|
||||
private const val SURFACE_COLOR: Int = 789
|
||||
private const val VAULT_IS_LOCKED = "Vault is locked"
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.PendingIntent
|
|||
import android.app.slice.Slice
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Bundle
|
||||
import android.widget.inline.InlinePresentationSpec
|
||||
|
@ -18,7 +19,8 @@ import io.mockk.mockkStatic
|
|||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
@ -53,6 +55,20 @@ class InlinePresentationSpecExtensionsTest {
|
|||
@Test
|
||||
fun `createCipherInlinePresentationOrNull should return null if incompatible`() {
|
||||
// Setup
|
||||
val autofillCipher: AutofillCipher.Card = mockk {
|
||||
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
||||
every { this@mockk.subtitle } returns AUTOFILL_CIPHER_SUBTITLE
|
||||
every { this@mockk.iconRes } returns R.drawable.ic_card_item
|
||||
}
|
||||
val pendingIntent: PendingIntent = mockk()
|
||||
every {
|
||||
PendingIntent.getService(
|
||||
testContext,
|
||||
PENDING_INTENT_CODE,
|
||||
any<Intent>(),
|
||||
PENDING_INTENT_FLAGS,
|
||||
)
|
||||
} returns pendingIntent
|
||||
every {
|
||||
UiVersions.getVersions(testStyle)
|
||||
} returns emptyList()
|
||||
|
@ -60,11 +76,11 @@ class InlinePresentationSpecExtensionsTest {
|
|||
// Test
|
||||
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillCipher = mockk(),
|
||||
autofillCipher = autofillCipher,
|
||||
)
|
||||
|
||||
// Verify
|
||||
Assertions.assertNull(actual)
|
||||
assertNull(actual)
|
||||
verify(exactly = 1) {
|
||||
UiVersions.getVersions(testStyle)
|
||||
}
|
||||
|
@ -75,41 +91,19 @@ class InlinePresentationSpecExtensionsTest {
|
|||
fun `createCipherInlinePresentationOrNull should return presentation with card icon when card cipher and compatible`() {
|
||||
// Setup
|
||||
val icon: Icon = mockk()
|
||||
val iconRes = R.drawable.ic_card_item
|
||||
val autofillCipher: AutofillCipher.Card = mockk {
|
||||
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
||||
every { this@mockk.subtitle } returns AUTOFILL_CIPHER_SUBTITLE
|
||||
every { this@mockk.iconRes } returns R.drawable.ic_card_item
|
||||
every { this@mockk.iconRes } returns iconRes
|
||||
}
|
||||
val pendingIntent: PendingIntent = mockk()
|
||||
val slice: Slice = mockk()
|
||||
every {
|
||||
UiVersions.getVersions(testStyle)
|
||||
} returns listOf(UiVersions.INLINE_UI_VERSION_1)
|
||||
every {
|
||||
PendingIntent.getService(
|
||||
testContext,
|
||||
PENDING_INTENT_CODE,
|
||||
any<Intent>(),
|
||||
PENDING_INTENT_FLAGS,
|
||||
)
|
||||
} returns pendingIntent
|
||||
every { testContext.isSystemDarkMode } returns true
|
||||
every { testContext.getColor(R.color.dark_on_surface) } returns ICON_TINT
|
||||
every {
|
||||
Icon.createWithResource(
|
||||
testContext,
|
||||
R.drawable.ic_card_item,
|
||||
)
|
||||
.setTint(ICON_TINT)
|
||||
} returns icon
|
||||
every {
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||
.setTitle(AUTOFILL_CIPHER_NAME)
|
||||
.setSubtitle(AUTOFILL_CIPHER_SUBTITLE)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
} returns slice
|
||||
prepareForCompatibleCipherInlinePresentation(
|
||||
iconRes = iconRes,
|
||||
icon = icon,
|
||||
pendingIntent = pendingIntent,
|
||||
isSystemDarkMode = true,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||
|
@ -118,7 +112,7 @@ class InlinePresentationSpecExtensionsTest {
|
|||
)
|
||||
|
||||
// Verify not-null because we can't properly mock Intent constructors.
|
||||
Assertions.assertNotNull(actual)
|
||||
assertNotNull(actual)
|
||||
verify(exactly = 1) {
|
||||
UiVersions.getVersions(testStyle)
|
||||
PendingIntent.getService(
|
||||
|
@ -131,7 +125,7 @@ class InlinePresentationSpecExtensionsTest {
|
|||
testContext.getColor(R.color.dark_on_surface)
|
||||
Icon.createWithResource(
|
||||
testContext,
|
||||
R.drawable.ic_card_item,
|
||||
iconRes,
|
||||
)
|
||||
.setTint(ICON_TINT)
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||
|
@ -148,41 +142,19 @@ class InlinePresentationSpecExtensionsTest {
|
|||
fun `createCipherInlinePresentationOrNull should return presentation with login icon when login cipher and compatible`() {
|
||||
// Setup
|
||||
val icon: Icon = mockk()
|
||||
val iconRes = R.drawable.ic_login_item
|
||||
val autofillCipher: AutofillCipher.Login = mockk {
|
||||
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
||||
every { this@mockk.subtitle } returns AUTOFILL_CIPHER_SUBTITLE
|
||||
every { this@mockk.iconRes } returns R.drawable.ic_login_item
|
||||
}
|
||||
val pendingIntent: PendingIntent = mockk()
|
||||
val slice: Slice = mockk()
|
||||
every {
|
||||
UiVersions.getVersions(testStyle)
|
||||
} returns listOf(UiVersions.INLINE_UI_VERSION_1)
|
||||
every {
|
||||
PendingIntent.getService(
|
||||
testContext,
|
||||
PENDING_INTENT_CODE,
|
||||
any<Intent>(),
|
||||
PENDING_INTENT_FLAGS,
|
||||
)
|
||||
} returns pendingIntent
|
||||
every { testContext.isSystemDarkMode } returns false
|
||||
every { testContext.getColor(R.color.on_surface) } returns ICON_TINT
|
||||
every {
|
||||
Icon.createWithResource(
|
||||
testContext,
|
||||
R.drawable.ic_login_item,
|
||||
)
|
||||
.setTint(ICON_TINT)
|
||||
} returns icon
|
||||
every {
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||
.setTitle(AUTOFILL_CIPHER_NAME)
|
||||
.setSubtitle(AUTOFILL_CIPHER_SUBTITLE)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
} returns slice
|
||||
prepareForCompatibleCipherInlinePresentation(
|
||||
iconRes = iconRes,
|
||||
icon = icon,
|
||||
pendingIntent = pendingIntent,
|
||||
isSystemDarkMode = false,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||
|
@ -191,7 +163,7 @@ class InlinePresentationSpecExtensionsTest {
|
|||
)
|
||||
|
||||
// Verify not-null because we can't properly mock Intent constructors.
|
||||
Assertions.assertNotNull(actual)
|
||||
assertNotNull(actual)
|
||||
verify(exactly = 1) {
|
||||
UiVersions.getVersions(testStyle)
|
||||
PendingIntent.getService(
|
||||
|
@ -204,7 +176,7 @@ class InlinePresentationSpecExtensionsTest {
|
|||
testContext.getColor(R.color.on_surface)
|
||||
Icon.createWithResource(
|
||||
testContext,
|
||||
R.drawable.ic_login_item,
|
||||
iconRes,
|
||||
)
|
||||
.setTint(ICON_TINT)
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||
|
@ -215,11 +187,196 @@ class InlinePresentationSpecExtensionsTest {
|
|||
.slice
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createVaultItemInlinePresentationOrNull should return null if incompatible`() {
|
||||
// Setup
|
||||
val pendingIntent: PendingIntent = mockk()
|
||||
every {
|
||||
UiVersions.getVersions(testStyle)
|
||||
} returns emptyList()
|
||||
|
||||
every { testContext.getString(R.string.app_name) } returns APP_NAME
|
||||
every { testContext.getString(R.string.vault_is_locked) } returns VAULT_IS_LOCKED
|
||||
every { testContext.getString(R.string.my_vault) } returns MY_VAULT
|
||||
|
||||
// Test
|
||||
val actual = inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
isLocked = true,
|
||||
)
|
||||
|
||||
// Verify
|
||||
assertNull(actual)
|
||||
verify(exactly = 1) {
|
||||
UiVersions.getVersions(testStyle)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createVaultItemInlinePresentationOrNull should return presentation with locked vault title when vault is locked and compatible`() {
|
||||
// Setup
|
||||
val icon: Icon = mockk()
|
||||
val pendingIntent: PendingIntent = mockk()
|
||||
prepareForCompatibleVaultItemInlinePresentation(
|
||||
icon = icon,
|
||||
pendingIntent = pendingIntent,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
isLocked = true,
|
||||
)
|
||||
|
||||
// Verify not-null because we can't properly mock Intent constructors.
|
||||
assertNotNull(actual)
|
||||
verify(exactly = 1) {
|
||||
UiVersions.getVersions(testStyle)
|
||||
Icon
|
||||
.createWithResource(
|
||||
testContext,
|
||||
R.drawable.icon,
|
||||
)
|
||||
.setTintBlendMode(BlendMode.DST)
|
||||
testContext.getString(R.string.app_name)
|
||||
testContext.getString(R.string.vault_is_locked)
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||
.setTitle(APP_NAME)
|
||||
.setSubtitle(VAULT_IS_LOCKED)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createVaultItemInlinePresentationOrNull should return presentation with my vault title when vault is locked and compatible`() {
|
||||
// Setup
|
||||
val icon: Icon = mockk()
|
||||
val pendingIntent: PendingIntent = mockk()
|
||||
prepareForCompatibleVaultItemInlinePresentation(
|
||||
icon = icon,
|
||||
pendingIntent = pendingIntent,
|
||||
)
|
||||
|
||||
// Test
|
||||
val actual = inlinePresentationSpec.createVaultItemInlinePresentationOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
pendingIntent = pendingIntent,
|
||||
isLocked = false,
|
||||
)
|
||||
|
||||
// Verify not-null because we can't properly mock Intent constructors.
|
||||
assertNotNull(actual)
|
||||
verify(exactly = 1) {
|
||||
UiVersions.getVersions(testStyle)
|
||||
Icon
|
||||
.createWithResource(
|
||||
testContext,
|
||||
R.drawable.icon,
|
||||
)
|
||||
.setTintBlendMode(BlendMode.DST)
|
||||
testContext.getString(R.string.app_name)
|
||||
testContext.getString(R.string.my_vault)
|
||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||
.setTitle(APP_NAME)
|
||||
.setSubtitle(MY_VAULT)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareForCompatibleCipherInlinePresentation(
|
||||
iconRes: Int,
|
||||
icon: Icon,
|
||||
pendingIntent: PendingIntent,
|
||||
isSystemDarkMode: Boolean,
|
||||
) {
|
||||
val slice: Slice = mockk()
|
||||
every {
|
||||
UiVersions.getVersions(testStyle)
|
||||
} returns listOf(UiVersions.INLINE_UI_VERSION_1)
|
||||
every {
|
||||
PendingIntent.getService(
|
||||
testContext,
|
||||
PENDING_INTENT_CODE,
|
||||
any<Intent>(),
|
||||
PENDING_INTENT_FLAGS,
|
||||
)
|
||||
} returns pendingIntent
|
||||
every { testContext.isSystemDarkMode } returns isSystemDarkMode
|
||||
every { testContext.getColor(R.color.on_surface) } returns ICON_TINT
|
||||
every { testContext.getColor(R.color.dark_on_surface) } returns ICON_TINT
|
||||
every {
|
||||
Icon.createWithResource(
|
||||
testContext,
|
||||
iconRes,
|
||||
)
|
||||
.setTint(ICON_TINT)
|
||||
} returns icon
|
||||
every {
|
||||
InlineSuggestionUi
|
||||
.newContentBuilder(pendingIntent)
|
||||
.setTitle(AUTOFILL_CIPHER_NAME)
|
||||
.setSubtitle(AUTOFILL_CIPHER_SUBTITLE)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
} returns slice
|
||||
}
|
||||
|
||||
private fun prepareForCompatibleVaultItemInlinePresentation(
|
||||
icon: Icon,
|
||||
pendingIntent: PendingIntent,
|
||||
) {
|
||||
val slice: Slice = mockk()
|
||||
every {
|
||||
UiVersions.getVersions(testStyle)
|
||||
} returns listOf(UiVersions.INLINE_UI_VERSION_1)
|
||||
every {
|
||||
Icon
|
||||
.createWithResource(
|
||||
testContext,
|
||||
R.drawable.icon,
|
||||
)
|
||||
.setTintBlendMode(BlendMode.DST)
|
||||
} returns icon
|
||||
every {
|
||||
InlineSuggestionUi
|
||||
.newContentBuilder(pendingIntent)
|
||||
.setTitle(APP_NAME)
|
||||
.setSubtitle(VAULT_IS_LOCKED)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
} returns slice
|
||||
every {
|
||||
InlineSuggestionUi
|
||||
.newContentBuilder(pendingIntent)
|
||||
.setTitle(APP_NAME)
|
||||
.setSubtitle(MY_VAULT)
|
||||
.setStartIcon(icon)
|
||||
.build()
|
||||
.slice
|
||||
} returns slice
|
||||
every { testContext.getString(R.string.app_name) } returns APP_NAME
|
||||
every { testContext.getString(R.string.vault_is_locked) } returns VAULT_IS_LOCKED
|
||||
every { testContext.getString(R.string.my_vault) } returns MY_VAULT
|
||||
}
|
||||
}
|
||||
|
||||
private const val APP_NAME = "Bitwarden"
|
||||
private const val AUTOFILL_CIPHER_NAME = "Cipher1"
|
||||
private const val AUTOFILL_CIPHER_SUBTITLE = "Subtitle"
|
||||
private const val ICON_TINT: Int = 6123751
|
||||
private const val MY_VAULT = "My vault"
|
||||
private const val PENDING_INTENT_CODE: Int = 0
|
||||
private const val PENDING_INTENT_FLAGS: Int =
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
private const val VAULT_IS_LOCKED = "Vault is locked"
|
||||
|
|
Loading…
Reference in a new issue