package com.peachpasswords.autofill import android.content.Context import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState import android.os.Build import com.peachpasswords.shared.extractDomain import com.peachpasswords.passkeys.PasskeyCallerOrigin import com.peachpasswords.vault.VaultEntry /** * Origin resolution for autofill matching. * * The caller package comes from the framework-owned activity component. A raw * ViewNode.webDomain is only accepted when that package is a signed browser in * the same allowlist used by passkeys; otherwise the request fails closed. */ /** * Populate [ParsedStructure.originDomains] and [ParsedStructure.appTitle] from * the authenticated origin. Verified browsers match on the page domain; native * apps match on their app-link domains and display label. Unverified web claims * produce no origin matches. */ internal fun resolveAutofillOrigin(context: Context, parsed: ParsedStructure): ParsedStructure { val webDomain = parsed.webDomain if (webDomain != null) { if (!PasskeyCallerOrigin.isVerifiedBrowser(context, parsed.packageName)) { return parsed.copy(webDomain = null, originDomains = emptyList(), appTitle = null) } return parsed.copy(originDomains = listOfNotNull(extractDomain(webDomain)), appTitle = null) } return parsed.copy( originDomains = appLinkDomains(context, parsed.packageName), appTitle = appLabel(context, parsed.packageName), ) } /** The display label of the calling app, for matching entries by app title. */ internal fun appLabel(context: Context, packageName: String?): String? { if (packageName.isNullOrBlank()) return null return runCatching { context.packageManager.getApplicationLabel( context.packageManager.getApplicationInfo(packageName, 0), ).toString().takeIf { it.isNotBlank() } }.getOrNull() } /** A native app's verified app-link domains (Android's cached Digital Asset Links verdict). */ internal fun appLinkDomains(context: Context, packageName: String?): List { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || packageName.isNullOrBlank()) { return emptyList() } return runCatching { val state = context.getSystemService(DomainVerificationManager::class.java) ?.getDomainVerificationUserState(packageName) state?.hostToStateMap.orEmpty() .filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED } .keys .mapNotNull { extractDomain(it.removePrefix("*.")) } .distinct() .sorted() }.getOrDefault(emptyList()) } /** * Domains to offer to "remember" when the user manually picks [entry] for this * origin (Android counterpart of the browser's remember-this-site toast). * Mirrors the browser trigger: only offer when the entry already carries at * least one known domain and none of them cover this origin. Empty = no offer. */ internal fun rememberOfferDomains(entry: VaultEntry, parsed: ParsedStructure): List { val existing = sequenceOf(entry.url) .plus(entry.additionalUrls.orEmpty().asSequence()) .filterNotNull() .mapNotNull(::extractDomain) .toSet() if (existing.isEmpty() || parsed.originDomains.isEmpty()) return emptyList() return parsed.originDomains.filter { it !in existing } }