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 = authenticateAutofillOrigin(context, AssistStructureParser.parse(structure))47 if (!parsed.hasUsableField) { callback.onSuccess(null); return }4849 val inlineSpecs = inlineSpecs(request)50 val response = FillResponse.Builder()51 var datasetCount = 05253 if (vault.isUnlocked.value) {54 val matches = AutofillResponseBuilder55 .matchEntries(vault.getEntries(), parsed)56 .all.take(MAX_DATASETS)57 matches.forEachIndexed { index, entry ->58 val spec = inlineSpecs.getOrNull(index)59 AutofillResponseBuilder.datasetFor(context, parsed, entry, spec)?.let {60 response.addDataset(it)61 datasetCount++62 }63 }64 }6566 if (parsed.hasCardField && vault.isUnlocked.value) {67 vault.getEntries().filter { it.isCard() }.take(MAX_DATASETS - datasetCount).forEachIndexed { index, entry ->68 val spec = inlineSpecs.getOrNull(datasetCount + index)69 AutofillResponseBuilder.cardDatasetFor(context, parsed, entry, spec)?.let {70 response.addDataset(it)71 datasetCount++72 }73 }74 }7576 77 val pickerLabel = if (parsed.originTrust == AutofillOriginTrust.UNKNOWN && vault.isUnlocked.value) {78 "Verify site in Peach…"79 } else {80 autofillPickerLabel(81 unlocked = vault.isUnlocked.value,82 datasetCount = datasetCount,83 )84 }85 runCatching {86 response.addDataset(pickerDataset(context, parsed, pickerLabel, inlineSpecs.getOrNull(datasetCount)))87 datasetCount++88 }8990 if (datasetCount == 0) {91 callback.onSuccess(null)92 } else {93 ignoreUnfilledIds(response, parsed)94 if (parsed.originTrust == AutofillOriginTrust.TRUSTED_WEB) {95 addSaveInfo(response, parsed)96 }97 callback.onSuccess(response.build())98 }99 }100101 private fun pickerDataset(102 context: android.content.Context,103 parsed: ParsedStructure,104 label: String,105 inlineSpec: android.widget.inline.InlinePresentationSpec?,106 ): android.service.autofill.Dataset {107 val subtitle = if (parsed.originTrust == AutofillOriginTrust.UNKNOWN) {108 "Origin not verified"109 } else {110 parsed.origin.orEmpty()111 }112 val menu = AutofillResponseBuilder.simpleRow(context, label, subtitle)113 val builder = android.service.autofill.Dataset.Builder(menu)114 115 116 listOfNotNull(117 parsed.identityId, parsed.passwordId, parsed.otpId,118 parsed.cardNumberId, parsed.cardExpMonthId, parsed.cardExpYearId,119 parsed.cardExpDateId, parsed.cardCvcId, parsed.cardNameId,120 ).forEach { id ->121 builder.setValue(id, null, menu)122 }123 builder.setAuthentication(AutofillIntents.pickerIntentSender(context, parsed))124 return builder.build()125 }126127 128 private fun ignoreUnfilledIds(builder: FillResponse.Builder, parsed: ParsedStructure) {129 val ids = listOfNotNull(130 parsed.identityId, parsed.passwordId, parsed.otpId,131 parsed.cardNumberId, parsed.cardExpMonthId, parsed.cardExpYearId,132 parsed.cardExpDateId, parsed.cardCvcId, parsed.cardNameId,133 )134 if (ids.isNotEmpty()) builder.setIgnoredIds(*ids.toTypedArray())135 }136137 private fun addSaveInfo(builder: FillResponse.Builder, parsed: ParsedStructure) {138 val passwordId = parsed.passwordId ?: return139 val optionalIds = listOfNotNull(parsed.identityId, parsed.otpId).toTypedArray()140 val saveInfo = SaveInfo.Builder(141 SaveInfo.SAVE_DATA_TYPE_USERNAME or SaveInfo.SAVE_DATA_TYPE_PASSWORD,142 arrayOf(passwordId),143 ).apply {144 if (optionalIds.isNotEmpty()) setOptionalIds(optionalIds)145 }.build()146 builder.setSaveInfo(saveInfo)147 }148149 private fun latestStructure(request: FillRequest): android.app.assist.AssistStructure? =150 request.fillContexts.lastOrNull()?.structure151152 private fun inlineSpecs(request: FillRequest): List<android.widget.inline.InlinePresentationSpec> {153 if (Build.VERSION.SDK_INT < 30) return emptyList()154 return request.inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList()155 }156157 override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {158 val structure = request.fillContexts.lastOrNull()?.structure159 if (structure == null || !vault.isUnlocked.value) {160 callback.onSuccess()161 return162 }163 val parsed = authenticateAutofillOrigin(applicationContext, AssistStructureParser.parse(structure))164 if (parsed.originTrust != AutofillOriginTrust.TRUSTED_WEB) {165 callback.onSuccess()166 return167 }168 val draft = autofillSaveDraft(parsed)169 if (draft == null) {170 callback.onSuccess()171 return172 }173 serviceScope.launch {174 try {175 withContext(Dispatchers.IO) {176 when (val plan = planAutofillSave(vault.getEntries(), draft)) {177 is AutofillSavePlan.Add -> vault.addEntry(plan.entry)178 is AutofillSavePlan.Update -> vault.updateEntry(plan.entry)179 }180 }181 callback.onSuccess()182 } catch (e: Exception) {183 callback.onFailure(e.message ?: "Peach could not save this login")184 }185 }186 }187188 override fun onDestroy() {189 serviceScope.cancel()190 super.onDestroy()191 }192193 private companion object {194 const val MAX_DATASETS = 6195 }196}197198internal fun autofillPickerLabel(unlocked: Boolean, datasetCount: Int): String = when {199 !unlocked -> "Unlock Peach"200 datasetCount == 0 -> "No saved logins for this site"201 else -> "Search Peach…"202}