1package com.peachpasswords.credentials23import com.peachpasswords.shared.extractDomain4import com.peachpasswords.vault.VaultEntry56data class PasswordCredentialCandidate(7 val entryId: String,8 val title: String,9 val username: String,10 val lastUsedAt: Long?,11)1213141516171819202122object PasswordCredentialPlanner {2324 fun candidates(25 entries: List<VaultEntry>,26 domains: Set<String>,27 packageName: String? = null,28 allowedUserIds: Set<String>? = null,29 signerDigests: Set<String> = emptySet(),30 ): List<PasswordCredentialCandidate> {31 val eligible = entries.filter { it.fillable() }32 val byDomain = if (domains.isEmpty()) {33 emptyList()34 } else {35 eligible.filter { entry -> domains.any { entry.matchesDomain(it) } }36 }37 val byPackage = if (packageName.isNullOrBlank() || signerDigests.isEmpty()) {38 emptyList()39 } else {40 eligible.filter {41 (it.url == packageName || it.additionalUrls.orEmpty().any { url -> url == packageName }) &&42 it.nativeSignerDigests.orEmpty().any { digest -> digest in signerDigests }43 }44 }45 return (byDomain + byPackage)46 .distinctBy { it.id }47 .mapNotNull { entry ->48 val username = entry.username ?: entry.email ?: return@mapNotNull null49 if (!allowedUserIds.isNullOrEmpty() && username !in allowedUserIds) {50 return@mapNotNull null51 }52 PasswordCredentialCandidate(53 entryId = entry.id,54 title = entry.title,55 username = username,56 lastUsedAt = entry.lastUsedAt,57 )58 }59 .sortedWith(60 compareByDescending<PasswordCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }61 .thenBy { it.username.lowercase() },62 )63 }6465 66 private fun VaultEntry.fillable(): Boolean =67 deletedAt == null && !password.isNullOrBlank()6869 private fun VaultEntry.matchesDomain(domain: String): Boolean =70 sequenceOf(url)71 .plus(additionalUrls.orEmpty().asSequence())72 .filterNotNull()73 .mapNotNull { extractDomain(it) }74 .any { it == domain }75}