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

302 lines
16 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.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()
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, mqttConnectionState) },
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,
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) },
)
}
composable(Screen.Sensors.route) {
SensorScreen(state = state)
}
composable(Screen.Record.route) {
RecordingScreen(
state = state,
mqttConnectionState = mqttConnectionState,
tripServiceState = tripServiceState,
showBatteryOptPrompt = showBatteryOptPrompt,
2026-06-03 14:25:20 +02:00
onToggleRecording = {
// Start / stop both CSV recording and the event detection service
viewModel.toggleRecording()
tripViewModel.toggleRecording()
},
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,
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,
onBack = { navController.popBackStack() },
)
}
composable(Screen.SettingsAbout.route) {
AboutSettingsScreen(onBack = { navController.popBackStack() })
}
composable(Screen.Connection.route) {
ConnectionSetupScreen()
}
composable(Screen.Map.route) {
MapScreen(gnss = state.gnss)
}
}
}
}
}
}
}