diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt index e1b157d5..07a38862 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt @@ -88,8 +88,7 @@ val autofillStrategy = strategy { breakTieOnPair { any { isFocused } } } username(optional = true) { - takeSingle() - breakTieOnSingle { usernameCertainty >= Likely } + takeSingle { usernameCertainty >= Likely } breakTieOnSingle { usernameCertainty >= Certain } breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) } breakTieOnSingle { isFocused } @@ -104,8 +103,7 @@ val autofillStrategy = strategy { breakTieOnSingle { isFocused } } username(optional = true) { - takeSingle() - breakTieOnSingle { usernameCertainty >= Likely } + takeSingle { usernameCertainty >= Likely } breakTieOnSingle { usernameCertainty >= Certain } breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) } breakTieOnSingle { isFocused } @@ -176,4 +174,23 @@ val autofillStrategy = strategy { breakTieOnSingle { hasAutocompleteHintUsername } } } + + // Match any focused password field with optional username field on manual request. + rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) { + genericPassword { + takeSingle { isFocused } + } + username(optional = true) { + takeSingle { alreadyMatched -> + usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) + } + } + } + + // Match any focused username field on manual request. + rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) { + username { + takeSingle { isFocused } + } + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt index 32ffaa2a..eea5c337 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt @@ -149,6 +149,7 @@ private class PairOfFieldsMatcher( class AutofillRule private constructor( private val matchers: List, private val applyInSingleOriginMode: Boolean, + private val applyOnManualRequestOnly: Boolean, private val name: String ) { @@ -164,7 +165,10 @@ class AutofillRule private constructor( } @AutofillDsl - class Builder(private val applyInSingleOriginMode: Boolean) { + class Builder( + private val applyInSingleOriginMode: Boolean, + private val applyOnManualRequestOnly: Boolean + ) { companion object { private var ruleId = 1 } @@ -231,20 +235,25 @@ class AutofillRule private constructor( require(matchers.none { it.matchHidden }) { "Rules with applyInSingleOriginMode set to true must not fill into hidden fields" } } return AutofillRule( - matchers, applyInSingleOriginMode, name ?: "Rule #$ruleId" + matchers, applyInSingleOriginMode, applyOnManualRequestOnly, name ?: "Rule #$ruleId" ).also { ruleId++ } } } - fun apply( + fun match( allPassword: List, allUsername: List, - singleOriginMode: Boolean + singleOriginMode: Boolean, + isManualRequest: Boolean ): AutofillScenario? { if (singleOriginMode && !applyInSingleOriginMode) { d { "$name: Skipped in single origin mode" } return null } + if (!isManualRequest && applyOnManualRequestOnly) { + d { "$name: Skipped since not a manual request" } + return null + } d { "$name: Applying..." } val scenarioBuilder = AutofillScenario.Builder() val alreadyMatched = mutableListOf() @@ -299,15 +308,25 @@ class AutofillStrategy private constructor(private val rules: List fun rule( applyInSingleOriginMode: Boolean = false, + applyOnManualRequestOnly: Boolean = false, block: AutofillRule.Builder.() -> Unit ) { - rules.add(AutofillRule.Builder(applyInSingleOriginMode).apply(block).build()) + rules.add( + AutofillRule.Builder( + applyInSingleOriginMode = applyInSingleOriginMode, + applyOnManualRequestOnly = applyOnManualRequestOnly + ).apply(block).build() + ) } fun build() = AutofillStrategy(rules) } - fun apply(fields: List, multiOriginSupport: Boolean): AutofillScenario? { + fun match( + fields: List, + singleOriginMode: Boolean, + isManualRequest: Boolean + ): AutofillScenario? { val possiblePasswordFields = fields.filter { it.passwordCertainty >= CertaintyLevel.Possible } d { "Possible password fields: ${possiblePasswordFields.size}" } @@ -317,7 +336,12 @@ class AutofillStrategy private constructor(private val rules: List // Return the result of the first rule that matches d { "Rules: ${rules.size}" } for (rule in rules) { - return rule.apply(possiblePasswordFields, possibleUsernameFields, multiOriginSupport) + return rule.match( + possiblePasswordFields, + possibleUsernameFields, + singleOriginMode = singleOriginMode, + isManualRequest = isManualRequest + ) ?: continue } return null diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt index 308391ec..ab21f00c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt @@ -69,7 +69,7 @@ sealed class FormOrigin(open val identifier: String) { * Manages the detection of fields to fill in an [AssistStructure] and determines the [FormOrigin]. */ @RequiresApi(Build.VERSION_CODES.O) -private class Form(context: Context, structure: AssistStructure) { +private class Form(context: Context, structure: AssistStructure, isManualRequest: Boolean) { companion object { private val SUPPORTED_SCHEMES = listOf("http", "https") @@ -98,7 +98,7 @@ private class Form(context: Context, structure: AssistStructure) { parseStructure(structure) } - val scenario = detectFieldsToFill() + val scenario = detectFieldsToFill(isManualRequest) val formOrigin = determineFormOrigin(context) init { @@ -133,7 +133,11 @@ private class Form(context: Context, structure: AssistStructure) { } } - private fun detectFieldsToFill() = autofillStrategy.apply(relevantFields, singleOriginMode) + private fun detectFieldsToFill(isManualRequest: Boolean) = autofillStrategy.match( + relevantFields, + singleOriginMode = singleOriginMode, + isManualRequest = isManualRequest + ) private fun trackOrigin(node: AssistStructure.ViewNode) { if (!isTrustedBrowser) return @@ -219,8 +223,12 @@ class FillableForm private constructor( /** * Returns a [FillableForm] if a login form could be detected in [structure]. */ - fun parseAssistStructure(context: Context, structure: AssistStructure): FillableForm? { - val form = Form(context, structure) + fun parseAssistStructure( + context: Context, + structure: AssistStructure, + isManualRequest: Boolean + ): FillableForm? { + val form = Form(context, structure, isManualRequest) if (form.formOrigin == null || form.scenario == null) return null return FillableForm( form.formOrigin, diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt index 00fa3aa4..350d187b 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt @@ -18,6 +18,7 @@ import com.github.ajalt.timberkt.e import com.zeapo.pwdstore.BuildConfig import com.zeapo.pwdstore.R import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity +import com.zeapo.pwdstore.utils.hasFlag @RequiresApi(Build.VERSION_CODES.O) class OreoAutofillService : AutofillService() { @@ -62,7 +63,10 @@ class OreoAutofillService : AutofillService() { } return } - val formToFill = FillableForm.parseAssistStructure(this, structure) ?: run { + val formToFill = FillableForm.parseAssistStructure( + this, structure, + isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST + ) ?: run { d { "Form cannot be filled" } callback.onSuccess(null) return diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index 68f8df27..f7a637c3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -10,6 +10,10 @@ import android.util.TypedValue import android.view.autofill.AutofillManager import androidx.annotation.RequiresApi +infix fun Int.hasFlag(flag: Int): Boolean { + return this and flag == flag +} + fun String.splitLines(): Array { return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() }