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 unlocked = vault.isUnlocked.value52 val response = FillResponse.Builder()53 var datasetCount = 054 var hasLoginMatch = false55 56 57 58 59 60 val systemChipCount = inlineSystemChipCount(inlineSpecs.size)61 val entryChipBudget = (inlineSpecs.size - systemChipCount).coerceAtLeast(0)62 val pickerSpec = if (inlineSpecs.size >= 2) {63 inlineSpecs[inlineSpecs.size - 2]64 } else {65 inlineSpecs.lastOrNull()66 }67 val createSpec = inlineSpecs.lastOrNull()68 var entryChips = 06970 if (unlocked) {71 72 73 74 75 val matches = AutofillResponseBuilder.matchEntries(vault.getEntries(), parsed).all76 matches.forEach { entry ->77 val spec = if (entryChips < entryChipBudget) inlineSpecs[entryChips++] else null78 AutofillResponseBuilder.datasetFor(context, parsed, entry, spec, privatePreview)?.let {79 response.addDataset(it)80 datasetCount++81 hasLoginMatch = true82 }83 }84 }8586 if (parsed.hasCardField && unlocked) {87 vault.getEntries().filter { it.isCard() }.forEach { entry ->88 val spec = if (entryChips < entryChipBudget) inlineSpecs[entryChips++] else null89 AutofillResponseBuilder.cardDatasetFor(context, parsed, entry, spec, privatePreview)?.let {90 response.addDataset(it)91 datasetCount++92 }93 }94 }9596 97 98 99 100 if (unlocked && parsed.passwordId != null &&101 (parsed.isNewPassword || !hasLoginMatch)102 ) {103 runCatching {104 AutofillResponseBuilder.generateDatasetFor(context, parsed)?.let {105 response.addDataset(it)106 datasetCount++107 }108 }109 }110111 112 113 114 115 runCatching {116 response.addDataset(117 AutofillResponseBuilder.searchPeachDataset(118 context = context,119 parsed = parsed,120 inlineSpec = pickerSpec,121 unlocked = unlocked,122 hasMatches = hasLoginMatch,123 )124 )125 datasetCount++126 }127 AutofillResponseBuilder.createNewEntryDataset(128 context = context,129 parsed = parsed,130 inlineSpec = createSpec,131 unlocked = unlocked,132 origin = parsed.origin,133 )?.let {134 response.addDataset(it)135 datasetCount++136 }137138 if (datasetCount == 0) {139 callback.onSuccess(null)140 } else {141 ignoreUnfilledIds(response, parsed)142 addSaveInfo(response, parsed)143 callback.onSuccess(response.build())144 }145 }146147 148 private fun ignoreUnfilledIds(builder: FillResponse.Builder, parsed: ParsedStructure) {149 val ids = listOfNotNull(150 parsed.identityId, parsed.passwordId, parsed.otpId,151 parsed.cardNumberId, parsed.cardExpMonthId, parsed.cardExpYearId,152 parsed.cardExpDateId, parsed.cardCvcId, parsed.cardNameId,153 ) + parsed.otpDigitBoxIds154 if (ids.isNotEmpty()) builder.setIgnoredIds(*ids.toTypedArray())155 }156157 private fun addSaveInfo(builder: FillResponse.Builder, parsed: ParsedStructure) {158 val passwordId = parsed.passwordId ?: return159 val optionalIds = listOfNotNull(parsed.identityId, parsed.otpId).toTypedArray()160 val saveInfo = SaveInfo.Builder(161 SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,162 arrayOf(passwordId),163 ).apply {164 if (optionalIds.isNotEmpty()) setOptionalIds(optionalIds)165 }.build()166 builder.setSaveInfo(saveInfo)167 }168169 private fun latestStructure(request: FillRequest): android.app.assist.AssistStructure? =170 request.fillContexts.lastOrNull()?.structure171172 private fun inlineSpecs(request: FillRequest): List<android.widget.inline.InlinePresentationSpec> {173 if (Build.VERSION.SDK_INT < 30) return emptyList()174 return request.inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()175 }176177 override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {178 val structure = request.fillContexts.lastOrNull()?.structure179 if (structure == null || !vault.isUnlocked.value) {180 callback.onSuccess()181 return182 }183 val parsed = resolveAutofillOrigin(applicationContext, AssistStructureParser.parse(structure))184 val draft = autofillSaveDraft(parsed)185 if (draft == null) {186 callback.onSuccess()187 return188 }189 serviceScope.launch {190 try {191 withContext(Dispatchers.IO) {192 when (val plan = planAutofillSave(vault.getEntries(), draft)) {193 is AutofillSavePlan.Add -> vault.addEntry(plan.entry)194 is AutofillSavePlan.Update -> vault.updateEntry(plan.entry)195 }196 }197 callback.onSuccess()198 } catch (e: Exception) {199 callback.onFailure(e.message ?: "Peach could not save this login")200 }201 }202 }203204 override fun onDestroy() {205 serviceScope.cancel()206 super.onDestroy()207 }208209}210211212213214215216217internal fun inlineSystemChipCount(totalSpecs: Int): Int = if (totalSpecs >= 2) 2 else if (totalSpecs == 1) 1 else 0218219220221222223224internal fun inlineChipBudget(totalSpecs: Int): Int = (totalSpecs - inlineSystemChipCount(totalSpecs)).coerceAtLeast(0)225226internal fun autofillPickerLabel(unlocked: Boolean, datasetCount: Int): String = when {227 !unlocked -> "Unlock Peach"228 datasetCount == 0 -> "No saved logins for this site"229 else -> "Search Peach…"230}