AutofillOrigin.kt

3.4 KB · raw

1package com.peachpasswords.autofill23import android.content.Context4import android.content.pm.verify.domain.DomainVerificationManager5import android.content.pm.verify.domain.DomainVerificationUserState6import android.os.Build7import com.peachpasswords.shared.extractDomain8import com.peachpasswords.vault.VaultEntry910/**11 * Origin resolution for autofill matching.12 *13 * Origin *verification* (browser certificate pinning + caller-trust gating) was14 * removed (queue #3, June 2026-08-02) — it flagged nearly every request as15 * "unverified" and withheld matches, adding friction without value. Matches are16 * now ranked purely by content: the page domain for web origins, and a native17 * app's app-link domains + display label for app origins. The caller package is18 * still the framework-authenticated identity (never a caller-supplied extra).19 */2021/**22 * Populate [ParsedStructure.originDomains] and [ParsedStructure.appTitle] from23 * the authenticated origin. Web origins match on the page domain; native apps24 * match on their app-link domains and their display label. No trust25 * classification — every origin is eligible to see its matching entries.26 */27internal fun resolveAutofillOrigin(context: Context, parsed: ParsedStructure): ParsedStructure {28    val webDomain = parsed.webDomain29    if (webDomain != null) {30        return parsed.copy(originDomains = listOfNotNull(extractDomain(webDomain)), appTitle = null)31    }32    return parsed.copy(33        originDomains = appLinkDomains(context, parsed.packageName),34        appTitle = appLabel(context, parsed.packageName),35    )36}3738/** The display label of the calling app, for matching entries by app title. */39internal fun appLabel(context: Context, packageName: String?): String? {40    if (packageName.isNullOrBlank()) return null41    return runCatching {42        context.packageManager.getApplicationLabel(43            context.packageManager.getApplicationInfo(packageName, 0),44        ).toString().takeIf { it.isNotBlank() }45    }.getOrNull()46}4748/** A native app's verified app-link domains (Android's cached Digital Asset Links verdict). */49internal fun appLinkDomains(context: Context, packageName: String?): List<String> {50    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || packageName.isNullOrBlank()) {51        return emptyList()52    }53    return runCatching {54        val state = context.getSystemService(DomainVerificationManager::class.java)55            ?.getDomainVerificationUserState(packageName)56        state?.hostToStateMap.orEmpty()57            .filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED }58            .keys59            .mapNotNull { extractDomain(it.removePrefix("*.")) }60            .distinct()61            .sorted()62    }.getOrDefault(emptyList())63}6465/**66 * Domains to offer to "remember" when the user manually picks [entry] for this67 * origin (Android counterpart of the browser's remember-this-site toast).68 * Mirrors the browser trigger: only offer when the entry already carries at69 * least one known domain and none of them cover this origin. Empty = no offer.70 */71internal fun rememberOfferDomains(entry: VaultEntry, parsed: ParsedStructure): List<String> {72    val existing = sequenceOf(entry.url)73        .plus(entry.additionalUrls.orEmpty().asSequence())74        .filterNotNull()75        .mapNotNull(::extractDomain)76        .toSet()77    if (existing.isEmpty() || parsed.originDomains.isEmpty()) return emptyList()78    return parsed.originDomains.filter { it !in existing }79}