diff options
6 files changed, 237 insertions, 204 deletions
diff --git a/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt b/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt new file mode 100644 index 00000000..75b4a525 --- /dev/null +++ b/app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt @@ -0,0 +1,26 @@ +package app.passwordstore.injection + +import android.os.Build +import app.passwordstore.util.autofill.Api26AutofillResponseBuilder +import app.passwordstore.util.autofill.Api30AutofillResponseBuilder +import app.passwordstore.util.autofill.AutofillResponseBuilder +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object AutofillResponseBuilderModule { + + @Provides + @Reusable + fun provideAutofillResponseBuilder(): AutofillResponseBuilder.Factory { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Api30AutofillResponseBuilder.Factory + } else { + Api26AutofillResponseBuilder.Factory + } + } +} diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt new file mode 100644 index 00000000..77322116 --- /dev/null +++ b/app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt @@ -0,0 +1,189 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package app.passwordstore.util.autofill + +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.FillRequest +import android.service.autofill.FillResponse +import android.service.autofill.SaveInfo +import app.passwordstore.autofill.oreo.ui.AutofillSmsActivity +import app.passwordstore.ui.autofill.AutofillDecryptActivity +import app.passwordstore.ui.autofill.AutofillFilterView +import app.passwordstore.ui.autofill.AutofillPublisherChangedActivity +import app.passwordstore.ui.autofill.AutofillSaveActivity +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 java.io.File +import logcat.LogPriority.ERROR +import logcat.asLog +import logcat.logcat + +class Api26AutofillResponseBuilder +private constructor( + form: FillableForm, +) : AutofillResponseBuilder { + + object Factory : AutofillResponseBuilder.Factory { + override fun create( + form: FillableForm, + ): AutofillResponseBuilder = Api26AutofillResponseBuilder(form) + } + + 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 + + @Suppress("DEPRECATION") + private fun makeIntentDataset( + context: Context, + action: AutofillAction, + intentSender: IntentSender, + metadata: DatasetMetadata, + ): Dataset { + return Dataset.Builder(makeRemoteView(context, metadata)).run { + fillWith(scenario, action, credentials = null) + setAuthentication(intentSender) + build() + } + } + + private fun makeMatchDataset(context: Context, file: File): 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) + } + + private fun makeSearchDataset(context: Context): 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) + } + + private fun makeGenerateDataset(context: Context): 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) + } + + private fun makeFillOtpFromSmsDataset(context: Context): 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) + } + + private fun makePublisherChangedDataset( + context: Context, + publisherChangedException: AutofillPublisherChangedException, + ): 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, emptyList()) + val intentSender = + AutofillPublisherChangedActivity.makePublisherChangedIntentSender( + context, + publisherChangedException, + fillResponseAfterReset + ) + return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata) + } + + private fun makePublisherChangedResponse( + context: Context, + publisherChangedException: AutofillPublisherChangedException + ): FillResponse { + return FillResponse.Builder().run { + addDataset(makePublisherChangedDataset(context, publisherChangedException)) + 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() + } + } + + private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? { + var datasetCount = 0 + return FillResponse.Builder().run { + for (file in matchedFiles) { + makeMatchDataset(context, file)?.let { + datasetCount++ + addDataset(it) + } + } + makeGenerateDataset(context)?.let { + datasetCount++ + addDataset(it) + } + makeFillOtpFromSmsDataset(context)?.let { + datasetCount++ + addDataset(it) + } + makeSearchDataset(context)?.let { + datasetCount++ + addDataset(it) + } + if (datasetCount == 0) return null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setHeader( + makeRemoteView( + context, + makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)) + ) + ) + } + makeSaveInfo()?.let { setSaveInfo(it) } + setClientState(clientState) + setIgnoredIds(*ignoredIds.toTypedArray()) + build() + } + } + + /** Creates and returns a suitable [FillResponse] to the Autofill framework. */ + override fun fillCredentials(context: Context, fillRequest: FillRequest, callback: FillCallback) { + AutofillMatcher.getMatchesFor(context, formOrigin) + .fold( + success = { matchedFiles -> callback.onSuccess(makeFillResponse(context, matchedFiles)) }, + failure = { e -> + logcat(ERROR) { e.asLog() } + callback.onSuccess(makePublisherChangedResponse(context, e)) + } + ) + } +} diff --git a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt index 43c2e7b9..49970fa6 100644 --- a/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt +++ b/app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt @@ -10,6 +10,7 @@ import android.content.IntentSender import android.os.Build import android.service.autofill.Dataset import android.service.autofill.FillCallback +import android.service.autofill.FillRequest import android.service.autofill.FillResponse import android.service.autofill.Presentations import android.service.autofill.SaveInfo @@ -25,9 +26,6 @@ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject import java.io.File import logcat.LogPriority.ERROR import logcat.asLog @@ -36,14 +34,14 @@ import logcat.logcat /** Implements [AutofillResponseBuilder]'s methods for API 30 and above */ @RequiresApi(Build.VERSION_CODES.R) class Api30AutofillResponseBuilder -@AssistedInject -constructor( - @Assisted form: FillableForm, -) { +private constructor( + form: FillableForm, +) : AutofillResponseBuilder { - @AssistedFactory - interface Factory { - fun create(form: FillableForm): Api30AutofillResponseBuilder + object Factory : AutofillResponseBuilder.Factory { + override fun create( + form: FillableForm, + ): AutofillResponseBuilder = Api30AutofillResponseBuilder(form) } private val formOrigin = form.formOrigin @@ -260,19 +258,19 @@ constructor( } /** Creates and returns a suitable [FillResponse] to the Autofill framework. */ - fun fillCredentials( - context: Context, - inlineSuggestionsRequest: InlineSuggestionsRequest?, - callback: FillCallback - ) { + override fun fillCredentials(context: Context, fillRequest: FillRequest, callback: FillCallback) { AutofillMatcher.getMatchesFor(context, formOrigin) .fold( success = { matchedFiles -> - callback.onSuccess(makeFillResponse(context, inlineSuggestionsRequest, matchedFiles)) + callback.onSuccess( + makeFillResponse(context, fillRequest.inlineSuggestionsRequest, matchedFiles) + ) }, failure = { e -> logcat(ERROR) { e.asLog() } - callback.onSuccess(makePublisherChangedResponse(context, inlineSuggestionsRequest, e)) + callback.onSuccess( + makePublisherChangedResponse(context, fillRequest.inlineSuggestionsRequest, e) + ) } ) } diff --git a/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt b/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt index 050ded17..762603e1 100644 --- a/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt +++ b/app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt @@ -1,199 +1,27 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ package app.passwordstore.util.autofill import android.content.Context -import android.content.IntentSender import android.os.Build import android.os.Bundle import android.service.autofill.Dataset import android.service.autofill.FillCallback -import android.service.autofill.FillResponse -import android.service.autofill.SaveInfo -import app.passwordstore.autofill.oreo.ui.AutofillSmsActivity -import app.passwordstore.ui.autofill.AutofillDecryptActivity -import app.passwordstore.ui.autofill.AutofillFilterView -import app.passwordstore.ui.autofill.AutofillPublisherChangedActivity -import app.passwordstore.ui.autofill.AutofillSaveActivity +import android.service.autofill.FillRequest import com.github.androidpasswordstore.autofillparser.AutofillAction import com.github.androidpasswordstore.autofillparser.AutofillScenario import com.github.androidpasswordstore.autofillparser.Credentials import com.github.androidpasswordstore.autofillparser.FillableForm import com.github.androidpasswordstore.autofillparser.fillWith -import com.github.michaelbull.result.fold -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import java.io.File -import logcat.LogPriority.ERROR -import logcat.asLog +import logcat.LogPriority import logcat.logcat -class AutofillResponseBuilder -@AssistedInject -constructor( - @Assisted form: FillableForm, -) { +interface AutofillResponseBuilder { + fun fillCredentials(context: Context, fillRequest: FillRequest, callback: FillCallback) - @AssistedFactory interface Factory { fun create(form: FillableForm): AutofillResponseBuilder } - 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 - - @Suppress("DEPRECATION") - private fun makeIntentDataset( - context: Context, - action: AutofillAction, - intentSender: IntentSender, - metadata: DatasetMetadata, - ): Dataset { - return Dataset.Builder(makeRemoteView(context, metadata)).run { - fillWith(scenario, action, credentials = null) - setAuthentication(intentSender) - build() - } - } - - private fun makeMatchDataset(context: Context, file: File): 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) - } - - private fun makeSearchDataset(context: Context): 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) - } - - private fun makeGenerateDataset(context: Context): 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) - } - - private fun makeFillOtpFromSmsDataset(context: Context): 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) - } - - private fun makePublisherChangedDataset( - context: Context, - publisherChangedException: AutofillPublisherChangedException, - ): 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, emptyList()) - val intentSender = - AutofillPublisherChangedActivity.makePublisherChangedIntentSender( - context, - publisherChangedException, - fillResponseAfterReset - ) - return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata) - } - - private fun makePublisherChangedResponse( - context: Context, - publisherChangedException: AutofillPublisherChangedException - ): FillResponse { - return FillResponse.Builder().run { - addDataset(makePublisherChangedDataset(context, publisherChangedException)) - 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() - } - } - - private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? { - var datasetCount = 0 - return FillResponse.Builder().run { - for (file in matchedFiles) { - makeMatchDataset(context, file)?.let { - datasetCount++ - addDataset(it) - } - } - makeGenerateDataset(context)?.let { - datasetCount++ - addDataset(it) - } - makeFillOtpFromSmsDataset(context)?.let { - datasetCount++ - addDataset(it) - } - makeSearchDataset(context)?.let { - datasetCount++ - addDataset(it) - } - if (datasetCount == 0) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - setHeader( - makeRemoteView( - context, - makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)) - ) - ) - } - makeSaveInfo()?.let { setSaveInfo(it) } - setClientState(clientState) - setIgnoredIds(*ignoredIds.toTypedArray()) - build() - } - } - - /** Creates and returns a suitable [FillResponse] to the Autofill framework. */ - fun fillCredentials(context: Context, callback: FillCallback) { - AutofillMatcher.getMatchesFor(context, formOrigin) - .fold( - success = { matchedFiles -> callback.onSuccess(makeFillResponse(context, matchedFiles)) }, - failure = { e -> - logcat(ERROR) { e.asLog() } - callback.onSuccess(makePublisherChangedResponse(context, e)) - } - ) - } - companion object { - fun makeFillInDataset( context: Context, credentials: Credentials, @@ -215,7 +43,7 @@ constructor( } return builder.run { if (scenario != null) fillWith(scenario, action, credentials) - else logcat(ERROR) { "Failed to recover scenario from client state" } + else logcat(LogPriority.ERROR) { "Failed to recover scenario from client state" } build() } } diff --git a/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt b/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt index 6a9cea8d..9e5051e5 100644 --- a/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt +++ b/app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt @@ -16,7 +16,6 @@ import android.service.autofill.SaveRequest import app.passwordstore.BuildConfig import app.passwordstore.R import app.passwordstore.ui.autofill.AutofillSaveActivity -import app.passwordstore.util.autofill.Api30AutofillResponseBuilder import app.passwordstore.util.autofill.AutofillResponseBuilder import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.hasFlag @@ -56,7 +55,6 @@ class OreoAutofillService : AutofillService() { private const val DISABLE_AUTOFILL_DURATION_MS = 1000 * 60 * 60 * 24L } - @Inject lateinit var api30ResponseBuilderFactory: Api30AutofillResponseBuilder.Factory @Inject lateinit var responseBuilderFactory: AutofillResponseBuilder.Factory override fun onCreate() { @@ -100,13 +98,7 @@ class OreoAutofillService : AutofillService() { callback.onSuccess(null) return } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - api30ResponseBuilderFactory - .create(formToFill) - .fillCredentials(this, request.inlineSuggestionsRequest, callback) - } else { - responseBuilderFactory.create(formToFill).fillCredentials(this, callback) - } + responseBuilderFactory.create(formToFill).fillCredentials(this, request, callback) } override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e1d7dbe..986c2b2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.3.0-alpha04" +agp = "8.3.0-alpha05" androidxActivity = "1.8.0-rc01" bouncycastle = "1.76" moshi = "1.15.0" |