Resolve Autofill breakage below API 30 (#1187)

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2020-11-05 01:50:32 +05:30 committed by GitHub
parent 2845e01cd4
commit 354687e3a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 214 additions and 47 deletions

View file

@ -0,0 +1,189 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore.autofill.oreo
import android.content.Context
import android.content.IntentSender
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.FillCallback
import android.service.autofill.FillResponse
import android.service.autofill.SaveInfo
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.e
import com.github.androidpasswordstore.autofillparser.AutofillAction
import com.github.androidpasswordstore.autofillparser.FillableForm
import com.github.androidpasswordstore.autofillparser.fillWith
import com.github.michaelbull.result.fold
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillDecryptActivity
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSmsActivity
import java.io.File
/**
* Implements [AutofillResponseBuilder]'s methods for API 30 and above
*/
@RequiresApi(Build.VERSION_CODES.R)
class Api30AutofillResponseBuilder(form: FillableForm) {
private val formOrigin = form.formOrigin
private val scenario = form.scenario
private val ignoredIds = form.ignoredIds
private val saveFlags = form.saveFlags
private val clientState = form.toClientState()
// We do not offer save when the only relevant field is a username field or there is no field.
private val scenarioSupportsSave = scenario.hasPasswordFieldsToSave
private val canBeSaved = saveFlags != null && scenarioSupportsSave
private fun makeIntentDataset(
context: Context,
action: AutofillAction,
intentSender: IntentSender,
metadata: DatasetMetadata,
imeSpec: InlinePresentationSpec?,
): Dataset {
return Dataset.Builder(makeRemoteView(context, metadata)).run {
fillWith(scenario, action, credentials = null)
setAuthentication(intentSender)
if (imeSpec != null) {
val inlinePresentation = makeInlinePresentation(context, imeSpec, metadata)
if (inlinePresentation != null) {
setInlinePresentation(inlinePresentation)
}
}
build()
}
}
private fun makeMatchDataset(context: Context, file: File, imeSpec: InlinePresentationSpec?): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
val metadata = makeFillMatchMetadata(context, file)
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec)
}
private fun makeSearchDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Search)) return null
val metadata = makeSearchAndFillMetadata(context)
val intentSender =
AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin)
return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata, imeSpec)
}
private fun makeGenerateDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Generate)) return null
val metadata = makeGenerateAndFillMetadata(context)
val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin)
return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec)
}
private fun makeFillOtpFromSmsDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null
if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
val metadata = makeFillOtpFromSmsMetadata(context)
val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata, imeSpec)
}
private fun makePublisherChangedDataset(
context: Context,
publisherChangedException: AutofillPublisherChangedException,
imeSpec: InlinePresentationSpec?
): Dataset {
val metadata = makeWarningMetadata(context)
// If the user decides to trust the new publisher, they can choose reset the list of
// matches. In this case we need to immediately show a new `FillResponse` as if the app were
// autofilled for the first time. This `FillResponse` needs to be returned as a result from
// `AutofillPublisherChangedActivity`, which is why we create and pass it on here.
val fillResponseAfterReset = makeFillResponse(context, null, emptyList())
val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender(
context, publisherChangedException, fillResponseAfterReset
)
return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec)
}
private fun makePublisherChangedResponse(
context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
publisherChangedException: AutofillPublisherChangedException
): FillResponse {
val imeSpec = inlineSuggestionsRequest?.inlinePresentationSpecs?.firstOrNull()
return FillResponse.Builder().run {
addDataset(makePublisherChangedDataset(context, publisherChangedException, imeSpec))
setIgnoredIds(*ignoredIds.toTypedArray())
build()
}
}
private fun makeFillResponse(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, matchedFiles: List<File>): FillResponse? {
var datasetCount = 0
val imeSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()
return FillResponse.Builder().run {
for (file in matchedFiles) {
makeMatchDataset(context, file, imeSpecs.getOrNull(datasetCount))?.let {
datasetCount++
addDataset(it)
}
}
makeSearchDataset(context, imeSpecs.getOrNull(datasetCount))?.let {
datasetCount++
addDataset(it)
}
makeGenerateDataset(context, imeSpecs.getOrNull(datasetCount))?.let {
datasetCount++
addDataset(it)
}
makeFillOtpFromSmsDataset(context, imeSpecs.getOrNull(datasetCount))?.let {
datasetCount++
addDataset(it)
}
if (datasetCount == 0) return null
setHeader(makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))))
makeSaveInfo()?.let { setSaveInfo(it) }
setClientState(clientState)
setIgnoredIds(*ignoredIds.toTypedArray())
build()
}
}
// TODO: Support multi-step authentication flows in apps via FLAG_DELAY_SAVE
// See: https://developer.android.com/reference/android/service/autofill/SaveInfo#FLAG_DELAY_SAVE
private fun makeSaveInfo(): SaveInfo? {
if (!canBeSaved) return null
check(saveFlags != null)
val idsToSave = scenario.fieldsToSave.toTypedArray()
if (idsToSave.isEmpty()) return null
var saveDataTypes = SaveInfo.SAVE_DATA_TYPE_PASSWORD
if (scenario.hasUsername) {
saveDataTypes = saveDataTypes or SaveInfo.SAVE_DATA_TYPE_USERNAME
}
return SaveInfo.Builder(saveDataTypes, idsToSave).run {
setFlags(saveFlags)
build()
}
}
/**
* Creates and returns a suitable [FillResponse] to the Autofill framework.
*/
fun fillCredentials(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) {
AutofillMatcher.getMatchesFor(context, formOrigin).fold(
success = { matchedFiles ->
callback.onSuccess(makeFillResponse(context, inlineSuggestionsRequest, matchedFiles))
},
failure = { e ->
e(e)
callback.onSuccess(makePublisherChangedResponse(context, inlineSuggestionsRequest, e))
}
)
}
}

View file

@ -12,8 +12,6 @@ import android.service.autofill.Dataset
import android.service.autofill.FillCallback import android.service.autofill.FillCallback
import android.service.autofill.FillResponse import android.service.autofill.FillResponse
import android.service.autofill.SaveInfo import android.service.autofill.SaveInfo
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.androidpasswordstore.autofillparser.AutofillAction import com.github.androidpasswordstore.autofillparser.AutofillAction
@ -46,80 +44,67 @@ class AutofillResponseBuilder(form: FillableForm) {
action: AutofillAction, action: AutofillAction,
intentSender: IntentSender, intentSender: IntentSender,
metadata: DatasetMetadata, metadata: DatasetMetadata,
imeSpec: InlinePresentationSpec?,
): Dataset { ): Dataset {
return Dataset.Builder(makeRemoteView(context, metadata)).run { return Dataset.Builder(makeRemoteView(context, metadata)).run {
fillWith(scenario, action, credentials = null) fillWith(scenario, action, credentials = null)
setAuthentication(intentSender) setAuthentication(intentSender)
if (imeSpec != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation = makeInlinePresentation(context, imeSpec, metadata)
if (inlinePresentation != null) {
setInlinePresentation(inlinePresentation)
}
}
build() build()
} }
} }
private fun makeMatchDataset(context: Context, file: File, imeSpec: InlinePresentationSpec?): Dataset? { private fun makeMatchDataset(context: Context, file: File): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
val metadata = makeFillMatchMetadata(context, file) val metadata = makeFillMatchMetadata(context, file)
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec) return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata)
} }
private fun makeSearchDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
private fun makeSearchDataset(context: Context): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Search)) return null if (!scenario.hasFieldsToFillOn(AutofillAction.Search)) return null
val metadata = makeSearchAndFillMetadata(context) val metadata = makeSearchAndFillMetadata(context)
val intentSender = val intentSender =
AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin) AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin)
return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata, imeSpec) return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata)
} }
private fun makeGenerateDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? { private fun makeGenerateDataset(context: Context): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Generate)) return null if (!scenario.hasFieldsToFillOn(AutofillAction.Generate)) return null
val metadata = makeGenerateAndFillMetadata(context) val metadata = makeGenerateAndFillMetadata(context)
val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin) val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin)
return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec) return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata)
} }
private fun makeFillOtpFromSmsDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? { private fun makeFillOtpFromSmsDataset(context: Context): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null
if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
val metadata = makeFillOtpFromSmsMetadata(context) val metadata = makeFillOtpFromSmsMetadata(context)
val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context) val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata, imeSpec) return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata)
} }
private fun makePublisherChangedDataset( private fun makePublisherChangedDataset(
context: Context, context: Context,
publisherChangedException: AutofillPublisherChangedException, publisherChangedException: AutofillPublisherChangedException,
imeSpec: InlinePresentationSpec?
): Dataset { ): Dataset {
val metadata = makeWarningMetadata(context) val metadata = makeWarningMetadata(context)
// If the user decides to trust the new publisher, they can choose reset the list of // If the user decides to trust the new publisher, they can choose reset the list of
// matches. In this case we need to immediately show a new `FillResponse` as if the app were // matches. In this case we need to immediately show a new `FillResponse` as if the app were
// autofilled for the first time. This `FillResponse` needs to be returned as a result from // autofilled for the first time. This `FillResponse` needs to be returned as a result from
// `AutofillPublisherChangedActivity`, which is why we create and pass it on here. // `AutofillPublisherChangedActivity`, which is why we create and pass it on here.
val fillResponseAfterReset = makeFillResponse(context, null, emptyList()) val fillResponseAfterReset = makeFillResponse(context, emptyList())
val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender( val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender(
context, publisherChangedException, fillResponseAfterReset context, publisherChangedException, fillResponseAfterReset
) )
return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec) return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata)
} }
private fun makePublisherChangedResponse( private fun makePublisherChangedResponse(
context: Context, context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
publisherChangedException: AutofillPublisherChangedException publisherChangedException: AutofillPublisherChangedException
): FillResponse { ): FillResponse {
val imeSpec = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.inlinePresentationSpecs?.firstOrNull()
} else {
null
}
return FillResponse.Builder().run { return FillResponse.Builder().run {
addDataset(makePublisherChangedDataset(context, publisherChangedException, imeSpec)) addDataset(makePublisherChangedDataset(context, publisherChangedException))
setIgnoredIds(*ignoredIds.toTypedArray()) setIgnoredIds(*ignoredIds.toTypedArray())
build() build()
} }
@ -142,29 +127,24 @@ class AutofillResponseBuilder(form: FillableForm) {
} }
} }
private fun makeFillResponse(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, matchedFiles: List<File>): FillResponse? { private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? {
var datasetCount = 0 var datasetCount = 0
val imeSpecs: List<InlinePresentationSpec> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.inlinePresentationSpecs
} else {
null
} ?: emptyList()
return FillResponse.Builder().run { return FillResponse.Builder().run {
for (file in matchedFiles) { for (file in matchedFiles) {
makeMatchDataset(context, file, imeSpecs.getOrNull(datasetCount))?.let { makeMatchDataset(context, file)?.let {
datasetCount++ datasetCount++
addDataset(it) addDataset(it)
} }
} }
makeSearchDataset(context, imeSpecs.getOrNull(datasetCount))?.let { makeSearchDataset(context)?.let {
datasetCount++ datasetCount++
addDataset(it) addDataset(it)
} }
makeGenerateDataset(context, imeSpecs.getOrNull(datasetCount))?.let { makeGenerateDataset(context)?.let {
datasetCount++ datasetCount++
addDataset(it) addDataset(it)
} }
makeFillOtpFromSmsDataset(context, imeSpecs.getOrNull(datasetCount))?.let { makeFillOtpFromSmsDataset(context)?.let {
datasetCount++ datasetCount++
addDataset(it) addDataset(it)
} }
@ -182,14 +162,14 @@ class AutofillResponseBuilder(form: FillableForm) {
/** /**
* Creates and returns a suitable [FillResponse] to the Autofill framework. * Creates and returns a suitable [FillResponse] to the Autofill framework.
*/ */
fun fillCredentials(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { fun fillCredentials(context: Context, callback: FillCallback) {
AutofillMatcher.getMatchesFor(context, formOrigin).fold( AutofillMatcher.getMatchesFor(context, formOrigin).fold(
success = { matchedFiles -> success = { matchedFiles ->
callback.onSuccess(makeFillResponse(context, inlineSuggestionsRequest, matchedFiles)) callback.onSuccess(makeFillResponse(context, matchedFiles))
}, },
failure = { e -> failure = { e ->
e(e) e(e)
callback.onSuccess(makePublisherChangedResponse(context, inlineSuggestionsRequest, e)) callback.onSuccess(makePublisherChangedResponse(context, e))
} }
) )
} }

View file

@ -86,13 +86,11 @@ class OreoAutofillService : AutofillService() {
callback.onSuccess(null) callback.onSuccess(null)
return return
} }
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Api30AutofillResponseBuilder(formToFill).fillCredentials(this, request.inlineSuggestionsRequest, callback)
request.inlineSuggestionsRequest } else {
} else { AutofillResponseBuilder(formToFill).fillCredentials(this, callback)
null }
}
AutofillResponseBuilder(formToFill).fillCredentials(this, inlineSuggestionsRequest, callback)
} }
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {