Add more lenient rules that apply only on manual request (#662)

Add rules that match password/username fields even if no heuristic matches, but
only when the user explicitly requests Autofill. Since there is now a generic
way to always trigger Autofill (at least in apps), other rules no longer need
to match fields that fail the heuristics.

Along the way, the apply functions in AutofillStrategy.kt are renamed to match
in order to not conflict with the Kotlin apply() extension function.
Furthermore, named parameters are used more widely now to pass around Booleans.
This commit is contained in:
Fabian Henneke 2020-03-25 17:53:58 +01:00 committed by GitHub
parent fde8137b62
commit 5164b6951b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 74 additions and 17 deletions

View file

@ -88,8 +88,7 @@ val autofillStrategy = strategy {
breakTieOnPair { any { isFocused } } breakTieOnPair { any { isFocused } }
} }
username(optional = true) { username(optional = true) {
takeSingle() takeSingle { usernameCertainty >= Likely }
breakTieOnSingle { usernameCertainty >= Likely }
breakTieOnSingle { usernameCertainty >= Certain } breakTieOnSingle { usernameCertainty >= Certain }
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) } breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
breakTieOnSingle { isFocused } breakTieOnSingle { isFocused }
@ -104,8 +103,7 @@ val autofillStrategy = strategy {
breakTieOnSingle { isFocused } breakTieOnSingle { isFocused }
} }
username(optional = true) { username(optional = true) {
takeSingle() takeSingle { usernameCertainty >= Likely }
breakTieOnSingle { usernameCertainty >= Likely }
breakTieOnSingle { usernameCertainty >= Certain } breakTieOnSingle { usernameCertainty >= Certain }
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) } breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
breakTieOnSingle { isFocused } breakTieOnSingle { isFocused }
@ -176,4 +174,23 @@ val autofillStrategy = strategy {
breakTieOnSingle { hasAutocompleteHintUsername } 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 }
}
}
} }

View file

@ -149,6 +149,7 @@ private class PairOfFieldsMatcher(
class AutofillRule private constructor( class AutofillRule private constructor(
private val matchers: List<AutofillRuleMatcher>, private val matchers: List<AutofillRuleMatcher>,
private val applyInSingleOriginMode: Boolean, private val applyInSingleOriginMode: Boolean,
private val applyOnManualRequestOnly: Boolean,
private val name: String private val name: String
) { ) {
@ -164,7 +165,10 @@ class AutofillRule private constructor(
} }
@AutofillDsl @AutofillDsl
class Builder(private val applyInSingleOriginMode: Boolean) { class Builder(
private val applyInSingleOriginMode: Boolean,
private val applyOnManualRequestOnly: Boolean
) {
companion object { companion object {
private var ruleId = 1 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" } require(matchers.none { it.matchHidden }) { "Rules with applyInSingleOriginMode set to true must not fill into hidden fields" }
} }
return AutofillRule( return AutofillRule(
matchers, applyInSingleOriginMode, name ?: "Rule #$ruleId" matchers, applyInSingleOriginMode, applyOnManualRequestOnly, name ?: "Rule #$ruleId"
).also { ruleId++ } ).also { ruleId++ }
} }
} }
fun apply( fun match(
allPassword: List<FormField>, allPassword: List<FormField>,
allUsername: List<FormField>, allUsername: List<FormField>,
singleOriginMode: Boolean singleOriginMode: Boolean,
isManualRequest: Boolean
): AutofillScenario<FormField>? { ): AutofillScenario<FormField>? {
if (singleOriginMode && !applyInSingleOriginMode) { if (singleOriginMode && !applyInSingleOriginMode) {
d { "$name: Skipped in single origin mode" } d { "$name: Skipped in single origin mode" }
return null return null
} }
if (!isManualRequest && applyOnManualRequestOnly) {
d { "$name: Skipped since not a manual request" }
return null
}
d { "$name: Applying..." } d { "$name: Applying..." }
val scenarioBuilder = AutofillScenario.Builder<FormField>() val scenarioBuilder = AutofillScenario.Builder<FormField>()
val alreadyMatched = mutableListOf<FormField>() val alreadyMatched = mutableListOf<FormField>()
@ -299,15 +308,25 @@ class AutofillStrategy private constructor(private val rules: List<AutofillRule>
fun rule( fun rule(
applyInSingleOriginMode: Boolean = false, applyInSingleOriginMode: Boolean = false,
applyOnManualRequestOnly: Boolean = false,
block: AutofillRule.Builder.() -> Unit 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 build() = AutofillStrategy(rules)
} }
fun apply(fields: List<FormField>, multiOriginSupport: Boolean): AutofillScenario<FormField>? { fun match(
fields: List<FormField>,
singleOriginMode: Boolean,
isManualRequest: Boolean
): AutofillScenario<FormField>? {
val possiblePasswordFields = val possiblePasswordFields =
fields.filter { it.passwordCertainty >= CertaintyLevel.Possible } fields.filter { it.passwordCertainty >= CertaintyLevel.Possible }
d { "Possible password fields: ${possiblePasswordFields.size}" } d { "Possible password fields: ${possiblePasswordFields.size}" }
@ -317,7 +336,12 @@ class AutofillStrategy private constructor(private val rules: List<AutofillRule>
// Return the result of the first rule that matches // Return the result of the first rule that matches
d { "Rules: ${rules.size}" } d { "Rules: ${rules.size}" }
for (rule in rules) { for (rule in rules) {
return rule.apply(possiblePasswordFields, possibleUsernameFields, multiOriginSupport) return rule.match(
possiblePasswordFields,
possibleUsernameFields,
singleOriginMode = singleOriginMode,
isManualRequest = isManualRequest
)
?: continue ?: continue
} }
return null return null

View file

@ -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]. * Manages the detection of fields to fill in an [AssistStructure] and determines the [FormOrigin].
*/ */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private class Form(context: Context, structure: AssistStructure) { private class Form(context: Context, structure: AssistStructure, isManualRequest: Boolean) {
companion object { companion object {
private val SUPPORTED_SCHEMES = listOf("http", "https") private val SUPPORTED_SCHEMES = listOf("http", "https")
@ -98,7 +98,7 @@ private class Form(context: Context, structure: AssistStructure) {
parseStructure(structure) parseStructure(structure)
} }
val scenario = detectFieldsToFill() val scenario = detectFieldsToFill(isManualRequest)
val formOrigin = determineFormOrigin(context) val formOrigin = determineFormOrigin(context)
init { 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) { private fun trackOrigin(node: AssistStructure.ViewNode) {
if (!isTrustedBrowser) return if (!isTrustedBrowser) return
@ -219,8 +223,12 @@ class FillableForm private constructor(
/** /**
* Returns a [FillableForm] if a login form could be detected in [structure]. * Returns a [FillableForm] if a login form could be detected in [structure].
*/ */
fun parseAssistStructure(context: Context, structure: AssistStructure): FillableForm? { fun parseAssistStructure(
val form = Form(context, structure) context: Context,
structure: AssistStructure,
isManualRequest: Boolean
): FillableForm? {
val form = Form(context, structure, isManualRequest)
if (form.formOrigin == null || form.scenario == null) return null if (form.formOrigin == null || form.scenario == null) return null
return FillableForm( return FillableForm(
form.formOrigin, form.formOrigin,

View file

@ -18,6 +18,7 @@ import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.BuildConfig import com.zeapo.pwdstore.BuildConfig
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
import com.zeapo.pwdstore.utils.hasFlag
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class OreoAutofillService : AutofillService() { class OreoAutofillService : AutofillService() {
@ -62,7 +63,10 @@ class OreoAutofillService : AutofillService() {
} }
return 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" } d { "Form cannot be filled" }
callback.onSuccess(null) callback.onSuccess(null)
return return

View file

@ -10,6 +10,10 @@ import android.util.TypedValue
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
infix fun Int.hasFlag(flag: Int): Boolean {
return this and flag == flag
}
fun String.splitLines(): Array<String> { fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
} }