package com.mindmachine.mvp import android.app.Activity import android.content.pm.ActivityInfo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.darkColorScheme 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 import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext 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 import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.mindmachine.mvp.audio.BinauralAudioEngine import com.mindmachine.mvp.audio.HeadsetMonitor import com.mindmachine.mvp.data.SettingsRepository import com.mindmachine.mvp.domain.CountdownPreference import com.mindmachine.mvp.domain.RuntimeState import com.mindmachine.mvp.domain.SessionMode import com.mindmachine.mvp.session.FlashColor import com.mindmachine.mvp.session.MainViewModel 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( primary = Color(0xFF8AB4FF), onPrimary = Color(0xFF001B3D), primaryContainer = Color(0xFF004689), onPrimaryContainer = Color(0xFFD7E3FF), secondary = Color(0xFFFFD166), onSecondary = Color(0xFF2E2300), background = Color(0xFF05070D), onBackground = Color(0xFFF3F6FF), surface = Color(0xFF121621), onSurface = Color(0xFFF3F6FF), error = Color(0xFFFF6B6B), onError = Color(0xFF3A0002) ) class MainActivity : ComponentActivity() { private val vm: MainViewModel by viewModels { MainViewModel.Factory(SettingsRepository(applicationContext), HeadsetMonitor(applicationContext), BinauralAudioEngine()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme(colorScheme = MindMachineColors) { App(vm) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun App(vm: MainViewModel = viewModel()) { val nav = rememberNavController() val ui by vm.ui.collectAsStateWithLifecycle() val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_STOP && ui.runtimeState == RuntimeState.RUNNING) { vm.pause("Session paused because app moved to background.") } } lifecycle.addObserver(observer) onDispose { lifecycle.removeObserver(observer) } } val startRoute = if (ui.settings.safetyAcknowledged) "home" else "welcome" NavHost(navController = nav, startDestination = startRoute, modifier = Modifier.background(MaterialTheme.colorScheme.background)) { composable("welcome") { SimpleScreen("MindMachine", "Blinking light + binaural audio. Stereo headphones required for binaural mode. Not a medical device.") { Button(onClick = { nav.navigate("safety") }) { Text("Continue") } } } composable("safety") { SafetyScreen( onAck = { vm.acknowledgeSafety() nav.navigate("home") { popUpTo(0) } }, onHolder = { nav.navigate("holder") } ) } composable("home") { Column(Modifier.fillMaxSize()) { TopAppBar(title = { Text("MindMachine") }, actions = { TextButton(onClick = { nav.navigate("settings") }) { Text("Settings") } }) LazyColumn(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { item { TextButton(onClick = { nav.navigate("holder") }) { Text("Holder Guidance") } } items(ui.presets) { p -> Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onSurface ), modifier = Modifier.fillMaxWidth().clickable { vm.choosePreset(p.id) nav.navigate("setup") }.padding(4.dp) ) { Column(Modifier.padding(12.dp)) { Text(p.name, style = MaterialTheme.typography.titleMedium) Text(p.description) Text("${p.defaultDurationSec / 60} min • ${p.visualPatternType} • binaural ${p.binauralDifferenceHz}") } } } } } } composable("setup") { SetupScreen(vm = vm, onStart = { vm.startSession() nav.navigate("active") }, onHolder = { nav.navigate("holder") }) } composable("active") { ActiveSessionScreen( vm = vm, onFinish = { nav.navigate("complete") { popUpTo("setup") } }, onBackToSetup = { nav.popBackStack("setup", inclusive = false) } ) } composable("complete") { SimpleScreen( if (ui.endedEarly) "Session ended" else "Session complete.", ui.selectedPreset.name ) { Button(onClick = { vm.startSession() nav.navigate("active") }) { Text("Repeat Session") } OutlinedButton(onClick = { nav.navigate("home") { popUpTo(0) } }) { Text("Return Home") } } } composable("settings") { SettingsScreen(vm) { nav.popBackStack() } } composable("holder") { HolderScreen { nav.popBackStack() } } } } @Composable fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnScope.() -> Unit) { Column( modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text(title, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground) Text(subtitle, color = MaterialTheme.colorScheme.onBackground) actions() } } @Composable fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) { var checked by remember { mutableStateOf(false) } Column(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp)) { Text("Read before using MindMachine", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Spacer(Modifier.height(12.dp)) Text( "Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers.\n\nDo not use while driving, walking, cycling, or operating machinery.\n\nStop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain.\n\nBinaural mode requires stereo headphones.\n\nThis app is not a medical device.", color = MaterialTheme.colorScheme.onBackground ) Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = checked, onCheckedChange = { checked = it }) Text("I understand the risks and will stop immediately if I feel discomfort.", color = MaterialTheme.colorScheme.onBackground) } Button(onClick = onAck, enabled = checked, modifier = Modifier.semantics { contentDescription = "I Understand" }) { Text("I Understand") } TextButton(onClick = onHolder) { Text("Holder Guidance") } } } @Composable fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() val cfg = ui.config val scrollState = rememberScrollState() val containerModifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .then(if (setupScreenUsesScrollableContainer()) Modifier.verticalScroll(scrollState) else Modifier) .safeDrawingPadding() .padding(horizontal = 16.dp, vertical = 12.dp) Column( containerModifier, verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text(ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Text("Duration: ${cfg.durationSec / 60} min", color = MaterialTheme.colorScheme.onBackground) Slider(value = (cfg.durationSec / 60).toFloat(), onValueChange = { vm.setDurationMin(it.roundToInt()) }, valueRange = 1f..30f) Text("Mode", color = MaterialTheme.colorScheme.onBackground) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { SessionMode.values().forEachIndexed { index, mode -> SegmentedButton( shape = androidx.compose.material3.SegmentedButtonDefaults.itemShape(index = index, count = SessionMode.values().size), selected = cfg.mode == mode, onClick = { vm.setMode(mode) }, label = { Text(mode.name.replace("_", " ")) } ) } } Text(setupScreenFlashIntervalLabel(cfg.flashIntervalMs), color = MaterialTheme.colorScheme.onBackground) Slider( value = flashIntervalMsToSeconds(cfg.flashIntervalMs), onValueChange = { vm.setFlashIntervalMs(flashIntervalSecondsToMs(it)) }, valueRange = 0.05f..2.0f ) Text("Controls how long each frame is shown in the red/green/black pattern.", color = MaterialTheme.colorScheme.onBackground) Text("Carrier frequency: ${cfg.carrierFrequencyHz.roundToInt()}", color = MaterialTheme.colorScheme.onBackground) Slider(value = cfg.carrierFrequencyHz, onValueChange = vm::setCarrier, valueRange = 80f..400f) Text("Binaural difference: ${cfg.binauralDifferenceHz}", color = MaterialTheme.colorScheme.onBackground) Slider(value = cfg.binauralDifferenceHz, onValueChange = vm::setDifference, valueRange = 0.5f..20f) Text("Brightness recommendation: keep screen comfortable and avoid eye strain.", color = MaterialTheme.colorScheme.onBackground) TextButton(onClick = onHolder) { Text("Holder Guidance") } if (ui.error != null) Text(ui.error!!, color = MaterialTheme.colorScheme.error) Button(onClick = onStart, modifier = Modifier.fillMaxWidth().height(52.dp)) { Text("Start") } Spacer(Modifier.height(8.dp)) } } internal fun setupScreenFlashIntervalLabel(flashIntervalMs: Int): String = "Flash interval: ${"%.2f".format(flashIntervalMsToSeconds(flashIntervalMs))} s" internal fun setupScreenUsesScrollableContainer(): Boolean = true @Composable fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() if (ui.runtimeState == RuntimeState.COMPLETED) onFinish() BackHandler { vm.stop() onBackToSetup() } val isVisualMode = ui.config.mode != SessionMode.AUDIO_ONLY val context = LocalContext.current DisposableEffect(isVisualMode) { val activity = context as? Activity val previous = activity?.requestedOrientation if (isVisualMode) { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } onDispose { if (isVisualMode) { activity?.requestedOrientation = previous ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } } 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) } val flashFrame by rememberSplitFlashFrame( isRunning = ui.runtimeState == RuntimeState.RUNNING, mode = ui.config.mode, flashIntervalMs = ui.config.flashIntervalMs, ) Box( modifier = Modifier .fillMaxSize() .background(Color.Black) .clickable { showOverlay = !showOverlay if (shouldRevealSystemBarsOnTap(isVisualMode, ui.runtimeState)) { revealSystemBarsSignal += 1 } } ) { if (isVisualMode) { Row(Modifier.fillMaxSize()) { Box( modifier = Modifier .weight(1f) .fillMaxHeight() .background(flashFrame.left.toComposeColor()) ) Box( modifier = Modifier .weight(1f) .fillMaxHeight() .background(flashFrame.right.toComposeColor()) ) } } if (ui.runtimeState == RuntimeState.COUNTDOWN) { Text( "${ui.countdownSec}", modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.displayLarge, color = Color.White ) } if (showOverlay) { Column( Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .background(Color.Black.copy(alpha = 0.72f)) .padding(16.dp) ) { Text("${ui.selectedPreset.name} • ${ui.remainingSec}s", color = Color.White) if (ui.runtimeState == RuntimeState.RUNNING) { Button(onClick = { vm.pause() }, modifier = Modifier.fillMaxWidth()) { Text("Pause") } } else { Button(onClick = { vm.resume() }, modifier = Modifier.fillMaxWidth()) { Text("Resume") } } OutlinedButton(onClick = { vm.stop(); onFinish() }, modifier = Modifier.fillMaxWidth()) { Text("Stop") } if (ui.interruptionReason != null) { Text(ui.interruptionReason!!, color = Color.White) if (ui.runtimeState == RuntimeState.INTERRUPTED) { OutlinedButton(onClick = { vm.switchToVisualOnlyAndResume() }, modifier = Modifier.fillMaxWidth()) { Text("Resume Visual-only") } } } } } } } @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 FlashColor.BLACK -> Color.Black } @Composable private fun rememberSplitFlashFrame( isRunning: Boolean, mode: SessionMode, flashIntervalMs: Int, ) = produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning, mode, flashIntervalMs) { if (!isRunning || mode == SessionMode.AUDIO_ONLY) { value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK) return@produceState } while (true) { withFrameNanos { frameTimeNanos -> value = computeSplitFlashFrame(frameTimeNanos, flashIntervalMs) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(vm: MainViewModel, onBack: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() var expanded by remember { mutableStateOf(false) } Column( Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text("Settings", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Text("Countdown", color = MaterialTheme.colorScheme.onBackground) ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { TextButton(onClick = { expanded = true }) { Text(ui.settings.countdownPreference.name) } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { CountdownPreference.values().forEach { DropdownMenuItem(text = { Text(it.name) }, onClick = { vm.updateCountdown(it); expanded = false }) } } } Text("Default mode", color = MaterialTheme.colorScheme.onBackground) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { SessionMode.values().forEach { mode -> OutlinedButton(onClick = { vm.updateDefaultMode(mode) }) { Text(mode.name) } } } Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = ui.settings.showHolderGuidanceBeforeSession, onCheckedChange = vm::updateGuidance) Text("Show holder guidance before session", color = MaterialTheme.colorScheme.onBackground) } HorizontalDivider() Text("About/Disclaimer: MindMachine is a prototype and not a medical device.", color = MaterialTheme.colorScheme.onBackground) OutlinedButton(onClick = onBack) { Text("Back") } } } @Composable fun HolderScreen(onBack: () -> Unit) { Column( Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text("Holder Guidance", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Text("• Use a simple cardboard visor/holder.", color = MaterialTheme.colorScheme.onBackground) Text("• Keep phone stable and hands-free.", color = MaterialTheme.colorScheme.onBackground) Text("• Do not press device against eyes/face.", color = MaterialTheme.colorScheme.onBackground) Text("• Allow airflow and comfort.", color = MaterialTheme.colorScheme.onBackground) Text("• Test fit before session.", color = MaterialTheme.colorScheme.onBackground) Text("• Sit or lie down in a safe place.", color = MaterialTheme.colorScheme.onBackground) OutlinedButton(onClick = onBack) { Text("Back") } } }