Upgrade Autofill algorithm

This commit is contained in:
J-Jamet
2020-03-19 14:35:22 +01:00
parent b0e8a3ecd9
commit d098bf5e6a
3 changed files with 106 additions and 51 deletions

View File

@@ -26,15 +26,14 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.service.autofill.Dataset import android.service.autofill.Dataset
import android.service.autofill.FillResponse import android.service.autofill.FillResponse
import androidx.annotation.RequiresApi
import android.util.Log import android.util.Log
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue import android.view.autofill.AutofillValue
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import java.util.*
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
@@ -56,10 +55,10 @@ object AutofillHelper {
return String.format("%s (%s)", entryInfo.title, entryInfo.username) return String.format("%s (%s)", entryInfo.title, entryInfo.username)
if (entryInfo.title.isNotEmpty()) if (entryInfo.title.isNotEmpty())
return entryInfo.title return entryInfo.title
if (entryInfo.username.isNotEmpty())
return entryInfo.username
if (entryInfo.url.isNotEmpty()) if (entryInfo.url.isNotEmpty())
return entryInfo.url return entryInfo.url
if (entryInfo.username.isNotEmpty())
return entryInfo.username
return "" return ""
} }
@@ -71,12 +70,12 @@ object AutofillHelper {
val builder = Dataset.Builder(views) val builder = Dataset.Builder(views)
builder.setId(entryInfo.id) builder.setId(entryInfo.id)
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) } struct.usernameId?.let { usernameId ->
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
val ids = ArrayList(struct.username) }
if (entryInfo.username.contains("@") || struct.username.isEmpty()) struct.passwordId?.let { password ->
ids.addAll(struct.email) builder.setValue(password, AutofillValue.forText(entryInfo.password))
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) } }
return try { return try {
builder.build() builder.build()

View File

@@ -41,7 +41,7 @@ class KeeAutofillService : AutofillService() {
// Check user's settings for authenticating Responses and Datasets. // Check user's settings for authenticating Responses and Datasets.
val parseResult = StructureParser(latestStructure).parse() val parseResult = StructureParser(latestStructure).parse()
parseResult?.allAutofillIds()?.let { autofillIds -> parseResult?.allAutofillIds()?.let { autofillIds ->
if (listOf(*autofillIds).isNotEmpty()) { if (autofillIds.isNotEmpty()) {
// If the entire Autofill Response is authenticated, AuthActivity is used // If the entire Autofill Response is authenticated, AuthActivity is used
// to generate Response. // to generate Response.
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this) val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)

View File

@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.autofill
import android.app.assist.AssistStructure import android.app.assist.AssistStructure
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import android.text.InputType
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.autofill.AutofillId import android.view.autofill.AutofillId
@@ -40,69 +39,126 @@ internal class StructureParser(private val structure: AssistStructure) {
result = Result() result = Result()
result?.apply { result?.apply {
usernameCandidate = null usernameCandidate = null
for (i in 0 until structure.windowNodeCount) { mainLoop@ for (i in 0 until structure.windowNodeCount) {
val windowNode = structure.getWindowNodeAt(i) val windowNode = structure.getWindowNodeAt(i)
/*
title.add(windowNode.title) title.add(windowNode.title)
windowNode.rootViewNode.webDomain?.let { windowNode.rootViewNode.webDomain?.let {
webDomain.add(it) webDomain.add(it)
} }
parseViewNode(windowNode.rootViewNode) */
if (parseViewNode(windowNode.rootViewNode))
break@mainLoop
} }
// If not explicit username field found, add the field just before password field. // If not explicit username field found, add the field just before password field.
if (username.isEmpty() && email.isEmpty() if (usernameId == null && passwordId != null && usernameCandidate != null)
&& password.isNotEmpty() && usernameCandidate != null) usernameId = usernameCandidate
username.add(usernameCandidate!!)
} }
return result // Return the result only if password field is retrieved
return if (result?.passwordId != null)
result
else
null
} }
private fun parseViewNode(node: AssistStructure.ViewNode) { private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
val hints = node.autofillHints if (node.autofillId != null) {
val autofillId = node.autofillId val hints = node.autofillHints
if (autofillId != null) {
if (hints != null && hints.isNotEmpty()) { if (hints != null && hints.isNotEmpty()) {
when { if (parseNodeByAutofillHint(node))
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId) return true
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId) } else {
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId) if (parseNodeByHtmlAttributes(node))
else -> Log.d(TAG, "unsupported hints") return true
}
}
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
return true
}
return false
}
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
node.autofillHints?.forEach {
when {
it == View.AUTOFILL_HINT_USERNAME
|| it == View.AUTOFILL_HINT_EMAIL_ADDRESS
|| it == View.AUTOFILL_HINT_PHONE -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username hint")
} }
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) { it == View.AUTOFILL_HINT_PASSWORD
val inputType = node.inputType || it.contains("password") -> {
when { result?.passwordId = autofillId
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId) Log.d(TAG, "Autofill password hint")
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId) return true
result?.password?.isEmpty() == true -> usernameCandidate = autofillId }
it == "on" -> {
if (parseNodeByHtmlAttributes(node))
return true
}
else -> Log.d(TAG, "Autofill unsupported hint $it")
}
}
return false
}
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
val autofillId = node.autofillId
val nodHtml = node.htmlInfo
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
"input" -> {
nodHtml.attributes?.forEach { pairAttribute ->
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
"type" -> {
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
"tel", "email" -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"text" -> {
usernameCandidate = autofillId
Log.d(TAG, "Autofill type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
"password" -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
}
}
}
}
} }
} }
} }
return false
for (i in 0 until node.childCount)
parseViewNode(node.getChildAt(i))
} }
@RequiresApi(api = Build.VERSION_CODES.O) @RequiresApi(api = Build.VERSION_CODES.O)
internal class Result { internal class Result {
val title: MutableList<CharSequence> var usernameId: AutofillId? = null
val webDomain: MutableList<String> set(value) {
val username: MutableList<AutofillId> if (field == null)
val email: MutableList<AutofillId> field = value
val password: MutableList<AutofillId> }
init { var passwordId: AutofillId? = null
title = ArrayList() set(value) {
webDomain = ArrayList() if (field == null)
username = ArrayList() field = value
email = ArrayList() }
password = ArrayList()
}
fun allAutofillIds(): Array<AutofillId> { fun allAutofillIds(): Array<AutofillId> {
val all = ArrayList<AutofillId>() val all = ArrayList<AutofillId>()
all.addAll(username) usernameId?.let {
all.addAll(email) all.add(it)
all.addAll(password) }
passwordId?.let {
all.add(it)
}
return all.toTypedArray() return all.toTypedArray()
} }
} }