1package com.peachpasswords.autofill23import android.app.assist.AssistStructure4import android.text.InputType5import android.view.View6import android.view.autofill.AutofillId78enum class AutofillOriginTrust { TRUSTED_WEB, TRUSTED_NATIVE, UNKNOWN }91011121314data class ParsedStructure(15 val usernameId: AutofillId? = null,16 val emailId: AutofillId? = null,17 val passwordId: AutofillId? = null,18 val otpId: AutofillId? = null,19 val usernameValue: String? = null,20 val emailValue: String? = null,21 val passwordValue: String? = null,22 val otpValue: String? = null,23 val webDomain: String? = null,24 val packageName: String? = null,25 val originTrust: AutofillOriginTrust = AutofillOriginTrust.UNKNOWN,26 val verifiedDomains: List<String> = emptyList(),27) {28 29 val identityId: AutofillId? get() = emailId ?: usernameId30 val hasUsableField: Boolean get() = passwordId != null || identityId != null || otpId != null31 32 val origin: String? get() = webDomain ?: packageName33}3435363738394041object AssistStructureParser {4243 fun parse(structure: AssistStructure): ParsedStructure {44 45 46 var result = ParsedStructure(47 packageName = authenticatedPackageName(structure.activityComponent?.packageName),48 )49 val windows = structure.windowNodeCount50 for (i in 0 until windows) {51 val root = structure.getWindowNodeAt(i).rootViewNode52 result = traverse(root, result)53 }54 return result55 }5657 private fun traverse(node: AssistStructure.ViewNode, acc: ParsedStructure): ParsedStructure {58 var result = acc59 if (result.webDomain == null && !node.webDomain.isNullOrBlank()) {60 result = result.copy(webDomain = node.webDomain)61 }6263 val id = node.autofillId64 if (id != null && isFillable(node)) {65 val value = node.autofillValue?.takeIf { it.isText }?.textValue?.toString()66 ?: node.text?.toString()67 result = when (classify(node)) {68 FieldKind.PASSWORD -> if (result.passwordId == null) result.copy(passwordId = id, passwordValue = value) else result69 FieldKind.USERNAME -> if (result.usernameId == null) result.copy(usernameId = id, usernameValue = value) else result70 FieldKind.EMAIL -> if (result.emailId == null) result.copy(emailId = id, emailValue = value) else result71 FieldKind.OTP -> if (result.otpId == null) result.copy(otpId = id, otpValue = value) else result72 FieldKind.NONE -> result73 }74 }7576 for (i in 0 until node.childCount) {77 result = traverse(node.getChildAt(i), result)78 }79 return result80 }8182 private fun isFillable(node: AssistStructure.ViewNode): Boolean =83 node.autofillType == View.AUTOFILL_TYPE_TEXT &&84 node.visibility == View.VISIBLE8586 private enum class FieldKind { USERNAME, EMAIL, PASSWORD, OTP, NONE }8788 private fun classify(node: AssistStructure.ViewNode): FieldKind {89 node.autofillHints?.forEach { hint ->90 when (hint.lowercase()) {91 View.AUTOFILL_HINT_PASSWORD, "password" -> return FieldKind.PASSWORD92 View.AUTOFILL_HINT_USERNAME, "username" -> return FieldKind.USERNAME93 View.AUTOFILL_HINT_EMAIL_ADDRESS, "email", "emailaddress" -> return FieldKind.EMAIL94 "otp", "smsotpcode", "2facode" -> return FieldKind.OTP95 }96 }9798 val type = node.inputType99 val isPasswordType = type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_PASSWORD ||100 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||101 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_NUMBER_VARIATION_PASSWORD102 if (isPasswordType) return FieldKind.PASSWORD103104 val haystack = listOfNotNull(node.idEntry, node.hint, node.text?.toString())105 .joinToString(" ").lowercase()106 return when {107 haystack.isBlank() -> FieldKind.NONE108 Regex("\\b(otp|2fa|mfa|one[- ]?time|verification code|auth code)\\b").containsMatchIn(haystack) -> FieldKind.OTP109 haystack.contains("password") || haystack.contains("passwd") || haystack.contains("pwd") -> FieldKind.PASSWORD110 haystack.contains("email") || haystack.contains("e-mail") -> FieldKind.EMAIL111 haystack.contains("user") || haystack.contains("login") || haystack.contains("account") -> FieldKind.USERNAME112 else -> FieldKind.NONE113 }114 }115}116117118119120121122internal fun authenticatedPackageName(packageName: String?): String? =123 packageName?.takeIf { it.isNotBlank() }