Merge branch 'feature/Autofill_Improvement' into develop

This commit is contained in:
J-Jamet
2020-03-19 17:23:24 +01:00
3 changed files with 116 additions and 52 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

@@ -35,13 +35,13 @@ class KeeAutofillService : AutofillService() {
val fillContexts = request.fillContexts val fillContexts = request.fillContexts
val latestStructure = fillContexts[fillContexts.size - 1].structure val latestStructure = fillContexts[fillContexts.size - 1].structure
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") } cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
val responseBuilder = FillResponse.Builder() val responseBuilder = FillResponse.Builder()
// 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
@@ -35,74 +34,140 @@ import java.util.*
internal class StructureParser(private val structure: AssistStructure) { internal class StructureParser(private val structure: AssistStructure) {
private var result: Result? = null private var result: Result? = null
private var usernameCandidate: AutofillId? = null private var usernameCandidate: AutofillId? = null
private var lockHint: Boolean = false
fun parse(): Result? { fun parse(): Result? {
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 {
if (node.autofillId != null) {
val hints = node.autofillHints val hints = node.autofillHints
val autofillId = node.autofillId
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
}
} 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
} }
} }
// Recursive method to process each node
for (i in 0 until node.childCount) {
if (parseViewNode(node.getChildAt(i)))
return true
}
return false
} }
for (i in 0 until node.childCount) private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
parseViewNode(node.getChildAt(i)) val autofillId = node.autofillId
node.autofillHints?.forEach {
when {
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_USERNAME
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_EMAIL_ADDRESS
|| it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PHONE -> {
result?.usernameId = autofillId
Log.d(TAG, "Autofill username hint")
}
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PASSWORD
|| it.toLowerCase(Locale.ENGLISH).contains("password") -> {
result?.passwordId = autofillId
Log.d(TAG, "Autofill password hint")
return true
}
it.toLowerCase(Locale.ENGLISH) == "off" -> {
Log.d(TAG, "Autofill OFF hint")
lockHint = true
return false
}
it.toLowerCase(Locale.ENGLISH) == "on" -> {
Log.d(TAG, "Autofill ON hint")
if (parseNodeByHtmlAttributes(node))
return true
}
else -> Log.d(TAG, "Autofill unsupported hint $it")
}
}
return false
}
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
if (lockHint)
return false
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
} }
@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()
} }
} }