package com.peachpasswords.autofill import android.app.assist.AssistStructure import android.text.InputType import android.view.View import android.view.autofill.AutofillId enum class AutofillOriginTrust { TRUSTED_WEB, TRUSTED_NATIVE, UNKNOWN } /** * The autofillable fields discovered in one [AssistStructure], plus the origin * Android authenticated for ranking vault entries. */ data class ParsedStructure( val usernameId: AutofillId? = null, val emailId: AutofillId? = null, val passwordId: AutofillId? = null, val otpId: AutofillId? = null, val usernameValue: String? = null, val emailValue: String? = null, val passwordValue: String? = null, val otpValue: String? = null, val webDomain: String? = null, val packageName: String? = null, val originTrust: AutofillOriginTrust = AutofillOriginTrust.UNKNOWN, val verifiedDomains: List = emptyList(), ) { /** The id we put the account identifier into — email preferred, else username. */ val identityId: AutofillId? get() = emailId ?: usernameId val hasUsableField: Boolean get() = passwordId != null || identityId != null || otpId != null /** Best label to rank entries against. */ val origin: String? get() = webDomain ?: packageName } /** * Walks an [AssistStructure] to classify username/email/password/OTP fields. * Mirrors the browser content-script's field detection: trust explicit autofill * hints first, then fall back to input-type and id/hint keyword heuristics so * unannotated login forms still work. */ object AssistStructureParser { fun parse(structure: AssistStructure): ParsedStructure { // View-node package ids are supplied by the foreground app; the framework-owned // activity component is the authenticated caller identity. var result = ParsedStructure(packageName = structure.activityComponent?.packageName) val windows = structure.windowNodeCount for (i in 0 until windows) { val root = structure.getWindowNodeAt(i).rootViewNode result = traverse(root, result) } return result } private fun traverse(node: AssistStructure.ViewNode, acc: ParsedStructure): ParsedStructure { var result = acc if (result.webDomain == null && !node.webDomain.isNullOrBlank()) { result = result.copy(webDomain = node.webDomain) } val id = node.autofillId if (id != null && isFillable(node)) { val value = node.autofillValue?.takeIf { it.isText }?.textValue?.toString() ?: node.text?.toString() result = when (classify(node)) { FieldKind.PASSWORD -> if (result.passwordId == null) result.copy(passwordId = id, passwordValue = value) else result FieldKind.USERNAME -> if (result.usernameId == null) result.copy(usernameId = id, usernameValue = value) else result FieldKind.EMAIL -> if (result.emailId == null) result.copy(emailId = id, emailValue = value) else result FieldKind.OTP -> if (result.otpId == null) result.copy(otpId = id, otpValue = value) else result FieldKind.NONE -> result } } for (i in 0 until node.childCount) { result = traverse(node.getChildAt(i), result) } return result } private fun isFillable(node: AssistStructure.ViewNode): Boolean = node.autofillType == View.AUTOFILL_TYPE_TEXT && node.visibility == View.VISIBLE private enum class FieldKind { USERNAME, EMAIL, PASSWORD, OTP, NONE } private fun classify(node: AssistStructure.ViewNode): FieldKind { node.autofillHints?.forEach { hint -> when (hint.lowercase()) { View.AUTOFILL_HINT_PASSWORD, "password" -> return FieldKind.PASSWORD View.AUTOFILL_HINT_USERNAME, "username" -> return FieldKind.USERNAME View.AUTOFILL_HINT_EMAIL_ADDRESS, "email", "emailaddress" -> return FieldKind.EMAIL "otp", "smsotpcode", "2facode" -> return FieldKind.OTP } } val type = node.inputType val isPasswordType = type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_PASSWORD || type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD || type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_NUMBER_VARIATION_PASSWORD if (isPasswordType) return FieldKind.PASSWORD val haystack = listOfNotNull(node.idEntry, node.hint, node.text?.toString()) .joinToString(" ").lowercase() return when { haystack.isBlank() -> FieldKind.NONE Regex("\\b(otp|2fa|mfa|one[- ]?time|verification code|auth code)\\b").containsMatchIn(haystack) -> FieldKind.OTP haystack.contains("password") || haystack.contains("passwd") || haystack.contains("pwd") -> FieldKind.PASSWORD haystack.contains("email") || haystack.contains("e-mail") -> FieldKind.EMAIL haystack.contains("user") || haystack.contains("login") || haystack.contains("account") -> FieldKind.USERNAME else -> FieldKind.NONE } } }