1package com.peachpasswords.autofill23import android.os.Build4import android.os.CancellationSignal5import android.service.autofill.AutofillService6import android.service.autofill.FillCallback7import android.service.autofill.FillRequest8import android.service.autofill.FillResponse9import android.service.autofill.SaveCallback10import android.service.autofill.SaveRequest11import com.peachpasswords.vault.VaultRepository12import dagger.hilt.android.AndroidEntryPoint13import javax.inject.Inject141516171819202122232425@AndroidEntryPoint26class PeachAutofillService : AutofillService() {2728 @Inject lateinit var vault: VaultRepository2930 override fun onFillRequest(31 request: FillRequest,32 cancellationSignal: CancellationSignal,33 callback: FillCallback,34 ) {35 val context = applicationContext36 val structure = latestStructure(request) ?: run { callback.onSuccess(null); return }37 val parsed = AssistStructureParser.parse(structure)38 if (!parsed.hasUsableField) { callback.onSuccess(null); return }3940 41 42 43 if (parsed.webDomain != null) {44 val clientPackage = structure.activityComponent?.packageName ?: parsed.packageName45 if (!TrustedBrowsers.isTrusted(clientPackage)) { callback.onSuccess(null); return }46 }4748 val inlineSpecs = inlineSpecs(request)49 val response = FillResponse.Builder()50 var datasetCount = 05152 if (vault.isUnlocked.value) {53 val matches = AutofillResponseBuilder54 .matchEntries(vault.getEntries(), parsed.origin)55 .all.take(MAX_DATASETS)56 matches.forEachIndexed { index, entry ->57 val spec = inlineSpecs.getOrNull(index)58 AutofillResponseBuilder.datasetFor(context, parsed, entry, spec)?.let {59 response.addDataset(it)60 datasetCount++61 }62 }63 }6465 66 val pickerLabel = when {67 !vault.isUnlocked.value -> "Unlock Peach"68 datasetCount == 0 -> "No saved logins for this site"69 else -> "Search Peach…"70 }71 runCatching {72 response.addDataset(pickerDataset(context, parsed, pickerLabel, inlineSpecs.getOrNull(datasetCount)))73 datasetCount++74 }7576 if (datasetCount == 0) {77 callback.onSuccess(null)78 } else {79 ignoreUnfilledIds(response, parsed)80 callback.onSuccess(response.build())81 }82 }8384 private fun pickerDataset(85 context: android.content.Context,86 parsed: ParsedStructure,87 label: String,88 inlineSpec: android.widget.inline.InlinePresentationSpec?,89 ): android.service.autofill.Dataset {90 val menu = AutofillResponseBuilder.simpleRow(context, label, parsed.origin ?: "")91 val builder = android.service.autofill.Dataset.Builder(menu)92 93 94 listOfNotNull(parsed.identityId, parsed.passwordId, parsed.otpId).forEach { id ->95 builder.setValue(id, null, menu)96 }97 builder.setAuthentication(AutofillIntents.pickerIntentSender(context, parsed))98 return builder.build()99 }100101 102 private fun ignoreUnfilledIds(builder: FillResponse.Builder, parsed: ParsedStructure) {103 val ids = listOfNotNull(parsed.identityId, parsed.passwordId, parsed.otpId)104 if (ids.isNotEmpty()) builder.setIgnoredIds(*ids.toTypedArray())105 }106107 private fun latestStructure(request: FillRequest): android.app.assist.AssistStructure? =108 request.fillContexts.lastOrNull()?.structure109110 private fun inlineSpecs(request: FillRequest): List<android.widget.inline.InlinePresentationSpec> {111 if (Build.VERSION.SDK_INT < 30) return emptyList()112 return request.inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()113 }114115 override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {116 117 callback.onSuccess()118 }119120 private companion object {121 const val MAX_DATASETS = 6122 }123}