1package com.peachpasswords.autofill23import com.peachpasswords.shared.extractDomain4import com.peachpasswords.vault.VaultEntry5678910111213enum class IdentityType { EMAIL, USERNAME }1415enum class IdentityFilterMode(val label: String) {16 BOTH("Both"),17 EMAIL("Emails"),18 USERNAME("Usernames"),19}2021data class SavedIdentity(22 val value: String,23 val type: IdentityType,24 val lastUsed: Long,25 val useCount: Int,26 val exampleDomain: String? = null,27)2829private val EMAIL_RE = Regex("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")3031private class Tally(var lastUsed: Long, var count: Int, var domain: String)323334internal fun deriveIdentities(entries: List<VaultEntry>): List<SavedIdentity> {35 val active = entries.filter { it.deletedAt == null && it.type == "login" }36 val emailMap = HashMap<String, Tally>()37 val usernameMap = HashMap<String, Tally>()3839 fun tally(map: HashMap<String, Tally>, value: String, modifiedAt: Long, domain: String) {40 val existing = map[value]41 if (existing == null) {42 map[value] = Tally(modifiedAt, 1, domain)43 } else {44 existing.count++45 if (modifiedAt > existing.lastUsed) {46 existing.lastUsed = modifiedAt47 existing.domain = domain48 }49 }50 }5152 for (entry in active) {53 val domain = entry.url?.let(::extractDomain) ?: ""54 entry.username?.trim()?.takeIf { it.isNotEmpty() }?.let { value ->55 tally(if (EMAIL_RE.matches(value)) emailMap else usernameMap, value, entry.modifiedAt, domain)56 }57 58 59 entry.email?.trim()?.takeIf { it.isNotEmpty() }?.let { value ->60 tally(emailMap, value, entry.modifiedAt, domain)61 }62 }6364 fun toIdentities(map: HashMap<String, Tally>, type: IdentityType) =65 map.entries66 .map { (value, t) ->67 SavedIdentity(value, type, t.lastUsed, t.count, t.domain.takeIf { it.isNotEmpty() })68 }69 .sortedByDescending { it.useCount }7071 return toIdentities(emailMap, IdentityType.EMAIL) + toIdentities(usernameMap, IdentityType.USERNAME)72}737475internal fun filterIdentities(76 identities: List<SavedIdentity>,77 query: String,78 mode: IdentityFilterMode,79): List<SavedIdentity> {80 val typeFiltered = when (mode) {81 IdentityFilterMode.EMAIL -> identities.filter { it.type == IdentityType.EMAIL }82 IdentityFilterMode.USERNAME -> identities.filter { it.type == IdentityType.USERNAME }83 IdentityFilterMode.BOTH -> identities84 }85 val sorted = typeFiltered.sortedWith(86 compareByDescending<SavedIdentity> { it.lastUsed }87 .thenByDescending { it.useCount }88 .thenBy { it.value }89 .thenBy { it.type.name },90 )91 val trimmed = query.trim().lowercase()92 if (trimmed.isEmpty()) return sorted93 return sorted.filter { identity ->94 listOf(identity.value, identity.exampleDomain ?: "", identity.type.name.lowercase())95 .joinToString(" ")96 .lowercase()97 .contains(trimmed)98 }99}