1package com.peachpasswords.autofill23import android.annotation.SuppressLint4import android.content.Context5import android.os.Build6import android.service.autofill.Dataset7import android.service.autofill.InlinePresentation8import android.view.autofill.AutofillId9import android.view.autofill.AutofillValue10import android.widget.RemoteViews11import androidx.annotation.RequiresApi12import androidx.autofill.inline.v1.InlineSuggestionUi13import androidx.autofill.inline.UiVersions14import com.peachpasswords.R15import com.peachpasswords.shared.extractDomain16import com.peachpasswords.vault.PasswordGenerator17import com.peachpasswords.vault.TOTPGenerator18import com.peachpasswords.vault.VaultEntry19import com.peachpasswords.vault.isCard20212223242526object AutofillResponseBuilder {2728 29303132 data class MatchResult(val hits: List<VaultEntry>, val rest: List<VaultEntry>) {33 val all: List<VaultEntry> get() = hits + rest34 }3536 fun matchEntries(entries: List<VaultEntry>, origin: String?): MatchResult {37 val originDomain = origin?.let(::extractDomain)38 ?: return MatchResult(emptyList(), emptyList())39 return matchDomains(entries, setOf(originDomain))40 }4142 fun matchEntries(entries: List<VaultEntry>, parsed: ParsedStructure): MatchResult = when (parsed.originTrust) {43 AutofillOriginTrust.TRUSTED_WEB -> matchEntries(entries, parsed.webDomain)44 AutofillOriginTrust.TRUSTED_NATIVE -> matchDomains(45 entries,46 parsed.verifiedDomains.mapNotNull(::extractDomain).toSet(),47 )48 AutofillOriginTrust.UNKNOWN -> MatchResult(emptyList(), emptyList())49 }5051 private fun matchDomains(entries: List<VaultEntry>, domains: Set<String>): MatchResult =52 MatchResult(53 hits = entries.filter { entry ->54 domains.any { domain -> entry.matchesOriginDomain(domain) }55 },56 rest = emptyList(),57 )5859 private fun VaultEntry.matchesOriginDomain(originDomain: String): Boolean =60 sequenceOf(url)61 .plus(additionalUrls.orEmpty().asSequence())62 .filterNotNull()63 .mapNotNull(::extractDomain)64 .any { it == originDomain }6566 fun menuRow(context: Context, entry: VaultEntry): RemoteViews {67 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""68 return RemoteViews(context.packageName, R.layout.autofill_row).apply {69 setTextViewText(R.id.autofill_row_title, entry.title)70 setTextViewText(R.id.autofill_row_subtitle, subtitle)71 }72 }7374 fun simpleRow(context: Context, title: String, subtitle: String): RemoteViews =75 RemoteViews(context.packageName, R.layout.autofill_row).apply {76 setTextViewText(R.id.autofill_row_title, title)77 setTextViewText(R.id.autofill_row_subtitle, subtitle)78 }7980 81828384 fun datasetFor(85 context: Context,86 parsed: ParsedStructure,87 entry: VaultEntry,88 inlineSpec: android.widget.inline.InlinePresentationSpec?,89 ): Dataset? {90 val menu = menuRow(context, entry)91 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {92 buildInline(context, entry, inlineSpec)93 } else {94 null95 }9697 val builder = Dataset.Builder()98 var filledAny = false99 fun put(id: AutofillId?, value: String?) {100 if (id == null || value.isNullOrEmpty()) return101 val av = AutofillValue.forText(value)102 if (inline != null && Build.VERSION.SDK_INT >= 30) {103 builder.setValue(id, av, menu, inline)104 } else {105 builder.setValue(id, av, menu)106 }107 filledAny = true108 }109 put(parsed.identityId, entry.username ?: entry.email)110 put(parsed.passwordId, entry.password)111 put(parsed.otpId, entry.totp?.let { TOTPGenerator.generateCode(it) })112113 return if (filledAny) builder.build() else null114 }115116 117118119120 fun cardDatasetFor(121 context: Context,122 parsed: ParsedStructure,123 entry: VaultEntry,124 inlineSpec: android.widget.inline.InlinePresentationSpec?,125 ): Dataset? {126 if (!entry.isCard()) return null127 val menu = menuRow(context, entry)128 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {129 buildInline(context, entry, inlineSpec)130 } else {131 null132 }133134 val builder = Dataset.Builder()135 var filledAny = false136 fun put(id: AutofillId?, value: String?) {137 if (id == null || value.isNullOrEmpty()) return138 val av = AutofillValue.forText(value)139 if (inline != null && Build.VERSION.SDK_INT >= 30) {140 builder.setValue(id, av, menu, inline)141 } else {142 builder.setValue(id, av, menu)143 }144 filledAny = true145 }146147 val digits = entry.cardNumber?.filter { it.isDigit() }148 put(parsed.cardNumberId, digits)149 put(parsed.cardExpMonthId, entry.expMonth)150 put(parsed.cardExpYearId, entry.expYear)151 if (parsed.cardExpDateId != null && !entry.expMonth.isNullOrEmpty() && !entry.expYear.isNullOrEmpty()) {152 val yy = entry.expYear.takeLast(2)153 put(parsed.cardExpDateId, "${entry.expMonth.padStart(2, '0')}/$yy")154 }155 put(parsed.cardCvcId, entry.cvc)156 put(parsed.cardNameId, entry.cardholderName)157158 return if (filledAny) builder.build() else null159 }160161 162163164165166167168169 fun generateDatasetFor(context: Context, parsed: ParsedStructure): Dataset? {170 val passwordId = parsed.passwordId ?: return null171 val policy = parsed.passwordPolicy172 val length = policy?.generationLength() ?: PasswordPolicy.DEFAULT_LENGTH173 val password = PasswordGenerator.generate(PasswordGenerator.Options(length = length))174 val note = policy?.constraintNote(length).orEmpty()175 val menu = simpleRow(context, "Generate strong password", note.ifEmpty { "Peach" })176 return Dataset.Builder()177 .setValue(passwordId, AutofillValue.forText(password), menu)178 .build()179 }180181 182 183 @SuppressLint("RestrictedApi")184 @RequiresApi(30)185 private fun buildInline(186 context: Context,187 entry: VaultEntry,188 spec: android.widget.inline.InlinePresentationSpec,189 ): InlinePresentation? = try {190 val style = spec.style191 if (!UiVersions.getVersions(style).contains(UiVersions.INLINE_UI_VERSION_1)) {192 null193 } else {194 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""195 val pending = AutofillIntents.noopChipIntent(context)196 val slice = InlineSuggestionUi.newContentBuilder(pending)197 .setTitle(entry.title)198 .apply { if (subtitle.isNotBlank()) setSubtitle(subtitle) }199 .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))200 .build()201 .slice202 InlinePresentation(slice, spec, false)203 }204 } catch (_: Throwable) {205 206 null207 }208}