AssistStructureParser.kt

5.1 KB · raw

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 }910/**11 * The autofillable fields discovered in one [AssistStructure], plus the origin12 * Android authenticated for ranking vault entries.13 */14data 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    /** The id we put the account identifier into — email preferred, else username. */29    val identityId: AutofillId? get() = emailId ?: usernameId30    val hasUsableField: Boolean get() = passwordId != null || identityId != null || otpId != null31    /** Best label to rank entries against. */32    val origin: String? get() = webDomain ?: packageName33}3435/**36 * Walks an [AssistStructure] to classify username/email/password/OTP fields.37 * Mirrors the browser content-script's field detection: trust explicit autofill38 * hints first, then fall back to input-type and id/hint keyword heuristics so39 * unannotated login forms still work.40 */41object AssistStructureParser {4243    fun parse(structure: AssistStructure): ParsedStructure {44        // View-node package ids are supplied by the foreground app; the framework-owned45        // activity component is the authenticated caller identity.46        var result = ParsedStructure(packageName = structure.activityComponent?.packageName)47        val windows = structure.windowNodeCount48        for (i in 0 until windows) {49            val root = structure.getWindowNodeAt(i).rootViewNode50            result = traverse(root, result)51        }52        return result53    }5455    private fun traverse(node: AssistStructure.ViewNode, acc: ParsedStructure): ParsedStructure {56        var result = acc57        if (result.webDomain == null && !node.webDomain.isNullOrBlank()) {58            result = result.copy(webDomain = node.webDomain)59        }6061        val id = node.autofillId62        if (id != null && isFillable(node)) {63            val value = node.autofillValue?.takeIf { it.isText }?.textValue?.toString()64                ?: node.text?.toString()65            result = when (classify(node)) {66                FieldKind.PASSWORD -> if (result.passwordId == null) result.copy(passwordId = id, passwordValue = value) else result67                FieldKind.USERNAME -> if (result.usernameId == null) result.copy(usernameId = id, usernameValue = value) else result68                FieldKind.EMAIL -> if (result.emailId == null) result.copy(emailId = id, emailValue = value) else result69                FieldKind.OTP -> if (result.otpId == null) result.copy(otpId = id, otpValue = value) else result70                FieldKind.NONE -> result71            }72        }7374        for (i in 0 until node.childCount) {75            result = traverse(node.getChildAt(i), result)76        }77        return result78    }7980    private fun isFillable(node: AssistStructure.ViewNode): Boolean =81        node.autofillType == View.AUTOFILL_TYPE_TEXT &&82            node.visibility == View.VISIBLE8384    private enum class FieldKind { USERNAME, EMAIL, PASSWORD, OTP, NONE }8586    private fun classify(node: AssistStructure.ViewNode): FieldKind {87        node.autofillHints?.forEach { hint ->88            when (hint.lowercase()) {89                View.AUTOFILL_HINT_PASSWORD, "password" -> return FieldKind.PASSWORD90                View.AUTOFILL_HINT_USERNAME, "username" -> return FieldKind.USERNAME91                View.AUTOFILL_HINT_EMAIL_ADDRESS, "email", "emailaddress" -> return FieldKind.EMAIL92                "otp", "smsotpcode", "2facode" -> return FieldKind.OTP93            }94        }9596        val type = node.inputType97        val isPasswordType = type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_PASSWORD ||98            type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||99            type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_NUMBER_VARIATION_PASSWORD100        if (isPasswordType) return FieldKind.PASSWORD101102        val haystack = listOfNotNull(node.idEntry, node.hint, node.text?.toString())103            .joinToString(" ").lowercase()104        return when {105            haystack.isBlank() -> FieldKind.NONE106            Regex("\\b(otp|2fa|mfa|one[- ]?time|verification code|auth code)\\b").containsMatchIn(haystack) -> FieldKind.OTP107            haystack.contains("password") || haystack.contains("passwd") || haystack.contains("pwd") -> FieldKind.PASSWORD108            haystack.contains("email") || haystack.contains("e-mail") -> FieldKind.EMAIL109            haystack.contains("user") || haystack.contains("login") || haystack.contains("account") -> FieldKind.USERNAME110            else -> FieldKind.NONE111        }112    }113}