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.VaultEntry18import com.peachpasswords.vault.isCard19202122232425object AutofillResponseBuilder {2627 28293031 data class MatchResult(val hits: List<VaultEntry>, val rest: List<VaultEntry>) {32 val all: List<VaultEntry> get() = hits + rest33 }3435 fun matchEntries(entries: List<VaultEntry>, origin: String?): MatchResult {36 val originDomain = origin?.let(::extractDomain)37 ?: return MatchResult(emptyList(), emptyList())38 return matchDomains(entries, setOf(originDomain))39 }4041 fun matchEntries(entries: List<VaultEntry>, parsed: ParsedStructure): MatchResult = when (parsed.originTrust) {42 AutofillOriginTrust.TRUSTED_WEB -> matchEntries(entries, parsed.webDomain)43 AutofillOriginTrust.TRUSTED_NATIVE -> matchDomains(44 entries,45 parsed.verifiedDomains.mapNotNull(::extractDomain).toSet(),46 )47 AutofillOriginTrust.UNKNOWN -> MatchResult(emptyList(), emptyList())48 }4950 private fun matchDomains(entries: List<VaultEntry>, domains: Set<String>): MatchResult =51 MatchResult(52 hits = entries.filter { entry ->53 domains.any { domain -> entry.matchesOriginDomain(domain) }54 },55 rest = emptyList(),56 )5758 private fun VaultEntry.matchesOriginDomain(originDomain: String): Boolean =59 sequenceOf(url)60 .plus(additionalUrls.orEmpty().asSequence())61 .filterNotNull()62 .mapNotNull(::extractDomain)63 .any { it == originDomain }6465 fun menuRow(context: Context, entry: VaultEntry): RemoteViews {66 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""67 return RemoteViews(context.packageName, R.layout.autofill_row).apply {68 setTextViewText(R.id.autofill_row_title, entry.title)69 setTextViewText(R.id.autofill_row_subtitle, subtitle)70 }71 }7273 fun simpleRow(context: Context, title: String, subtitle: String): RemoteViews =74 RemoteViews(context.packageName, R.layout.autofill_row).apply {75 setTextViewText(R.id.autofill_row_title, title)76 setTextViewText(R.id.autofill_row_subtitle, subtitle)77 }7879 80818283 fun datasetFor(84 context: Context,85 parsed: ParsedStructure,86 entry: VaultEntry,87 inlineSpec: android.widget.inline.InlinePresentationSpec?,88 ): Dataset? {89 val menu = menuRow(context, entry)90 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {91 buildInline(context, entry, inlineSpec)92 } else {93 null94 }9596 val builder = Dataset.Builder()97 var filledAny = false98 fun put(id: AutofillId?, value: String?) {99 if (id == null || value.isNullOrEmpty()) return100 val av = AutofillValue.forText(value)101 if (inline != null && Build.VERSION.SDK_INT >= 30) {102 builder.setValue(id, av, menu, inline)103 } else {104 builder.setValue(id, av, menu)105 }106 filledAny = true107 }108 put(parsed.identityId, entry.username ?: entry.email)109 put(parsed.passwordId, entry.password)110 put(parsed.otpId, entry.totp?.let { TOTPGenerator.generateCode(it) })111112 return if (filledAny) builder.build() else null113 }114115 116117118119 fun cardDatasetFor(120 context: Context,121 parsed: ParsedStructure,122 entry: VaultEntry,123 inlineSpec: android.widget.inline.InlinePresentationSpec?,124 ): Dataset? {125 if (!entry.isCard()) return null126 val menu = menuRow(context, entry)127 val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {128 buildInline(context, entry, inlineSpec)129 } else {130 null131 }132133 val builder = Dataset.Builder()134 var filledAny = false135 fun put(id: AutofillId?, value: String?) {136 if (id == null || value.isNullOrEmpty()) return137 val av = AutofillValue.forText(value)138 if (inline != null && Build.VERSION.SDK_INT >= 30) {139 builder.setValue(id, av, menu, inline)140 } else {141 builder.setValue(id, av, menu)142 }143 filledAny = true144 }145146 val digits = entry.cardNumber?.filter { it.isDigit() }147 put(parsed.cardNumberId, digits)148 put(parsed.cardExpMonthId, entry.expMonth)149 put(parsed.cardExpYearId, entry.expYear)150 if (parsed.cardExpDateId != null && !entry.expMonth.isNullOrEmpty() && !entry.expYear.isNullOrEmpty()) {151 val yy = entry.expYear.takeLast(2)152 put(parsed.cardExpDateId, "${entry.expMonth.padStart(2, '0')}/$yy")153 }154 put(parsed.cardCvcId, entry.cvc)155 put(parsed.cardNameId, entry.cardholderName)156157 return if (filledAny) builder.build() else null158 }159160 161 162 @SuppressLint("RestrictedApi")163 @RequiresApi(30)164 private fun buildInline(165 context: Context,166 entry: VaultEntry,167 spec: android.widget.inline.InlinePresentationSpec,168 ): InlinePresentation? = try {169 val style = spec.style170 if (!UiVersions.getVersions(style).contains(UiVersions.INLINE_UI_VERSION_1)) {171 null172 } else {173 val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""174 val pending = AutofillIntents.noopChipIntent(context)175 val slice = InlineSuggestionUi.newContentBuilder(pending)176 .setTitle(entry.title)177 .apply { if (subtitle.isNotBlank()) setSubtitle(subtitle) }178 .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))179 .build()180 .slice181 InlinePresentation(slice, spec, false)182 }183 } catch (_: Throwable) {184 185 null186 }187}