package com.hawhamburg.micr0bu import android.Manifest import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.appcompat.app.AppCompatActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.hawhamburg.micr0bu.data.mqtt.MqttConnectionState import com.hawhamburg.micr0bu.data.transport.UsbSerialState import com.hawhamburg.micr0bu.data.transport.ObuHardware import com.hawhamburg.micr0bu.ui.components.StatusTopBar import com.hawhamburg.micr0bu.ui.navigation.BottomNavBar import com.hawhamburg.micr0bu.ui.navigation.Screen import com.hawhamburg.micr0bu.ui.screens.ConnectionSetupScreen import com.hawhamburg.micr0bu.ui.screens.DashboardScreen import com.hawhamburg.micr0bu.ui.screens.MqttTopicViewerScreen import com.hawhamburg.micr0bu.ui.screens.RecordingScreen import com.hawhamburg.micr0bu.ui.screens.SensorScreen import com.hawhamburg.micr0bu.ui.screens.SessionLogScreen import com.hawhamburg.micr0bu.ui.screens.MapScreen import com.hawhamburg.micr0bu.ui.screens.TripHistoryScreen import com.hawhamburg.micr0bu.ui.screens.TripReviewScreen import com.hawhamburg.micr0bu.ui.screens.AboutSettingsScreen import com.hawhamburg.micr0bu.ui.screens.AppearanceSettingsScreen import com.hawhamburg.micr0bu.ui.screens.ConnectionSettingsScreen import com.hawhamburg.micr0bu.ui.screens.DeveloperSettingsScreen import com.hawhamburg.micr0bu.ui.screens.LanguageSettingsScreen import com.hawhamburg.micr0bu.ui.screens.MqttSettingsScreen import com.hawhamburg.micr0bu.ui.screens.SensorsSettingsScreen import com.hawhamburg.micr0bu.ui.screens.SettingsScreen import com.hawhamburg.micr0bu.ui.screens.UnitsSettingsScreen import com.hawhamburg.micr0bu.ui.screens.UseCaseAlertsSettingsScreen import com.hawhamburg.micr0bu.ui.theme.MicrOBUTheme import com.hawhamburg.micr0bu.viewmodel.MqttViewModel import com.hawhamburg.micr0bu.viewmodel.SensorViewModel import com.hawhamburg.micr0bu.viewmodel.TripRecordingViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : AppCompatActivity() { private val viewModel: SensorViewModel by viewModels() private val mqttViewModel: MqttViewModel by viewModels() private val tripViewModel: TripRecordingViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) enableEdgeToEdge( statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), ) setContent { val state by viewModel.state.collectAsState() val mqttPrefs by mqttViewModel.mqttPrefs.collectAsState() val mqttConnectionState by mqttViewModel.connectionState.collectAsState() val activeTransport by mqttViewModel.activeTransport.collectAsState() val usbConnected by mqttViewModel.usbConnected.collectAsState() val obuStationTypeWarning by mqttViewModel.obuStationTypeWarning.collectAsState() val obuStationType by mqttViewModel.obuStationType.collectAsState() val tripServiceState by tripViewModel.serviceState.collectAsState() val showBatteryOptPrompt by tripViewModel.showBatteryOptPrompt.collectAsState() val useCaseEnabledMap by mqttViewModel.useCaseEnabledMap.collectAsState() val obuHardware by mqttViewModel.obuHardware.collectAsState() val usbSerialState by mqttViewModel.usbSerialState.collectAsState() // Received hazards and live signal state, for the Dashboard's V2X summary cards. // Both flows already expire their own entries on a clock, so nothing here has to // decide when a hazard or a traffic light has gone stale. val denmEvents by mqttViewModel.denmEvents.collectAsState() val spatIntersections by mqttViewModel.spatIntersections.collectAsState() val ownCamPosition by mqttViewModel.ownCamPosition.collectAsState() MicrOBUTheme(darkTheme = state.darkTheme) { val view = LocalView.current SideEffect { WindowCompat.getInsetsController(window, view).apply { isAppearanceLightStatusBars = !state.darkTheme isAppearanceLightNavigationBars = !state.darkTheme } } val navController = rememberNavController() val locationLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> val granted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true || permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true if (granted) viewModel.onLocationPermissionGranted() } // Request POST_NOTIFICATIONS on Android 13+ val notificationLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { /* ignored — notification permission is best-effort */ } LaunchedEffect(Unit) { viewModel.startImuStreams() locationLauncher.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, ) ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } } Scaffold( topBar = { StatusTopBar( state = state, mqttConnectionState = mqttConnectionState, isEsp32 = obuHardware == ObuHardware.ESP32_C5, usbSerialState = usbSerialState, ) }, bottomBar = { BottomNavBar(navController) }, ) { innerPadding -> NavHost( navController = navController, startDestination = Screen.Dashboard.route, modifier = Modifier.padding(innerPadding), ) { composable(Screen.Dashboard.route) { DashboardScreen( state = state, mqttConnectionState = mqttConnectionState, activeTransport = activeTransport, obuHardware = obuHardware, usbSerialState = usbSerialState, usbCableConnected = usbConnected, obuStationTypeWarning = obuStationTypeWarning, obuStationType = obuStationType, hazards = denmEvents, signals = spatIntersections, ownPosition = ownCamPosition, onNavigateToConnection = { navController.navigate(Screen.Connection.route) }, onNavigateToSensors = { navController.navigate(Screen.Sensors.route) { popUpTo(Screen.Dashboard.route) launchSingleTop = true } }, onNavigateToMap = { navController.navigate(Screen.Map.route) }, onNavigateToRecord = { navController.navigate(Screen.Record.route) { popUpTo(Screen.Dashboard.route) launchSingleTop = true } }, // Same options the bottom bar uses, so arriving at V2X from a // Dashboard card leaves the same back stack as tapping the tab. onNavigateToV2x = { navController.navigate(Screen.MqttViewer.route) { popUpTo(Screen.Dashboard.route) launchSingleTop = true } }, ) } composable(Screen.Sensors.route) { SensorScreen(state = state) } composable(Screen.Record.route) { RecordingScreen( state = state, mqttConnectionState = mqttConnectionState, obuConnected = if (obuHardware == ObuHardware.ESP32_C5) usbSerialState == UsbSerialState.CONNECTED else mqttConnectionState == MqttConnectionState.CONNECTED, tripServiceState = tripServiceState, showBatteryOptPrompt = showBatteryOptPrompt, onToggleRecording = { // Start / stop both CSV recording and the event detection // service. Order matters on start: the CSV session must exist // before the trip row is written, so the trip can store its // id and later delete the CSV along with itself. viewModel.toggleRecording() tripViewModel.toggleRecording(viewModel.activeSessionId) }, onOpenSessionLog = { navController.navigate(Screen.Log.route) }, onOpenBatterySettings = { startActivity( Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { data = Uri.parse("package:$packageName") } ) tripViewModel.recheckBatteryOpt() }, onDismissBatteryOptPrompt = tripViewModel::dismissBatteryOptPrompt, ) } composable(Screen.Log.route) { SessionLogScreen( state = state, onDeleteSession = viewModel::deleteSession, ) } // ── Phase A: Trip History ───────────────────────────── composable(Screen.TripHistory.route) { TripHistoryScreen( viewModel = tripViewModel, onOpenTrip = { tripId -> navController.navigate(Screen.TripReview.route(tripId)) }, ) } composable( route = Screen.TripReview.route, arguments = listOf( navArgument("tripId") { type = NavType.LongType } ), ) { backStackEntry -> val tripId = backStackEntry.arguments ?.getLong("tripId", -1L) ?: -1L if (tripId < 0L) return@composable val trips by tripViewModel.trips.collectAsState(initial = emptyList()) val trip = trips.firstOrNull { it.id == tripId } if (trip != null) { TripReviewScreen(trip = trip) } } composable(Screen.MqttViewer.route) { MqttTopicViewerScreen(viewModel = mqttViewModel) } composable(Screen.Settings.route) { SettingsScreen( onOpenLanguage = { navController.navigate(Screen.SettingsLanguage.route) }, onOpenAppearance = { navController.navigate(Screen.SettingsAppearance.route) }, onOpenSensors = { navController.navigate(Screen.SettingsSensors.route) }, onOpenUnits = { navController.navigate(Screen.SettingsUnits.route) }, onOpenConnection = { navController.navigate(Screen.SettingsConnection.route) }, onOpenMqtt = { navController.navigate(Screen.SettingsMqtt.route) }, onOpenUseCaseAlerts = { navController.navigate(Screen.SettingsUseCaseAlerts.route) }, onOpenDeveloper = { navController.navigate(Screen.SettingsDeveloper.route) }, onOpenAbout = { navController.navigate(Screen.SettingsAbout.route) }, ) } composable(Screen.SettingsUseCaseAlerts.route) { UseCaseAlertsSettingsScreen( enabledMap = useCaseEnabledMap, onToggle = mqttViewModel::setUseCaseEnabled, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsConnection.route) { ConnectionSettingsScreen( mqttPrefs = mqttPrefs, onMqttPrefsChange = mqttViewModel::updatePrefs, obuHardware = obuHardware, onObuHardwareChange = mqttViewModel::setObuHardware, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsLanguage.route) { LanguageSettingsScreen(onBack = { navController.popBackStack() }) } composable(Screen.SettingsAppearance.route) { AppearanceSettingsScreen( darkTheme = state.darkTheme, onDarkTheme = viewModel::setDarkTheme, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsSensors.route) { SensorsSettingsScreen( state = state, onAccelEnabled = viewModel::setAccelEnabled, onGyroEnabled = viewModel::setGyroEnabled, onMagnetEnabled = viewModel::setMagnetEnabled, onBarometerEnabled = viewModel::setBarometerEnabled, onGnssEnabled = viewModel::setGnssEnabled, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsUnits.route) { UnitsSettingsScreen( useMetric = state.useMetric, onUseMetric = viewModel::setUseMetric, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsMqtt.route) { MqttSettingsScreen( mqttPrefs = mqttPrefs, onMqttPrefsChange = mqttViewModel::updatePrefs, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsDeveloper.route) { DeveloperSettingsScreen( state = state, onDeveloperMode = viewModel::setDeveloperMode, onOpenSensorMonitor = { navController.navigate(Screen.Sensors.route) }, onBack = { navController.popBackStack() }, ) } composable(Screen.SettingsAbout.route) { AboutSettingsScreen(onBack = { navController.popBackStack() }) } composable(Screen.Connection.route) { // Must be the Activity-scoped instance (same one MainActivity holds // and MqttTopicViewerScreen is given below), not the default // hiltViewModel() — that would create a separate instance scoped to // this NavBackStackEntry, whose onCleared() (fired the moment you // navigate away) would disconnect the shared UsbSerialTransport out // from under every other screen still using. ConnectionSetupScreen(viewModel = mqttViewModel) } composable(Screen.Map.route) { MapScreen(gnss = state.gnss) } } } } } } }