mirror of
https://github.com/bitwarden/android.git
synced 2026-07-29 01:35:15 -05:00
BIT-1314: Add autofill node traversal with basic UI fulfillment (#541)
This commit is contained in:
committed by
Álison Fernandes
parent
35ef4e4252
commit
cea26f5e32
@@ -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?
|
||||
}
|
||||
+47
@@ -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,
|
||||
)
|
||||
}
|
||||
+92
@@ -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),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user