Skip to content

fix(theme): stop SYSTEM + Material You flipping light on Pixel (#677) - #696

Merged
9thLevelSoftware merged 9 commits into
mainfrom
fix/issue-677-system-dark-stability
Aug 14, 2026
Merged

fix(theme): stop SYSTEM + Material You flipping light on Pixel (#677)#696
9thLevelSoftware merged 9 commits into
mainfrom
fix/issue-677-system-dark-stability

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Fixes #677

Why

Two shipped one-boundary patches failed:

RCA: SYSTEM treated NIGHT_UNDEFINED as light, sampled a Light XML Activity context after lock recreate (keyboardHidden/colorMode), then #691 made that sample sticky. Secondary leak: large workout chrome used unclamped primaryContainer.

What this PR does (three boundaries)

  1. Appearance source. Pure resolveSystemDark prefers application night, keeps last-known when undefined, and refuses to go light from a lone Activity NO unless Compose also says light. rememberPlatformSystemDark() and Theme.kt use it. The #691 isDark = refreshed latch is gone.
  2. Platform window. Theme.Material.Light.NoActionBar is gone. Values + values-night use Theme.Material.NoActionBar with Slate900 windowBackground. enableEdgeToEdge sizes bars from persisted theme_mode + application night. ApplyStatusBarAppearance now updates nav-bar icons too.
  3. Chrome. Rep counter and next-exercise cards read clamped surfaceContainerHighest / onSurface. FABs and selected-routine 0.4 tint stay on primaryContainer. #640 clamp file is untouched.

uiMode is not added back to configChanges. Material You is kept. iOS actual is unchanged. DARK/LIGHT modes are unchanged.

Tests (parent-verified)

./gradlew -Pskip.supabase.check=true :shared:testAndroidHostTest focused:

  • SystemDarkResolverTest 10/10
  • ThemeModeUiContractGuardTest 4/4
  • Issue677WindowThemeContractTest 5/5
  • MaterialYouDarkClampContractTest 5/5
  • RoutinesChromeLuminanceContractTest 8/8

:androidApp:compileDebugKotlin BUILD SUCCESSFUL.

Plan: docs/superpowers/plans/2026-08-14-issue-677-system-dark-stability.md (this checkout does not include the plan file).

Human close gate (do not close from CI green)

On the reporter Pixel: Theme = System, Material You on, Android dark.

  1. Open the workout editor. It must stay dark.
  2. Lock, then unlock. It must stay dark (must not go lighter).

Optional logcat: PlatformSystemDark. app=YES activity=NO resolving to dark is the #691 failure mode being corrected.

phoenixworker added 5 commits August 14, 2026 14:19
Extract a pure resolver that prefers application night mode and refuses to treat NIGHT_UNDEFINED as light.
Resolve SYSTEM appearance from application night, Activity night, and Compose, and keep last-known dark when the night mask is undefined.
Replace Theme.Material.Light first-frame context and size enableEdgeToEdge from saved theme_mode plus application night.
Move the rep counter and next-exercise cards off unclamped primaryContainer so wallpaper Fixed/container roles cannot paint those surfaces light.
Copilot AI lite review requested due to automatic review settings August 14, 2026 19:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bc79f8bad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread androidApp/src/main/res/values/colors.xml Outdated
Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt Outdated
Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt Outdated
Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt
@kilo-code-bot

kilo-code-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: Critical resolved | Recommendation: Merge-ready on correctness; Ponytail cleanup optional

Overview

Severity Count
🚨 critical 0 (was 1, now resolved in 7468667)
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0
Issue Status (click to expand)

✅ Resolved in 7468667

  • androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt:L31-34super.attachBaseContext(base) now runs first, then applyPersistedApplicationNightMode(this). The NPE that survived the previous refactor (where it relocated from getSharedPreferences to getSystemService) is finally gone. Application's mBase is populated before any context-touching code runs. Confirmed by reading the live file: attachBaseContext calls super on L32, then applyPersistedApplicationNightMode(this) on L33.

⚠️ Ponytail findings still outstanding (carried over from previous summary, unchanged in this diff)

  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L9-16: yagni — ApplicationContextHolder singleton + rememberApplicationContext duplicate the fallback already at L48 (ActivityHolder.getActivity()?.applicationContext). applyPlatformNightMode only fires from ThemeViewModel.setThemeMode (after an Activity is resumed), so ActivityHolder is always populated. Delete the singleton + setter, drop both rememberApplicationContext calls.
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L33: shrink — rememberApplicationContext(context) in applyApplicationNightMode is redundant; already called in applyPersistedApplicationNightMode at L24.
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L35: shrink — context.applicationContext ?: context is a defensive fallback that never fires (Application returns itself). Drop the ?: context and use context.applicationContext directly.

🏆 Best part: A one-line reorder to fix a critical NPE that survived the previous refactor. The fix landed clean, and the test at Issue677WindowThemeContractTest.application_appliesPersistedNightModeBeforeActivity was already structured to pass either order — so the change didn't even need a test update. The author also wired ThemeViewModel.setThemeModeapplyPlatformNightMode(mode) to address the chatgpt-codex P1 about SYSTEM not following the device after LIGHT/DARK. That was the right thing.

💀 Worst part: The Ponytail findings on PlatformNightMode.android.kt were raised in the previous summary and the author shipped without addressing them. ApplicationContextHolder + rememberApplicationContext exist solely to thread the Application Context to a singleton that applyPlatformNightMode uses once — and that one use has an ActivityHolder fallback that already covers it. Dead flexibility, but not a bug.

📊 Overall: Ship it. The critical NPE from two reviews back is finally dead. The remaining findings are optional cleanup that the codebase can absorb forever.

Files Reviewed (7 changed files)
  • androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt — critical fix ✅
  • androidApp/src/main/kotlin/com/devil/phoenixproject/PersistedThemeNightMode.kt — deleted in 58ec2fb, replaced by PlatformNightMode.android.kt. No new issues.
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/theme/Issue677WindowThemeContractTest.kt — test updates align with the new structure (helper moved to shared, setBackgroundDrawable assertion added, VitruvianApp.attachBaseContext + ThemeViewModel.applyPlatformNightMode assertions added). No new issues.
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt — Ponytail carry-over, no new correctness issues.
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/ThemeViewModel.ktapplyPlatformNightMode(mode) wiring correct.
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.kt — expect declaration, minimal.
  • shared/src/iosMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.ios.kt — no-op actual.

Fix these issues in Kilo Cloud

Previous Review Summary (commit fb3778c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit fb3778c)

Verdict: 1 Critical Issue | Recommendation: Do not merge until the Context-before-super NPE is fixed

Overview

Severity Count
🚨 critical 1
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0
Issue Details (click to expand)
File Line Roast
androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt 32 Still calling applyPersistedApplicationNightMode(base) BEFORE super.attachBaseContext(base). The refactor moved the helper into PlatformNightMode.android.kt and split it into two functions — getSharedPreferences now uses base directly (no NPE there), but applyApplicationNightMode at L35 still does context.applicationContext ?: context and calls getSystemService on the Application whose mBase is still null. The crash just relocated; it didn't leave.

Correctness / Safety Findings

  • critical: androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt:L31-34 — Re-verified against 58ec2fb1. applyPersistedApplicationNightMode(base) runs before super.attachBaseContext(base). The refactored helper at shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L23-38 reads prefs via context.getSharedPreferences(...) on the base ContextImpl (no NPE there), but the inner applyApplicationNightMode resolves val appContext = context.applicationContext ?: context (L35) which returns the Application instance, and appContext.getSystemService(UiModeManager::class.java) (L36) delegates to mBase.getSystemService → NPE because Application.mBase is null until super runs. Fires on every cold start on API 31+. Required fix: call super.attachBaseContext(base) first, then applyPersistedApplicationNightMode(this) (or base — after super, base.applicationContext returns the Application with mBase = base populated, so getSystemService works). Existing comment 3787207933 at L32 still applies — root cause unchanged, only the NPE location moved from getSharedPreferences to getSystemService.

Ponytail Review

  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L9-16: yagni — ApplicationContextHolder singleton + rememberApplicationContext duplicate the fallback already at L48 (ActivityHolder.getActivity()?.applicationContext). applyPlatformNightMode is only called from ThemeViewModel.setThemeMode, which runs after an Activity is resumed, so ActivityHolder is always populated. Delete the singleton + setter, drop both rememberApplicationContext calls, and simplify applyPlatformNightMode to ActivityHolder.getActivity()?.applicationContext?.let { applyApplicationNightMode(it, themeMode) }.
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L33: shrink — rememberApplicationContext(context) in applyApplicationNightMode is redundant; already called in applyPersistedApplicationNightMode at L24 and would be deleted with the yagni finding above.
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt:L35: shrink — context.applicationContext ?: context is a defensive fallback that turns into a footgun before super (see critical). context is always a valid Context here; drop the ?: context and use context.applicationContext directly.

Ponytail net: -16 lines.

Suggested Minimal Patch

Reorder VitruvianApp.attachBaseContext:

override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    applyPersistedApplicationNightMode(this)
}

…and delete ApplicationContextHolder + rememberApplicationContext from PlatformNightMode.android.kt, remove the redundant rememberApplicationContext call at L33, and drop the ?: context fallback at L35. Simplify applyPlatformNightMode to use ActivityHolder.getActivity()?.applicationContext directly.

Final Merge Guidance

Do not merge until the critical NPE is fixed. The refactor in 58ec2fb1 addressed the chatgpt-codex P1 about reapplying night mode on theme change (good — ThemeViewModel.setThemeMode now calls applyPlatformNightMode), but the context-before-super crash from the previous review was not fixed. The helper moved; the bug tagged along. One-line reorder and we're cooking. Until then this is still a bug dressed up as a fix.

🏆 Best part: The expect/actual split (PlatformNightMode.kt + .android.kt + .ios.kt) is the right shape for multiplatform, and the ThemeViewModel.setThemeModeapplyPlatformNightMode wiring correctly addresses the live SYSTEM→device-following gap that the previous review flagged.

💀 Worst part: Splitting the helper into two functions to dodge the previous review's getSharedPreferences NPE callout, without fixing the underlying context-before-super. The NPE just relocated to getSystemService in the same lifecycle position. This is the code equivalent of moving the smoke detector to the room that's not on fire.

📊 Overall: The intent is right, the multiplatform shape is right, the ThemeViewModel wiring is right — but the one-line reorder that was the entire fix from the previous review still hasn't landed. Until it does, this crashes on every cold start on the only API level it targets.

Files Reviewed (3 changed files)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/ui/theme/PlatformNightMode.android.kt - 1 issue (critical carryover) + 3 Ponytail
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/ThemeViewModel.kt - no new issues
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/theme/Issue677WindowThemeContractTest.kt - no new issues

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 51.2K · Output: 11.3K · Cached: 465.3K

Review guidance: REVIEW.md from base branch main

Qualify day/night window colors, sync permission-screen system bars, and share theme pref keys so cold-start no longer flashes the wrong palette.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 172dc31b23

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/MainActivity.kt
Force application night mode from saved theme_mode in Application.attachBaseContext so values vs values-night match explicit LIGHT/DARK before MainActivity.onCreate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb3778cbb0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt Outdated
Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/PersistedThemeNightMode.kt Outdated
Comment thread androidApp/src/main/kotlin/com/devil/phoenixproject/VitruvianApp.kt Outdated
phoenixworker added 2 commits August 14, 2026 17:18
ThemeViewModel.setThemeMode now calls applyPlatformNightMode so LIGHT/DARK/SYSTEM updates UiModeManager live. The helper lives in shared so attachBaseContext and the ViewModel share one path.
Call applyPersistedApplicationNightMode(this) after super so SharedPreferences does not run against an Application with a null mBase.
@9thLevelSoftware
9thLevelSoftware merged commit ade389f into main Aug 14, 2026
10 checks passed
@9thLevelSoftware
9thLevelSoftware deleted the fix/issue-677-system-dark-stability branch August 14, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dark mode color scheme sporadically switches to light with Material You colors

2 participants