34ba9216975c

feat: keyboard-row chip polish — entry monograms and an always-reachable picker chip (v2.14.0)

- ChipMonogramIcon: inline chips now carry the entry's monogram — the same titleInitials/titleBackground visual language as the vault list, rendered once per title into a cached bitmap (IMEs render chips outside any Peach window). Privacy mode keeps the plain Peach mark: initials would leak entry titles too. - PreviewLabels gains iconSource (null = Peach mark) - The branded picker chip now always gets an inline slot: the last IME spec is reserved for it (inlineChipBudget), so the full vault stays reachable from the keyboard row even when entry/card chips fill every other slot - 3 new inlineChipBudget tests; autofill + credentials suites green

time
2026-08-04 22:05 UTC · 2 months ago
machine
koh2
parent
5d31fe2bb493
5 files VERSION+1 −1app/src/main/java/com/peachpasswords/autofill/AutofillResponseBuilder.kt+15 −5app/src/main/java/com/peachpasswords/autofill/ChipMonogramIcon.kt+41 −0app/src/main/java/com/peachpasswords/autofill/PeachAutofillService.kt+14 −5app/src/test/java/com/peachpasswords/autofill/AutofillChipBudgetTest.kt+19 −0
VERSION+1 −1
@@ -1 +1 @@1-2.13.01+2.14.0

view file

app/src/main/java/com/peachpasswords/autofill/AutofillResponseBuilder.kt+15 −5
@@ -75,8 +75,12 @@7575             .mapNotNull(::extractDomain)7676             .any { it == originDomain }7777 78-    /** What a suggestion row or keyboard chip shows about an entry. */79-    data class PreviewLabels(val title: String, val subtitle: String)78+    /**79+     * What a suggestion row or keyboard chip shows about an entry.80+     * [iconSource] seeds the chip monogram; null keeps the Peach mark81+     * (always the case in privacy mode — initials would leak titles too).82+     */83+    data class PreviewLabels(val title: String, val subtitle: String, val iconSource: String?)8084 8185     /**8286      * Privacy previews: with [privatePreview] the rows and chips render only@@ -85,9 +89,13 @@8589      */8690     fun previewLabels(privatePreview: Boolean, entry: VaultEntry, originDomain: String?): PreviewLabels =8791         if (privatePreview) {88-            PreviewLabels("Peach", originDomain ?: "")92+            PreviewLabels("Peach", originDomain ?: "", null)8993         } else {90-            PreviewLabels(entry.title, entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: "")94+            PreviewLabels(95+                entry.title,96+                entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: "",97+                entry.title,98+            )9199         }92100 93101     fun rowFor(context: Context, labels: PreviewLabels): RemoteViews =@@ -251,10 +259,12 @@251259             null252260         } else {253261             val pending = AutofillIntents.noopChipIntent(context)262+            val startIcon = labels.iconSource?.let { ChipMonogramIcon.icon(it) }263+                ?: android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark)254264             val slice = InlineSuggestionUi.newContentBuilder(pending)255265                 .setTitle(labels.title)256266                 .apply { if (labels.subtitle.isNotBlank()) setSubtitle(labels.subtitle) }257-                .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))267+                .setStartIcon(startIcon)258268                 .build()259269                 .slice260270             InlinePresentation(slice, spec, false)

view file

app/src/main/java/com/peachpasswords/autofill/ChipMonogramIcon.kt+41 −0
@@ -0,0 +1,41 @@1+package com.peachpasswords.autofill2+3+import android.graphics.Bitmap4+import android.graphics.Canvas5+import android.graphics.Paint6+import android.graphics.Typeface7+import android.graphics.drawable.Icon8+import android.util.LruCache9+import androidx.compose.ui.graphics.toArgb10+import com.peachpasswords.ui.components.titleBackground11+import com.peachpasswords.ui.components.titleInitials12+13+/**14+ * Monogram chip icons for inline autofill suggestions — the same initials and15+ * deterministic background as the vault list (titleInitials/titleBackground),16+ * rendered once per source and cached. The IME renders these outside any17+ * Peach window, so they must be plain bitmaps.18+ */19+object ChipMonogramIcon {20+    private const val SIZE_PX = 6421+    private val cache = LruCache<String, Icon>(32)22+23+    fun icon(source: String): Icon {24+        cache.get(source)?.let { return it }25+        val bitmap = Bitmap.createBitmap(SIZE_PX, SIZE_PX, Bitmap.Config.ARGB_8888)26+        val canvas = Canvas(bitmap)27+        val background = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = titleBackground(source).toArgb() }28+        val radius = SIZE_PX * 0.23f29+        canvas.drawRoundRect(0f, 0f, SIZE_PX.toFloat(), SIZE_PX.toFloat(), radius, radius, background)30+        val text = Paint(Paint.ANTI_ALIAS_FLAG).apply {31+            // T0 ink at ~92%, matching the vault-list initials chip.32+            color = 0xEBF0E8D8.toInt()33+            textSize = SIZE_PX * 0.34f34+            typeface = Typeface.MONOSPACE35+            textAlign = Paint.Align.CENTER36+        }37+        val centerY = SIZE_PX / 2f - (text.descent() + text.ascent()) / 2f38+        canvas.drawText(titleInitials(source), SIZE_PX / 2f, centerY, text)39+        return Icon.createWithBitmap(bitmap).also { cache.put(source, it) }40+    }41+}

view file

app/src/main/java/com/peachpasswords/autofill/PeachAutofillService.kt+14 −5
@@ -51,13 +51,19 @@5151         val response = FillResponse.Builder()5252         var datasetCount = 05353         var hasLoginMatch = false54+        // Reserve the last inline slot for the branded picker chip so the full55+        // vault stays reachable from the keyboard row even when entry chips56+        // fill every other slot.57+        val pickerSpec = inlineSpecs.lastOrNull()58+        val entryChipBudget = inlineChipBudget(inlineSpecs.size)59+        var entryChips = 05460 5561         if (vault.isUnlocked.value) {5662             val matches = AutofillResponseBuilder5763                 .matchEntries(vault.getEntries(), parsed)5864                 .all.take(MAX_DATASETS)59-            matches.forEachIndexed { index, entry ->60-                val spec = inlineSpecs.getOrNull(index)65+            matches.forEach { entry ->66+                val spec = if (entryChips < entryChipBudget) inlineSpecs[entryChips++] else null6167                 AutofillResponseBuilder.datasetFor(context, parsed, entry, spec, privatePreview)?.let {6268                     response.addDataset(it)6369                     datasetCount++@@ -67,8 +73,8 @@6773         }6874 6975         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)76+            vault.getEntries().filter { it.isCard() }.take(MAX_DATASETS - datasetCount).forEach { entry ->77+                val spec = if (entryChips < entryChipBudget) inlineSpecs[entryChips++] else null7278                 AutofillResponseBuilder.cardDatasetFor(context, parsed, entry, spec, privatePreview)?.let {7379                     response.addDataset(it)7480                     datasetCount++@@ -97,7 +103,7 @@97103             datasetCount = datasetCount,98104         )99105         runCatching {100-            response.addDataset(pickerDataset(context, parsed, pickerLabel, inlineSpecs.getOrNull(datasetCount)))106+            response.addDataset(pickerDataset(context, parsed, pickerLabel, pickerSpec))101107             datasetCount++102108         }103109 @@ -198,6 +204,9 @@198204         const val MAX_DATASETS = 6199205     }200206 }207+208+/** Entry/card chips get every inline slot except the one reserved for the picker. */209+internal fun inlineChipBudget(totalSpecs: Int): Int = if (totalSpecs > 0) totalSpecs - 1 else 0201210 202211 internal fun autofillPickerLabel(unlocked: Boolean, datasetCount: Int): String = when {203212     !unlocked -> "Unlock Peach"

view file

app/src/test/java/com/peachpasswords/autofill/AutofillChipBudgetTest.kt+19 −0
@@ -0,0 +1,19 @@1+package com.peachpasswords.autofill2+3+import org.junit.Assert.assertEquals4+import org.junit.Test5+6+class AutofillChipBudgetTest {7+    @Test fun no_specs_means_no_entry_chips() {8+        assertEquals(0, inlineChipBudget(0))9+    }10+11+    @Test fun one_spec_is_fully_reserved_for_the_picker() {12+        assertEquals(0, inlineChipBudget(1))13+    }14+15+    @Test fun picker_always_keeps_the_last_slot() {16+        assertEquals(1, inlineChipBudget(2))17+        assertEquals(2, inlineChipBudget(3))18+    }19+}

view file