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 ): List<PasswordCredentialCandidate> {30 val eligible = entries.filter { it.fillable() }31 val byDomain = if (domains.isEmpty()) {32 emptyList()33 } else {34 eligible.filter { entry -> domains.any { entry.matchesDomain(it) } }35 }36 val byPackage = if (packageName.isNullOrBlank()) {37 emptyList()38 } else {39 eligible.filter { it.url == packageName || it.additionalUrls.orEmpty().any { url -> url == packageName } }40 }41 return (byDomain + byPackage)42 .distinctBy { it.id }43 .mapNotNull { entry ->44 val username = entry.username ?: entry.email ?: return@mapNotNull null45 if (!allowedUserIds.isNullOrEmpty() && username !in allowedUserIds) {46 return@mapNotNull null47 }48 PasswordCredentialCandidate(49 entryId = entry.id,50 title = entry.title,51 username = username,52 lastUsedAt = entry.lastUsedAt,53 )54 }55 .sortedWith(56 compareByDescending<PasswordCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }57 .thenBy { it.username.lowercase() },58 )59 }6061 62 private fun VaultEntry.fillable(): Boolean =63 deletedAt == null && !password.isNullOrBlank()6465 private fun VaultEntry.matchesDomain(domain: String): Boolean =66 sequenceOf(url)67 .plus(additionalUrls.orEmpty().asSequence())68 .filterNotNull()69 .mapNotNull { extractDomain(it) }70 .any { it == domain }71}