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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleInstancePerTask"
|
android:launchMode="@integer/launchModeAPIlevel"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<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.AutofillAppInfo
|
||||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
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
|
* The default implementation for [FillResponseBuilder]. This is a component for compiling fulfilled
|
||||||
|
@ -14,7 +16,7 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||||
autofillAppInfo: AutofillAppInfo,
|
autofillAppInfo: AutofillAppInfo,
|
||||||
filledData: FilledData,
|
filledData: FilledData,
|
||||||
): FillResponse? =
|
): FillResponse? =
|
||||||
if (filledData.filledPartitions.any { it.filledItems.isNotEmpty() }) {
|
if (filledData.fillableAutofillIds.isNotEmpty()) {
|
||||||
val fillResponseBuilder = FillResponse.Builder()
|
val fillResponseBuilder = FillResponse.Builder()
|
||||||
|
|
||||||
filledData
|
filledData
|
||||||
|
@ -35,9 +37,14 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add vault item dataset (BIT-1296)
|
|
||||||
|
|
||||||
fillResponseBuilder
|
fillResponseBuilder
|
||||||
|
// Add the Vault Item
|
||||||
|
.addDataset(
|
||||||
|
filledData
|
||||||
|
.buildVaultItemDataset(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
),
|
||||||
|
)
|
||||||
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,6 +19,7 @@ class FilledDataBuilderImpl(
|
||||||
) : FilledDataBuilder {
|
) : FilledDataBuilder {
|
||||||
override suspend fun build(autofillRequest: AutofillRequest.Fillable): FilledData {
|
override suspend fun build(autofillRequest: AutofillRequest.Fillable): FilledData {
|
||||||
// TODO: determine whether or not the vault is locked (BIT-1296)
|
// 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.
|
// Subtract one to make sure there is space for the vault item.
|
||||||
val maxCipherInlineSuggestionsCount = autofillRequest.maxInlineSuggestionsCount - 1
|
val maxCipherInlineSuggestionsCount = autofillRequest.maxInlineSuggestionsCount - 1
|
||||||
|
@ -78,7 +79,10 @@ class FilledDataBuilderImpl(
|
||||||
return FilledData(
|
return FilledData(
|
||||||
filledPartitions = filledPartitions,
|
filledPartitions = filledPartitions,
|
||||||
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
||||||
|
originalPartition = autofillRequest.partition,
|
||||||
|
uri = autofillRequest.uri,
|
||||||
vaultItemInlinePresentationSpec = vaultItemInlinePresentationSpec,
|
vaultItemInlinePresentationSpec = vaultItemInlinePresentationSpec,
|
||||||
|
isVaultLocked = isVaultLocked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,5 +10,8 @@ import android.widget.inline.InlinePresentationSpec
|
||||||
data class FilledData(
|
data class FilledData(
|
||||||
val filledPartitions: List<FilledPartition>,
|
val filledPartitions: List<FilledPartition>,
|
||||||
val ignoreAutofillIds: List<AutofillId>,
|
val ignoreAutofillIds: List<AutofillId>,
|
||||||
|
val originalPartition: AutofillPartition,
|
||||||
|
val uri: String?,
|
||||||
val vaultItemInlinePresentationSpec: InlinePresentationSpec?,
|
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.content.Context
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
|
@ -13,6 +14,42 @@ import com.x8bit.bitwarden.ui.autofill.util.isSystemDarkMode
|
||||||
fun buildAutofillRemoteViews(
|
fun buildAutofillRemoteViews(
|
||||||
autofillAppInfo: AutofillAppInfo,
|
autofillAppInfo: AutofillAppInfo,
|
||||||
autofillCipher: AutofillCipher,
|
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 =
|
||||||
RemoteViews(
|
RemoteViews(
|
||||||
autofillAppInfo.packageName,
|
autofillAppInfo.packageName,
|
||||||
|
@ -21,27 +58,21 @@ fun buildAutofillRemoteViews(
|
||||||
.apply {
|
.apply {
|
||||||
setTextViewText(
|
setTextViewText(
|
||||||
R.id.title,
|
R.id.title,
|
||||||
autofillCipher.name,
|
name,
|
||||||
)
|
)
|
||||||
setTextViewText(
|
setTextViewText(
|
||||||
R.id.subtitle,
|
R.id.subtitle,
|
||||||
autofillCipher.subtitle,
|
subtitle,
|
||||||
)
|
)
|
||||||
setImageViewResource(
|
setImageViewResource(
|
||||||
R.id.icon,
|
R.id.icon,
|
||||||
autofillCipher.iconRes,
|
iconRes,
|
||||||
)
|
)
|
||||||
|
|
||||||
setInt(
|
setInt(
|
||||||
R.id.container,
|
R.id.container,
|
||||||
"setBackgroundColor",
|
"setBackgroundColor",
|
||||||
autofillAppInfo.context.surface,
|
autofillAppInfo.context.surface,
|
||||||
)
|
)
|
||||||
setInt(
|
|
||||||
R.id.icon,
|
|
||||||
"setColorFilter",
|
|
||||||
autofillAppInfo.context.onSurface,
|
|
||||||
)
|
|
||||||
setInt(
|
setInt(
|
||||||
R.id.title,
|
R.id.title,
|
||||||
"setTextColor",
|
"setTextColor",
|
||||||
|
@ -52,6 +83,13 @@ fun buildAutofillRemoteViews(
|
||||||
"setTextColor",
|
"setTextColor",
|
||||||
autofillAppInfo.context.onSurfaceVariant,
|
autofillAppInfo.context.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
if (shouldTintIcon) {
|
||||||
|
setInt(
|
||||||
|
R.id.icon,
|
||||||
|
"setColorFilter",
|
||||||
|
autofillAppInfo.context.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Context.onSurface: Int
|
private val Context.onSurface: Int
|
||||||
|
|
|
@ -3,10 +3,12 @@ package com.x8bit.bitwarden.ui.autofill.util
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.autofill.InlinePresentation
|
import android.service.autofill.InlinePresentation
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
|
@ -19,10 +21,59 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
* it fails, return null.
|
* it fails, return null.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
|
fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
autofillAppInfo: AutofillAppInfo,
|
autofillAppInfo: AutofillAppInfo,
|
||||||
autofillCipher: AutofillCipher,
|
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? {
|
): InlinePresentation? {
|
||||||
val isInlineCompatible = UiVersions
|
val isInlineCompatible = UiVersions
|
||||||
.getVersions(style)
|
.getVersions(style)
|
||||||
|
@ -30,24 +81,23 @@ fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
|
||||||
if (!isInlineCompatible) return null
|
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
|
val icon = Icon
|
||||||
.createWithResource(
|
.createWithResource(
|
||||||
autofillAppInfo.context,
|
autofillAppInfo.context,
|
||||||
autofillCipher.iconRes,
|
iconRes,
|
||||||
)
|
)
|
||||||
.setTint(autofillAppInfo.contentColor)
|
.run {
|
||||||
|
if (shouldTintIcon) {
|
||||||
|
setTint(autofillAppInfo.contentColor)
|
||||||
|
} else {
|
||||||
|
// Remove tinting
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
}
|
||||||
|
}
|
||||||
val slice = InlineSuggestionUi
|
val slice = InlineSuggestionUi
|
||||||
.newContentBuilder(pendingIntent)
|
.newContentBuilder(pendingIntent)
|
||||||
.setTitle(autofillCipher.name)
|
.setTitle(title)
|
||||||
.setSubtitle(autofillCipher.subtitle)
|
.setSubtitle(subtitle)
|
||||||
.setStartIcon(icon)
|
.setStartIcon(icon)
|
||||||
.build()
|
.build()
|
||||||
.slice
|
.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.service.autofill.FillResponse
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
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.FilledData
|
||||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||||
|
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkConstructor
|
import io.mockk.mockkConstructor
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkConstructor
|
import io.mockk.unmockkConstructor
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
@ -26,6 +30,7 @@ class FillResponseBuilderTest {
|
||||||
|
|
||||||
private val context: Context = mockk()
|
private val context: Context = mockk()
|
||||||
private val dataset: Dataset = mockk()
|
private val dataset: Dataset = mockk()
|
||||||
|
private val vaultItemDataSet: Dataset = mockk()
|
||||||
private val fillResponse: FillResponse = mockk()
|
private val fillResponse: FillResponse = mockk()
|
||||||
private val appInfo: AutofillAppInfo = AutofillAppInfo(
|
private val appInfo: AutofillAppInfo = AutofillAppInfo(
|
||||||
context = context,
|
context = context,
|
||||||
|
@ -42,6 +47,7 @@ class FillResponseBuilderTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockkConstructor(FillResponse.Builder::class)
|
mockkConstructor(FillResponse.Builder::class)
|
||||||
|
mockkStatic(FilledData::buildVaultItemDataset)
|
||||||
mockkStatic(FilledPartition::buildDataset)
|
mockkStatic(FilledPartition::buildDataset)
|
||||||
every { anyConstructed<FillResponse.Builder>().build() } returns fillResponse
|
every { anyConstructed<FillResponse.Builder>().build() } returns fillResponse
|
||||||
|
|
||||||
|
@ -51,28 +57,12 @@ class FillResponseBuilderTest {
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
unmockkConstructor(FillResponse.Builder::class)
|
unmockkConstructor(FillResponse.Builder::class)
|
||||||
mockkStatic(FilledPartition::buildDataset)
|
unmockkStatic(FilledData::buildVaultItemDataset)
|
||||||
|
unmockkStatic(FilledPartition::buildDataset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `build should return null when filledPartitions is empty`() {
|
fun `build should return null when original partition contains no views`() {
|
||||||
// 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`() {
|
|
||||||
// Test
|
// Test
|
||||||
val filledPartitions = FilledPartition(
|
val filledPartitions = FilledPartition(
|
||||||
autofillCipher = mockk(),
|
autofillCipher = mockk(),
|
||||||
|
@ -84,7 +74,12 @@ class FillResponseBuilderTest {
|
||||||
filledPartitions,
|
filledPartitions,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
originalPartition = AutofillPartition.Login(
|
||||||
|
views = emptyList(),
|
||||||
|
),
|
||||||
|
uri = null,
|
||||||
vaultItemInlinePresentationSpec = null,
|
vaultItemInlinePresentationSpec = null,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
val actual = fillResponseBuilder.build(
|
val actual = fillResponseBuilder.build(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
|
@ -111,14 +106,37 @@ class FillResponseBuilderTest {
|
||||||
val filledData = FilledData(
|
val filledData = FilledData(
|
||||||
filledPartitions = filledPartitions,
|
filledPartitions = filledPartitions,
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
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,
|
vaultItemInlinePresentationSpec = null,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
filledPartitionOne.buildDataset(
|
filledPartitionOne.buildDataset(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
)
|
)
|
||||||
} returns dataset
|
} 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> {
|
mockBuilder<FillResponse.Builder> {
|
||||||
it.setIgnoredIds(
|
it.setIgnoredIds(
|
||||||
ignoredAutofillIdOne,
|
ignoredAutofillIdOne,
|
||||||
|
@ -139,7 +157,11 @@ class FillResponseBuilderTest {
|
||||||
filledPartitionOne.buildDataset(
|
filledPartitionOne.buildDataset(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
)
|
)
|
||||||
|
filledData.buildVaultItemDataset(
|
||||||
|
autofillAppInfo = appInfo,
|
||||||
|
)
|
||||||
anyConstructed<FillResponse.Builder>().addDataset(dataset)
|
anyConstructed<FillResponse.Builder>().addDataset(dataset)
|
||||||
|
anyConstructed<FillResponse.Builder>().addDataset(vaultItemDataSet)
|
||||||
anyConstructed<FillResponse.Builder>().setIgnoredIds(
|
anyConstructed<FillResponse.Builder>().setIgnoredIds(
|
||||||
ignoredAutofillIdOne,
|
ignoredAutofillIdOne,
|
||||||
ignoredAutofillIdTwo,
|
ignoredAutofillIdTwo,
|
||||||
|
|
|
@ -106,7 +106,10 @@ class FilledDataBuilderTest {
|
||||||
filledPartition,
|
filledPartition,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
originalPartition = autofillPartition,
|
||||||
|
uri = URI,
|
||||||
vaultItemInlinePresentationSpec = null,
|
vaultItemInlinePresentationSpec = null,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
coEvery {
|
coEvery {
|
||||||
autofillCipherProvider.getLoginAutofillCiphers(
|
autofillCipherProvider.getLoginAutofillCiphers(
|
||||||
|
@ -167,7 +170,10 @@ class FilledDataBuilderTest {
|
||||||
val expected = FilledData(
|
val expected = FilledData(
|
||||||
filledPartitions = emptyList(),
|
filledPartitions = emptyList(),
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
originalPartition = autofillPartition,
|
||||||
|
uri = null,
|
||||||
vaultItemInlinePresentationSpec = null,
|
vaultItemInlinePresentationSpec = null,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
@ -242,7 +248,10 @@ class FilledDataBuilderTest {
|
||||||
filledPartition,
|
filledPartition,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
originalPartition = autofillPartition,
|
||||||
|
uri = URI,
|
||||||
vaultItemInlinePresentationSpec = null,
|
vaultItemInlinePresentationSpec = null,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
coEvery { autofillCipherProvider.getCardAutofillCiphers() } returns listOf(autofillCipher)
|
coEvery { autofillCipherProvider.getCardAutofillCiphers() } returns listOf(autofillCipher)
|
||||||
every { autofillViewCode.buildFilledItemOrNull(code) } returns filledItemCode
|
every { autofillViewCode.buildFilledItemOrNull(code) } returns filledItemCode
|
||||||
|
@ -337,7 +346,10 @@ class FilledDataBuilderTest {
|
||||||
filledPartitionThree,
|
filledPartitionThree,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
originalPartition = autofillPartition,
|
||||||
|
uri = URI,
|
||||||
vaultItemInlinePresentationSpec = inlinePresentationSpec,
|
vaultItemInlinePresentationSpec = inlinePresentationSpec,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
coEvery {
|
coEvery {
|
||||||
autofillCipherProvider.getLoginAutofillCiphers(
|
autofillCipherProvider.getLoginAutofillCiphers(
|
||||||
|
|
|
@ -111,7 +111,10 @@ class AutofillProcessorTest {
|
||||||
val filledData = FilledData(
|
val filledData = FilledData(
|
||||||
filledPartitions = listOf(mockk()),
|
filledPartitions = listOf(mockk()),
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
originalPartition = mockk(),
|
||||||
|
uri = null,
|
||||||
vaultItemInlinePresentationSpec = null,
|
vaultItemInlinePresentationSpec = null,
|
||||||
|
isVaultLocked = false,
|
||||||
)
|
)
|
||||||
val fillResponse: FillResponse = mockk()
|
val fillResponse: FillResponse = mockk()
|
||||||
val autofillRequest: AutofillRequest.Fillable = 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) } returns ON_SURFACE_COLOR
|
||||||
every { this@mockk.getColor(R.color.on_surface_variant) } returns ON_SURFACE_VARIANT_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.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 {
|
private val autofillAppInfo: AutofillAppInfo = mockk {
|
||||||
every { this@mockk.context } returns testContext
|
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`() {
|
fun `buildAutofillRemoteViews should set values and light mode colors when not night mode`() {
|
||||||
// Setup
|
// Setup
|
||||||
every { testContext.isSystemDarkMode } returns false
|
every { testContext.isSystemDarkMode } returns false
|
||||||
every {
|
prepareRemoteViews(
|
||||||
anyConstructed<RemoteViews>()
|
name = NAME,
|
||||||
.setTextViewText(
|
subtitle = SUBTITLE,
|
||||||
R.id.title,
|
iconRes = ICON_RES,
|
||||||
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
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
buildAutofillRemoteViews(
|
buildAutofillRemoteViews(
|
||||||
|
@ -168,59 +123,11 @@ class BitwardenRemoteViewsTest {
|
||||||
fun `buildAutofillRemoteViews should set values and dark mode colors when night mode`() {
|
fun `buildAutofillRemoteViews should set values and dark mode colors when night mode`() {
|
||||||
// Setup
|
// Setup
|
||||||
every { testContext.isSystemDarkMode } returns true
|
every { testContext.isSystemDarkMode } returns true
|
||||||
every {
|
prepareRemoteViews(
|
||||||
anyConstructed<RemoteViews>()
|
name = NAME,
|
||||||
.setTextViewText(
|
subtitle = SUBTITLE,
|
||||||
R.id.title,
|
iconRes = ICON_RES,
|
||||||
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
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
buildAutofillRemoteViews(
|
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_COLOR: Int = 321
|
||||||
private const val DARK_ON_SURFACE_VARIANT_COLOR: Int = 654
|
private const val DARK_ON_SURFACE_VARIANT_COLOR: Int = 654
|
||||||
private const val DARK_SURFACE_COLOR: Int = 987
|
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 ICON_RES: Int = 41421421
|
||||||
private const val NAME: String = "NAME"
|
private const val NAME: String = "NAME"
|
||||||
private const val ON_SURFACE_COLOR: Int = 123
|
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 PACKAGE_NAME: String = "com.x8bit.bitwarden"
|
||||||
private const val SUBTITLE: String = "SUBTITLE"
|
private const val SUBTITLE: String = "SUBTITLE"
|
||||||
private const val SURFACE_COLOR: Int = 789
|
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.app.slice.Slice
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
@ -18,7 +19,8 @@ import io.mockk.mockkStatic
|
||||||
import io.mockk.unmockkStatic
|
import io.mockk.unmockkStatic
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.jupiter.api.AfterEach
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
@ -53,6 +55,20 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
@Test
|
@Test
|
||||||
fun `createCipherInlinePresentationOrNull should return null if incompatible`() {
|
fun `createCipherInlinePresentationOrNull should return null if incompatible`() {
|
||||||
// Setup
|
// 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 {
|
every {
|
||||||
UiVersions.getVersions(testStyle)
|
UiVersions.getVersions(testStyle)
|
||||||
} returns emptyList()
|
} returns emptyList()
|
||||||
|
@ -60,11 +76,11 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
// Test
|
// Test
|
||||||
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
autofillAppInfo = autofillAppInfo,
|
autofillAppInfo = autofillAppInfo,
|
||||||
autofillCipher = mockk(),
|
autofillCipher = autofillCipher,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
Assertions.assertNull(actual)
|
assertNull(actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
UiVersions.getVersions(testStyle)
|
UiVersions.getVersions(testStyle)
|
||||||
}
|
}
|
||||||
|
@ -75,41 +91,19 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
fun `createCipherInlinePresentationOrNull should return presentation with card icon when card cipher and compatible`() {
|
fun `createCipherInlinePresentationOrNull should return presentation with card icon when card cipher and compatible`() {
|
||||||
// Setup
|
// Setup
|
||||||
val icon: Icon = mockk()
|
val icon: Icon = mockk()
|
||||||
|
val iconRes = R.drawable.ic_card_item
|
||||||
val autofillCipher: AutofillCipher.Card = mockk {
|
val autofillCipher: AutofillCipher.Card = mockk {
|
||||||
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
||||||
every { this@mockk.subtitle } returns AUTOFILL_CIPHER_SUBTITLE
|
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 pendingIntent: PendingIntent = mockk()
|
||||||
val slice: Slice = mockk()
|
prepareForCompatibleCipherInlinePresentation(
|
||||||
every {
|
iconRes = iconRes,
|
||||||
UiVersions.getVersions(testStyle)
|
icon = icon,
|
||||||
} returns listOf(UiVersions.INLINE_UI_VERSION_1)
|
pendingIntent = pendingIntent,
|
||||||
every {
|
isSystemDarkMode = true,
|
||||||
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
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
@ -118,7 +112,7 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify not-null because we can't properly mock Intent constructors.
|
// Verify not-null because we can't properly mock Intent constructors.
|
||||||
Assertions.assertNotNull(actual)
|
assertNotNull(actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
UiVersions.getVersions(testStyle)
|
UiVersions.getVersions(testStyle)
|
||||||
PendingIntent.getService(
|
PendingIntent.getService(
|
||||||
|
@ -131,7 +125,7 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
testContext.getColor(R.color.dark_on_surface)
|
testContext.getColor(R.color.dark_on_surface)
|
||||||
Icon.createWithResource(
|
Icon.createWithResource(
|
||||||
testContext,
|
testContext,
|
||||||
R.drawable.ic_card_item,
|
iconRes,
|
||||||
)
|
)
|
||||||
.setTint(ICON_TINT)
|
.setTint(ICON_TINT)
|
||||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||||
|
@ -148,41 +142,19 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
fun `createCipherInlinePresentationOrNull should return presentation with login icon when login cipher and compatible`() {
|
fun `createCipherInlinePresentationOrNull should return presentation with login icon when login cipher and compatible`() {
|
||||||
// Setup
|
// Setup
|
||||||
val icon: Icon = mockk()
|
val icon: Icon = mockk()
|
||||||
|
val iconRes = R.drawable.ic_login_item
|
||||||
val autofillCipher: AutofillCipher.Login = mockk {
|
val autofillCipher: AutofillCipher.Login = mockk {
|
||||||
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
every { this@mockk.name } returns AUTOFILL_CIPHER_NAME
|
||||||
every { this@mockk.subtitle } returns AUTOFILL_CIPHER_SUBTITLE
|
every { this@mockk.subtitle } returns AUTOFILL_CIPHER_SUBTITLE
|
||||||
every { this@mockk.iconRes } returns R.drawable.ic_login_item
|
every { this@mockk.iconRes } returns R.drawable.ic_login_item
|
||||||
}
|
}
|
||||||
val pendingIntent: PendingIntent = mockk()
|
val pendingIntent: PendingIntent = mockk()
|
||||||
val slice: Slice = mockk()
|
prepareForCompatibleCipherInlinePresentation(
|
||||||
every {
|
iconRes = iconRes,
|
||||||
UiVersions.getVersions(testStyle)
|
icon = icon,
|
||||||
} returns listOf(UiVersions.INLINE_UI_VERSION_1)
|
pendingIntent = pendingIntent,
|
||||||
every {
|
isSystemDarkMode = false,
|
||||||
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
|
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
@ -191,7 +163,7 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify not-null because we can't properly mock Intent constructors.
|
// Verify not-null because we can't properly mock Intent constructors.
|
||||||
Assertions.assertNotNull(actual)
|
assertNotNull(actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
UiVersions.getVersions(testStyle)
|
UiVersions.getVersions(testStyle)
|
||||||
PendingIntent.getService(
|
PendingIntent.getService(
|
||||||
|
@ -204,7 +176,7 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
testContext.getColor(R.color.on_surface)
|
testContext.getColor(R.color.on_surface)
|
||||||
Icon.createWithResource(
|
Icon.createWithResource(
|
||||||
testContext,
|
testContext,
|
||||||
R.drawable.ic_login_item,
|
iconRes,
|
||||||
)
|
)
|
||||||
.setTint(ICON_TINT)
|
.setTint(ICON_TINT)
|
||||||
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||||
|
@ -215,11 +187,196 @@ class InlinePresentationSpecExtensionsTest {
|
||||||
.slice
|
.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_NAME = "Cipher1"
|
||||||
private const val AUTOFILL_CIPHER_SUBTITLE = "Subtitle"
|
private const val AUTOFILL_CIPHER_SUBTITLE = "Subtitle"
|
||||||
private const val ICON_TINT: Int = 6123751
|
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_CODE: Int = 0
|
||||||
private const val PENDING_INTENT_FLAGS: Int =
|
private const val PENDING_INTENT_FLAGS: Int =
|
||||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
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