From a4d81f41b01240177240b3a4cbd8cd591caded8b Mon Sep 17 00:00:00 2001 From: Tretzi Date: Tue, 10 Mar 2026 23:51:55 -0500 Subject: [PATCH] MindMachine: add immersive fullscreen and tap-to-reveal bars --- .../java/com/mindmachine/mvp/MainActivity.kt | 56 ++++++++++++++++++- .../mindmachine/mvp/session/ImmersiveMode.kt | 11 ++++ .../mindmachine/mvp/ActiveSessionUiTest.kt | 23 ++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/mindmachine/mvp/session/ImmersiveMode.kt diff --git a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt index b955d0a..8519672 100644 --- a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt +++ b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt @@ -48,6 +48,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -61,6 +62,9 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -80,7 +84,10 @@ import com.mindmachine.mvp.session.SplitFlashFrame import com.mindmachine.mvp.session.computeSplitFlashFrame import com.mindmachine.mvp.session.flashIntervalMsToSeconds import com.mindmachine.mvp.session.flashIntervalSecondsToMs +import com.mindmachine.mvp.session.shouldRevealSystemBarsOnTap import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault +import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen +import kotlinx.coroutines.delay import kotlin.math.roundToInt private val MindMachineColors = darkColorScheme( @@ -320,6 +327,13 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: } } + val immersiveFullscreen = shouldUseImmersiveFullscreen(isVisualMode, ui.runtimeState) + var revealSystemBarsSignal by remember { mutableIntStateOf(0) } + ApplyImmersiveMode( + enabled = immersiveFullscreen, + revealSignal = revealSystemBarsSignal, + ) + var showOverlay by remember(ui.runtimeState) { mutableStateOf(shouldShowActiveControlsByDefault(ui.runtimeState)) } LaunchedEffect(ui.runtimeState) { showOverlay = shouldShowActiveControlsByDefault(ui.runtimeState) @@ -334,7 +348,12 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: modifier = Modifier .fillMaxSize() .background(Color.Black) - .clickable { showOverlay = !showOverlay } + .clickable { + showOverlay = !showOverlay + if (shouldRevealSystemBarsOnTap(isVisualMode, ui.runtimeState)) { + revealSystemBarsSignal += 1 + } + } ) { if (isVisualMode) { Row(Modifier.fillMaxSize()) { @@ -389,6 +408,41 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: } } +@Composable +private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) { + val context = LocalContext.current + val activity = context as? Activity ?: return + val window = activity.window + val controller = remember(window) { WindowInsetsControllerCompat(window, window.decorView) } + + DisposableEffect(window) { + onDispose { + controller.show(WindowInsetsCompat.Type.systemBars()) + WindowCompat.setDecorFitsSystemWindows(window, true) + } + } + + LaunchedEffect(enabled) { + if (enabled) { + WindowCompat.setDecorFitsSystemWindows(window, false) + controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + controller.hide(WindowInsetsCompat.Type.systemBars()) + } else { + controller.show(WindowInsetsCompat.Type.systemBars()) + WindowCompat.setDecorFitsSystemWindows(window, true) + } + } + + LaunchedEffect(revealSignal, enabled) { + if (!enabled || revealSignal == 0) return@LaunchedEffect + controller.show(WindowInsetsCompat.Type.systemBars()) + delay(2500) + if (enabled) { + controller.hide(WindowInsetsCompat.Type.systemBars()) + } + } +} + private fun FlashColor.toComposeColor(): Color = when (this) { FlashColor.RED -> Color.Red FlashColor.GREEN -> Color.Green diff --git a/app/src/main/java/com/mindmachine/mvp/session/ImmersiveMode.kt b/app/src/main/java/com/mindmachine/mvp/session/ImmersiveMode.kt new file mode 100644 index 0000000..1f00469 --- /dev/null +++ b/app/src/main/java/com/mindmachine/mvp/session/ImmersiveMode.kt @@ -0,0 +1,11 @@ +package com.mindmachine.mvp.session + +import com.mindmachine.mvp.domain.RuntimeState + +fun shouldUseImmersiveFullscreen(isVisualMode: Boolean, runtimeState: RuntimeState): Boolean { + return isVisualMode && (runtimeState == RuntimeState.COUNTDOWN || runtimeState == RuntimeState.RUNNING) +} + +fun shouldRevealSystemBarsOnTap(isVisualMode: Boolean, runtimeState: RuntimeState): Boolean { + return shouldUseImmersiveFullscreen(isVisualMode, runtimeState) +} diff --git a/app/src/test/java/com/mindmachine/mvp/ActiveSessionUiTest.kt b/app/src/test/java/com/mindmachine/mvp/ActiveSessionUiTest.kt index cd841b9..86a4763 100644 --- a/app/src/test/java/com/mindmachine/mvp/ActiveSessionUiTest.kt +++ b/app/src/test/java/com/mindmachine/mvp/ActiveSessionUiTest.kt @@ -1,7 +1,9 @@ package com.mindmachine.mvp import com.mindmachine.mvp.domain.RuntimeState +import com.mindmachine.mvp.session.shouldRevealSystemBarsOnTap import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault +import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -21,4 +23,25 @@ class ActiveSessionUiTest { assertTrue(shouldShowActiveControlsByDefault(RuntimeState.COMPLETED)) assertTrue(shouldShowActiveControlsByDefault(RuntimeState.STOPPED)) } + + @Test + fun immersive_fullscreen_applies_only_to_visual_countdown_and_running_states() { + assertTrue(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.COUNTDOWN)) + assertTrue(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.RUNNING)) + + assertFalse(shouldUseImmersiveFullscreen(isVisualMode = false, runtimeState = RuntimeState.COUNTDOWN)) + assertFalse(shouldUseImmersiveFullscreen(isVisualMode = false, runtimeState = RuntimeState.RUNNING)) + assertFalse(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.PAUSED)) + assertFalse(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.INTERRUPTED)) + } + + @Test + fun tap_to_reveal_system_bars_is_only_enabled_during_active_visual_stimulation() { + assertTrue(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.COUNTDOWN)) + assertTrue(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.RUNNING)) + + assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = false, runtimeState = RuntimeState.RUNNING)) + assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.PAUSED)) + assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.COMPLETED)) + } }