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)121314151617181920object PasswordCredentialPlanner {2122 fun candidates(23 entries: List<VaultEntry>,24 domains: Set<String>,25 appTitle: String?,26 allowedUserIds: Set<String>? = null,27 ): List<PasswordCredentialCandidate> {28 val eligible = entries.filter { it.fillable() }29 val byDomain = if (domains.isEmpty()) {30 emptyList()31 } else {32 eligible.filter { entry -> domains.any { entry.matchesDomain(it) } }33 }34 val byTitle = if (appTitle.isNullOrBlank()) {35 emptyList()36 } else {37 eligible.filter { it.title.trim().equals(appTitle, ignoreCase = true) }38 }39 return (byDomain + byTitle)40 .distinctBy { it.id }41 .mapNotNull { entry ->42 val username = entry.username ?: entry.email ?: return@mapNotNull null43 if (!allowedUserIds.isNullOrEmpty() && username !in allowedUserIds) {44 return@mapNotNull null45 }46 PasswordCredentialCandidate(47 entryId = entry.id,48 title = entry.title,49 username = username,50 lastUsedAt = entry.lastUsedAt,51 )52 }53 .sortedWith(54 compareByDescending<PasswordCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }55 .thenBy { it.username.lowercase() },56 )57 }5859 60 private fun VaultEntry.fillable(): Boolean =61 deletedAt == null && !password.isNullOrBlank()6263 private fun VaultEntry.matchesDomain(domain: String): Boolean =64 sequenceOf(url)65 .plus(additionalUrls.orEmpty().asSequence())66 .filterNotNull()67 .mapNotNull { extractDomain(it) }68 .any { it == domain }69}