From e3ac06b047378de7493c5da1d77354ff09fdd2b3 Mon Sep 17 00:00:00 2001 From: Tretzi Date: Mon, 13 Apr 2026 19:01:01 -0500 Subject: [PATCH] Auto commit (MindMachine) Mon Apr 13 07:01:01 PM CDT 2026 --- .../java/com/mindmachine/mvp/MainActivity.kt | 89 +++++++++++++++++-- .../mvp/data/SettingsRepository.kt | 15 ++++ .../java/com/mindmachine/mvp/domain/Models.kt | 4 + .../mindmachine/mvp/session/MainViewModel.kt | 3 + .../mindmachine/mvp/session/TimelineModel.kt | 1 + 5 files changed, 105 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt index 55be8bd..6f83681 100644 --- a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt +++ b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt @@ -519,6 +519,14 @@ fun SetupScreen( onCurveGranularityChanged = vm::setCurveGranularitySec, ) + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = ui.config.monochromeFlashMode, + onCheckedChange = vm::setMonochromeFlashMode, + ) + Text("Use black/white flashes for this session", color = MaterialTheme.colorScheme.onBackground) + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { if (hasUnsavedChanges) { OutlinedButton(onClick = { vm.saveCurrentProgram(programName) }, modifier = Modifier.weight(1f).height(52.dp)) { @@ -587,6 +595,7 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry: } var brightnessOverlayVisible by remember { mutableStateOf(false) } var brightnessOverlayToken by remember { mutableIntStateOf(0) } + val timingLogTag = "MindMachineTiming" LaunchedEffect(brightnessOverlayToken) { if (!brightnessOverlayVisible) return@LaunchedEffect @@ -594,6 +603,17 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry: brightnessOverlayVisible = false } + LaunchedEffect(ui.runtimeState) { + when (ui.runtimeState) { + RuntimeState.COUNTDOWN -> Log.d(timingLogTag, "COUNTDOWN_START") + RuntimeState.RUNNING -> Log.d(timingLogTag, "IMMERSIVE_START") + RuntimeState.STOPPED, RuntimeState.COMPLETED, RuntimeState.PAUSED, RuntimeState.INTERRUPTED, RuntimeState.ERROR -> { + Log.d(timingLogTag, "IMMERSIVE_STOP state=${ui.runtimeState}") + } + else -> Unit + } + } + ApplyKeepScreenOn(enabled = true) ApplyScreenBrightnessOverride( @@ -601,6 +621,11 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry: brightnessPercent = ui.settings.immersiveBrightnessPercent, ) + ApplyPreferredRefreshRate( + enabled = true, + preferLowest = ui.settings.preferLowestRefreshRate, + ) + val progress = sessionProgressFraction( durationSec = ui.timeline.durationSec, remainingSec = ui.remainingSec, @@ -628,10 +653,12 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry: SplitFlashRenderView(ctx).apply { setRunning(ui.runtimeState == RuntimeState.RUNNING) updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs) + setMonochrome(ui.config.monochromeFlashMode) } }, update = { view -> view.updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs) + view.setMonochrome(ui.config.monochromeFlashMode) view.setRunning(ui.runtimeState == RuntimeState.RUNNING) } ) @@ -759,6 +786,40 @@ private fun ApplyScreenBrightnessOverride(enabled: Boolean, brightnessPercent: I } } +@Composable +private fun ApplyPreferredRefreshRate(enabled: Boolean, preferLowest: Boolean) { + val activity = LocalContext.current as? Activity ?: return + val window = activity.window + val timingLogTag = "MindMachineTiming" + + DisposableEffect(window) { + onDispose { + val params = window.attributes + params.preferredRefreshRate = 0f + params.preferredDisplayModeId = 0 + window.attributes = params + } + } + + LaunchedEffect(enabled, preferLowest) { + val params = window.attributes + if (enabled && preferLowest) { + val display = activity.display + val chosenMode = display?.supportedModes?.minByOrNull { it.refreshRate } + params.preferredDisplayModeId = chosenMode?.modeId ?: 0 + params.preferredRefreshRate = chosenMode?.refreshRate ?: 0f + Log.d( + timingLogTag, + "DISPLAY_MODE_REQUEST modeId=${chosenMode?.modeId ?: 0} refreshHz=${chosenMode?.refreshRate ?: 0f} supported=${display?.supportedModes?.joinToString { "${it.modeId}:${it.refreshRate}" }}" + ) + } else { + params.preferredDisplayModeId = 0 + params.preferredRefreshRate = 0f + } + window.attributes = params + } +} + @Composable private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) { val context = LocalContext.current @@ -806,6 +867,7 @@ private class SplitFlashRenderView(context: Context) : View(context) { private var rawOffMs = 167L private var onMs = 167L private var offMs = 167L + private var monochrome = false private var started = false private var startUptimeMs = 0L private var nextToggleUptimeMs = 0L @@ -855,10 +917,12 @@ private class SplitFlashRenderView(context: Context) : View(context) { onMs = snapToFrameMs(rawOnMs, frameMs) offMs = snapToFrameMs(rawOffMs, frameMs) - Log.d( - timingLogTag, - "config onMs=$rawOnMs offMs=$rawOffMs refreshHz=${"%.2f".format(refreshRate())} frameMs=$frameMs snappedOnMs=$onMs snappedOffMs=$offMs" - ) + if (isRunning) { + Log.d( + timingLogTag, + "config onMs=$rawOnMs offMs=$rawOffMs refreshHz=${"%.2f".format(refreshRate())} frameMs=$frameMs snappedOnMs=$onMs snappedOffMs=$offMs" + ) + } if (isRunning && started) { scheduleFromNow() @@ -886,12 +950,18 @@ private class SplitFlashRenderView(context: Context) : View(context) { maxLatenessMs = 0L Log.d( timingLogTag, - "session-start phase=${phaseName(phase)} refreshHz=${"%.2f".format(refreshRate())} frameMs=${framePeriodMs()} nowMs=$startUptimeMs" + "session-start phase=${phaseName(phase)} modeId=${display?.mode?.modeId ?: 0} refreshHz=${"%.2f".format(refreshRate())} frameMs=${framePeriodMs()} nowMs=$startUptimeMs" ) invalidate() scheduleNext() } + fun setMonochrome(enabled: Boolean) { + if (monochrome == enabled) return + monochrome = enabled + invalidate() + } + override fun onDetachedFromWindow() { handler.removeCallbacks(toggleRunnable) super.onDetachedFromWindow() @@ -919,8 +989,8 @@ private class SplitFlashRenderView(context: Context) : View(context) { } private fun colorsForPhase(phase: Int): Pair = when (phase) { - 0 -> Pair(android.graphics.Color.RED, android.graphics.Color.GREEN) - 2 -> Pair(android.graphics.Color.GREEN, android.graphics.Color.RED) + 0 -> if (monochrome) Pair(android.graphics.Color.WHITE, android.graphics.Color.WHITE) else Pair(android.graphics.Color.RED, android.graphics.Color.GREEN) + 2 -> if (monochrome) Pair(android.graphics.Color.WHITE, android.graphics.Color.WHITE) else Pair(android.graphics.Color.GREEN, android.graphics.Color.RED) else -> Pair(android.graphics.Color.BLACK, android.graphics.Color.BLACK) } @@ -1121,6 +1191,11 @@ fun SettingsScreen(vm: MainViewModel, onRemoveAds: () -> Unit) { Text("Show immersive bottom progress bar", color = MaterialTheme.colorScheme.onBackground) } + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = ui.settings.preferLowestRefreshRate, onCheckedChange = vm::updatePreferLowestRefreshRate) + Text("Prefer lowest display refresh rate during sessions", color = MaterialTheme.colorScheme.onBackground) + } + Button(onClick = onRemoveAds) { Text(if (ui.settings.noAds) "Ads removed" else "Remove ads") } diff --git a/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt b/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt index d5b119b..3870bec 100644 --- a/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt +++ b/app/src/main/java/com/mindmachine/mvp/data/SettingsRepository.kt @@ -20,6 +20,8 @@ class SettingsRepository(private val context: Context) { val countdownSeconds = intPreferencesKey("countdown_seconds") val forceAudioWithoutHeadphones = booleanPreferencesKey("force_audio_without_headphones") val showImmersiveProgressBar = booleanPreferencesKey("show_immersive_progress_bar") + val preferLowestRefreshRate = booleanPreferencesKey("prefer_lowest_refresh_rate") + val monochromeFlashMode = booleanPreferencesKey("monochrome_flash_mode") val immersiveBrightnessPercent = intPreferencesKey("immersive_brightness_percent") val lastPresetId = stringPreferencesKey("last_preset") val noAds = booleanPreferencesKey("no_ads") @@ -34,6 +36,8 @@ class SettingsRepository(private val context: Context) { ?: 5, forceAudioWithoutHeadphones = p[Keys.forceAudioWithoutHeadphones] ?: false, showImmersiveProgressBar = p[Keys.showImmersiveProgressBar] ?: true, + preferLowestRefreshRate = p[Keys.preferLowestRefreshRate] ?: true, + monochromeFlashMode = p[Keys.monochromeFlashMode] ?: false, immersiveBrightnessPercent = (p[Keys.immersiveBrightnessPercent] ?: 50).coerceIn(5, 100), lastPresetId = p[Keys.lastPresetId], noAds = p[Keys.noAds] ?: false, @@ -51,6 +55,9 @@ class SettingsRepository(private val context: Context) { if (prefs[Keys.immersiveBrightnessPercent] == null) { prefs[Keys.immersiveBrightnessPercent] = 50 } + if (prefs[Keys.preferLowestRefreshRate] == null) { + prefs[Keys.preferLowestRefreshRate] = true + } } } @@ -73,6 +80,14 @@ class SettingsRepository(private val context: Context) { it[Keys.showImmersiveProgressBar] = value } + suspend fun updatePreferLowestRefreshRate(value: Boolean) = context.dataStore.edit { + it[Keys.preferLowestRefreshRate] = value + } + + suspend fun updateMonochromeFlashMode(value: Boolean) = context.dataStore.edit { + it[Keys.monochromeFlashMode] = value + } + suspend fun updateImmersiveBrightnessPercent(value: Int) = context.dataStore.edit { it[Keys.immersiveBrightnessPercent] = value.coerceIn(5, 100) } diff --git a/app/src/main/java/com/mindmachine/mvp/domain/Models.kt b/app/src/main/java/com/mindmachine/mvp/domain/Models.kt index 0b0b161..6040e0f 100644 --- a/app/src/main/java/com/mindmachine/mvp/domain/Models.kt +++ b/app/src/main/java/com/mindmachine/mvp/domain/Models.kt @@ -25,6 +25,7 @@ data class SessionConfig( val mode: SessionMode, val visualPatternType: VisualPattern, val flashIntervalMs: Int, + val monochromeFlashMode: Boolean = false, val intensityPercent: Int, val carrierFrequencyHz: Float, val binauralDifferenceHz: Float, @@ -36,6 +37,8 @@ data class AppSettings( val countdownSeconds: Int = 5, val forceAudioWithoutHeadphones: Boolean = false, val showImmersiveProgressBar: Boolean = true, + val preferLowestRefreshRate: Boolean = true, + val monochromeFlashMode: Boolean = false, // Default used only before DataStore emits (and as a fallback). // We want the first-run experience to be less blinding. val immersiveBrightnessPercent: Int = 50, @@ -94,6 +97,7 @@ fun SessionPreset.toConfig(defaultMode: SessionMode = SessionMode.AUDIO_VISUAL) mode = defaultMode, visualPatternType = visualPatternType, flashIntervalMs = flashIntervalMs, + monochromeFlashMode = false, intensityPercent = intensityPercent, carrierFrequencyHz = carrierFrequencyHz, binauralDifferenceHz = binauralDifferenceHz, diff --git a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt index 60a27ae..e1de450 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt @@ -280,6 +280,7 @@ class MainViewModel( } fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) } + fun setMonochromeFlashMode(value: Boolean) = _ui.update { it.copy(config = it.config.copy(monochromeFlashMode = value), error = null) } fun setCarrier(value: Float) = _ui.update { val stableCarrier = value.coerceIn(ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX) it.copy(config = it.config.copy(carrierFrequencyHz = stableCarrier), error = null) @@ -291,6 +292,8 @@ class MainViewModel( settingsRepository.updateForceAudioWithoutHeadphones(value) } fun updateShowImmersiveProgressBar(value: Boolean) = viewModelScope.launch { settingsRepository.updateShowImmersiveProgressBar(value) } + fun updatePreferLowestRefreshRate(value: Boolean) = viewModelScope.launch { settingsRepository.updatePreferLowestRefreshRate(value) } + fun updateMonochromeFlashMode(value: Boolean) = viewModelScope.launch { settingsRepository.updateMonochromeFlashMode(value) } fun updateImmersiveBrightnessPercent(value: Int) = viewModelScope.launch { settingsRepository.updateImmersiveBrightnessPercent(value) } diff --git a/app/src/main/java/com/mindmachine/mvp/session/TimelineModel.kt b/app/src/main/java/com/mindmachine/mvp/session/TimelineModel.kt index 78416b2..d23c7cb 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/TimelineModel.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/TimelineModel.kt @@ -84,6 +84,7 @@ data class TimelineModel( mode = mode, visualPatternType = preset.visualPatternType, flashIntervalMs = flashIntervalMs, + monochromeFlashMode = false, intensityPercent = preset.intensityPercent, carrierFrequencyHz = carrier, binauralDifferenceHz = diff