5d31fe2bb493

feat: hide-autofill-previews privacy mode for keyboard chips (v2.13.0)

New vault setting 'Hide autofill previews': inline keyboard chips and suggestion rows render only 'Peach' plus the caller's domain — no entry titles or usernames in the IME strip, for lock screens and shoulder- surfing. Values still fill on tap; the picker itself is unchanged. - VaultSettings.hideAutofillPreviews (default off, current behavior) - AutofillResponseBuilder: PreviewLabels + previewLabels() decide what a row/chip shows; menuRow replaced by labels-based rowFor; datasetFor / cardDatasetFor / buildInline all render from labels - wired through PeachAutofillService and the picker's returnDataset - settings toggle in the autofill card (above PASSKEYS) - 4 new previewLabels unit tests; autofill + credentials suites green

time
2026-08-04 22:01 UTC · 2 months ago
machine
koh2
parent
dfa6c3b5a925
8 files VERSION+1 −1app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt+3 −2app/src/main/java/com/peachpasswords/autofill/AutofillResponseBuilder.kt+30 −14app/src/main/java/com/peachpasswords/autofill/PeachAutofillService.kt+3 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+12 −0app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+1 −0app/src/main/java/com/peachpasswords/vault/VaultSettings.kt+2 −0app/src/test/java/com/peachpasswords/autofill/AutofillPreviewLabelsTest.kt+42 −0
VERSION+1 −1
@@ -1 +1 @@1-2.12.01+2.13.0

view file

app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt+3 −2
@@ -333,10 +333,11 @@333333         parsed: ParsedStructure,334334         entry: VaultEntry,335335     ) {336+        val privatePreview = vault.settings.value.hideAutofillPreviews336337         val dataset = if (entry.isCard()) {337-            AutofillResponseBuilder.cardDatasetFor(this, parsed, entry, inlineSpec = null)338+            AutofillResponseBuilder.cardDatasetFor(this, parsed, entry, inlineSpec = null, privatePreview)338339         } else {339-            AutofillResponseBuilder.datasetFor(this, parsed, entry, inlineSpec = null)340+            AutofillResponseBuilder.datasetFor(this, parsed, entry, inlineSpec = null, privatePreview)340341         }341342         finishWithDataset(dataset)342343     }

view file

app/src/main/java/com/peachpasswords/autofill/AutofillResponseBuilder.kt+30 −14
@@ -75,14 +75,27 @@7575             .mapNotNull(::extractDomain)7676             .any { it == originDomain }7777 78-    fun menuRow(context: Context, entry: VaultEntry): RemoteViews {79-        val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""80-        return RemoteViews(context.packageName, R.layout.autofill_row).apply {81-            setTextViewText(R.id.autofill_row_title, entry.title)82-            setTextViewText(R.id.autofill_row_subtitle, subtitle)78+    /** What a suggestion row or keyboard chip shows about an entry. */79+    data class PreviewLabels(val title: String, val subtitle: String)80+81+    /**82+     * Privacy previews: with [privatePreview] the rows and chips render only83+     * the Peach brand and the caller's domain — no entry titles or usernames84+     * in the IME's suggestion strip. The values still fill on tap.85+     */86+    fun previewLabels(privatePreview: Boolean, entry: VaultEntry, originDomain: String?): PreviewLabels =87+        if (privatePreview) {88+            PreviewLabels("Peach", originDomain ?: "")89+        } else {90+            PreviewLabels(entry.title, entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: "")8391         }84-    }8592 93+    fun rowFor(context: Context, labels: PreviewLabels): RemoteViews =94+        RemoteViews(context.packageName, R.layout.autofill_row).apply {95+            setTextViewText(R.id.autofill_row_title, labels.title)96+            setTextViewText(R.id.autofill_row_subtitle, labels.subtitle)97+        }98+8699     fun simpleRow(context: Context, title: String, subtitle: String): RemoteViews =87100         RemoteViews(context.packageName, R.layout.autofill_row).apply {88101             setTextViewText(R.id.autofill_row_title, title)@@ -98,10 +111,12 @@98111         parsed: ParsedStructure,99112         entry: VaultEntry,100113         inlineSpec: android.widget.inline.InlinePresentationSpec?,114+        privatePreview: Boolean,101115     ): Dataset? {102-        val menu = menuRow(context, entry)116+        val labels = previewLabels(privatePreview, entry, parsed.origin?.let(::extractDomain))117+        val menu = rowFor(context, labels)103118         val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {104-            buildInline(context, entry, inlineSpec)119+            buildInline(context, labels, inlineSpec)105120         } else {106121             null107122         }@@ -143,11 +158,13 @@143158         parsed: ParsedStructure,144159         entry: VaultEntry,145160         inlineSpec: android.widget.inline.InlinePresentationSpec?,161+        privatePreview: Boolean,146162     ): Dataset? {147163         if (!entry.isCard()) return null148-        val menu = menuRow(context, entry)164+        val labels = previewLabels(privatePreview, entry, parsed.origin?.let(::extractDomain))165+        val menu = rowFor(context, labels)149166         val inline = if (Build.VERSION.SDK_INT >= 30 && inlineSpec != null) {150-            buildInline(context, entry, inlineSpec)167+            buildInline(context, labels, inlineSpec)151168         } else {152169             null153170         }@@ -226,18 +243,17 @@226243     @RequiresApi(30)227244     private fun buildInline(228245         context: Context,229-        entry: VaultEntry,246+        labels: PreviewLabels,230247         spec: android.widget.inline.InlinePresentationSpec,231248     ): InlinePresentation? = try {232249         val style = spec.style233250         if (!UiVersions.getVersions(style).contains(UiVersions.INLINE_UI_VERSION_1)) {234251             null235252         } else {236-            val subtitle = entry.username ?: entry.email ?: entry.url?.let(::extractDomain) ?: ""237253             val pending = AutofillIntents.noopChipIntent(context)238254             val slice = InlineSuggestionUi.newContentBuilder(pending)239-                .setTitle(entry.title)240-                .apply { if (subtitle.isNotBlank()) setSubtitle(subtitle) }255+                .setTitle(labels.title)256+                .apply { if (labels.subtitle.isNotBlank()) setSubtitle(labels.subtitle) }241257                 .setStartIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_peach_mark))242258                 .build()243259                 .slice

view file

app/src/main/java/com/peachpasswords/autofill/PeachAutofillService.kt+3 −2
@@ -47,6 +47,7 @@4747         if (!parsed.hasCredentialField) { callback.onSuccess(null); return }4848 4949         val inlineSpecs = inlineSpecs(request)50+        val privatePreview = vault.settings.value.hideAutofillPreviews5051         val response = FillResponse.Builder()5152         var datasetCount = 05253         var hasLoginMatch = false@@ -57,7 +58,7 @@5758                 .all.take(MAX_DATASETS)5859             matches.forEachIndexed { index, entry ->5960                 val spec = inlineSpecs.getOrNull(index)60-                AutofillResponseBuilder.datasetFor(context, parsed, entry, spec)?.let {61+                AutofillResponseBuilder.datasetFor(context, parsed, entry, spec, privatePreview)?.let {6162                     response.addDataset(it)6263                     datasetCount++6364                     hasLoginMatch = true@@ -68,7 +69,7 @@6869         if (parsed.hasCardField && vault.isUnlocked.value) {6970             vault.getEntries().filter { it.isCard() }.take(MAX_DATASETS - datasetCount).forEachIndexed { index, entry ->7071                 val spec = inlineSpecs.getOrNull(datasetCount + index)71-                AutofillResponseBuilder.cardDatasetFor(context, parsed, entry, spec)?.let {72+                AutofillResponseBuilder.cardDatasetFor(context, parsed, entry, spec, privatePreview)?.let {7273                     response.addDataset(it)7374                     datasetCount++7475                 }

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+12 −0
@@ -412,6 +412,18 @@412412         }413413     }414414 415+    RowLine(fg) {416+        Column(Modifier.weight(1f)) {417+            Text("Hide autofill previews", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)418+            Text(419+                "Keyboard chips and suggestion rows show only Peach until tapped — no entry names or usernames over your shoulder.",420+                color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,421+                modifier = Modifier.padding(top = 2.dp),422+            )423+        }424+        Toggle(s.hideAutofillPreviews, fg) { vm.setHideAutofillPreviews(it) }425+    }426+415427     SubLabel("PASSKEYS", fg)416428     RowLine(fg) {417429         Column(Modifier.weight(1f)) {

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+1 −0
@@ -283,6 +283,7 @@283283     fun setBreachMonitoring(on: Boolean) = mutate { it.copy(breachMonitoring = on) }284284     fun setBiometrics(on: Boolean) = mutate { it.copy(biometricsEnabled = on) }285285     fun setPasskeyProviderEnabled(on: Boolean) = mutate { it.copy(passkeyProviderEnabled = on) }286+    fun setHideAutofillPreviews(on: Boolean) = mutate { it.copy(hideAutofillPreviews = on) }286287     fun setAllowScreenshots(on: Boolean) = mutate { it.copy(allowScreenshots = on) }287288 288289     /**

view file

app/src/main/java/com/peachpasswords/vault/VaultSettings.kt+2 −0
@@ -15,6 +15,8 @@1515     val biometricsEnabled: Boolean = false,1616     val trashRetentionDays: Int? = null,1717     val sortOrder: String = "url-first",18+    /** Keyboard chips and suggestion rows show only "Peach" until tapped. */19+    val hideAutofillPreviews: Boolean = false,1820     val passkeyProviderEnabled: Boolean = false,1921     val duressConfigured: Boolean = false,2022     val shortcuts: Map<String, ShortcutBinding> = emptyMap(),

view file

app/src/test/java/com/peachpasswords/autofill/AutofillPreviewLabelsTest.kt+42 −0
@@ -0,0 +1,42 @@1+package com.peachpasswords.autofill2+3+import com.peachpasswords.vault.VaultEntry4+import org.junit.Assert.assertEquals5+import org.junit.Test6+7+class AutofillPreviewLabelsTest {8+    private fun entry(9+        title: String = "GitHub",10+        username: String? = "june",11+        email: String? = null,12+        url: String? = "https://github.com",13+    ) = VaultEntry(14+        id = title, title = title, username = username, email = email, url = url,15+        createdAt = 0, modifiedAt = 0,16+    )17+18+    @Test fun normal_mode_shows_entry_title_and_identity() {19+        val labels = AutofillResponseBuilder.previewLabels(false, entry(), "github.com")20+        assertEquals("GitHub", labels.title)21+        assertEquals("june", labels.subtitle)22+    }23+24+    @Test fun normal_mode_falls_back_email_then_domain() {25+        val emailOnly = entry(username = null, email = "[email protected]")26+        assertEquals("[email protected]", AutofillResponseBuilder.previewLabels(false, emailOnly, null).subtitle)27+        val bare = entry(username = null, email = null)28+        assertEquals("github.com", AutofillResponseBuilder.previewLabels(false, bare, null).subtitle)29+    }30+31+    @Test fun private_mode_hides_title_and_identity() {32+        val labels = AutofillResponseBuilder.previewLabels(true, entry(), "github.com")33+        assertEquals("Peach", labels.title)34+        assertEquals("github.com", labels.subtitle)35+    }36+37+    @Test fun private_mode_without_origin_has_blank_subtitle() {38+        val labels = AutofillResponseBuilder.previewLabels(true, entry(), null)39+        assertEquals("Peach", labels.title)40+        assertEquals("", labels.subtitle)41+    }42+}

view file