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.SaveInfo11import android.service.autofill.SaveRequest12import com.peachpasswords.vault.VaultRepository13import dagger.hilt.android.AndroidEntryPoint14import kotlinx.coroutines.CoroutineScope15import kotlinx.coroutines.Dispatchers16import kotlinx.coroutines.SupervisorJob17import kotlinx.coroutines.cancel18import kotlinx.coroutines.launch19import kotlinx.coroutines.withContext20import javax.inject.Inject212223242526272829303132@AndroidEntryPoint33class PeachAutofillService : AutofillService() {3435 @Inject lateinit var vault: VaultRepository36 private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)3738 override fun onFillRequest(39 request: FillRequest,40 cancellationSignal: CancellationSignal,41 callback: FillCallback,42 ) {43 val context = applicationContext44 val structure = latestStructure(request) ?: run { callback.onSuccess(null); return }45 val parsed = AssistStructureParser.parse(structure)46 if (!parsed.hasUsableField) { callback.onSuccess(null); return }4748 49 50 51 if (parsed.webDomain != null) {52 val clientPackage = structure.activityComponent?.packageName ?: parsed.packageName53 if (!TrustedBrowsers.isTrusted(clientPackage)) { callback.onSuccess(null); return }54 }5556 val inlineSpecs = inlineSpecs(request)57 val response = FillResponse.Builder()58 var datasetCount = 05960 if (vault.isUnlocked.value) {61 val matches = AutofillResponseBuilder62 .matchEntries(vault.getEntries(), parsed.origin)63 .all.take(MAX_DATASETS)64 matches.forEachIndexed { index, entry ->65 val spec = inlineSpecs.getOrNull(index)66 AutofillResponseBuilder.datasetFor(context, parsed, entry, spec)?.let {67 response.addDataset(it)68 datasetCount++69 }70 }71 }7273 74 val pickerLabel = when {75 !vault.isUnlocked.value -> "Unlock Peach"76 datasetCount == 0 -> "No saved logins for this site"77 else -> "Search Peach…"78 }79 runCatching {80 response.addDataset(pickerDataset(context, parsed, pickerLabel, inlineSpecs.getOrNull(datasetCount)))81 datasetCount++82 }8384 if (datasetCount == 0) {85 callback.onSuccess(null)86 } else {87 ignoreUnfilledIds(response, parsed)88 addSaveInfo(response, parsed)89 callback.onSuccess(response.build())90 }91 }9293 private fun pickerDataset(94 context: android.content.Context,95 parsed: ParsedStructure,96 label: String,97 inlineSpec: android.widget.inline.InlinePresentationSpec?,98 ): android.service.autofill.Dataset {99 val menu = AutofillResponseBuilder.simpleRow(context, label, parsed.origin ?: "")100 val builder = android.service.autofill.Dataset.Builder(menu)101 102 103 listOfNotNull(parsed.identityId, parsed.passwordId, parsed.otpId).forEach { id ->104 builder.setValue(id, null, menu)105 }106 builder.setAuthentication(AutofillIntents.pickerIntentSender(context, parsed))107 return builder.build()108 }109110 111 private fun ignoreUnfilledIds(builder: FillResponse.Builder, parsed: ParsedStructure) {112 val ids = listOfNotNull(parsed.identityId, parsed.passwordId, parsed.otpId)113 if (ids.isNotEmpty()) builder.setIgnoredIds(*ids.toTypedArray())114 }115116 private fun addSaveInfo(builder: FillResponse.Builder, parsed: ParsedStructure) {117 val passwordId = parsed.passwordId ?: return118 val optionalIds = listOfNotNull(parsed.identityId, parsed.otpId).toTypedArray()119 val saveInfo = SaveInfo.Builder(120 SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,121 arrayOf(passwordId),122 ).apply {123 if (optionalIds.isNotEmpty()) setOptionalIds(optionalIds)124 }.build()125 builder.setSaveInfo(saveInfo)126 }127128 private fun latestStructure(request: FillRequest): android.app.assist.AssistStructure? =129 request.fillContexts.lastOrNull()?.structure130131 private fun inlineSpecs(request: FillRequest): List<android.widget.inline.InlinePresentationSpec> {132 if (Build.VERSION.SDK_INT < 30) return emptyList()133 return request.inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()134 }135136 override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {137 val structure = request.fillContexts.lastOrNull()?.structure138 if (structure == null || !vault.isUnlocked.value) {139 callback.onSuccess()140 return141 }142 val parsed = AssistStructureParser.parse(structure)143 if (parsed.webDomain != null) {144 val clientPackage = structure.activityComponent?.packageName ?: parsed.packageName145 if (!TrustedBrowsers.isTrusted(clientPackage)) {146 callback.onSuccess()147 return148 }149 }150 val draft = autofillSaveDraft(parsed)151 if (draft == null) {152 callback.onSuccess()153 return154 }155 serviceScope.launch {156 try {157 withContext(Dispatchers.IO) {158 when (val plan = planAutofillSave(vault.getEntries(), draft)) {159 is AutofillSavePlan.Add -> vault.addEntry(plan.entry)160 is AutofillSavePlan.Update -> vault.updateEntry(plan.entry)161 }162 }163 callback.onSuccess()164 } catch (e: Exception) {165 callback.onFailure(e.message ?: "Peach could not save this login")166 }167 }168 }169170 override fun onDestroy() {171 serviceScope.cancel()172 super.onDestroy()173 }174175 private companion object {176 const val MAX_DATASETS = 6177 }178}