mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-1320: Add inline UI (#624)
This commit is contained in:
parent
fd8293ba55
commit
224395004f
27 changed files with 928 additions and 22 deletions
|
@ -67,6 +67,11 @@ The following is a list of all third-party dependencies included as part of the
|
||||||
- Purpose: Allows access to new APIs on older API versions.
|
- Purpose: Allows access to new APIs on older API versions.
|
||||||
- License: Apache 2.0
|
- License: Apache 2.0
|
||||||
|
|
||||||
|
- **AndroidX Autofill**
|
||||||
|
- https://developer.android.com/jetpack/androidx/releases/autofill
|
||||||
|
- Purpose: Allows access to tools for building inline autofill UI.
|
||||||
|
- License: Apache 2.0
|
||||||
|
|
||||||
- **AndroidX Browser**
|
- **AndroidX Browser**
|
||||||
- https://developer.android.com/jetpack/androidx/releases/browser
|
- https://developer.android.com/jetpack/androidx/releases/browser
|
||||||
- Purpose: Displays webpages with the user's default browser.
|
- Purpose: Displays webpages with the user's default browser.
|
||||||
|
|
|
@ -149,6 +149,7 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.androidx.autofill)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(libs.androidx.camera.camera2)
|
implementation(libs.androidx.camera.camera2)
|
||||||
implementation(libs.androidx.camera.lifecycle)
|
implementation(libs.androidx.camera.lifecycle)
|
||||||
|
|
|
@ -51,6 +51,9 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="Bitwarden"
|
android:label="Bitwarden"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.autofill"
|
||||||
|
android:resource="@xml/autofill_service_configuration" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.autofill.AutofillService" />
|
<action android:name="android.service.autofill.AutofillService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -34,6 +34,9 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||||
fillResponseBuilder.addDataset(dataset)
|
fillResponseBuilder.addDataset(dataset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: add vault item dataset (BIT-1296)
|
||||||
|
|
||||||
fillResponseBuilder
|
fillResponseBuilder
|
||||||
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.x8bit.bitwarden.data.autofill.builder
|
package com.x8bit.bitwarden.data.autofill.builder
|
||||||
|
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||||
|
@ -19,6 +20,23 @@ class FilledDataBuilderImpl(
|
||||||
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)
|
||||||
|
|
||||||
|
// Subtract one to make sure there is space for the vault item.
|
||||||
|
val maxCipherInlineSuggestionsCount = autofillRequest.maxInlineSuggestionsCount - 1
|
||||||
|
// Track the number of inline suggestions that have been added.
|
||||||
|
var inlineSuggestionsAdded = 0
|
||||||
|
|
||||||
|
// A function for managing the cipher InlinePresentationSpecs.
|
||||||
|
fun getCipherInlinePresentationOrNull(): InlinePresentationSpec? =
|
||||||
|
if (inlineSuggestionsAdded < maxCipherInlineSuggestionsCount) {
|
||||||
|
// Use getOrLastOrNull so if the list has run dry take the last spec.
|
||||||
|
autofillRequest
|
||||||
|
.inlinePresentationSpecs
|
||||||
|
.getOrLastOrNull(inlineSuggestionsAdded)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
?.also { inlineSuggestionsAdded += 1 }
|
||||||
|
|
||||||
val filledPartitions = when (autofillRequest.partition) {
|
val filledPartitions = when (autofillRequest.partition) {
|
||||||
is AutofillPartition.Card -> {
|
is AutofillPartition.Card -> {
|
||||||
autofillCipherProvider
|
autofillCipherProvider
|
||||||
|
@ -27,6 +45,7 @@ class FilledDataBuilderImpl(
|
||||||
fillCardPartition(
|
fillCardPartition(
|
||||||
autofillCipher = autofillCipher,
|
autofillCipher = autofillCipher,
|
||||||
autofillViews = autofillRequest.partition.views,
|
autofillViews = autofillRequest.partition.views,
|
||||||
|
inlinePresentationSpec = getCipherInlinePresentationOrNull(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +62,7 @@ class FilledDataBuilderImpl(
|
||||||
fillLoginPartition(
|
fillLoginPartition(
|
||||||
autofillCipher = autofillCipher,
|
autofillCipher = autofillCipher,
|
||||||
autofillViews = autofillRequest.partition.views,
|
autofillViews = autofillRequest.partition.views,
|
||||||
|
inlinePresentationSpec = getCipherInlinePresentationOrNull(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,9 +70,15 @@ class FilledDataBuilderImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use getOrLastOrNull so if the list has run dry take the last spec.
|
||||||
|
val vaultItemInlinePresentationSpec = autofillRequest
|
||||||
|
.inlinePresentationSpecs
|
||||||
|
.getOrLastOrNull(inlineSuggestionsAdded)
|
||||||
|
|
||||||
return FilledData(
|
return FilledData(
|
||||||
filledPartitions = filledPartitions,
|
filledPartitions = filledPartitions,
|
||||||
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
||||||
|
vaultItemInlinePresentationSpec = vaultItemInlinePresentationSpec,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +89,7 @@ class FilledDataBuilderImpl(
|
||||||
private fun fillCardPartition(
|
private fun fillCardPartition(
|
||||||
autofillCipher: AutofillCipher.Card,
|
autofillCipher: AutofillCipher.Card,
|
||||||
autofillViews: List<AutofillView.Card>,
|
autofillViews: List<AutofillView.Card>,
|
||||||
|
inlinePresentationSpec: InlinePresentationSpec?,
|
||||||
): FilledPartition {
|
): FilledPartition {
|
||||||
val filledItems = autofillViews
|
val filledItems = autofillViews
|
||||||
.map { autofillView ->
|
.map { autofillView ->
|
||||||
|
@ -80,6 +107,7 @@ class FilledDataBuilderImpl(
|
||||||
return FilledPartition(
|
return FilledPartition(
|
||||||
autofillCipher = autofillCipher,
|
autofillCipher = autofillCipher,
|
||||||
filledItems = filledItems,
|
filledItems = filledItems,
|
||||||
|
inlinePresentationSpec = inlinePresentationSpec,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +118,7 @@ class FilledDataBuilderImpl(
|
||||||
private fun fillLoginPartition(
|
private fun fillLoginPartition(
|
||||||
autofillCipher: AutofillCipher.Login,
|
autofillCipher: AutofillCipher.Login,
|
||||||
autofillViews: List<AutofillView.Login>,
|
autofillViews: List<AutofillView.Login>,
|
||||||
|
inlinePresentationSpec: InlinePresentationSpec?,
|
||||||
): FilledPartition {
|
): FilledPartition {
|
||||||
val filledItems = autofillViews
|
val filledItems = autofillViews
|
||||||
.map { autofillView ->
|
.map { autofillView ->
|
||||||
|
@ -108,6 +137,15 @@ class FilledDataBuilderImpl(
|
||||||
return FilledPartition(
|
return FilledPartition(
|
||||||
autofillCipher = autofillCipher,
|
autofillCipher = autofillCipher,
|
||||||
filledItems = filledItems,
|
filledItems = filledItems,
|
||||||
|
inlinePresentationSpec = inlinePresentationSpec,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item at the [index]. If that fails, return the last item in the list. If that also fails,
|
||||||
|
* return null.
|
||||||
|
*/
|
||||||
|
private fun <T> List<T>.getOrLastOrNull(index: Int): T? =
|
||||||
|
getOrNull(index)
|
||||||
|
?: lastOrNull()
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
package com.x8bit.bitwarden.data.autofill.model
|
package com.x8bit.bitwarden.data.autofill.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A paired down model of the CipherView for use within the autofill feature.
|
* A paired down model of the CipherView for use within the autofill feature.
|
||||||
*/
|
*/
|
||||||
sealed class AutofillCipher {
|
sealed class AutofillCipher {
|
||||||
|
/**
|
||||||
|
* The icon res to represent this [AutofillCipher].
|
||||||
|
*/
|
||||||
|
abstract val iconRes: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the cipher.
|
* The name of the cipher.
|
||||||
*/
|
*/
|
||||||
|
@ -26,7 +34,10 @@ sealed class AutofillCipher {
|
||||||
val expirationMonth: String,
|
val expirationMonth: String,
|
||||||
val expirationYear: String,
|
val expirationYear: String,
|
||||||
val number: String,
|
val number: String,
|
||||||
) : AutofillCipher()
|
) : AutofillCipher() {
|
||||||
|
override val iconRes: Int
|
||||||
|
@DrawableRes get() = R.drawable.ic_card_item
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The card [AutofillCipher] model. This contains all of the data for building fulfilling a
|
* The card [AutofillCipher] model. This contains all of the data for building fulfilling a
|
||||||
|
@ -37,5 +48,8 @@ sealed class AutofillCipher {
|
||||||
override val subtitle: String,
|
override val subtitle: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
) : AutofillCipher()
|
) : AutofillCipher() {
|
||||||
|
override val iconRes: Int
|
||||||
|
@DrawableRes get() = R.drawable.ic_login_item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.data.autofill.model
|
package com.x8bit.bitwarden.data.autofill.model
|
||||||
|
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parsed autofill request.
|
* The parsed autofill request.
|
||||||
|
@ -12,6 +13,8 @@ sealed class AutofillRequest {
|
||||||
*/
|
*/
|
||||||
data class Fillable(
|
data class Fillable(
|
||||||
val ignoreAutofillIds: List<AutofillId>,
|
val ignoreAutofillIds: List<AutofillId>,
|
||||||
|
val inlinePresentationSpecs: List<InlinePresentationSpec>,
|
||||||
|
val maxInlineSuggestionsCount: Int,
|
||||||
val partition: AutofillPartition,
|
val partition: AutofillPartition,
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
) : AutofillRequest()
|
) : AutofillRequest()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.data.autofill.model
|
package com.x8bit.bitwarden.data.autofill.model
|
||||||
|
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fulfilled autofill dataset. This is all of the data to fulfill each view of the autofill
|
* A fulfilled autofill dataset. This is all of the data to fulfill each view of the autofill
|
||||||
|
@ -9,4 +10,5 @@ import android.view.autofill.AutofillId
|
||||||
data class FilledData(
|
data class FilledData(
|
||||||
val filledPartitions: List<FilledPartition>,
|
val filledPartitions: List<FilledPartition>,
|
||||||
val ignoreAutofillIds: List<AutofillId>,
|
val ignoreAutofillIds: List<AutofillId>,
|
||||||
|
val vaultItemInlinePresentationSpec: InlinePresentationSpec?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package com.x8bit.bitwarden.data.autofill.model
|
package com.x8bit.bitwarden.data.autofill.model
|
||||||
|
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All of the data required to build a `Dataset` for fulfilling a partition of data based on an
|
* All of the data required to build a `Dataset` for fulfilling a partition of data based on an
|
||||||
* [AutofillCipher].
|
* [AutofillCipher].
|
||||||
*
|
*
|
||||||
* @param autofillCipher The cipher used to fulfill these [filledItems].
|
* @param autofillCipher The cipher used to fulfill these [filledItems].
|
||||||
* @param filledItems A filled copy of each view from this partition.
|
* @param filledItems A filled copy of each view from this partition.
|
||||||
|
* @param inlinePresentationSpec The spec for the inline presentation given one is expected.
|
||||||
*/
|
*/
|
||||||
data class FilledPartition(
|
data class FilledPartition(
|
||||||
val autofillCipher: AutofillCipher,
|
val autofillCipher: AutofillCipher,
|
||||||
val filledItems: List<FilledItem>,
|
val filledItems: List<FilledItem>,
|
||||||
|
val inlinePresentationSpec: InlinePresentationSpec?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.data.autofill.parser
|
package com.x8bit.bitwarden.data.autofill.parser
|
||||||
|
|
||||||
import android.service.autofill.FillRequest
|
import android.service.autofill.FillRequest
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,6 +11,12 @@ interface AutofillParser {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the useful information from [fillRequest] into an [AutofillRequest].
|
* Parse the useful information from [fillRequest] into an [AutofillRequest].
|
||||||
|
*
|
||||||
|
* @param autofillAppInfo Provides app context that is required to properly parse the request.
|
||||||
|
* @param fillRequest The request that needs parsing.
|
||||||
*/
|
*/
|
||||||
fun parse(fillRequest: FillRequest): AutofillRequest
|
fun parse(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
|
fillRequest: FillRequest,
|
||||||
|
): AutofillRequest
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,14 @@ package com.x8bit.bitwarden.data.autofill.parser
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.service.autofill.FillRequest
|
import android.service.autofill.FillRequest
|
||||||
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.AutofillPartition
|
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||||
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
||||||
import com.x8bit.bitwarden.data.autofill.util.buildUriOrNull
|
import com.x8bit.bitwarden.data.autofill.util.buildUriOrNull
|
||||||
|
import com.x8bit.bitwarden.data.autofill.util.getInlinePresentationSpecs
|
||||||
|
import com.x8bit.bitwarden.data.autofill.util.getMaxInlineSuggestionsCount
|
||||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
|
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,7 +18,10 @@ import com.x8bit.bitwarden.data.autofill.util.toAutofillView
|
||||||
* from the OS into domain models.
|
* from the OS into domain models.
|
||||||
*/
|
*/
|
||||||
class AutofillParserImpl : AutofillParser {
|
class AutofillParserImpl : AutofillParser {
|
||||||
override fun parse(fillRequest: FillRequest): AutofillRequest =
|
override fun parse(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
|
fillRequest: FillRequest,
|
||||||
|
): AutofillRequest =
|
||||||
// Attempt to get the most recent autofill context.
|
// Attempt to get the most recent autofill context.
|
||||||
fillRequest
|
fillRequest
|
||||||
.fillContexts
|
.fillContexts
|
||||||
|
@ -24,6 +30,8 @@ class AutofillParserImpl : AutofillParser {
|
||||||
?.let { assistStructure ->
|
?.let { assistStructure ->
|
||||||
parseInternal(
|
parseInternal(
|
||||||
assistStructure = assistStructure,
|
assistStructure = assistStructure,
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
?: AutofillRequest.Unfillable
|
?: AutofillRequest.Unfillable
|
||||||
|
@ -33,6 +41,8 @@ class AutofillParserImpl : AutofillParser {
|
||||||
*/
|
*/
|
||||||
private fun parseInternal(
|
private fun parseInternal(
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
|
fillRequest: FillRequest,
|
||||||
): AutofillRequest {
|
): AutofillRequest {
|
||||||
// Parse the `assistStructure` into internal models.
|
// Parse the `assistStructure` into internal models.
|
||||||
val traversalDataList = assistStructure.traverse()
|
val traversalDataList = assistStructure.traverse()
|
||||||
|
@ -69,8 +79,18 @@ class AutofillParserImpl : AutofillParser {
|
||||||
.map { it.ignoreAutofillIds }
|
.map { it.ignoreAutofillIds }
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
|
val maxInlineSuggestionsCount = fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
val inlinePresentationSpecs = fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
return AutofillRequest.Fillable(
|
return AutofillRequest.Fillable(
|
||||||
|
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
maxInlineSuggestionsCount = maxInlineSuggestionsCount,
|
||||||
partition = partition,
|
partition = partition,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
)
|
)
|
||||||
|
|
|
@ -54,7 +54,11 @@ class AutofillProcessorImpl(
|
||||||
fillRequest: FillRequest,
|
fillRequest: FillRequest,
|
||||||
) {
|
) {
|
||||||
// Parse the OS data into an [AutofillRequest] for easier processing.
|
// Parse the OS data into an [AutofillRequest] for easier processing.
|
||||||
when (val autofillRequest = parser.parse(fillRequest)) {
|
val autofillRequest = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
when (autofillRequest) {
|
||||||
is AutofillRequest.Fillable -> {
|
is AutofillRequest.Fillable -> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Fulfill the [autofillRequest].
|
// Fulfill the [autofillRequest].
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.autofill.FillRequest
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the list of [InlinePresentationSpec]s. If it fails, return an empty list.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun FillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
|
): List<InlinePresentationSpec> =
|
||||||
|
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||||
|
inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the max inline suggestions count. If the OS is below Android R, this will always
|
||||||
|
* return 0.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun FillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
|
): Int =
|
||||||
|
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||||
|
inlineSuggestionsRequest?.maxSuggestionCount ?: 0
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
|
@ -5,14 +5,17 @@ import android.os.Build
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.Presentations
|
import android.service.autofill.Presentations
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||||
import com.x8bit.bitwarden.ui.autofill.buildAutofillRemoteViews
|
import com.x8bit.bitwarden.ui.autofill.buildAutofillRemoteViews
|
||||||
|
import com.x8bit.bitwarden.ui.autofill.util.createCipherInlinePresentationOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a [Dataset] to represent the [FilledPartition]. This dataset includes an overlay UI
|
* Build a [Dataset] to represent the [FilledPartition]. This dataset includes an overlay UI
|
||||||
* presentation for each filled item.
|
* presentation for each filled item.
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
fun FilledPartition.buildDataset(
|
fun FilledPartition.buildDataset(
|
||||||
autofillAppInfo: AutofillAppInfo,
|
autofillAppInfo: AutofillAppInfo,
|
||||||
): Dataset {
|
): Dataset {
|
||||||
|
@ -24,11 +27,13 @@ fun FilledPartition.buildDataset(
|
||||||
|
|
||||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
applyToDatasetPostTiramisu(
|
applyToDatasetPostTiramisu(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
datasetBuilder = datasetBuilder,
|
datasetBuilder = datasetBuilder,
|
||||||
remoteViews = remoteViewsPlaceholder,
|
remoteViews = remoteViewsPlaceholder,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
buildDatasetPreTiramisu(
|
buildDatasetPreTiramisu(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
datasetBuilder = datasetBuilder,
|
datasetBuilder = datasetBuilder,
|
||||||
remoteViews = remoteViewsPlaceholder,
|
remoteViews = remoteViewsPlaceholder,
|
||||||
)
|
)
|
||||||
|
@ -41,12 +46,23 @@ fun FilledPartition.buildDataset(
|
||||||
* Apply this [FilledPartition] to the [datasetBuilder] on devices running OS version Tiramisu or
|
* Apply this [FilledPartition] to the [datasetBuilder] on devices running OS version Tiramisu or
|
||||||
* greater.
|
* greater.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("NewApi")
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
private fun FilledPartition.applyToDatasetPostTiramisu(
|
private fun FilledPartition.applyToDatasetPostTiramisu(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
datasetBuilder: Dataset.Builder,
|
datasetBuilder: Dataset.Builder,
|
||||||
remoteViews: RemoteViews,
|
remoteViews: RemoteViews,
|
||||||
) {
|
) {
|
||||||
val presentation = Presentations.Builder()
|
val presentationBuilder = Presentations.Builder()
|
||||||
|
inlinePresentationSpec
|
||||||
|
?.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
?.let { inlinePresentation ->
|
||||||
|
presentationBuilder.setInlinePresentation(inlinePresentation)
|
||||||
|
}
|
||||||
|
|
||||||
|
val presentation = presentationBuilder
|
||||||
.setMenuPresentation(remoteViews)
|
.setMenuPresentation(remoteViews)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -62,10 +78,24 @@ private fun FilledPartition.applyToDatasetPostTiramisu(
|
||||||
* Apply this [FilledPartition] to the [datasetBuilder] on devices running OS versions that predate
|
* Apply this [FilledPartition] to the [datasetBuilder] on devices running OS versions that predate
|
||||||
* Tiramisu.
|
* Tiramisu.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@SuppressLint("NewApi")
|
||||||
private fun FilledPartition.buildDatasetPreTiramisu(
|
private fun FilledPartition.buildDatasetPreTiramisu(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
datasetBuilder: Dataset.Builder,
|
datasetBuilder: Dataset.Builder,
|
||||||
remoteViews: RemoteViews,
|
remoteViews: RemoteViews,
|
||||||
) {
|
) {
|
||||||
|
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
|
||||||
|
inlinePresentationSpec
|
||||||
|
?.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
?.let { inlinePresentation ->
|
||||||
|
datasetBuilder.setInlinePresentation(inlinePresentation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filledItems.forEach { filledItem ->
|
filledItems.forEach { filledItem ->
|
||||||
filledItem.applyToDatasetPreTiramisu(
|
filledItem.applyToDatasetPreTiramisu(
|
||||||
datasetBuilder = datasetBuilder,
|
datasetBuilder = datasetBuilder,
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.x8bit.bitwarden.ui.autofill.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not dark mode is currently active at the system level.
|
||||||
|
*/
|
||||||
|
val Context.isSystemDarkMode: Boolean
|
||||||
|
get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
||||||
|
Configuration.UI_MODE_NIGHT_YES
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.x8bit.bitwarden.ui.autofill.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.autofill.InlinePresentation
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.autofill.inline.UiVersions
|
||||||
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try creating an [InlinePresentation] for [autofillCipher] with this [InlinePresentationSpec]. If
|
||||||
|
* it fails, return null.
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
fun InlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo: AutofillAppInfo,
|
||||||
|
autofillCipher: AutofillCipher,
|
||||||
|
): InlinePresentation? {
|
||||||
|
val isInlineCompatible = UiVersions
|
||||||
|
.getVersions(style)
|
||||||
|
.contains(UiVersions.INLINE_UI_VERSION_1)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
.setTint(autofillAppInfo.contentColor)
|
||||||
|
val slice = InlineSuggestionUi
|
||||||
|
.newContentBuilder(pendingIntent)
|
||||||
|
.setTitle(autofillCipher.name)
|
||||||
|
.setSubtitle(autofillCipher.subtitle)
|
||||||
|
.setStartIcon(icon)
|
||||||
|
.build()
|
||||||
|
.slice
|
||||||
|
|
||||||
|
return InlinePresentation(
|
||||||
|
slice,
|
||||||
|
this,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content color for the inline presentation.
|
||||||
|
*/
|
||||||
|
private val AutofillAppInfo.contentColor: Int
|
||||||
|
get() {
|
||||||
|
val colorRes = if (context.isSystemDarkMode) {
|
||||||
|
R.color.dark_on_surface
|
||||||
|
} else {
|
||||||
|
R.color.on_surface
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getColor(colorRes)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:supportsInlineSuggestions="true" />
|
2
app/src/main/res/xml/autofill_service_configuration.xml
Normal file
2
app/src/main/res/xml/autofill_service_configuration.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<autofill-service />
|
|
@ -60,6 +60,7 @@ class FillResponseBuilderTest {
|
||||||
val filledData = FilledData(
|
val filledData = FilledData(
|
||||||
filledPartitions = emptyList(),
|
filledPartitions = emptyList(),
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
val actual = fillResponseBuilder.build(
|
val actual = fillResponseBuilder.build(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
|
@ -76,12 +77,14 @@ class FillResponseBuilderTest {
|
||||||
val filledPartitions = FilledPartition(
|
val filledPartitions = FilledPartition(
|
||||||
autofillCipher = mockk(),
|
autofillCipher = mockk(),
|
||||||
filledItems = emptyList(),
|
filledItems = emptyList(),
|
||||||
|
inlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
val filledData = FilledData(
|
val filledData = FilledData(
|
||||||
filledPartitions = listOf(
|
filledPartitions = listOf(
|
||||||
filledPartitions,
|
filledPartitions,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
val actual = fillResponseBuilder.build(
|
val actual = fillResponseBuilder.build(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
|
@ -108,6 +111,7 @@ class FillResponseBuilderTest {
|
||||||
val filledData = FilledData(
|
val filledData = FilledData(
|
||||||
filledPartitions = filledPartitions,
|
filledPartitions = filledPartitions,
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
every {
|
every {
|
||||||
filledPartitionOne.buildDataset(
|
filledPartitionOne.buildDataset(
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.builder
|
||||||
|
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||||
|
@ -83,6 +84,8 @@ class FilledDataBuilderTest {
|
||||||
val ignoreAutofillIds: List<AutofillId> = mockk()
|
val ignoreAutofillIds: List<AutofillId> = mockk()
|
||||||
val autofillRequest = AutofillRequest.Fillable(
|
val autofillRequest = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
inlinePresentationSpecs = emptyList(),
|
||||||
|
maxInlineSuggestionsCount = 0,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
@ -96,12 +99,14 @@ class FilledDataBuilderTest {
|
||||||
filledItemPassword,
|
filledItemPassword,
|
||||||
filledItemUsername,
|
filledItemUsername,
|
||||||
),
|
),
|
||||||
|
inlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
val expected = FilledData(
|
val expected = FilledData(
|
||||||
filledPartitions = listOf(
|
filledPartitions = listOf(
|
||||||
filledPartition,
|
filledPartition,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
coEvery {
|
coEvery {
|
||||||
autofillCipherProvider.getLoginAutofillCiphers(
|
autofillCipherProvider.getLoginAutofillCiphers(
|
||||||
|
@ -154,12 +159,15 @@ class FilledDataBuilderTest {
|
||||||
val ignoreAutofillIds: List<AutofillId> = mockk()
|
val ignoreAutofillIds: List<AutofillId> = mockk()
|
||||||
val autofillRequest = AutofillRequest.Fillable(
|
val autofillRequest = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
inlinePresentationSpecs = emptyList(),
|
||||||
|
maxInlineSuggestionsCount = 0,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = null,
|
uri = null,
|
||||||
)
|
)
|
||||||
val expected = FilledData(
|
val expected = FilledData(
|
||||||
filledPartitions = emptyList(),
|
filledPartitions = emptyList(),
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
@ -210,6 +218,8 @@ class FilledDataBuilderTest {
|
||||||
val ignoreAutofillIds: List<AutofillId> = mockk()
|
val ignoreAutofillIds: List<AutofillId> = mockk()
|
||||||
val autofillRequest = AutofillRequest.Fillable(
|
val autofillRequest = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
inlinePresentationSpecs = emptyList(),
|
||||||
|
maxInlineSuggestionsCount = 0,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
@ -225,12 +235,14 @@ class FilledDataBuilderTest {
|
||||||
filledItemExpirationYear,
|
filledItemExpirationYear,
|
||||||
filledItemNumber,
|
filledItemNumber,
|
||||||
),
|
),
|
||||||
|
inlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
val expected = FilledData(
|
val expected = FilledData(
|
||||||
filledPartitions = listOf(
|
filledPartitions = listOf(
|
||||||
filledPartition,
|
filledPartition,
|
||||||
),
|
),
|
||||||
ignoreAutofillIds = ignoreAutofillIds,
|
ignoreAutofillIds = ignoreAutofillIds,
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
coEvery { autofillCipherProvider.getCardAutofillCiphers() } returns listOf(autofillCipher)
|
coEvery { autofillCipherProvider.getCardAutofillCiphers() } returns listOf(autofillCipher)
|
||||||
every { autofillViewCode.buildFilledItemOrNull(code) } returns filledItemCode
|
every { autofillViewCode.buildFilledItemOrNull(code) } returns filledItemCode
|
||||||
|
@ -258,6 +270,107 @@ class FilledDataBuilderTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `build should return filled data with max count of inline specs with one spec repeated`() =
|
||||||
|
runTest {
|
||||||
|
// Setup
|
||||||
|
val password = "Password"
|
||||||
|
val username = "johnDoe"
|
||||||
|
val autofillCipher = AutofillCipher.Login(
|
||||||
|
name = "Cipher One",
|
||||||
|
password = password,
|
||||||
|
username = username,
|
||||||
|
subtitle = "Subtitle",
|
||||||
|
)
|
||||||
|
val autofillViewEmail = AutofillView.Login.EmailAddress(
|
||||||
|
data = autofillViewData,
|
||||||
|
)
|
||||||
|
val autofillViewPassword = AutofillView.Login.Password(
|
||||||
|
data = autofillViewData,
|
||||||
|
)
|
||||||
|
val autofillViewUsername = AutofillView.Login.Username(
|
||||||
|
data = autofillViewData,
|
||||||
|
)
|
||||||
|
val autofillPartition = AutofillPartition.Login(
|
||||||
|
views = listOf(
|
||||||
|
autofillViewEmail,
|
||||||
|
autofillViewPassword,
|
||||||
|
autofillViewUsername,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val inlinePresentationSpec: InlinePresentationSpec = mockk()
|
||||||
|
val autofillRequest = AutofillRequest.Fillable(
|
||||||
|
ignoreAutofillIds = emptyList(),
|
||||||
|
inlinePresentationSpecs = listOf(
|
||||||
|
inlinePresentationSpec,
|
||||||
|
),
|
||||||
|
maxInlineSuggestionsCount = 3,
|
||||||
|
partition = autofillPartition,
|
||||||
|
uri = URI,
|
||||||
|
)
|
||||||
|
val filledItemEmail: FilledItem = mockk()
|
||||||
|
val filledItemPassword: FilledItem = mockk()
|
||||||
|
val filledItemUsername: FilledItem = mockk()
|
||||||
|
val filledPartitionOne = FilledPartition(
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
filledItems = listOf(
|
||||||
|
filledItemEmail,
|
||||||
|
filledItemPassword,
|
||||||
|
filledItemUsername,
|
||||||
|
),
|
||||||
|
inlinePresentationSpec = inlinePresentationSpec,
|
||||||
|
)
|
||||||
|
val filledPartitionTwo = filledPartitionOne.copy()
|
||||||
|
val filledPartitionThree = FilledPartition(
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
filledItems = listOf(
|
||||||
|
filledItemEmail,
|
||||||
|
filledItemPassword,
|
||||||
|
filledItemUsername,
|
||||||
|
),
|
||||||
|
inlinePresentationSpec = null,
|
||||||
|
)
|
||||||
|
val expected = FilledData(
|
||||||
|
filledPartitions = listOf(
|
||||||
|
filledPartitionOne,
|
||||||
|
filledPartitionTwo,
|
||||||
|
filledPartitionThree,
|
||||||
|
),
|
||||||
|
ignoreAutofillIds = emptyList(),
|
||||||
|
vaultItemInlinePresentationSpec = inlinePresentationSpec,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
autofillCipherProvider.getLoginAutofillCiphers(
|
||||||
|
uri = URI,
|
||||||
|
)
|
||||||
|
} returns listOf(autofillCipher, autofillCipher, autofillCipher)
|
||||||
|
every { autofillViewEmail.buildFilledItemOrNull(username) } returns filledItemEmail
|
||||||
|
every {
|
||||||
|
autofillViewPassword.buildFilledItemOrNull(password)
|
||||||
|
} returns filledItemPassword
|
||||||
|
every {
|
||||||
|
autofillViewUsername.buildFilledItemOrNull(username)
|
||||||
|
} returns filledItemUsername
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = filledDataBuilder.build(
|
||||||
|
autofillRequest = autofillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
autofillCipherProvider.getLoginAutofillCiphers(
|
||||||
|
uri = URI,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify(exactly = 3) {
|
||||||
|
autofillViewEmail.buildFilledItemOrNull(username)
|
||||||
|
autofillViewPassword.buildFilledItemOrNull(password)
|
||||||
|
autofillViewUsername.buildFilledItemOrNull(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val URI: String = "androidapp://com.x8bit.bitwarden"
|
private const val URI: String = "androidapp://com.x8bit.bitwarden"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,15 @@ import android.service.autofill.FillContext
|
||||||
import android.service.autofill.FillRequest
|
import android.service.autofill.FillRequest
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||||
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
|
||||||
import com.x8bit.bitwarden.data.autofill.util.buildUriOrNull
|
import com.x8bit.bitwarden.data.autofill.util.buildUriOrNull
|
||||||
|
import com.x8bit.bitwarden.data.autofill.util.getInlinePresentationSpecs
|
||||||
|
import com.x8bit.bitwarden.data.autofill.util.getMaxInlineSuggestionsCount
|
||||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
|
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -24,6 +28,7 @@ import org.junit.jupiter.api.Test
|
||||||
class AutofillParserTests {
|
class AutofillParserTests {
|
||||||
private lateinit var parser: AutofillParser
|
private lateinit var parser: AutofillParser
|
||||||
|
|
||||||
|
private val autofillAppInfo: AutofillAppInfo = mockk()
|
||||||
private val autofillViewData = AutofillView.Data(
|
private val autofillViewData = AutofillView.Data(
|
||||||
autofillId = mockk(),
|
autofillId = mockk(),
|
||||||
isFocused = true,
|
isFocused = true,
|
||||||
|
@ -58,11 +63,26 @@ class AutofillParserTests {
|
||||||
private val fillRequest: FillRequest = mockk {
|
private val fillRequest: FillRequest = mockk {
|
||||||
every { this@mockk.fillContexts } returns listOf(fillContext)
|
every { this@mockk.fillContexts } returns listOf(fillContext)
|
||||||
}
|
}
|
||||||
|
private val inlinePresentationSpecs: List<InlinePresentationSpec> = mockk()
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockkStatic(AssistStructure.ViewNode::toAutofillView)
|
mockkStatic(AssistStructure.ViewNode::toAutofillView)
|
||||||
|
mockkStatic(
|
||||||
|
FillRequest::getMaxInlineSuggestionsCount,
|
||||||
|
FillRequest::getInlinePresentationSpecs,
|
||||||
|
)
|
||||||
mockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
|
mockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
|
||||||
|
every {
|
||||||
|
fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
} returns inlinePresentationSpecs
|
||||||
|
every {
|
||||||
|
fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
} returns MAX_INLINE_SUGGESTION_COUNT
|
||||||
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure) } returns URI
|
every { any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure) } returns URI
|
||||||
parser = AutofillParserImpl()
|
parser = AutofillParserImpl()
|
||||||
}
|
}
|
||||||
|
@ -70,6 +90,10 @@ class AutofillParserTests {
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
unmockkStatic(AssistStructure.ViewNode::toAutofillView)
|
unmockkStatic(AssistStructure.ViewNode::toAutofillView)
|
||||||
|
unmockkStatic(
|
||||||
|
FillRequest::getMaxInlineSuggestionsCount,
|
||||||
|
FillRequest::getInlinePresentationSpecs,
|
||||||
|
)
|
||||||
unmockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
|
unmockkStatic(List<ViewNodeTraversalData>::buildUriOrNull)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +104,10 @@ class AutofillParserTests {
|
||||||
every { fillRequest.fillContexts } returns emptyList()
|
every { fillRequest.fillContexts } returns emptyList()
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = parser.parse(fillRequest)
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
|
@ -93,7 +120,10 @@ class AutofillParserTests {
|
||||||
every { assistStructure.windowNodeCount } returns 0
|
every { assistStructure.windowNodeCount } returns 0
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = parser.parse(fillRequest)
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
|
@ -134,6 +164,8 @@ class AutofillParserTests {
|
||||||
)
|
)
|
||||||
val expected = AutofillRequest.Fillable(
|
val expected = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = listOf(childAutofillId),
|
ignoreAutofillIds = listOf(childAutofillId),
|
||||||
|
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||||
|
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
@ -141,11 +173,20 @@ class AutofillParserTests {
|
||||||
every { assistStructure.getWindowNodeAt(0) } returns windowNode
|
every { assistStructure.getWindowNodeAt(0) } returns windowNode
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = parser.parse(fillRequest)
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,6 +212,8 @@ class AutofillParserTests {
|
||||||
)
|
)
|
||||||
val expected = AutofillRequest.Fillable(
|
val expected = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||||
|
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
@ -178,11 +221,20 @@ class AutofillParserTests {
|
||||||
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = parser.parse(fillRequest)
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,6 +260,8 @@ class AutofillParserTests {
|
||||||
)
|
)
|
||||||
val expected = AutofillRequest.Fillable(
|
val expected = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||||
|
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
@ -215,11 +269,20 @@ class AutofillParserTests {
|
||||||
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = parser.parse(fillRequest)
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +308,8 @@ class AutofillParserTests {
|
||||||
)
|
)
|
||||||
val expected = AutofillRequest.Fillable(
|
val expected = AutofillRequest.Fillable(
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
inlinePresentationSpecs = inlinePresentationSpecs,
|
||||||
|
maxInlineSuggestionsCount = MAX_INLINE_SUGGESTION_COUNT,
|
||||||
partition = autofillPartition,
|
partition = autofillPartition,
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
@ -252,11 +317,20 @@ class AutofillParserTests {
|
||||||
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
every { loginViewNode.toAutofillView() } returns loginAutofillView
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
val actual = parser.parse(fillRequest)
|
val actual = parser.parse(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
|
fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
any<List<ViewNodeTraversalData>>().buildUriOrNull(assistStructure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,8 +344,7 @@ class AutofillParserTests {
|
||||||
every { assistStructure.getWindowNodeAt(0) } returns cardWindowNode
|
every { assistStructure.getWindowNodeAt(0) } returns cardWindowNode
|
||||||
every { assistStructure.getWindowNodeAt(1) } returns loginWindowNode
|
every { assistStructure.getWindowNodeAt(1) } returns loginWindowNode
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val URI: String = "androidapp://com.x8bit.bitwarden"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MAX_INLINE_SUGGESTION_COUNT: Int = 42
|
||||||
|
private const val URI: String = "androidapp://com.x8bit.bitwarden"
|
||||||
|
|
|
@ -76,7 +76,12 @@ class AutofillProcessorTest {
|
||||||
val autofillRequest = AutofillRequest.Unfillable
|
val autofillRequest = AutofillRequest.Unfillable
|
||||||
val fillRequest: FillRequest = mockk()
|
val fillRequest: FillRequest = mockk()
|
||||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||||
every { parser.parse(fillRequest) } returns autofillRequest
|
every {
|
||||||
|
parser.parse(
|
||||||
|
autofillAppInfo = appInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
} returns autofillRequest
|
||||||
every { fillCallback.onSuccess(null) } just runs
|
every { fillCallback.onSuccess(null) } just runs
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
@ -90,7 +95,10 @@ class AutofillProcessorTest {
|
||||||
// Verify
|
// Verify
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
cancellationSignal.setOnCancelListener(any())
|
cancellationSignal.setOnCancelListener(any())
|
||||||
parser.parse(fillRequest)
|
parser.parse(
|
||||||
|
autofillAppInfo = appInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
fillCallback.onSuccess(null)
|
fillCallback.onSuccess(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +111,7 @@ class AutofillProcessorTest {
|
||||||
val filledData = FilledData(
|
val filledData = FilledData(
|
||||||
filledPartitions = listOf(mockk()),
|
filledPartitions = listOf(mockk()),
|
||||||
ignoreAutofillIds = emptyList(),
|
ignoreAutofillIds = emptyList(),
|
||||||
|
vaultItemInlinePresentationSpec = null,
|
||||||
)
|
)
|
||||||
val fillResponse: FillResponse = mockk()
|
val fillResponse: FillResponse = mockk()
|
||||||
val autofillRequest: AutofillRequest.Fillable = mockk()
|
val autofillRequest: AutofillRequest.Fillable = mockk()
|
||||||
|
@ -112,7 +121,12 @@ class AutofillProcessorTest {
|
||||||
)
|
)
|
||||||
} returns filledData
|
} returns filledData
|
||||||
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
every { cancellationSignal.setOnCancelListener(any()) } just runs
|
||||||
every { parser.parse(fillRequest) } returns autofillRequest
|
every {
|
||||||
|
parser.parse(
|
||||||
|
autofillAppInfo = appInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
|
} returns autofillRequest
|
||||||
every {
|
every {
|
||||||
fillResponseBuilder.build(
|
fillResponseBuilder.build(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
|
@ -137,7 +151,10 @@ class AutofillProcessorTest {
|
||||||
}
|
}
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
cancellationSignal.setOnCancelListener(any())
|
cancellationSignal.setOnCancelListener(any())
|
||||||
parser.parse(fillRequest)
|
parser.parse(
|
||||||
|
autofillAppInfo = appInfo,
|
||||||
|
fillRequest = fillRequest,
|
||||||
|
)
|
||||||
fillResponseBuilder.build(
|
fillResponseBuilder.build(
|
||||||
autofillAppInfo = appInfo,
|
autofillAppInfo = appInfo,
|
||||||
filledData = filledData,
|
filledData = filledData,
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
package com.x8bit.bitwarden.data.autofill.util
|
||||||
|
|
||||||
|
import android.service.autofill.FillRequest
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FillRequestExtensionsTest {
|
||||||
|
private val expectedInlinePresentationSpecs: List<InlinePresentationSpec> = mockk()
|
||||||
|
private val expectedInlineSuggestionsRequest: InlineSuggestionsRequest = mockk {
|
||||||
|
every { this@mockk.inlinePresentationSpecs } returns expectedInlinePresentationSpecs
|
||||||
|
every { this@mockk.maxSuggestionCount } returns MAX_INLINE_SUGGESTIONS_COUNT
|
||||||
|
}
|
||||||
|
private val fillRequest: FillRequest = mockk {
|
||||||
|
every { this@mockk.inlineSuggestionsRequest } returns expectedInlineSuggestionsRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getInlinePresentationSpecs should return empty list when pre-R`() {
|
||||||
|
// Setup
|
||||||
|
val autofillAppInfo = AutofillAppInfo(
|
||||||
|
context = mockk(),
|
||||||
|
packageName = "com.x8bit.bitwarden",
|
||||||
|
sdkInt = 17,
|
||||||
|
)
|
||||||
|
val expected: List<InlinePresentationSpec> = emptyList()
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getInlinePresentationSpecs should return populated list when post-R`() {
|
||||||
|
// Setup
|
||||||
|
val autofillAppInfo = AutofillAppInfo(
|
||||||
|
context = mockk(),
|
||||||
|
packageName = "com.x8bit.bitwarden",
|
||||||
|
sdkInt = 34,
|
||||||
|
)
|
||||||
|
val expected = expectedInlinePresentationSpecs
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = fillRequest.getInlinePresentationSpecs(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getMaxInlineSuggestionsCount should return 0 when pre-R`() {
|
||||||
|
// Setup
|
||||||
|
val autofillAppInfo = AutofillAppInfo(
|
||||||
|
context = mockk(),
|
||||||
|
packageName = "com.x8bit.bitwarden",
|
||||||
|
sdkInt = 17,
|
||||||
|
)
|
||||||
|
val expected = 0
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getMaxInlineSuggestionsCount should return the max count when post-R`() {
|
||||||
|
// Setup
|
||||||
|
val autofillAppInfo = AutofillAppInfo(
|
||||||
|
context = mockk(),
|
||||||
|
packageName = "com.x8bit.bitwarden",
|
||||||
|
sdkInt = 34,
|
||||||
|
)
|
||||||
|
val expected = MAX_INLINE_SUGGESTIONS_COUNT
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = fillRequest.getMaxInlineSuggestionsCount(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val MAX_INLINE_SUGGESTIONS_COUNT: Int = 42
|
|
@ -3,14 +3,17 @@ package com.x8bit.bitwarden.data.autofill.util
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
|
import android.service.autofill.InlinePresentation
|
||||||
import android.service.autofill.Presentations
|
import android.service.autofill.Presentations
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
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
|
||||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||||
import com.x8bit.bitwarden.data.util.mockBuilder
|
import com.x8bit.bitwarden.data.util.mockBuilder
|
||||||
import com.x8bit.bitwarden.ui.autofill.buildAutofillRemoteViews
|
import com.x8bit.bitwarden.ui.autofill.buildAutofillRemoteViews
|
||||||
|
import com.x8bit.bitwarden.ui.autofill.util.createCipherInlinePresentationOrNull
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -35,11 +38,13 @@ class FilledPartitionExtensionsTest {
|
||||||
}
|
}
|
||||||
private val dataset: Dataset = mockk()
|
private val dataset: Dataset = mockk()
|
||||||
private val filledItem: FilledItem = mockk()
|
private val filledItem: FilledItem = mockk()
|
||||||
|
private val inlinePresentationSpec: InlinePresentationSpec = mockk()
|
||||||
private val filledPartition = FilledPartition(
|
private val filledPartition = FilledPartition(
|
||||||
autofillCipher = autofillCipher,
|
autofillCipher = autofillCipher,
|
||||||
filledItems = listOf(
|
filledItems = listOf(
|
||||||
filledItem,
|
filledItem,
|
||||||
),
|
),
|
||||||
|
inlinePresentationSpec = inlinePresentationSpec,
|
||||||
)
|
)
|
||||||
private val presentations: Presentations = mockk()
|
private val presentations: Presentations = mockk()
|
||||||
private val remoteViews: RemoteViews = mockk()
|
private val remoteViews: RemoteViews = mockk()
|
||||||
|
@ -51,6 +56,7 @@ class FilledPartitionExtensionsTest {
|
||||||
mockkStatic(::buildAutofillRemoteViews)
|
mockkStatic(::buildAutofillRemoteViews)
|
||||||
mockkStatic(FilledItem::applyToDatasetPostTiramisu)
|
mockkStatic(FilledItem::applyToDatasetPostTiramisu)
|
||||||
mockkStatic(FilledItem::applyToDatasetPreTiramisu)
|
mockkStatic(FilledItem::applyToDatasetPreTiramisu)
|
||||||
|
mockkStatic(InlinePresentationSpec::createCipherInlinePresentationOrNull)
|
||||||
every { anyConstructed<Dataset.Builder>().build() } returns dataset
|
every { anyConstructed<Dataset.Builder>().build() } returns dataset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +67,7 @@ class FilledPartitionExtensionsTest {
|
||||||
unmockkStatic(::buildAutofillRemoteViews)
|
unmockkStatic(::buildAutofillRemoteViews)
|
||||||
unmockkStatic(FilledItem::applyToDatasetPostTiramisu)
|
unmockkStatic(FilledItem::applyToDatasetPostTiramisu)
|
||||||
unmockkStatic(FilledItem::applyToDatasetPreTiramisu)
|
unmockkStatic(FilledItem::applyToDatasetPreTiramisu)
|
||||||
|
unmockkStatic(InlinePresentationSpec::createCipherInlinePresentationOrNull)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -71,12 +78,20 @@ class FilledPartitionExtensionsTest {
|
||||||
packageName = PACKAGE_NAME,
|
packageName = PACKAGE_NAME,
|
||||||
sdkInt = 34,
|
sdkInt = 34,
|
||||||
)
|
)
|
||||||
|
val inlinePresentation: InlinePresentation = mockk()
|
||||||
every {
|
every {
|
||||||
buildAutofillRemoteViews(
|
buildAutofillRemoteViews(
|
||||||
packageName = PACKAGE_NAME,
|
packageName = PACKAGE_NAME,
|
||||||
title = CIPHER_NAME,
|
title = CIPHER_NAME,
|
||||||
)
|
)
|
||||||
} returns remoteViews
|
} returns remoteViews
|
||||||
|
every {
|
||||||
|
inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
} returns inlinePresentation
|
||||||
|
mockBuilder<Presentations.Builder> { it.setInlinePresentation(inlinePresentation) }
|
||||||
mockBuilder<Presentations.Builder> { it.setMenuPresentation(remoteViews) }
|
mockBuilder<Presentations.Builder> { it.setMenuPresentation(remoteViews) }
|
||||||
every {
|
every {
|
||||||
filledItem.applyToDatasetPostTiramisu(
|
filledItem.applyToDatasetPostTiramisu(
|
||||||
|
@ -98,6 +113,11 @@ class FilledPartitionExtensionsTest {
|
||||||
packageName = PACKAGE_NAME,
|
packageName = PACKAGE_NAME,
|
||||||
title = CIPHER_NAME,
|
title = CIPHER_NAME,
|
||||||
)
|
)
|
||||||
|
inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
anyConstructed<Presentations.Builder>().setInlinePresentation(inlinePresentation)
|
||||||
anyConstructed<Presentations.Builder>().setMenuPresentation(remoteViews)
|
anyConstructed<Presentations.Builder>().setMenuPresentation(remoteViews)
|
||||||
anyConstructed<Presentations.Builder>().build()
|
anyConstructed<Presentations.Builder>().build()
|
||||||
filledItem.applyToDatasetPostTiramisu(
|
filledItem.applyToDatasetPostTiramisu(
|
||||||
|
@ -108,14 +128,16 @@ class FilledPartitionExtensionsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `buildDataset should applyToDatasetPreTiramisu when sdkInt is less than 33`() {
|
fun `buildDataset should skip inline and applyToDatasetPreTiramisu when sdkInt is less than 30`() {
|
||||||
// Setup
|
// Setup
|
||||||
val autofillAppInfo = AutofillAppInfo(
|
val autofillAppInfo = AutofillAppInfo(
|
||||||
context = context,
|
context = context,
|
||||||
packageName = PACKAGE_NAME,
|
packageName = PACKAGE_NAME,
|
||||||
sdkInt = 18,
|
sdkInt = 18,
|
||||||
)
|
)
|
||||||
|
val inlinePresentation: InlinePresentation = mockk()
|
||||||
every {
|
every {
|
||||||
buildAutofillRemoteViews(
|
buildAutofillRemoteViews(
|
||||||
packageName = PACKAGE_NAME,
|
packageName = PACKAGE_NAME,
|
||||||
|
@ -149,6 +171,61 @@ class FilledPartitionExtensionsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("Deprecation", "MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `buildDataset 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 {
|
||||||
|
buildAutofillRemoteViews(
|
||||||
|
packageName = PACKAGE_NAME,
|
||||||
|
title = CIPHER_NAME,
|
||||||
|
)
|
||||||
|
} returns remoteViews
|
||||||
|
every {
|
||||||
|
inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
} returns inlinePresentation
|
||||||
|
mockBuilder<Dataset.Builder> { it.setInlinePresentation(inlinePresentation) }
|
||||||
|
every {
|
||||||
|
filledItem.applyToDatasetPreTiramisu(
|
||||||
|
datasetBuilder = any(),
|
||||||
|
remoteViews = remoteViews,
|
||||||
|
)
|
||||||
|
} just runs
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = filledPartition.buildDataset(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(dataset, actual)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
buildAutofillRemoteViews(
|
||||||
|
packageName = PACKAGE_NAME,
|
||||||
|
title = CIPHER_NAME,
|
||||||
|
)
|
||||||
|
inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
anyConstructed<Dataset.Builder>().setInlinePresentation(inlinePresentation)
|
||||||
|
filledItem.applyToDatasetPreTiramisu(
|
||||||
|
datasetBuilder = any(),
|
||||||
|
remoteViews = remoteViews,
|
||||||
|
)
|
||||||
|
anyConstructed<Dataset.Builder>().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CIPHER_NAME: String = "Autofill Cipher"
|
private const val CIPHER_NAME: String = "Autofill Cipher"
|
||||||
private const val PACKAGE_NAME: String = "com.x8bit.bitwarden"
|
private const val PACKAGE_NAME: String = "com.x8bit.bitwarden"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.x8bit.bitwarden.ui.autofill.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ContextExtensionsTest {
|
||||||
|
private val testConfiguration: Configuration = mockk()
|
||||||
|
private val testResources: Resources = mockk {
|
||||||
|
every { this@mockk.configuration } returns testConfiguration
|
||||||
|
}
|
||||||
|
private val context: Context = mockk {
|
||||||
|
every { this@mockk.resources } returns testResources
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isSystemDarkMode should return true when UI_MODE_NIGHT_YES`() {
|
||||||
|
// Setup
|
||||||
|
testConfiguration.uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
assertTrue(context.isSystemDarkMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isSystemDarkMode should return false when UI_MODE_NIGHT_NO`() {
|
||||||
|
// Setup
|
||||||
|
testConfiguration.uiMode = Configuration.UI_MODE_NIGHT_NO
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
assertFalse(context.isSystemDarkMode)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
package com.x8bit.bitwarden.ui.autofill.util
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.slice.Slice
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import androidx.autofill.inline.UiVersions
|
||||||
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||||
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class InlinePresentationSpecExtensionsTest {
|
||||||
|
private val testContext: Context = mockk()
|
||||||
|
private val autofillAppInfo: AutofillAppInfo = mockk {
|
||||||
|
every { this@mockk.context } returns testContext
|
||||||
|
}
|
||||||
|
private val testStyle: Bundle = mockk()
|
||||||
|
private val inlinePresentationSpec: InlinePresentationSpec = mockk {
|
||||||
|
every { this@mockk.style } returns testStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic(Context::isSystemDarkMode)
|
||||||
|
mockkStatic(Icon::class)
|
||||||
|
mockkStatic(InlineSuggestionUi::class)
|
||||||
|
mockkStatic(PendingIntent::getService)
|
||||||
|
mockkStatic(UiVersions::getVersions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun teardown() {
|
||||||
|
mockkStatic(Context::isSystemDarkMode)
|
||||||
|
unmockkStatic(Icon::class)
|
||||||
|
unmockkStatic(InlineSuggestionUi::class)
|
||||||
|
unmockkStatic(PendingIntent::getService)
|
||||||
|
unmockkStatic(UiVersions::getVersions)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `createCipherInlinePresentationOrNull should return null if incompatible`() {
|
||||||
|
// Setup
|
||||||
|
every {
|
||||||
|
UiVersions.getVersions(testStyle)
|
||||||
|
} returns emptyList()
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = mockk(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
Assertions.assertNull(actual)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
UiVersions.getVersions(testStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `createCipherInlinePresentationOrNull should return presentation with card icon when card cipher and compatible`() {
|
||||||
|
// Setup
|
||||||
|
val icon: Icon = mockk()
|
||||||
|
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()
|
||||||
|
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
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify not-null because we can't properly mock Intent constructors.
|
||||||
|
Assertions.assertNotNull(actual)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
UiVersions.getVersions(testStyle)
|
||||||
|
PendingIntent.getService(
|
||||||
|
testContext,
|
||||||
|
PENDING_INTENT_CODE,
|
||||||
|
any<Intent>(),
|
||||||
|
PENDING_INTENT_FLAGS,
|
||||||
|
)
|
||||||
|
testContext.isSystemDarkMode
|
||||||
|
testContext.getColor(R.color.dark_on_surface)
|
||||||
|
Icon.createWithResource(
|
||||||
|
testContext,
|
||||||
|
R.drawable.ic_card_item,
|
||||||
|
)
|
||||||
|
.setTint(ICON_TINT)
|
||||||
|
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||||
|
.setTitle(AUTOFILL_CIPHER_NAME)
|
||||||
|
.setSubtitle(AUTOFILL_CIPHER_SUBTITLE)
|
||||||
|
.setStartIcon(icon)
|
||||||
|
.build()
|
||||||
|
.slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `createCipherInlinePresentationOrNull should return presentation with login icon when login cipher and compatible`() {
|
||||||
|
// Setup
|
||||||
|
val icon: Icon = mockk()
|
||||||
|
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
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = inlinePresentationSpec.createCipherInlinePresentationOrNull(
|
||||||
|
autofillAppInfo = autofillAppInfo,
|
||||||
|
autofillCipher = autofillCipher,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify not-null because we can't properly mock Intent constructors.
|
||||||
|
Assertions.assertNotNull(actual)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
UiVersions.getVersions(testStyle)
|
||||||
|
PendingIntent.getService(
|
||||||
|
testContext,
|
||||||
|
PENDING_INTENT_CODE,
|
||||||
|
any<Intent>(),
|
||||||
|
PENDING_INTENT_FLAGS,
|
||||||
|
)
|
||||||
|
testContext.isSystemDarkMode
|
||||||
|
testContext.getColor(R.color.on_surface)
|
||||||
|
Icon.createWithResource(
|
||||||
|
testContext,
|
||||||
|
R.drawable.ic_login_item,
|
||||||
|
)
|
||||||
|
.setTint(ICON_TINT)
|
||||||
|
InlineSuggestionUi.newContentBuilder(pendingIntent)
|
||||||
|
.setTitle(AUTOFILL_CIPHER_NAME)
|
||||||
|
.setSubtitle(AUTOFILL_CIPHER_SUBTITLE)
|
||||||
|
.setStartIcon(icon)
|
||||||
|
.build()
|
||||||
|
.slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val AUTOFILL_CIPHER_NAME = "Cipher1"
|
||||||
|
private const val AUTOFILL_CIPHER_SUBTITLE = "Subtitle"
|
||||||
|
private const val ICON_TINT: Int = 6123751
|
||||||
|
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
|
|
@ -24,6 +24,7 @@ androidxRoom = "2.6.1"
|
||||||
androidXSecurityCrypto = "1.1.0-alpha06"
|
androidXSecurityCrypto = "1.1.0-alpha06"
|
||||||
androidxSplash = "1.1.0-alpha02"
|
androidxSplash = "1.1.0-alpha02"
|
||||||
androidXAppCompat = "1.6.1"
|
androidXAppCompat = "1.6.1"
|
||||||
|
androdixAutofill = "1.1.0"
|
||||||
# Once the app and SDK reach a critical point of completeness we should begin fixing the version
|
# Once the app and SDK reach a critical point of completeness we should begin fixing the version
|
||||||
# here (BIT-311).
|
# here (BIT-311).
|
||||||
bitwardenSdk = "0.4.0-20240115.154650-43"
|
bitwardenSdk = "0.4.0-20240115.154650-43"
|
||||||
|
@ -55,6 +56,7 @@ zxing = "3.5.2"
|
||||||
# Format: <maintainer>-<artifact-name>
|
# Format: <maintainer>-<artifact-name>
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
|
||||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidXAppCompat" }
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidXAppCompat" }
|
||||||
|
androidx-autofill = { group = "androidx.autofill", name = "autofill", version.ref = "androdixAutofill" }
|
||||||
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidxBrowser" }
|
androidx-browser = { module = "androidx.browser:browser", version.ref = "androidxBrowser" }
|
||||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" }
|
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" }
|
||||||
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidxCamera" }
|
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "androidxCamera" }
|
||||||
|
|
Loading…
Reference in a new issue