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 com.peachpasswords.vault.isCard14import dagger.hilt.android.AndroidEntryPoint15import kotlinx.coroutines.CoroutineScope16import kotlinx.coroutines.Dispatchers17import kotlinx.coroutines.SupervisorJob18import kotlinx.coroutines.cancel19import kotlinx.coroutines.launch20import kotlinx.coroutines.withContext21import javax.inject.Inject222324252627282930313233@AndroidEntryPoint34class PeachAutofillService : AutofillService() {3536 @Inject lateinit var vault: VaultRepository37 private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)3839 override fun onFillRequest(40 request: FillRequest,41 cancellationSignal: CancellationSignal,42 callback: FillCallback,43 ) {44 val context = applicationContext45 val structure = latestStructure(request) ?: run { callback.onSuccess(null); return }46 val parsed = resolveAutofillOrigin(context, AssistStructureParser.parse(structure))47 if (!parsed.hasCredentialField) { callback.onSuccess(null); return }4849 val inlineSpecs = inlineSpecs(request)50 val privatePreview = vault.settings.value.hideAutofillPreviews51 val response = FillResponse.Builder()52 var datasetCount = 053 var hasLoginMatch = false5455 if (vault.isUnlocked.value) {56 val matches = AutofillResponseBuilder57 .matchEntries(vault.getEntries(), parsed)58 .all.take(MAX_DATASETS)59 matches.forEachIndexed { index, entry ->60 val spec = inlineSpecs.getOrNull(index)61 AutofillResponseBuilder.datasetFor(context, parsed, entry, spec, privatePreview)?.let {62 response.addDataset(it)63 datasetCount++64 hasLoginMatch = true65 }66 }67 }6869 if (parsed.hasCardField && vault.isUnlocked.value) {70 vault.getEntries().filter { it.isCard() }.take(MAX_DATASETS - datasetCount).forEachIndexed { index, entry ->71 val spec = inlineSpecs.getOrNull(datasetCount + index)72 AutofillResponseBuilder.cardDatasetFor(context, parsed, entry, spec, privatePreview)?.let {73 response.addDataset(it)74 datasetCount++75 }76 }77 }7879 80 81 82 83 if (vault.isUnlocked.value && parsed.passwordId != null &&84 (parsed.isNewPassword || !hasLoginMatch)85 ) {86 runCatching {87 AutofillResponseBuilder.generateDatasetFor(context, parsed)?.let {88 response.addDataset(it)89 datasetCount++90 }91 }92 }9394 95 val pickerLabel = autofillPickerLabel(96 unlocked = vault.isUnlocked.value,97 datasetCount = datasetCount,98 )99 runCatching {100 response.addDataset(pickerDataset(context, parsed, pickerLabel, inlineSpecs.getOrNull(datasetCount)))101 datasetCount++102 }103104 if (datasetCount == 0) {105 callback.onSuccess(null)106 } else {107 ignoreUnfilledIds(response, parsed)108 addSaveInfo(response, parsed)109 callback.onSuccess(response.build())110 }111 }112113 private fun pickerDataset(114 context: android.content.Context,115 parsed: ParsedStructure,116 label: String,117 inlineSpec: android.widget.inline.InlinePresentationSpec?,118 ): android.service.autofill.Dataset {119 val subtitle = parsed.origin.orEmpty()120 val menu = AutofillResponseBuilder.simpleRow(context, label, subtitle)121 val builder = android.service.autofill.Dataset.Builder(menu)122 123 124 (listOfNotNull(125 parsed.identityId, parsed.passwordId, parsed.otpId,126 parsed.cardNumberId, parsed.cardExpMonthId, parsed.cardExpYearId,127 parsed.cardExpDateId, parsed.cardCvcId, parsed.cardNameId,128 ) + parsed.otpDigitBoxIds).forEach { id ->129 builder.setValue(id, null, menu)130 }131 builder.setAuthentication(AutofillIntents.pickerIntentSender(context, parsed))132 return builder.build()133 }134135 136 private fun ignoreUnfilledIds(builder: FillResponse.Builder, parsed: ParsedStructure) {137 val ids = listOfNotNull(138 parsed.identityId, parsed.passwordId, parsed.otpId,139 parsed.cardNumberId, parsed.cardExpMonthId, parsed.cardExpYearId,140 parsed.cardExpDateId, parsed.cardCvcId, parsed.cardNameId,141 ) + parsed.otpDigitBoxIds142 if (ids.isNotEmpty()) builder.setIgnoredIds(*ids.toTypedArray())143 }144145 private fun addSaveInfo(builder: FillResponse.Builder, parsed: ParsedStructure) {146 val passwordId = parsed.passwordId ?: return147 val optionalIds = listOfNotNull(parsed.identityId, parsed.otpId).toTypedArray()148 val saveInfo = SaveInfo.Builder(149 SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,150 arrayOf(passwordId),151 ).apply {152 if (optionalIds.isNotEmpty()) setOptionalIds(optionalIds)153 }.build()154 builder.setSaveInfo(saveInfo)155 }156157 private fun latestStructure(request: FillRequest): android.app.assist.AssistStructure? =158 request.fillContexts.lastOrNull()?.structure159160 private fun inlineSpecs(request: FillRequest): List<android.widget.inline.InlinePresentationSpec> {161 if (Build.VERSION.SDK_INT < 30) return emptyList()162 return request.inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()163 }164165 override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {166 val structure = request.fillContexts.lastOrNull()?.structure167 if (structure == null || !vault.isUnlocked.value) {168 callback.onSuccess()169 return170 }171 val parsed = resolveAutofillOrigin(applicationContext, AssistStructureParser.parse(structure))172 val draft = autofillSaveDraft(parsed)173 if (draft == null) {174 callback.onSuccess()175 return176 }177 serviceScope.launch {178 try {179 withContext(Dispatchers.IO) {180 when (val plan = planAutofillSave(vault.getEntries(), draft)) {181 is AutofillSavePlan.Add -> vault.addEntry(plan.entry)182 is AutofillSavePlan.Update -> vault.updateEntry(plan.entry)183 }184 }185 callback.onSuccess()186 } catch (e: Exception) {187 callback.onFailure(e.message ?: "Peach could not save this login")188 }189 }190 }191192 override fun onDestroy() {193 serviceScope.cancel()194 super.onDestroy()195 }196197 private companion object {198 const val MAX_DATASETS = 6199 }200}201202internal fun autofillPickerLabel(unlocked: Boolean, datasetCount: Int): String = when {203 !unlocked -> "Unlock Peach"204 datasetCount == 0 -> "No saved logins for this site"205 else -> "Search Peach…"206}