fix(data): default new-node notifications off for event firmware (#5323)

This commit is contained in:
James Rich
2026-05-01 21:02:30 -05:00
committed by GitHub
parent 1ba6229a6f
commit 400e0404f6
8 changed files with 111 additions and 0 deletions
@@ -31,12 +31,14 @@ import org.meshtastic.core.repository.MeshConfigFlowManager
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FileInfo
import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.NodeInfo
import org.meshtastic.core.model.MyNodeInfo as SharedMyNodeInfo
@@ -54,6 +56,7 @@ class MeshConfigFlowManagerImpl(
private val analytics: PlatformAnalytics,
private val commandSender: CommandSender,
private val heartbeatSender: DataLayerHeartbeatSender,
private val notificationPrefs: NotificationPrefs,
@Named("ServiceScope") private val scope: CoroutineScope,
) : MeshConfigFlowManager {
private val wantConfigDelay = 100L
@@ -199,6 +202,8 @@ class MeshConfigFlowManagerImpl(
// Transition to Stage 1, discarding any stale data from a prior interrupted handshake.
handshakeState = HandshakeState.ReceivingConfig(rawMyNodeInfo = myInfo)
nodeManager.setMyNodeNum(myInfo.my_node_num)
nodeManager.setFirmwareEdition(myInfo.firmware_edition)
applyEventFirmwareNotificationDefaults(myInfo.firmware_edition)
// Bump the generation so that a pending clear from a prior (interrupted) handshake
// will see a stale snapshot and skip its writes, preventing it from wiping config
@@ -288,4 +293,18 @@ class MeshConfigFlowManagerImpl(
Logger.e(ex) { "Failed to build MyNodeInfo" }
null
}
private fun applyEventFirmwareNotificationDefaults(edition: FirmwareEdition) {
if (edition != FirmwareEdition.VANILLA) {
if (!notificationPrefs.nodeEventsAutoDisabledForEvent.value) {
notificationPrefs.setNodeEventsEnabled(false)
notificationPrefs.setNodeEventsAutoDisabledForEvent(true)
}
} else {
if (notificationPrefs.nodeEventsAutoDisabledForEvent.value) {
notificationPrefs.setNodeEventsEnabled(true)
notificationPrefs.setNodeEventsAutoDisabledForEvent(false)
}
}
}
}
@@ -45,6 +45,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.resources.new_node_seen
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.StatusMessage
@@ -89,6 +90,12 @@ class NodeManagerImpl(
myNodeNum.value = num
}
override val firmwareEdition = MutableStateFlow<FirmwareEdition?>(null)
override fun setFirmwareEdition(edition: FirmwareEdition?) {
firmwareEdition.value = edition
}
companion object {
private const val TIME_MS_TO_S = 1000L
}
@@ -112,6 +119,7 @@ class NodeManagerImpl(
isNodeDbReady.value = false
allowNodeDbWrites.value = false
myNodeNum.value = null
firmwareEdition.value = null
}
override fun getMyNodeInfo(): MyNodeInfo? {
@@ -23,6 +23,7 @@ import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import dev.mokkery.verify.VerifyMode
import dev.mokkery.verifySuspend
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,6 +37,7 @@ import org.meshtastic.core.repository.HandshakeConstants
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioConfigRepository
@@ -43,6 +45,7 @@ import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FileInfo
import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.NodeInfo
import kotlin.test.BeforeTest
@@ -62,6 +65,7 @@ class MeshConfigFlowManagerImplTest {
private val analytics = mock<PlatformAnalytics>(MockMode.autofill)
private val commandSender = mock<CommandSender>(MockMode.autofill)
private val packetHandler = mock<PacketHandler>(MockMode.autofill)
private val notificationPrefs = mock<NotificationPrefs>(MockMode.autofill)
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -87,6 +91,8 @@ class MeshConfigFlowManagerImplTest {
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } returns Unit
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
every { nodeManager.myNodeNum } returns MutableStateFlow(null)
every { notificationPrefs.nodeEventsAutoDisabledForEvent } returns MutableStateFlow(false)
every { notificationPrefs.nodeEventsEnabled } returns MutableStateFlow(true)
manager =
MeshConfigFlowManagerImpl(
@@ -99,6 +105,7 @@ class MeshConfigFlowManagerImplTest {
analytics = analytics,
commandSender = commandSender,
heartbeatSender = DataLayerHeartbeatSender(packetHandler),
notificationPrefs = notificationPrefs,
scope = testScope,
)
}
@@ -418,4 +425,51 @@ class MeshConfigFlowManagerImplTest {
verify { nodeManager.setMyNodeNum(99999) }
}
// ---------- Event firmware notification defaults ----------
@Test
fun `handleMyInfo disables node notifications for event firmware`() = testScope.runTest {
every { notificationPrefs.nodeEventsAutoDisabledForEvent } returns MutableStateFlow(false)
val eventMyInfo = protoMyNodeInfo.copy(firmware_edition = FirmwareEdition.DEFCON)
manager.handleMyInfo(eventMyInfo)
advanceUntilIdle()
verify { notificationPrefs.setNodeEventsEnabled(false) }
verify { notificationPrefs.setNodeEventsAutoDisabledForEvent(true) }
}
@Test
fun `handleMyInfo does not re-disable if already auto-disabled`() = testScope.runTest {
every { notificationPrefs.nodeEventsAutoDisabledForEvent } returns MutableStateFlow(true)
val eventMyInfo = protoMyNodeInfo.copy(firmware_edition = FirmwareEdition.DEFCON)
manager.handleMyInfo(eventMyInfo)
advanceUntilIdle()
verify(mode = VerifyMode.not) { notificationPrefs.setNodeEventsEnabled(any()) }
}
@Test
fun `handleMyInfo re-enables node notifications when vanilla firmware reconnects`() = testScope.runTest {
every { notificationPrefs.nodeEventsAutoDisabledForEvent } returns MutableStateFlow(true)
manager.handleMyInfo(protoMyNodeInfo)
advanceUntilIdle()
verify { notificationPrefs.setNodeEventsEnabled(true) }
verify { notificationPrefs.setNodeEventsAutoDisabledForEvent(false) }
}
@Test
fun `handleMyInfo does not touch prefs for vanilla when not previously auto-disabled`() = testScope.runTest {
every { notificationPrefs.nodeEventsAutoDisabledForEvent } returns MutableStateFlow(false)
manager.handleMyInfo(protoMyNodeInfo)
advanceUntilIdle()
verify(mode = VerifyMode.not) { notificationPrefs.setNodeEventsEnabled(any()) }
verify(mode = VerifyMode.not) { notificationPrefs.setNodeEventsAutoDisabledForEvent(any()) }
}
}
@@ -53,6 +53,13 @@ class NotificationPrefsImpl(
scope.launch { dataStore.edit { it[KEY_NODE_EVENTS_ENABLED] = enabled } }
}
override val nodeEventsAutoDisabledForEvent: StateFlow<Boolean> =
dataStore.data.map { it[KEY_NODE_EVENTS_AUTO_DISABLED] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
override fun setNodeEventsAutoDisabledForEvent(disabled: Boolean) {
scope.launch { dataStore.edit { it[KEY_NODE_EVENTS_AUTO_DISABLED] = disabled } }
}
override val lowBatteryEnabled: StateFlow<Boolean> =
dataStore.data.map { it[KEY_LOW_BATTERY_ENABLED] ?: true }.stateIn(scope, SharingStarted.Eagerly, true)
@@ -63,6 +70,7 @@ class NotificationPrefsImpl(
private companion object {
val KEY_MESSAGES_ENABLED = booleanPreferencesKey("notif_messages_enabled")
val KEY_NODE_EVENTS_ENABLED = booleanPreferencesKey("notif_node_events_enabled")
val KEY_NODE_EVENTS_AUTO_DISABLED = booleanPreferencesKey("notif_node_events_auto_disabled_event")
val KEY_LOW_BATTERY_ENABLED = booleanPreferencesKey("notif_low_battery_enabled")
}
}
@@ -164,6 +164,10 @@ interface NotificationPrefs {
fun setNodeEventsEnabled(enabled: Boolean)
val nodeEventsAutoDisabledForEvent: StateFlow<Boolean>
fun setNodeEventsAutoDisabledForEvent(disabled: Boolean)
val lowBatteryEnabled: StateFlow<Boolean>
fun setLowBatteryEnabled(enabled: Boolean)
@@ -22,6 +22,7 @@ import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.util.NodeIdLookup
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.StatusMessage
import org.meshtastic.proto.Telemetry
@@ -56,6 +57,12 @@ interface NodeManager : NodeIdLookup {
/** Sets the local node number. */
fun setMyNodeNum(num: Int?)
/** The firmware edition reported by the connected device. */
val firmwareEdition: StateFlow<FirmwareEdition?>
/** Sets the firmware edition of the connected device. */
fun setFirmwareEdition(edition: FirmwareEdition?)
/** Loads the cached node database from the repository. */
fun loadCachedNodeDB()
@@ -32,6 +32,12 @@ class FakeNotificationPrefs : NotificationPrefs {
nodeEventsEnabled.value = enabled
}
override val nodeEventsAutoDisabledForEvent = MutableStateFlow(false)
override fun setNodeEventsAutoDisabledForEvent(disabled: Boolean) {
nodeEventsAutoDisabledForEvent.value = disabled
}
override val lowBatteryEnabled = MutableStateFlow(true)
override fun setLowBatteryEnabled(enabled: Boolean) {
@@ -49,6 +49,7 @@ class DesktopNotificationManagerTest {
) : NotificationPrefs {
override val messagesEnabled = MutableStateFlow(messages)
override val nodeEventsEnabled = MutableStateFlow(nodeEvents)
override val nodeEventsAutoDisabledForEvent = MutableStateFlow(false)
override val lowBatteryEnabled = MutableStateFlow(lowBattery)
override fun setMessagesEnabled(enabled: Boolean) {
@@ -59,6 +60,10 @@ class DesktopNotificationManagerTest {
nodeEventsEnabled.value = enabled
}
override fun setNodeEventsAutoDisabledForEvent(disabled: Boolean) {
nodeEventsAutoDisabledForEvent.value = disabled
}
override fun setLowBatteryEnabled(enabled: Boolean) {
lowBatteryEnabled.value = enabled
}