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.TOTPGenerator17import com.peachpasswords.vault.VaultEntry18192021222324object AutofillResponseBuilder {2526 27282930 data class MatchResult(val hits: List<VaultEntry>, val rest: List<VaultEntry>) {31 val all: List<VaultEntry> get() = hits + rest32 }3334 fun matchEntries(entries: List<VaultEntry>, origin: String?): MatchResult {35 val originDomain = origin?.let(::extractDomain)36 ?: return MatchResult(emptyList(), emptyList())37 return matchDomains(entries, setOf(originDomain))38 }3940 fun matchEntries(entries: List<VaultEntry>, parsed: ParsedStructure): MatchResult = when (parsed.originTrust) {41 AutofillOriginTrust.TRUSTED_WEB -> matchEntries(entries, parsed.webDomain)42 AutofillOriginTrust.TRUSTED_NATIVE -> matchDomains(43 entries,44 parsed.verifiedDomains.mapNotNull(::extractDomain).toSet(),45 )46 AutofillOriginTrust.UNKNOWN -> MatchResult(emptyList(), emptyList())47 }4849 private fun matchDomains(entries: List<VaultEntry>, domains: Set<String>): MatchResult =50 MatchResult(51 hits = entries.filter { entry ->52 domains.any { domain -> entry.matchesOriginDomain(domain) }53 },54 rest = emptyList(),55 )5657 private fun VaultEntry.matchesOriginDomain(originDomain: String): Boolean =58 sequenceOf(url)59 .plus(additionalUrls.orEmpty().asSequence())60 .filterNotNull()61 .mapNotNull(::extractDomain)62 .any { it == originDomain }6364 fun menuRow(context: Context, entry: VaultEntry): RemoteViews {65 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""66 return RemoteViews(context.packageName, R.layout.autofill_row).apply {67 setTextViewText(R.id.autofill_row_title, entry.title)68 setTextViewText(R.id.autofill_row_subtitle, subtitle)69 }70 }7172 fun simpleRow(context: Context, title: String, subtitle: String): RemoteViews =73 RemoteViews(context.packageName, R.layout.autofill_row).apply {74 setTextViewText(R.id.autofill_row_title, title)75 setTextViewText(R.id.autofill_row_subtitle, subtitle)76 }7778 79808182 fun datasetFor(83 context: Context,84 parsed: ParsedStructure,85 entry: VaultEntry,86 inlineSpec: android.widget.inline.InlinePresentationSpec?,87 ): Dataset? {88 val menu = menuRow(context, entry)89 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {90 buildInline(context, entry, inlineSpec)91 } else {92 null93 }9495 val builder = Dataset.Builder()96 var filledAny = false97 fun put(id: AutofillId?, value: String?) {98 if (id == null || value.isNullOrEmpty()) return99 val av = AutofillValue.forText(value)100 if (inline != null && Build.VERSION.SDK_INT >= 30) {101 builder.setValue(id, av, menu, inline)102 } else {103 builder.setValue(id, av, menu)104 }105 filledAny = true106 }107 put(parsed.identityId, entry.username ?: entry.email)108 put(parsed.passwordId, entry.password)109 put(parsed.otpId, entry.totp?.let { TOTPGenerator.generateCode(it) })110111 return if (filledAny) builder.build() else null112 }113114 115 116 @SuppressLint("RestrictedApi")117 @RequiresApi(30)118 private fun buildInline(119 context: Context,120 entry: VaultEntry,121 spec: android.widget.inline.InlinePresentationSpec,122 ): InlinePresentation? = try {123 val style = spec.style124 if (!UiVersions.getVersions(style).contains(UiVersions.INLINE_UI_VERSION_1)) {125 null126 } else {127 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""128 val pending = AutofillIntents.noopChipIntent(context)129 val slice = InlineSuggestionUi.newContentBuilder(pending)130 .setTitle(entry.title)131 .apply { if (subtitle.isNotBlank()) setSubtitle(subtitle) }132 .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))133 .build()134 .slice135 InlinePresentation(slice, spec, false)136 }137 } catch (_: Throwable) {138 139 null140 }141}