Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,7 @@ dependencies {
implementation(libs.okhttp)
// Log
implementation(libs.stream.log)
// UI Rework
implementation("androidx.navigation:navigation-compose:2.9.7")
implementation("androidx.compose.material:material-icons-extended:1.7.8")
}
198 changes: 41 additions & 157 deletions app/src/main/java/org/WenuLink/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,25 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import io.getstream.log.taggedLogger
import org.WenuLink.sdk.SDKManager
import org.WenuLink.ui.navigation.AppNavigation
import org.WenuLink.ui.theme.WenuLinkTheme
import org.WenuLink.ui.utils.PrefsManager
import org.WenuLink.views.HomeViewModel
import org.WenuLink.views.ServicesViewModel
import org.WenuLink.views.SettingsViewModel
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainActivity : ComponentActivity() {
companion object {
Expand All @@ -57,6 +45,7 @@ class MainActivity : ComponentActivity() {

private val homeViewModel: HomeViewModel by viewModels()
private val servicesViewModel: ServicesViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()

private fun checkAndRequestPermissions() {
homeViewModel.updateWorkflow("Checking permissions")
Expand Down Expand Up @@ -91,10 +80,7 @@ class MainActivity : ComponentActivity() {
if (missingPermissions.isNotEmpty()) {
homeViewModel.updateWorkflow("Waiting for pending permissions")
val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts
.RequestMultiplePermissions()
) { permissionsMap ->
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissionsMap ->
if (permissionsMap.all { it.value }) {
onPermissionsGranted()
} else {
Expand All @@ -107,28 +93,43 @@ class MainActivity : ComponentActivity() {
}
}

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

checkAndRequestPermissions()

enableEdgeToEdge()

setContent {
WenuLinkTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(title = { Text("WenuLink status") })
val context = LocalContext.current

val initialTheme = remember { PrefsManager.getThemeMode(context) }
val themeMode by PrefsManager.themeFlow.collectAsState(initial = initialTheme)

val darkTheme = when (themeMode) {
1 -> false
2 -> true
else -> isSystemInDarkTheme()
}

WenuLinkTheme(darkTheme = darkTheme) {
var logMessages by remember { mutableStateOf(listOf("Waiting System Initialization...")) }

val addLog: (String) -> Unit = { message ->
val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
val formattedMessage = "[$time] $message"

logMessages = buildList {
add(formattedMessage)
addAll(logMessages.take(49))
}
) { innerPadding ->
MainScreen(
viewModel = homeViewModel,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
)
}

AppNavigation(
homeViewModel = homeViewModel,
servicesViewModel = servicesViewModel,
settingsViewModel = settingsViewModel,
logMessages = logMessages,
addLog = addLog
)
}
}
}
Expand All @@ -152,122 +153,5 @@ class MainActivity : ComponentActivity() {
if (!servicesViewModel.isServiceRunning.value) {
homeViewModel.stopSDK(applicationContext)
}
// TODO: mostrar aviso para forzar salida
}

@Composable
fun MainScreen(viewModel: HomeViewModel, modifier: Modifier = Modifier) {
// Basic
val isPermissionsGranted by viewModel.isPermissionsGranted.observeAsState(false)
val workflowStatus by viewModel.workflowStatus.observeAsState("Idle")
val isServiceRunning by servicesViewModel.isServiceRunning.collectAsState(false)
val isSimulationReady by servicesViewModel.isSimReady.collectAsState(false)
// DJI
val isSDKOk by viewModel.isRegistered.observeAsState(false)
val sdkStatus by viewModel.sdkStatus.observeAsState("Idle")
// val canRunService by viewModel.canRunService.observeAsState(false)
// val bindingState by viewModel.bindingState.observeAsState("Waiting Binding")
// val activationState by viewModel.activationState.observeAsState("Waiting Activation")
// MAVLink
val telemetry by servicesViewModel.telemetryData.observeAsState()
val isDataFlowing by servicesViewModel.isDataFlowing.collectAsState(false)
val isMAVLinkRunning by servicesViewModel.isMAVLinkRunning.collectAsState(false)
// WebRTC
val isWebRTCRunning by servicesViewModel.isWebRTCRunning.collectAsState(false)
// Logs
var logMessages by remember { mutableStateOf(listOf<String>()) }

fun buttonLabel(isRunning: Boolean) = if (isRunning) "Stop" else "Start"

// UI code here using telemetry and status
Column(
modifier = modifier.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("App status:")
Text(workflowStatus)
if (isPermissionsGranted) {
Spacer(Modifier.height(4.dp))
Text("SDK registered?: $isSDKOk")
Spacer(Modifier.height(2.dp))
Text("DataFlow active?: $isDataFlowing")
Spacer(Modifier.height(2.dp))
Text("MAVLinkService up?: $isMAVLinkRunning")
Spacer(Modifier.height(2.dp))
Text("WebRTCService up?: $isWebRTCRunning")
Spacer(Modifier.height(8.dp))
Text("SDK status:")
Text(sdkStatus)
}

if (isSDKOk) {
Spacer(Modifier.height(8.dp))
Button(onClick = {
servicesViewModel.runService(!isServiceRunning, false)
}) {
Text("${buttonLabel(isServiceRunning)} WenuLink Service")
}

if (isSimulationReady && !isServiceRunning) {
Button(onClick = {
servicesViewModel.runService(run = true, simEnabled = true)
}) {
Text("Start SIM WenuLink Service")
}
}

if (isServiceRunning) {
HorizontalDivider()

Button(onClick = {
servicesViewModel.forceStop()
}) {
Text("FORCE STOP")
}

HorizontalDivider()

Button(onClick = {
servicesViewModel.runMAVLink(!isMAVLinkRunning)
}) {
Text("${buttonLabel(isMAVLinkRunning)} MAVLink")
}

Button(onClick = {
servicesViewModel.runWebRTC(!isWebRTCRunning)
}) {
Text("${buttonLabel(isWebRTCRunning)} WebRTC")
}
}
}

HorizontalDivider()

Button(
onClick = {
logMessages = logMessages + "Manual Log at ${System.currentTimeMillis()}"
},
modifier = Modifier.padding(bottom = 8.dp)
) {
Text("Add Test Log")
}

Card(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.padding(8.dp),
reverseLayout = true
) {
items(logMessages) { message ->
Text(text = message, modifier = Modifier.padding(4.dp))
HorizontalDivider()
}
}
}

telemetry?.let {
Text("Telemetry: R=${it.roll}, P=${it.pitch}, Y=${it.yaw}, Alt=${it.altitude}")
}
}
}
}
}
116 changes: 116 additions & 0 deletions app/src/main/java/org/WenuLink/ui/navigation/AppNavigation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.WenuLink.ui.navigation

import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.WenuLink.views.HomeViewModel
import org.WenuLink.views.ServicesViewModel
import org.WenuLink.views.SettingsViewModel
import org.WenuLink.ui.screens.about.AboutScreen
import org.WenuLink.ui.screens.config.*
import org.WenuLink.ui.screens.main.DashboardUiState
import org.WenuLink.ui.screens.main.MainScreen

@Composable
fun AppNavigation(
homeViewModel: HomeViewModel,
servicesViewModel: ServicesViewModel,
settingsViewModel: SettingsViewModel,
logMessages: List<String>,
addLog: (String) -> Unit
) {
val navController = rememberNavController()

val workflowStatus by homeViewModel.workflowStatus.observeAsState("Idle")
val isPermissionsGranted by homeViewModel.isPermissionsGranted.observeAsState(false)
val isRegistered by homeViewModel.isRegistered.observeAsState(false)
val sdkStatus by homeViewModel.sdkStatus.observeAsState("")

val canRunService = sdkStatus.contains("Connected")

val isServiceRunning by servicesViewModel.isServiceRunning.collectAsState()
val isDataFlowing by servicesViewModel.isDataFlowing.collectAsState()
val isMAVLinkRunning by servicesViewModel.isMAVLinkRunning.collectAsState()
val isWebRTCRunning by servicesViewModel.isWebRTCRunning.collectAsState()
val telemetry by servicesViewModel.telemetryData.observeAsState()

val telemetrySummary = if (telemetry != null)
"R:${"%.1f".format(telemetry!!.roll)} P:${"%.1f".format(telemetry!!.pitch)} Y:${"%.1f".format(telemetry!!.yaw)}"
else "No Telemetry Data"

val uiState = DashboardUiState(
workflowStatus = workflowStatus,
isPermissionsGranted = isPermissionsGranted,
isSDKRegistered = isRegistered,
canRunService = canRunService,
isServiceRunning = isServiceRunning,
isDataFlowing = isDataFlowing,
isMAVLinkRunning = isMAVLinkRunning,
isWebRTCRunning = isWebRTCRunning,
telemetrySummary = telemetrySummary,
recentLogs = logMessages
)

NavHost(
navController = navController,
startDestination = Screen.Main.route,

enterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(200)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(200)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(200)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(200)
)
}
) {

composable(Screen.Main.route) {
MainScreen(
uiState = uiState,
onServiceToggle = { servicesViewModel.runService(!isServiceRunning) },
onMavlinkToggle = { servicesViewModel.runMAVLink(!isMAVLinkRunning) },
onWebRTCToggle = { servicesViewModel.runWebRTC(!isWebRTCRunning) },
onNavigateToConfig = { navController.navigate(Screen.ConfigMenu.route) },
onNavigateToAbout = { navController.navigate(Screen.About.route) }
)
}

composable(Screen.About.route) {
AboutScreen(navController)
}
composable(Screen.ConfigMenu.route) {
MenuScreen(navController)
}
composable(Screen.ConfigIp.route) {
AddressScreen(navController, settingsViewModel, isServiceRunning)
}
composable(Screen.ConfigDji.route) {
KeyScreen(navController)
}
composable(Screen.ConfigTheme.route) {
ThemeScreen(navController, settingsViewModel)
}
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/org/WenuLink/ui/navigation/Screen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.WenuLink.ui.navigation

sealed class Screen(val route: String) {
object Main : Screen("main")
object About : Screen("about")

object ConfigMenu : Screen("config_menu")
object ConfigIp : Screen("config_ip")
object ConfigDji : Screen("config_dji")
object ConfigTheme : Screen("config_theme")
}
Loading