2026-03-10 22:50:50 -05:00
package com.mindmachine.mvp
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.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
2026-03-10 23:14:33 -05:00
import androidx.compose.material3.CardDefaults
2026-03-10 22:50:50 -05:00
import androidx.compose.material3.Checkbox
2026-03-10 23:03:46 -05:00
import androidx.compose.material3.DropdownMenu
2026-03-10 22:50:50 -05:00
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.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
2026-03-10 23:14:33 -05:00
import androidx.compose.material3.darkColorScheme
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
2026-03-10 23:14:33 -05:00
import androidx.compose.runtime.mutableFloatStateOf
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.mutableStateOf
2026-03-10 23:14:33 -05:00
import androidx.compose.runtime.produceState
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
2026-03-10 23:14:33 -05:00
import androidx.compose.runtime.withFrameNanos
2026-03-10 22:50:50 -05:00
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
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
2026-03-10 23:14:33 -05:00
import com.mindmachine.mvp.domain.VisualPattern
2026-03-10 22:50:50 -05:00
import com.mindmachine.mvp.session.MainViewModel
2026-03-10 23:14:33 -05:00
import com.mindmachine.mvp.session.computeVisualAlpha
2026-03-10 22:50:50 -05:00
import kotlin.math.roundToInt
2026-03-10 23:14:33 -05:00
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 )
)
2026-03-10 22:50:50 -05:00
class MainActivity : ComponentActivity ( ) {
private val vm : MainViewModel by viewModels {
MainViewModel . Factory ( SettingsRepository ( applicationContext ) , HeadsetMonitor ( applicationContext ) , BinauralAudioEngine ( ) )
}
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
2026-03-10 23:14:33 -05:00
setContent {
MaterialTheme ( colorScheme = MindMachineColors ) {
App ( vm )
}
}
2026-03-10 22:50:50 -05:00
}
}
2026-03-10 23:03:46 -05:00
@OptIn ( ExperimentalMaterial3Api :: class )
2026-03-10 22:50:50 -05:00
@Composable
fun App ( vm : MainViewModel = viewModel ( ) ) {
val nav = rememberNavController ( )
val ui by vm . ui . collectAsStateWithLifecycle ( )
val lifecycle = LocalLifecycleOwner . current . lifecycle
androidx . compose . runtime . 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 "
2026-03-10 23:14:33 -05:00
NavHost ( navController = nav , startDestination = startRoute , modifier = Modifier . background ( MaterialTheme . colorScheme . background ) ) {
2026-03-10 22:50:50 -05:00
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 ->
2026-03-10 23:14:33 -05:00
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 )
) {
2026-03-10 22:50:50 -05:00
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 (
2026-03-10 23:14:33 -05:00
modifier = Modifier . fillMaxSize ( ) . background ( MaterialTheme . colorScheme . background ) . padding ( 16. dp ) ,
2026-03-10 22:50:50 -05:00
verticalArrangement = Arrangement . spacedBy ( 12. dp ) ,
) {
2026-03-10 23:14:33 -05:00
Text ( title , style = MaterialTheme . typography . headlineMedium , color = MaterialTheme . colorScheme . onBackground )
Text ( subtitle , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
actions ( )
}
}
@Composable
fun SafetyScreen ( onAck : ( ) -> Unit , onHolder : ( ) -> Unit ) {
var checked by remember { mutableStateOf ( false ) }
2026-03-10 23:14:33 -05:00
Column ( Modifier . fillMaxSize ( ) . background ( MaterialTheme . colorScheme . background ) . padding ( 16. dp ) ) {
Text ( " Read before using MindMachine " , style = MaterialTheme . typography . headlineSmall , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Spacer ( Modifier . height ( 12. dp ) )
2026-03-10 23:14:33 -05:00
Text (
" Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers. \n \n Do not use while driving, walking, cycling, or operating machinery. \n \n Stop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain. \n \n Binaural mode requires stereo headphones. \n \n This app is not a medical device. " ,
color = MaterialTheme . colorScheme . onBackground
)
2026-03-10 22:50:50 -05:00
Row ( verticalAlignment = Alignment . CenterVertically ) {
Checkbox ( checked = checked , onCheckedChange = { checked = it } )
2026-03-10 23:14:33 -05:00
Text ( " I understand the risks and will stop immediately if I feel discomfort. " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
}
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
2026-03-10 23:14:33 -05:00
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 )
2026-03-10 22:50:50 -05:00
Slider ( value = ( cfg . durationSec / 60 ) . toFloat ( ) , onValueChange = { vm . setDurationMin ( it . roundToInt ( ) ) } , valueRange = 1f .. 30f )
2026-03-10 23:14:33 -05:00
Text ( " Mode " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Row ( horizontalArrangement = Arrangement . spacedBy ( 8. dp ) ) {
SessionMode . values ( ) . forEach { mode ->
OutlinedButton ( onClick = { vm . setMode ( mode ) } ) { Text ( mode . name . replace ( " _ " , " " ) ) }
}
}
2026-03-10 23:14:33 -05:00
Text ( " Blink ${cfg.blinkFrequencyHz} Hz " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Slider ( value = cfg . blinkFrequencyHz , onValueChange = vm :: setBlinkFrequency , valueRange = 1f .. 20f )
2026-03-10 23:14:33 -05:00
Text ( " Carrier ${cfg.carrierFrequencyHz.roundToInt()} Hz " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Slider ( value = cfg . carrierFrequencyHz , onValueChange = vm :: setCarrier , valueRange = 80f .. 400f )
2026-03-10 23:14:33 -05:00
Text ( " Difference ${cfg.binauralDifferenceHz} Hz " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Slider ( value = cfg . binauralDifferenceHz , onValueChange = vm :: setDifference , valueRange = 0.5f .. 20f )
2026-03-10 23:14:33 -05:00
Text ( " Brightness recommendation: keep screen comfortable and avoid eye strain. " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
TextButton ( onClick = onHolder ) { Text ( " Holder Guidance " ) }
2026-03-10 23:14:33 -05:00
if ( ui . error != null ) Text ( ui . error !! , color = MaterialTheme . colorScheme . error )
2026-03-10 22:50:50 -05:00
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 ( )
var showOverlay by remember { mutableStateOf ( true ) }
2026-03-10 23:14:33 -05:00
val visualAlpha by rememberVisualAlpha (
isRunning = ui . runtimeState == RuntimeState . RUNNING ,
mode = ui . config . mode ,
visualPattern = ui . config . visualPatternType ,
blinkFrequencyHz = ui . config . blinkFrequencyHz ,
intensityPercent = ui . config . intensityPercent
)
2026-03-10 22:50:50 -05:00
Box (
2026-03-10 23:14:33 -05:00
modifier = Modifier
. fillMaxSize ( )
. background ( Color . Black )
. background ( Color . White . copy ( alpha = visualAlpha ) )
2026-03-10 22:50:50 -05:00
. clickable { showOverlay = ! showOverlay }
) {
if ( ui . runtimeState == RuntimeState . COUNTDOWN ) {
Text (
" ${ui.countdownSec} " ,
modifier = Modifier . align ( Alignment . Center ) ,
style = MaterialTheme . typography . displayLarge ,
2026-03-10 23:14:33 -05:00
color = Color . White
2026-03-10 22:50:50 -05:00
)
}
if ( showOverlay ) {
2026-03-10 23:14:33 -05:00
Column (
Modifier
. align ( Alignment . BottomCenter )
. fillMaxWidth ( )
. background ( Color . Black . copy ( alpha = 0.68f ) )
. padding ( 16. dp )
) {
2026-03-10 22:50:50 -05:00
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 " )
}
}
}
}
}
}
}
2026-03-10 23:14:33 -05:00
@Composable
private fun rememberVisualAlpha (
isRunning : Boolean ,
mode : SessionMode ,
visualPattern : VisualPattern ,
blinkFrequencyHz : Float ,
intensityPercent : Int ,
) : androidx . compose . runtime . State < Float > {
return produceState ( initialValue = 0f , isRunning , mode , visualPattern , blinkFrequencyHz , intensityPercent ) {
if ( !is Running || mode == SessionMode . AUDIO _ONLY ) {
value = 0f
return @produceState
}
while ( true ) {
withFrameNanos { frameTimeNanos ->
value = computeVisualAlpha (
visualPattern = visualPattern ,
frameTimeNanos = frameTimeNanos ,
blinkFrequencyHz = blinkFrequencyHz ,
intensityPercent = intensityPercent
)
}
}
}
}
2026-03-10 22:50:50 -05:00
@OptIn ( ExperimentalMaterial3Api :: class )
@Composable
fun SettingsScreen ( vm : MainViewModel , onBack : ( ) -> Unit ) {
val ui by vm . ui . collectAsStateWithLifecycle ( )
var expanded by remember { mutableStateOf ( false ) }
2026-03-10 23:14:33 -05:00
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 )
2026-03-10 22:50:50 -05:00
ExposedDropdownMenuBox ( expanded = expanded , onExpandedChange = { expanded = it } ) {
TextButton ( onClick = { expanded = true } ) { Text ( ui . settings . countdownPreference . name ) }
2026-03-10 23:03:46 -05:00
DropdownMenu ( expanded = expanded , onDismissRequest = { expanded = false } ) {
2026-03-10 22:50:50 -05:00
CountdownPreference . values ( ) . forEach {
DropdownMenuItem ( text = { Text ( it . name ) } , onClick = { vm . updateCountdown ( it ) ; expanded = false } )
}
}
}
2026-03-10 23:14:33 -05:00
Text ( " Default mode " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
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 )
2026-03-10 23:14:33 -05:00
Text ( " Show holder guidance before session " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
}
HorizontalDivider ( )
2026-03-10 23:14:33 -05:00
Text ( " About/Disclaimer: MindMachine is a prototype and not a medical device. " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
OutlinedButton ( onClick = onBack ) { Text ( " Back " ) }
}
}
@Composable
fun HolderScreen ( onBack : ( ) -> Unit ) {
2026-03-10 23:14:33 -05:00
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 )
2026-03-10 22:50:50 -05:00
OutlinedButton ( onClick = onBack ) { Text ( " Back " ) }
}
}