aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/src/main/java/app/passwordstore/injection/AutofillResponseBuilderModule.kt26
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/Api26AutofillResponseBuilder.kt189
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/Api30AutofillResponseBuilder.kt32
-rw-r--r--app/src/main/java/app/passwordstore/util/autofill/AutofillResponseBuilder.kt182
-rw-r--r--app/src/main/java/app/passwordstore/util/services/OreoAutofillService.kt10
5 files changed, 236 insertions, 203 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) {