From d098bf5e6ace1b72adb17677ff312bf643d1acc5 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 19 Mar 2020 14:35:22 +0100 Subject: [PATCH] Upgrade Autofill algorithm --- .../keepass/autofill/AutofillHelper.kt | 19 ++- .../keepass/autofill/KeeAutofillService.kt | 2 +- .../keepass/autofill/StructureParser.kt | 136 ++++++++++++------ 3 files changed, 106 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt index 57b431296..06f492206 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt @@ -26,15 +26,14 @@ import android.content.Intent import android.os.Build import android.service.autofill.Dataset import android.service.autofill.FillResponse -import androidx.annotation.RequiresApi import android.util.Log import android.view.autofill.AutofillManager import android.view.autofill.AutofillValue import android.widget.RemoteViews +import androidx.annotation.RequiresApi import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.model.EntryInfo -import java.util.* @RequiresApi(api = Build.VERSION_CODES.O) @@ -56,10 +55,10 @@ object AutofillHelper { return String.format("%s (%s)", entryInfo.title, entryInfo.username) if (entryInfo.title.isNotEmpty()) return entryInfo.title - if (entryInfo.username.isNotEmpty()) - return entryInfo.username if (entryInfo.url.isNotEmpty()) return entryInfo.url + if (entryInfo.username.isNotEmpty()) + return entryInfo.username return "" } @@ -71,12 +70,12 @@ object AutofillHelper { val builder = Dataset.Builder(views) builder.setId(entryInfo.id) - struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) } - - val ids = ArrayList(struct.username) - if (entryInfo.username.contains("@") || struct.username.isEmpty()) - ids.addAll(struct.email) - ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) } + struct.usernameId?.let { usernameId -> + builder.setValue(usernameId, AutofillValue.forText(entryInfo.username)) + } + struct.passwordId?.let { password -> + builder.setValue(password, AutofillValue.forText(entryInfo.password)) + } return try { builder.build() diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt index 305bc239e..11f3b8dca 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt @@ -41,7 +41,7 @@ class KeeAutofillService : AutofillService() { // Check user's settings for authenticating Responses and Datasets. val parseResult = StructureParser(latestStructure).parse() parseResult?.allAutofillIds()?.let { autofillIds -> - if (listOf(*autofillIds).isNotEmpty()) { + if (autofillIds.isNotEmpty()) { // If the entire Autofill Response is authenticated, AuthActivity is used // to generate Response. val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this) diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt index 89474dbee..1ddfef95d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt +++ b/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt @@ -21,7 +21,6 @@ package com.kunzisoft.keepass.autofill import android.app.assist.AssistStructure import android.os.Build import androidx.annotation.RequiresApi -import android.text.InputType import android.util.Log import android.view.View import android.view.autofill.AutofillId @@ -40,69 +39,126 @@ internal class StructureParser(private val structure: AssistStructure) { result = Result() result?.apply { usernameCandidate = null - for (i in 0 until structure.windowNodeCount) { + mainLoop@ for (i in 0 until structure.windowNodeCount) { val windowNode = structure.getWindowNodeAt(i) + /* title.add(windowNode.title) windowNode.rootViewNode.webDomain?.let { 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 (username.isEmpty() && email.isEmpty() - && password.isNotEmpty() && usernameCandidate != null) - username.add(usernameCandidate!!) + if (usernameId == null && passwordId != null && usernameCandidate != null) + usernameId = 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) { - val hints = node.autofillHints - val autofillId = node.autofillId - if (autofillId != null) { + private fun parseViewNode(node: AssistStructure.ViewNode): Boolean { + if (node.autofillId != null) { + val hints = node.autofillHints if (hints != null && hints.isNotEmpty()) { - when { - Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId) - Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId) - Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId) - else -> Log.d(TAG, "unsupported hints") + if (parseNodeByAutofillHint(node)) + return true + } else { + if (parseNodeByHtmlAttributes(node)) + 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) { - val inputType = node.inputType - when { - inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId) - inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId) - result?.password?.isEmpty() == true -> usernameCandidate = autofillId + it == View.AUTOFILL_HINT_PASSWORD + || it.contains("password") -> { + result?.passwordId = autofillId + Log.d(TAG, "Autofill password hint") + return true + } + 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}") + } + } + } + } } } } - - for (i in 0 until node.childCount) - parseViewNode(node.getChildAt(i)) + return false } @RequiresApi(api = Build.VERSION_CODES.O) internal class Result { - val title: MutableList - val webDomain: MutableList - val username: MutableList - val email: MutableList - val password: MutableList + var usernameId: AutofillId? = null + set(value) { + if (field == null) + field = value + } - init { - title = ArrayList() - webDomain = ArrayList() - username = ArrayList() - email = ArrayList() - password = ArrayList() - } + var passwordId: AutofillId? = null + set(value) { + if (field == null) + field = value + } fun allAutofillIds(): Array { val all = ArrayList() - all.addAll(username) - all.addAll(email) - all.addAll(password) + usernameId?.let { + all.add(it) + } + passwordId?.let { + all.add(it) + } return all.toTypedArray() } }