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.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.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.getValue 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.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 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} • ${p.binauralDifferenceHz} Hz") } } } } } } 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") } }) } 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 Column( Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp), 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("Flash interval: ${cfg.flashIntervalMs} ms", color = MaterialTheme.colorScheme.onBackground) Slider( value = cfg.flashIntervalMs.toFloat(), onValueChange = { vm.setFlashIntervalMs(it.roundToInt()) }, valueRange = 50f..2000f ) Text("Controls how long each frame is shown (red/green/black pattern).", color = MaterialTheme.colorScheme.onBackground) Text("Carrier ${cfg.carrierFrequencyHz.roundToInt()} Hz", color = MaterialTheme.colorScheme.onBackground) Slider(value = cfg.carrierFrequencyHz, onValueChange = vm::setCarrier, valueRange = 80f..400f) Text("Difference ${cfg.binauralDifferenceHz} Hz", 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") } } } @Composable fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() if (ui.runtimeState == RuntimeState.COMPLETED || ui.runtimeState == RuntimeState.STOPPED) onFinish() 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 } } } var showOverlay by remember { mutableStateOf(true) } 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 (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() }, 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") } } } } } } } 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") } } }