a35080188c1c
add debug-only QA capture bypass (QaFlags + am start --ez qa_disable_secure_flag) so FLAG_SECURE screens can be screenshot-verified without weakening release security; dedupe EntryDetailScreen/NotesModeScreen's inline FLAG_SECURE code into the shared SecureFlagEffect
- time
- 2026-07-24 02:30 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- gate1-gate2-verification-pass
- parent
- 9e4b71ed94ce
↓ peach-android-a3508018.tar.gzatlas at this save
files changed5 files
app/src/main/java/com/peachpasswords/MainActivity.kt+18 −0app/src/main/java/com/peachpasswords/ui/components/QaFlags.kt+13 −0app/src/main/java/com/peachpasswords/ui/components/SecureFlagEffect.kt+8 −0app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+29 −18app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+2 −10app/src/main/java/com/peachpasswords/MainActivity.kt+18 −0
@@ -1,5 +1,6 @@11 package com.peachpasswords22 3+import android.content.Intent34 import android.content.res.Configuration45 import android.graphics.Color as AndroidColor56 import android.graphics.drawable.ColorDrawable@@ -15,6 +16,7 @@1516 import dagger.hilt.android.AndroidEntryPoint1617 import javax.inject.Inject1718 import com.peachpasswords.ui.PeachAppNavHost19+import com.peachpasswords.ui.components.QaFlags1820 import com.peachpasswords.ui.onboarding.ONBOARD_KEY1921 import com.peachpasswords.ui.onboarding.ONBOARD_PREFS2022 import com.peachpasswords.ui.onboarding.onboardingStartDestination@@ -36,6 +38,17 @@3638 lockTimer.poke()3739 }3840 41+ // launchMode="singleTask" reuses this instance across `adb shell am start`42+ // relaunches, so the QA capture-bypass extra (see onCreate) needs to be43+ // re-read here too, not just once at process start.44+ override fun onNewIntent(intent: Intent) {45+ super.onNewIntent(intent)46+ setIntent(intent)47+ if (BuildConfig.DEBUG) {48+ QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra("qa_disable_secure_flag", false)49+ }50+ }51+3952 override fun onCreate(savedInstanceState: Bundle?) {4053 installSplashScreen()4154 super.onCreate(savedInstanceState)@@ -62,6 +75,11 @@6275 WindowManager.LayoutParams.FLAG_SECURE,6376 WindowManager.LayoutParams.FLAG_SECURE,6477 )78+ } else {79+ // QA-only, debug-build-only: `adb shell am start -n <pkg>/.MainActivity80+ // --ez qa_disable_secure_flag true` lets automated visual QA screenshot81+ // otherwise-FLAG_SECURE screens (SecureFlagEffect). Off unless asked.82+ QaFlags.disableSecureCaptureForQa = intent.getBooleanExtra("qa_disable_secure_flag", false)6583 }6684 6785 // Onboarding runs once, only on a fresh install with no vault yet.app/src/main/java/com/peachpasswords/ui/components/QaFlags.kt+13 −0
@@ -0,0 +1,13 @@1+package com.peachpasswords.ui.components2+3+/**4+ * Debug-only, opt-in escape hatch so automated visual QA (screenshot-based Gate 25+ * comparisons) can capture screens that are normally FLAG_SECURE-protected. Off by6+ * default; MainActivity only ever flips it from `false` when both BuildConfig.DEBUG7+ * is true AND the launching intent explicitly asks for it — never in release builds,8+ * never implicitly. [SecureFlagEffect] re-checks BuildConfig.DEBUG itself as a second9+ * gate, so this object being non-private doesn't widen the actual bypass surface.10+ */11+object QaFlags {12+ var disableSecureCaptureForQa: Boolean = false13+}app/src/main/java/com/peachpasswords/ui/components/SecureFlagEffect.kt+8 −0
@@ -5,6 +5,7 @@55 import androidx.compose.runtime.DisposableEffect66 import androidx.compose.ui.platform.LocalContext77 import androidx.fragment.app.FragmentActivity8+import com.peachpasswords.BuildConfig89 910 /**1011 * Sets WindowManager.FLAG_SECURE while this composable is in the composition and@@ -12,11 +13,18 @@1213 * thumbnail of screens that display secrets (MN-3: recovery words, export blob,1314 * codex generation). Mirrors the inline effect EntryDetailScreen / NotesModeScreen1415 * already use.16+ *17+ * Skipped only when BuildConfig.DEBUG is true AND [QaFlags.disableSecureCaptureForQa]18+ * was explicitly opted into (see that file) — automated visual QA needs to capture19+ * these screens too. Always enforced in release builds regardless of the flag.1520 */1621 @Composable1722 fun SecureFlagEffect() {1823 val context = LocalContext.current1924 DisposableEffect(Unit) {25+ if (BuildConfig.DEBUG && QaFlags.disableSecureCaptureForQa) {26+ return@DisposableEffect onDispose { }27+ }2028 val window = (context as? FragmentActivity)?.window2129 window?.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)2230 onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+29 −18
@@ -1,6 +1,5 @@11 package com.peachpasswords.ui.vault22 3-import android.view.WindowManager43 import androidx.compose.animation.AnimatedVisibility54 import androidx.compose.animation.core.LinearEasing65 import androidx.compose.animation.core.RepeatMode@@ -43,7 +42,6 @@4342 import androidx.compose.material3.Icon4443 import androidx.compose.material3.Text4544 import androidx.compose.runtime.Composable46-import androidx.compose.runtime.DisposableEffect4745 import androidx.compose.runtime.getValue4846 import androidx.compose.runtime.mutableStateOf4947 import androidx.compose.runtime.produceState@@ -78,6 +76,7 @@7876 import com.peachpasswords.ui.components.FaviconInitials7977 import com.peachpasswords.ui.components.PeachEmptyOrEndState8078 import com.peachpasswords.ui.components.PeachTopBar79+import com.peachpasswords.ui.components.SecureFlagEffect8180 import com.peachpasswords.ui.components.TOTPRing8281 import com.peachpasswords.ui.lock.Biometrics8382 import com.peachpasswords.ui.theme.LocalPeachAccent@@ -117,12 +116,7 @@117116 var generatorOpen by remember { mutableStateOf(false) }118117 var historyExpanded by remember { mutableStateOf(false) }119118 120- // FLAG_SECURE only while this screen is shown — blocks screenshots of secrets.121- DisposableEffect(Unit) {122- val window = (context as? FragmentActivity)?.window123- window?.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)124- onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }125- }119+ SecureFlagEffect()126120 127121 val e = entry128122 ShellBackground {@@ -311,16 +305,33 @@311305 onRemoveTag = vm::removeTag,312306 )313307 if (!e.isNote() && !e.isCard()) {314- CanaryDetailSection(315- entryId = e.id,316- modifiedAt = e.modifiedAt,317- canary = e.canary,318- isPro = isPro,319- message = canaryMessage,320- testing = canaryTesting,321- onSave = vm::saveCanary,322- onTest = vm::testCanary,323- )308+ // Collapsed by default (mirrors the browser's "+ ADVANCED"),309+ // unless a canary is already configured for this entry — every310+ // entry showing an "Off" canary block permanently was clutter311+ // most people never touch.312+ var advancedExpanded by remember(e.id) { mutableStateOf(e.canary != null) }313+ if (advancedExpanded) {314+ CanaryDetailSection(315+ entryId = e.id,316+ modifiedAt = e.modifiedAt,317+ canary = e.canary,318+ isPro = isPro,319+ message = canaryMessage,320+ testing = canaryTesting,321+ onSave = vm::saveCanary,322+ onTest = vm::testCanary,323+ )324+ } else {325+ Text(326+ "+ advanced",327+ color = LocalPeachAccent.current,328+ style = PeachType.labelMono,329+ modifier = Modifier330+ .clip(RoundedCornerShape(6.dp))331+ .clickable { advancedExpanded = true }332+ .padding(vertical = 6.dp),333+ )334+ }324335 }325336 326337 PeachEmptyOrEndState(app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+2 −10
@@ -1,6 +1,5 @@11 package com.peachpasswords.ui.vault22 3-import android.view.WindowManager43 import androidx.compose.animation.core.animateFloatAsState54 import androidx.compose.animation.core.tween65 import androidx.compose.foundation.background@@ -42,7 +41,6 @@4241 import androidx.compose.ui.draw.alpha4342 import androidx.compose.ui.draw.clip4443 import androidx.compose.ui.graphics.SolidColor45-import androidx.compose.ui.platform.LocalContext4644 import androidx.compose.ui.text.AnnotatedString4745 import androidx.compose.ui.text.SpanStyle4846 import androidx.compose.ui.text.TextStyle@@ -56,10 +54,10 @@5654 import androidx.compose.ui.unit.dp5755 import androidx.compose.ui.unit.sp5856 import androidx.compose.runtime.collectAsState59-import androidx.fragment.app.FragmentActivity6057 import androidx.hilt.navigation.compose.hiltViewModel6158 import com.peachpasswords.ui.components.PeachEmptyOrEndState6259 import com.peachpasswords.ui.components.PeachTopBar60+import com.peachpasswords.ui.components.SecureFlagEffect6361 import com.peachpasswords.ui.theme.PeachSpacing6462 import com.peachpasswords.vault.VaultEntry6563 import com.peachpasswords.ui.theme.LocalPeachAccent@@ -87,14 +85,8 @@8785 val activeNoteId by vm.activeNoteId.collectAsState()8886 val savedPulse by vm.savedPulse.collectAsState()8987 val accent = LocalPeachAccent.current90- val context = LocalContext.current9188 92- // FLAG_SECURE only while shown.93- androidx.compose.runtime.DisposableEffect(Unit) {94- val window = (context as? FragmentActivity)?.window95- window?.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)96- onDispose { window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }97- }89+ SecureFlagEffect()9890 9991 // "saved." indicator fades in on each save pulse, holds, then fades out.10092 var showSaved by remember { mutableStateOf(false) }