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 ->46 val entryDomain = extractDomain(entry.url ?: "") ?: return@filter false47 entryDomain == originDomain48 }49 return MatchResult(hits = hits, rest = emptyList())50 }5152 fun menuRow(context: Context, entry: VaultEntry): RemoteViews {53 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""54 return RemoteViews(context.packageName, R.layout.autofill_row).apply {55 setTextViewText(R.id.autofill_row_title, entry.title)56 setTextViewText(R.id.autofill_row_subtitle, subtitle)57 }58 }5960 fun simpleRow(context: Context, title: String, subtitle: String): RemoteViews =61 RemoteViews(context.packageName, R.layout.autofill_row).apply {62 setTextViewText(R.id.autofill_row_title, title)63 setTextViewText(R.id.autofill_row_subtitle, subtitle)64 }6566 67686970 fun datasetFor(71 context: Context,72 parsed: ParsedStructure,73 entry: VaultEntry,74 inlineSpec: android.widget.inline.InlinePresentationSpec?,75 ): Dataset? {76 val menu = menuRow(context, entry)77 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {78 buildInline(context, entry, inlineSpec)79 } else {80 null81 }8283 val builder = Dataset.Builder()84 var filledAny = false85 fun put(id: AutofillId?, value: String?) {86 if (id == null || value.isNullOrEmpty()) return87 val av = AutofillValue.forText(value)88 if (inline != null && Build.VERSION.SDK_INT >= 30) {89 builder.setValue(id, av, menu, inline)90 } else {91 builder.setValue(id, av, menu)92 }93 filledAny = true94 }95 put(parsed.identityId, entry.username ?: entry.email)96 put(parsed.passwordId, entry.password)97 put(parsed.otpId, entry.totp?.let { TOTPGenerator.generateCode(it) })9899 return if (filledAny) builder.build() else null100 }101102 103 104 @SuppressLint("RestrictedApi")105 @RequiresApi(30)106 private fun buildInline(107 context: Context,108 entry: VaultEntry,109 spec: android.widget.inline.InlinePresentationSpec,110 ): InlinePresentation? = try {111 val style = spec.style112 if (!UiVersions.getVersions(style).contains(UiVersions.INLINE_UI_VERSION_1)) {113 null114 } else {115 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""116 val pending = AutofillIntents.noopChipIntent(context)117 val slice = InlineSuggestionUi.newContentBuilder(pending)118 .setTitle(entry.title)119 .apply { if (subtitle.isNotBlank()) setSubtitle(subtitle) }120 .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))121 .build()122 .slice123 InlinePresentation(slice, spec, false)124 }125 } catch (_: Throwable) {126 127 null128 }129}