Merge main into fix/event-driven-messages

This commit is contained in:
torlando-tech
2025-12-16 21:56:32 -05:00
5 changed files with 95 additions and 3 deletions

View File

@@ -527,6 +527,7 @@ fun ColumbaNavigation(pendingNavigation: MutableState<PendingNavigation?>) {
composable(Screen.Identity.route) {
IdentityScreen(
onBackClick = { navController.popBackStack() },
settingsViewModel = settingsViewModel,
onNavigateToBleStatus = {
navController.navigate("ble_connection_status")
},
@@ -535,6 +536,7 @@ fun ColumbaNavigation(pendingNavigation: MutableState<PendingNavigation?>) {
composable(Screen.Settings.route) {
SettingsScreen(
viewModel = settingsViewModel,
onNavigateToInterfaces = {
navController.navigate("interface_management")
},
@@ -681,6 +683,7 @@ fun ColumbaNavigation(pendingNavigation: MutableState<PendingNavigation?>) {
composable("my_identity") {
MyIdentityScreen(
onNavigateBack = { navController.popBackStack() },
settingsViewModel = settingsViewModel,
onNavigateToIdentityManager = {
navController.navigate("identity_manager")
},
@@ -690,6 +693,7 @@ fun ColumbaNavigation(pendingNavigation: MutableState<PendingNavigation?>) {
composable("network_status") {
IdentityScreen(
onBackClick = { navController.popBackStack() },
settingsViewModel = settingsViewModel,
onNavigateToBleStatus = {
navController.navigate("ble_connection_status")
},

View File

@@ -91,9 +91,9 @@ import com.lxmf.messenger.viewmodel.TestAnnounceResult
@Composable
fun IdentityScreen(
onBackClick: () -> Unit = {},
settingsViewModel: com.lxmf.messenger.viewmodel.SettingsViewModel,
viewModel: DebugViewModel = hiltViewModel(),
bleConnectionsViewModel: com.lxmf.messenger.viewmodel.BleConnectionsViewModel = hiltViewModel(),
settingsViewModel: com.lxmf.messenger.viewmodel.SettingsViewModel = hiltViewModel(),
onNavigateToBleStatus: () -> Unit = {},
) {
val context = LocalContext.current

View File

@@ -89,9 +89,9 @@ import com.lxmf.messenger.viewmodel.SettingsViewModel
@Composable
fun MyIdentityScreen(
onNavigateBack: () -> Unit,
settingsViewModel: SettingsViewModel,
onNavigateToIdentityManager: () -> Unit = {},
debugViewModel: DebugViewModel = hiltViewModel(),
settingsViewModel: SettingsViewModel = hiltViewModel(),
) {
val settingsState by settingsViewModel.state.collectAsState()

View File

@@ -51,7 +51,7 @@ import com.lxmf.messenger.viewmodel.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
viewModel: SettingsViewModel,
debugViewModel: DebugViewModel = hiltViewModel(),
onNavigateToInterfaces: () -> Unit = {},
onNavigateToIdentity: () -> Unit = {},

View File

@@ -1794,4 +1794,92 @@ class SettingsViewModelTest {
}
// endregion
// region Shared Instance Monitoring Tests
/**
* Verifies that monitors don't run when enableMonitors is false.
*
* This is the key test for the duplicate SettingsViewModel fix:
* - Before fix: Multiple screens created their own SettingsViewModel instances
* - Each instance started its own monitor, causing 3-4x battery drain
* - After fix: One shared SettingsViewModel means one monitor
*
* The enableMonitors flag allows disabling monitors in tests to prevent
* the infinite while(true) loop from running. In production, monitors
* are enabled and poll every 5 seconds.
*/
@Test
fun `monitors are disabled when enableMonitors flag is false`() =
runTest {
// Verify that the enableMonitors flag prevents monitoring loops from starting
// This is set to false in @Before, verify the monitor doesn't call isSharedInstanceAvailable
SettingsViewModel.enableMonitors = false
var monitorCallCount = 0
val serviceProtocol =
mockk<com.lxmf.messenger.reticulum.protocol.ServiceReticulumProtocol>(relaxed = true) {
every { networkStatus } returns networkStatusFlow
coEvery { isSharedInstanceAvailable() } coAnswers {
monitorCallCount++
false
}
}
viewModel =
SettingsViewModel(
settingsRepository = settingsRepository,
identityRepository = identityRepository,
reticulumProtocol = serviceProtocol,
interfaceConfigManager = interfaceConfigManager,
propagationNodeManager = propagationNodeManager,
)
// Wait for any potential async operations to settle
// With UnconfinedTestDispatcher, coroutines run eagerly
// If monitors were enabled, the while(true) loop would run infinitely
viewModel.state.test {
awaitItem() // initial state
cancelAndConsumeRemainingEvents()
}
// Should NOT have been called since monitors are disabled
assertEquals(
"Monitor should not run when enableMonitors is false",
0,
monitorCallCount,
)
}
@Test
fun `viewmodel passes ServiceReticulumProtocol check for monitoring`() =
runTest {
// Verify that the ViewModel correctly identifies ServiceReticulumProtocol
// for shared instance monitoring. This is important because the monitor
// only calls isSharedInstanceAvailable() on ServiceReticulumProtocol.
SettingsViewModel.enableMonitors = false
val serviceProtocol =
mockk<com.lxmf.messenger.reticulum.protocol.ServiceReticulumProtocol>(relaxed = true) {
every { networkStatus } returns networkStatusFlow
}
viewModel =
SettingsViewModel(
settingsRepository = settingsRepository,
identityRepository = identityRepository,
reticulumProtocol = serviceProtocol,
interfaceConfigManager = interfaceConfigManager,
propagationNodeManager = propagationNodeManager,
)
// The ViewModel should be created successfully with ServiceReticulumProtocol
viewModel.state.test {
val state = awaitItem()
assertFalse(state.isRestarting)
cancelAndConsumeRemainingEvents()
}
}
// endregion
}