1package com.peachpasswords.autofill23import android.app.assist.AssistStructure4import android.text.InputType5import android.view.View6import android.view.autofill.AutofillId789101112data class ParsedStructure(13 val usernameId: AutofillId? = null,14 val emailId: AutofillId? = null,15 val passwordId: AutofillId? = null,16 val otpId: AutofillId? = null,17 val webDomain: String? = null,18 val packageName: String? = null,19) {20 21 val identityId: AutofillId? get() = usernameId ?: emailId22 val hasUsableField: Boolean get() = passwordId != null || identityId != null || otpId != null23 24 val origin: String? get() = webDomain ?: packageName25}2627282930313233object AssistStructureParser {3435 fun parse(structure: AssistStructure): ParsedStructure {36 var result = ParsedStructure()37 val windows = structure.windowNodeCount38 for (i in 0 until windows) {39 val root = structure.getWindowNodeAt(i).rootViewNode40 result = traverse(root, result)41 }42 return result43 }4445 private fun traverse(node: AssistStructure.ViewNode, acc: ParsedStructure): ParsedStructure {46 var result = acc47 if (result.webDomain == null && !node.webDomain.isNullOrBlank()) {48 result = result.copy(webDomain = node.webDomain)49 }50 if (result.packageName == null && !node.idPackage.isNullOrBlank()) {51 result = result.copy(packageName = node.idPackage)52 }5354 val id = node.autofillId55 if (id != null && isFillable(node)) {56 result = when (classify(node)) {57 FieldKind.PASSWORD -> if (result.passwordId == null) result.copy(passwordId = id) else result58 FieldKind.USERNAME -> if (result.usernameId == null) result.copy(usernameId = id) else result59 FieldKind.EMAIL -> if (result.emailId == null) result.copy(emailId = id) else result60 FieldKind.OTP -> if (result.otpId == null) result.copy(otpId = id) else result61 FieldKind.NONE -> result62 }63 }6465 for (i in 0 until node.childCount) {66 result = traverse(node.getChildAt(i), result)67 }68 return result69 }7071 private fun isFillable(node: AssistStructure.ViewNode): Boolean =72 node.autofillType == View.AUTOFILL_TYPE_TEXT &&73 node.visibility == View.VISIBLE7475 private enum class FieldKind { USERNAME, EMAIL, PASSWORD, OTP, NONE }7677 private fun classify(node: AssistStructure.ViewNode): FieldKind {78 node.autofillHints?.forEach { hint ->79 when (hint.lowercase()) {80 View.AUTOFILL_HINT_PASSWORD, "password" -> return FieldKind.PASSWORD81 View.AUTOFILL_HINT_USERNAME, "username" -> return FieldKind.USERNAME82 View.AUTOFILL_HINT_EMAIL_ADDRESS, "email", "emailaddress" -> return FieldKind.EMAIL83 "otp", "smsotpcode", "2facode" -> return FieldKind.OTP84 }85 }8687 val type = node.inputType88 val isPasswordType = type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_PASSWORD ||89 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||90 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_NUMBER_VARIATION_PASSWORD91 if (isPasswordType) return FieldKind.PASSWORD9293 val haystack = listOfNotNull(node.idEntry, node.hint, node.text?.toString())94 .joinToString(" ").lowercase()95 return when {96 haystack.isBlank() -> FieldKind.NONE97 Regex("\\b(otp|2fa|mfa|one[- ]?time|verification code|auth code)\\b").containsMatchIn(haystack) -> FieldKind.OTP98 haystack.contains("password") || haystack.contains("passwd") || haystack.contains("pwd") -> FieldKind.PASSWORD99 haystack.contains("email") || haystack.contains("e-mail") -> FieldKind.EMAIL100 haystack.contains("user") || haystack.contains("login") || haystack.contains("account") -> FieldKind.USERNAME101 else -> FieldKind.NONE102 }103 }104}