BIT-1314: Add autofill node traversal with basic UI fulfillment (#541)

This commit is contained in:
Lucas Kivi
2024-06-20 17:08:07 +01:00
committed by Álison Fernandes
parent 35ef4e4252
commit cea26f5e32
30 changed files with 2016 additions and 55 deletions
@@ -1,12 +1,16 @@
package com.x8bit.bitwarden.data.autofill
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* The [AutofillService] implementation for the app. This fulfills autofill requests from other
@@ -14,12 +18,29 @@ import dagger.hilt.android.AndroidEntryPoint
*/
@AndroidEntryPoint
class BitwardenAutofillService : AutofillService() {
/**
* A processor to handle the autofill fulfillment. We want to keep this service light because
* it isn't easily tested.
*/
@Inject
lateinit var processor: AutofillProcessor
override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
fillCallback: FillCallback,
) {
// TODO: parse request and perform dummy autofill (BIT-1314)
processor.processFillRequest(
autofillAppInfo = AutofillAppInfo(
context = applicationContext,
packageName = packageName,
sdkInt = Build.VERSION.SDK_INT,
),
cancellationSignal = cancellationSignal,
fillCallback = fillCallback,
request = request,
)
}
override fun onSaveRequest(
@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.autofill.builder
import android.service.autofill.FillResponse
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.FilledData
/**
* A component for building fill responses out of fulfilled internal models.
*/
interface FillResponseBuilder {
/**
* Build the [filledData] into a [FillResponse]. Return null if not possible.
*/
fun build(
autofillAppInfo: AutofillAppInfo,
filledData: FilledData,
): FillResponse?
}
@@ -0,0 +1,47 @@
package com.x8bit.bitwarden.data.autofill.builder
import android.service.autofill.Dataset
import android.service.autofill.FillResponse
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.util.applyOverlayToDataset
import com.x8bit.bitwarden.ui.autofill.buildAutofillRemoteViews
/**
* The default implementation for [FillResponseBuilder]. This is a component for compiling fulfilled
* internal models into a [FillResponse] whenever possible.
*/
class FillResponseBuilderImpl : FillResponseBuilder {
override fun build(
autofillAppInfo: AutofillAppInfo,
filledData: FilledData,
): FillResponse? =
if (filledData.filledItems.isNotEmpty()) {
val remoteViewsPlaceholder = buildAutofillRemoteViews(
packageName = autofillAppInfo.packageName,
context = autofillAppInfo.context,
)
val datasetBuilder = Dataset.Builder()
// Set UI for each valid autofill view.
filledData.filledItems.forEach { filledItem ->
filledItem.applyOverlayToDataset(
appInfo = autofillAppInfo,
datasetBuilder = datasetBuilder,
remoteViews = remoteViewsPlaceholder,
)
}
val dataset = datasetBuilder.build()
FillResponse.Builder()
.addDataset(dataset)
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
.build()
} else {
// It is impossible for an `AutofillPartition` to be empty due to the way it is
// constructed. However, the [FillRequest] requires at least one dataset or an
// authentication intent with a presentation view. Neither of these make sense in the
// case where we have no views to fill. What we are supposed to do when we cannot
// fulfill a request is replace [FillResponse] with null in order to avoid this crash.
null
}
}
@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.autofill.builder
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
import com.x8bit.bitwarden.data.autofill.model.FilledData
/**
* A class for converting parsed autofill data into filled data that is ready to be loaded into a
* fill response.
*/
interface FilledDataBuilder {
/**
* Construct filled data from [autofillRequest].
*/
suspend fun build(
autofillRequest: AutofillRequest.Fillable,
): FilledData
}
@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.autofill.builder
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillView
import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledItem
/**
* The default [FilledDataBuilder]. This converts parsed autofill data into filled data that is
* ready to be loaded into an autofill response.
*/
class FilledDataBuilderImpl : FilledDataBuilder {
override suspend fun build(autofillRequest: AutofillRequest.Fillable): FilledData {
// TODO: determine whether or not the vault is locked (BIT-1296)
val filledItems = autofillRequest
.partition
.views
.map(AutofillView::toFilledItem)
// TODO: perform fulfillment with dummy data (BIT-1315)
return FilledData(
filledItems = filledItems,
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
)
}
}
/**
* Map this [AutofillView] to a [FilledItem].
*/
private fun AutofillView.toFilledItem(): FilledItem =
FilledItem(
autofillId = autofillId,
)
@@ -0,0 +1,45 @@
package com.x8bit.bitwarden.data.autofill.di
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilderImpl
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
/**
* Provides dependencies within the autofill package.
*/
@Module
@InstallIn(SingletonComponent::class)
object AutofillModule {
@Provides
fun providesAutofillParser(): AutofillParser = AutofillParserImpl()
@Provides
fun providesAutofillProcessor(
dispatcherManager: DispatcherManager,
filledDataBuilder: FilledDataBuilder,
fillResponseBuilder: FillResponseBuilder,
parser: AutofillParser,
): AutofillProcessor =
AutofillProcessorImpl(
dispatcherManager = dispatcherManager,
filledDataBuilder = filledDataBuilder,
fillResponseBuilder = fillResponseBuilder,
parser = parser,
)
@Provides
fun providesFillDataBuilder(): FilledDataBuilder = FilledDataBuilderImpl()
@Provides
fun providesFillResponseBuilder(): FillResponseBuilder = FillResponseBuilderImpl()
}
@@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.autofill.model
import android.content.Context
/**
* The app information required for the autofill service.
*/
data class AutofillAppInfo(
val context: Context,
val packageName: String,
val sdkInt: Int,
)
@@ -0,0 +1,32 @@
package com.x8bit.bitwarden.data.autofill.model
/**
* A partition of autofill data.
*/
sealed class AutofillPartition {
/**
* The views that correspond to this partition.
*/
abstract val views: List<AutofillView>
/**
* The credit card [AutofillPartition] data.
*/
data class Card(
override val views: List<AutofillView.Card>,
) : AutofillPartition()
/**
* The identity [AutofillPartition] data.
*/
data class Identity(
override val views: List<AutofillView.Identity>,
) : AutofillPartition()
/**
* The login [AutofillPartition] data.
*/
data class Login(
override val views: List<AutofillView.Login>,
) : AutofillPartition()
}
@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.autofill.model
import android.view.autofill.AutofillId
/**
* The parsed autofill request.
*/
sealed class AutofillRequest {
/**
* An autofill request that is fillable. This means it has [partition] of data that can be
* fulfilled.
*/
data class Fillable(
val ignoreAutofillIds: List<AutofillId>,
val partition: AutofillPartition,
) : AutofillRequest()
/**
* An autofill request that is unfillable.
*/
data object Unfillable : AutofillRequest()
}
@@ -0,0 +1,124 @@
package com.x8bit.bitwarden.data.autofill.model
import android.view.autofill.AutofillId
/**
* The processed, relevant data from an autofill view node.
*/
sealed class AutofillView {
/**
* The [AutofillId] associated with this view.
*/
abstract val autofillId: AutofillId
/**
* Whether the view is currently focused.
*/
abstract val isFocused: Boolean
/**
* A view that corresponds to the card data partition for autofill fields.
*/
sealed class Card : AutofillView() {
/**
* The expiration month [AutofillView] for the [Card] data partition.
*/
data class ExpirationMonth(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Card()
/**
* The expiration year [AutofillView] for the [Card] data partition.
*/
data class ExpirationYear(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Card()
/**
* The number [AutofillView] for the [Card] data partition.
*/
data class Number(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Card()
/**
* The security code [AutofillView] for the [Card] data partition.
*/
data class SecurityCode(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Card()
}
/**
* A view that corresponds to the personal info data partition for autofill fields.
*/
sealed class Identity : AutofillView() {
/**
* The name [AutofillView] for the [Identity] data partition.
*/
data class Name(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Identity()
/**
* The phone number [AutofillView] for the [Identity] data partition.
*/
data class PhoneNumber(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Identity()
/**
* The postal address [AutofillView] for the [Identity] data partition.
*/
data class PostalAddress(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Identity()
/**
* The postal code [AutofillView] for the [Identity] data partition.
*/
data class PostalCode(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Identity()
}
/**
* A view that corresponds to the login data partition for autofill fields.
*/
sealed class Login : AutofillView() {
/**
* The email address [AutofillView] for the [Login] data partition.
*/
data class EmailAddress(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Login()
/**
* The password [AutofillView] for the [Login] data partition.
*/
data class Password(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Login()
/**
* The username [AutofillView] for the [Login] data partition.
*/
data class Username(
override val autofillId: AutofillId,
override val isFocused: Boolean,
) : Login()
}
}
@@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.autofill.model
import android.view.autofill.AutofillId
/**
* The fulfilled autofill data to be loaded into the a fill response.
*/
data class FilledData(
val filledItems: List<FilledItem>,
val ignoreAutofillIds: List<AutofillId>,
)
@@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.autofill.model
import android.view.autofill.AutofillId
/**
* A fulfilled autofill view. This contains everything required to build the autofill UI
* representing this item.
*/
data class FilledItem(
val autofillId: AutofillId,
)
@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.autofill.parser
import android.app.assist.AssistStructure
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
/**
* A tool for parsing autofill data from the OS into domain models.
*/
interface AutofillParser {
/**
* Parse the useful information from [assistStructure] into an [AutofillRequest].
*/
fun parse(assistStructure: AssistStructure): AutofillRequest
}
@@ -0,0 +1,108 @@
package com.x8bit.bitwarden.data.autofill.parser
import android.app.assist.AssistStructure
import android.view.autofill.AutofillId
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillView
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
/**
* The default [AutofillParser] implementation for the app. This is a tool for parsing autofill data
* from the OS into domain models.
*/
class AutofillParserImpl : AutofillParser {
override fun parse(assistStructure: AssistStructure): AutofillRequest {
// Parse the `assistStructure` into internal models.
val traversalData = assistStructure.traverse()
// Flatten the autofill views for processing.
val autofillViews = traversalData
.map { it.autofillViews }
.flatten()
// Find the focused view (or indicate there is no fulfillment to be performed.)
val focusedView = autofillViews
.firstOrNull { it.isFocused }
?: return AutofillRequest.Unfillable
// Choose the first focused partition of data for fulfillment.
val partition = when (focusedView) {
is AutofillView.Card -> {
AutofillPartition.Card(
views = autofillViews.filterIsInstance<AutofillView.Card>(),
)
}
is AutofillView.Identity -> {
AutofillPartition.Identity(
views = autofillViews.filterIsInstance<AutofillView.Identity>(),
)
}
is AutofillView.Login -> {
AutofillPartition.Login(
views = autofillViews.filterIsInstance<AutofillView.Login>(),
)
}
}
// Flatten the ignorable autofill ids.
val ignoreAutofillIds = traversalData
.map { it.ignoreAutofillIds }
.flatten()
return AutofillRequest.Fillable(
ignoreAutofillIds = ignoreAutofillIds,
partition = partition,
)
}
}
/**
* Traverse the [AssistStructure] and convert it into a list of [ViewNodeTraversalData]s.
*/
private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
(0 until windowNodeCount)
.map { getWindowNodeAt(it) }
.mapNotNull { windowNode -> windowNode.rootViewNode?.traverse() }
/**
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
* data into [ViewNodeTraversalData].
*/
private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
// Set up mutable lists for collecting valid AutofillViews and ignorable view ids.
val mutableAutofillViewList: MutableList<AutofillView> = mutableListOf()
val mutableIgnoreAutofillIdList: MutableList<AutofillId> = mutableListOf()
// Try converting this `ViewNode` into an `AutofillView`. If a valid instance is returned, add
// it to the list. Otherwise, ignore the `AutofillId` associated with this `ViewNode`.
toAutofillView()
?.run(mutableAutofillViewList::add)
?: autofillId?.run(mutableIgnoreAutofillIdList::add)
// Recursively traverse all of this view node's children.
for (i in 0 until childCount) {
// Extract the traversal data from each child view node and add it to the lists.
getChildAt(i)
.traverse()
.let { viewNodeTraversalData ->
viewNodeTraversalData.autofillViews.forEach(mutableAutofillViewList::add)
viewNodeTraversalData.ignoreAutofillIds.forEach(mutableIgnoreAutofillIdList::add)
}
}
// Build a new traversal data structure with this view node's data, and that of all of its
// descendant's.
return ViewNodeTraversalData(
autofillViews = mutableAutofillViewList,
ignoreAutofillIds = mutableIgnoreAutofillIdList,
)
}
/**
* A convenience data structure for view node traversal.
*/
private data class ViewNodeTraversalData(
val autofillViews: List<AutofillView>,
val ignoreAutofillIds: List<AutofillId>,
)
@@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.autofill.processor
import android.os.CancellationSignal
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
/**
* A class to handle autofill request processing. This includes save and fill requests.
*/
interface AutofillProcessor {
/**
* Process the autofill [FillRequest] and invoke the [fillCallback] with the result.
*
* @param autofillAppInfo app data that is required for the autofill [request] processing.
* @param fillCallback the callback to invoke when the [request] has been processed.
* @param request the request data from the OS that contains data about the autofill hierarchy.
*/
fun processFillRequest(
autofillAppInfo: AutofillAppInfo,
cancellationSignal: CancellationSignal,
fillCallback: FillCallback,
request: FillRequest,
)
}
@@ -0,0 +1,92 @@
package com.x8bit.bitwarden.data.autofill.processor
import android.app.assist.AssistStructure
import android.os.CancellationSignal
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
* The default implementation of [AutofillProcessor]. Its purpose is to handle autofill related
* processing.
*/
class AutofillProcessorImpl(
dispatcherManager: DispatcherManager,
private val filledDataBuilder: FilledDataBuilder,
private val fillResponseBuilder: FillResponseBuilder,
private val parser: AutofillParser,
) : AutofillProcessor {
/**
* The coroutine scope for launching asynchronous operations.
*/
private val scope: CoroutineScope = CoroutineScope(dispatcherManager.unconfined)
override fun processFillRequest(
autofillAppInfo: AutofillAppInfo,
cancellationSignal: CancellationSignal,
fillCallback: FillCallback,
request: FillRequest,
) {
// Attempt to get the most recent autofill context.
val assistStructure = request
.fillContexts
.lastOrNull()
?.structure
?: run {
// There is no data for us to process.
fillCallback.onSuccess(null)
return
}
// Set the listener so that any long running work is cancelled when it is no longer needed.
cancellationSignal.setOnCancelListener { scope.cancel() }
// Process the OS data and handle invoking the callback with the result.
assistStructure.process(
autofillAppInfo = autofillAppInfo,
fillCallback = fillCallback,
)
}
/**
* Process the [AssistStructure] and invoke the [FillCallback] with the response.
*/
private fun AssistStructure.process(
autofillAppInfo: AutofillAppInfo,
fillCallback: FillCallback,
) {
scope.launch {
// Parse the OS data into an [AutofillRequest] for easier processing.
val fillResponse = when (val autofillRequest = parser.parse(this@process)) {
is AutofillRequest.Fillable -> {
// Fulfill the [autofillRequest].
val filledData = filledDataBuilder.build(
autofillRequest = autofillRequest,
)
// Load the [filledData] into a [FillResponse].
fillResponseBuilder.build(
autofillAppInfo = autofillAppInfo,
filledData = filledData,
)
}
AutofillRequest.Unfillable -> {
// If we are unable to fulfill the request, we should invoke the callback
// with null. This effectively disables autofill for this view set and
// allows the [AutofillService] to be unbound.
null
}
}
fillCallback.onSuccess(fillResponse)
}
}
}
@@ -0,0 +1,74 @@
package com.x8bit.bitwarden.data.autofill.util
import android.annotation.SuppressLint
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.Field
import android.service.autofill.Presentations
import android.view.autofill.AutofillId
import android.widget.RemoteViews
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.FilledItem
/**
* Apply this [FilledItem] to the dataset being built by [datasetBuilder] in the form of an
* overlay presentation.
*/
fun FilledItem.applyOverlayToDataset(
appInfo: AutofillAppInfo,
datasetBuilder: Dataset.Builder,
remoteViews: RemoteViews,
) {
if (appInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
setOverlay(
autoFillId = autofillId,
datasetBuilder = datasetBuilder,
remoteViews = remoteViews,
)
} else {
setOverlayPreTiramisu(
autoFillId = autofillId,
datasetBuilder = datasetBuilder,
remoteViews = remoteViews,
)
}
}
/**
* Set up an overlay presentation in the [datasetBuilder] for Android devices running on API
* Tiramisu or greater.
*/
@SuppressLint("NewApi")
private fun setOverlay(
autoFillId: AutofillId,
datasetBuilder: Dataset.Builder,
remoteViews: RemoteViews,
) {
val presentation = Presentations.Builder()
.setDialogPresentation(remoteViews)
.build()
datasetBuilder.setField(
autoFillId,
Field.Builder()
.setPresentations(presentation)
.build(),
)
}
/**
* Set up an overlay presentation in the [datasetBuilder] for Android devices running on APIs that
* predate Tiramisu.
*/
@Suppress("Deprecation")
private fun setOverlayPreTiramisu(
autoFillId: AutofillId,
datasetBuilder: Dataset.Builder,
remoteViews: RemoteViews,
) {
datasetBuilder.setValue(
autoFillId,
null,
remoteViews,
)
}
@@ -0,0 +1,131 @@
package com.x8bit.bitwarden.data.autofill.util
import android.app.assist.AssistStructure
import android.view.View
import android.view.autofill.AutofillId
import com.x8bit.bitwarden.data.autofill.model.AutofillView
/**
* Attempt to convert this [AssistStructure.ViewNode] into an [AutofillView]. If the view node
* doesn't contain a valid autofillId, it isn't an a view setup for autofill, so we return null. If
* it is has an autofillHint that we do not support, we also return null.
*/
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? = autofillId
// We only care about nodes with a valid `AutofillId`.
?.let { nonNullAutofillId ->
autofillHints
?.firstOrNull { SUPPORTED_HINTS.contains(it) }
?.let { supportedHint ->
buildAutofillView(
autofillId = nonNullAutofillId,
isFocused = isFocused,
hint = supportedHint,
)
}
}
/**
* Convert the data into an [AutofillView] if the [hint] is supported.
*/
@Suppress("LongMethod")
private fun buildAutofillView(
autofillId: AutofillId,
isFocused: Boolean,
hint: String,
): AutofillView? = when (hint) {
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
AutofillView.Card.ExpirationMonth(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR -> {
AutofillView.Card.ExpirationYear(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER -> {
AutofillView.Card.Number(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE -> {
AutofillView.Card.SecurityCode(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_EMAIL_ADDRESS -> {
AutofillView.Login.EmailAddress(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_NAME -> {
AutofillView.Identity.Name(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_PASSWORD -> {
AutofillView.Login.Password(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_PHONE -> {
AutofillView.Identity.PhoneNumber(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_POSTAL_ADDRESS -> {
AutofillView.Identity.PostalAddress(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_POSTAL_CODE -> {
AutofillView.Identity.PostalCode(
autofillId = autofillId,
isFocused = isFocused,
)
}
View.AUTOFILL_HINT_USERNAME -> {
AutofillView.Login.Username(
autofillId = autofillId,
isFocused = isFocused,
)
}
else -> null
}
/**
* All of the supported autofill hints for the app.
*/
private val SUPPORTED_HINTS: List<String> = listOf(
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
View.AUTOFILL_HINT_CREDIT_CARD_NUMBER,
View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
View.AUTOFILL_HINT_EMAIL_ADDRESS,
View.AUTOFILL_HINT_NAME,
View.AUTOFILL_HINT_PASSWORD,
View.AUTOFILL_HINT_PHONE,
View.AUTOFILL_HINT_POSTAL_ADDRESS,
View.AUTOFILL_HINT_POSTAL_CODE,
View.AUTOFILL_HINT_USERNAME,
)
@@ -0,0 +1,23 @@
package com.x8bit.bitwarden.ui.autofill
import android.content.Context
import android.widget.RemoteViews
import com.x8bit.bitwarden.R
/**
* Build [RemoteViews] for representing an autofill suggestion.
*/
fun buildAutofillRemoteViews(
context: Context,
packageName: String,
): RemoteViews =
RemoteViews(
packageName,
R.layout.autofill_remote_view,
)
.apply {
setTextViewText(
R.id.text,
context.resources.getText(R.string.app_name),
)
}