Files
MicrOBU/app/src/main/java/com/hawhamburg/micr0bu/MainActivity.kt
T

339 lines
18 KiB
Kotlin
Raw Normal View History

2026-06-03 14:25:20 +02:00
package com.hawhamburg.micr0bu
import android.Manifest
import android.content.Intent
import android.net.Uri
2026-06-03 14:25:20 +02:00
import android.os.Build
import android.os.Bundle
import android.provider.Settings
2026-06-03 14:25:20 +02:00
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
2026-06-03 14:25:20 +02:00
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
2026-06-03 14:25:20 +02:00
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()
2026-06-03 14:25:20 +02:00
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()
2026-06-03 14:25:20 +02:00
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,
)
},
2026-06-03 14:25:20 +02:00
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,
2026-06-03 14:25:20 +02:00
usbCableConnected = usbConnected,
obuStationTypeWarning = obuStationTypeWarning,
obuStationType = obuStationType,
2026-06-03 14:25:20 +02:00
onNavigateToConnection = { navController.navigate(Screen.Connection.route) },
onNavigateToSensors = {
navController.navigate(Screen.Sensors.route) {
popUpTo(Screen.Dashboard.route) { saveState = true }
launchSingleTop = true
restoreState = true
}
},
onNavigateToMap = { navController.navigate(Screen.Map.route) },
onNavigateToRecord = {
navController.navigate(Screen.Record.route) {
popUpTo(Screen.Dashboard.route) { saveState = true }
launchSingleTop = true
restoreState = true
}
},
2026-06-03 14:25:20 +02:00
)
}
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,
2026-06-03 14:25:20 +02:00
tripServiceState = tripServiceState,
showBatteryOptPrompt = showBatteryOptPrompt,
2026-06-03 14:25:20 +02:00
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.
2026-06-03 14:25:20 +02:00
viewModel.toggleRecording()
tripViewModel.toggleRecording(viewModel.activeSessionId)
2026-06-03 14:25:20 +02:00
},
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,
2026-06-03 14:25:20 +02:00
)
}
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,
viewModel = tripViewModel,
)
}
}
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) },
2026-06-03 14:25:20 +02:00
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() },
)
}
2026-06-03 14:25:20 +02:00
composable(Screen.SettingsConnection.route) {
ConnectionSettingsScreen(
mqttPrefs = mqttPrefs,
onMqttPrefsChange = mqttViewModel::updatePrefs,
obuHardware = obuHardware,
onObuHardwareChange = mqttViewModel::setObuHardware,
2026-06-03 14:25:20 +02:00
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) },
2026-06-03 14:25:20 +02:00
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)
2026-06-03 14:25:20 +02:00
}
composable(Screen.Map.route) {
MapScreen(gnss = state.gnss)
}
}
}
}
}
}
}