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 27 private const val FALLBACK_LIMIT = 62829 3031323334353637 data class MatchResult(val hits: List<VaultEntry>, val rest: List<VaultEntry>) {38 val all: List<VaultEntry> get() = hits + rest39 }4041 fun matchEntries(entries: List<VaultEntry>, origin: String?): MatchResult {42 if (origin.isNullOrBlank()) return MatchResult(emptyList(), entries.take(FALLBACK_LIMIT))43 val originDomain = extractDomain(origin)44 ?: return MatchResult(emptyList(), entries.take(FALLBACK_LIMIT))45 val hits = entries.filter { entry -> entry.matchesOriginDomain(originDomain) }46 return MatchResult(hits = hits, rest = emptyList())47 }4849 private fun VaultEntry.matchesOriginDomain(originDomain: String): Boolean =50 sequenceOf(url)51 .plus(additionalUrls.orEmpty().asSequence())52 .filterNotNull()53 .mapNotNull(::extractDomain)54 .any { it == originDomain }5556 fun menuRow(context: Context, entry: VaultEntry): RemoteViews {57 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""58 return RemoteViews(context.packageName, R.layout.autofill_row).apply {59 setTextViewText(R.id.autofill_row_title, entry.title)60 setTextViewText(R.id.autofill_row_subtitle, subtitle)61 }62 }6364 fun simpleRow(context: Context, title: String, subtitle: String): RemoteViews =65 RemoteViews(context.packageName, R.layout.autofill_row).apply {66 setTextViewText(R.id.autofill_row_title, title)67 setTextViewText(R.id.autofill_row_subtitle, subtitle)68 }6970 71727374 fun datasetFor(75 context: Context,76 parsed: ParsedStructure,77 entry: VaultEntry,78 inlineSpec: android.widget.inline.InlinePresentationSpec?,79 ): Dataset? {80 val menu = menuRow(context, entry)81 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {82 buildInline(context, entry, inlineSpec)83 } else {84 null85 }8687 val builder = Dataset.Builder()88 var filledAny = false89 fun put(id: AutofillId?, value: String?) {90 if (id == null || value.isNullOrEmpty()) return91 val av = AutofillValue.forText(value)92 if (inline != null && Build.VERSION.SDK_INT >= 30) {93 builder.setValue(id, av, menu, inline)94 } else {95 builder.setValue(id, av, menu)96 }97 filledAny = true98 }99 put(parsed.identityId, entry.username ?: entry.email)100 put(parsed.passwordId, entry.password)101 put(parsed.otpId, entry.totp?.let { TOTPGenerator.generateCode(it) })102103 return if (filledAny) builder.build() else null104 }105106 107 108 @SuppressLint("RestrictedApi")109 @RequiresApi(30)110 private fun buildInline(111 context: Context,112 entry: VaultEntry,113 spec: android.widget.inline.InlinePresentationSpec,114 ): InlinePresentation? = try {115 val style = spec.style116 if (!UiVersions.getVersions(style).contains(UiVersions.INLINE_UI_VERSION_1)) {117 null118 } else {119 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""120 val pending = AutofillIntents.noopChipIntent(context)121 val slice = InlineSuggestionUi.newContentBuilder(pending)122 .setTitle(entry.title)123 .apply { if (subtitle.isNotBlank()) setSubtitle(subtitle) }124 .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))125 .build()126 .slice127 InlinePresentation(slice, spec, false)128 }129 } catch (_: Throwable) {130 131 null132 }133}