5d31fe2bb493
feat: hide-autofill-previews privacy mode for keyboard chips (v2.13.0)
- time
- 2026-08-04 22:01 UTC · 2 months ago
- machine
- koh2
- parent
- dfa6c3b5a925
↓ peach-android-5d31fe2b.tar.gzatlas at this save
files changed8 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 −0app/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 }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 .sliceapp/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 }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)) {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 /**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(),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+}