mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/Autofill_Improvement' into develop
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
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.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")
|
||||||
}
|
}
|
||||||
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
it.toLowerCase(Locale.ENGLISH) == View.AUTOFILL_HINT_PASSWORD
|
||||||
val inputType = node.inputType
|
|| it.toLowerCase(Locale.ENGLISH).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.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
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user