fix(monitor): Make Extra notification toggle reactive

This commit is contained in:
Matthias Urhahn
2026-05-07 15:23:06 +02:00
committed by Matthias Urhahn
parent 83ef111599
commit 7d57845502
3 changed files with 143 additions and 12 deletions
@@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
@@ -23,9 +24,10 @@ class DataStoreValue<T>(
) {
val keyName: String get() = key.name
val flow: Flow<T> = dataStore.data.map { prefs ->
reader(prefs[key])
}
val flow: Flow<T> = dataStore.data
.map { prefs -> prefs[key] }
.distinctUntilChanged()
.map { raw -> reader(raw) }
data class Updated<T>(val old: T, val new: T)
@@ -15,7 +15,6 @@ import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.bluetooth.BluetoothDevice2
import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.datastore.valueBlocking
import eu.darken.capod.common.debug.Bugs
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
@@ -90,6 +89,10 @@ class MonitorService : Service() {
private var foregroundStartFailed = false
private var injectionComplete = false
@Volatile
private var latestNotificationSettings: NotificationSettings =
NotificationSettings(useExtraNotification = false, keepAfterDisconnect = false)
@SuppressLint("InlinedApi")
private fun promoteToForeground(notification: Notification): Boolean {
return try {
@@ -190,27 +193,53 @@ class MonitorService : Service() {
}
private suspend fun doMonitor() {
// Seed snapshot eagerly so onDestroy() has correct values even if we abort below.
latestNotificationSettings = NotificationSettings(
useExtraNotification = generalSettings.useExtraMonitorNotification.flow.first(),
keepAfterDisconnect = generalSettings.keepConnectedNotificationAfterDisconnect.flow.first(),
)
val permissionsMissingOnStart = permissionTool.missingScanPermissions.first()
if (permissionsMissingOnStart.isNotEmpty()) {
log(TAG, WARN) { "Aborting, missing scan permissions: $permissionsMissingOnStart" }
return
}
val monitorJob = deviceMonitor.primaryDevice()
val deviceFlow = deviceMonitor.primaryDevice()
.setupCommonEventHandlers(TAG) { "BlePodMonitor" }
.distinctUntilChangedBy { it?.toNotificationKey() }
.throttleLatest(1000)
.onEach { currentDevice ->
val useExtraNotification = generalSettings.useExtraMonitorNotification.valueBlocking
val notificationSettingsFlow = combine(
generalSettings.useExtraMonitorNotification.flow,
generalSettings.keepConnectedNotificationAfterDisconnect.flow,
) { useExtra, keepAfter ->
NotificationSettings(useExtraNotification = useExtra, keepAfterDisconnect = keepAfter)
}
val monitorJob = combine(deviceFlow, notificationSettingsFlow) { currentDevice, settings ->
currentDevice to settings
}
.onEach { (currentDevice, settings) ->
latestNotificationSettings = settings
notificationManager.notify(
MonitorNotifications.NOTIFICATION_ID,
notifications.getNotification(currentDevice, showHint = useExtraNotification),
notifications.getNotification(
currentDevice,
showHint = settings.useExtraNotification,
),
)
if (generalSettings.useExtraMonitorNotification.valueBlocking && currentDevice != null) {
notificationManager.notify(
when (val action = decideExtraNotificationAction(currentDevice, settings)) {
is ExtraNotificationAction.Post -> notificationManager.notify(
MonitorNotifications.NOTIFICATION_ID_CONNECTED,
notifications.getNotificationConnected(currentDevice),
notifications.getNotificationConnected(action.device),
)
ExtraNotificationAction.Cancel -> notificationManager.cancel(
MonitorNotifications.NOTIFICATION_ID_CONNECTED
)
ExtraNotificationAction.KeepExisting -> Unit
}
}
.catch {
@@ -312,7 +341,8 @@ class MonitorService : Service() {
monitorScope.cancel("Service destroyed")
if (injectionComplete) {
if (generalSettings.useExtraMonitorNotification.valueBlocking && !generalSettings.keepConnectedNotificationAfterDisconnect.valueBlocking) {
val snapshot = latestNotificationSettings
if (snapshot.useExtraNotification && !snapshot.keepAfterDisconnect) {
try {
notificationManager.cancel(MonitorNotifications.NOTIFICATION_ID_CONNECTED)
} catch (e: Exception) {
@@ -365,6 +395,7 @@ private data class NotificationDeviceKey(
val profileId: String?,
val label: String?,
val model: PodModel,
val isLive: Boolean,
val hasDualPods: Boolean,
val hasCase: Boolean,
val hasEarDetection: Boolean,
@@ -389,6 +420,7 @@ private fun PodDevice.toNotificationKey(): NotificationDeviceKey = NotificationD
profileId = profileId,
label = label,
model = model,
isLive = isLive,
hasDualPods = hasDualPods,
hasCase = hasCase,
hasEarDetection = hasEarDetection,
@@ -408,3 +440,24 @@ private fun PodDevice.toNotificationKey(): NotificationDeviceKey = NotificationD
rightPodIcon = rightPodIcon,
caseIcon = caseIcon,
)
internal data class NotificationSettings(
val useExtraNotification: Boolean,
val keepAfterDisconnect: Boolean,
)
internal sealed interface ExtraNotificationAction {
object Cancel : ExtraNotificationAction
data class Post(val device: PodDevice) : ExtraNotificationAction
object KeepExisting : ExtraNotificationAction
}
internal fun decideExtraNotificationAction(
device: PodDevice?,
settings: NotificationSettings,
): ExtraNotificationAction = when {
!settings.useExtraNotification -> ExtraNotificationAction.Cancel
device != null && device.isLive -> ExtraNotificationAction.Post(device)
settings.keepAfterDisconnect -> ExtraNotificationAction.KeepExisting
else -> ExtraNotificationAction.Cancel
}
@@ -0,0 +1,76 @@
package eu.darken.capod.monitor.core.worker
import eu.darken.capod.monitor.core.PodDevice
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
class MonitorServiceNotificationDecisionTest : BaseTest() {
private fun device(isLive: Boolean): PodDevice = mockk {
every { this@mockk.isLive } returns isLive
}
private fun settings(useExtra: Boolean, keepAfter: Boolean) = NotificationSettings(
useExtraNotification = useExtra,
keepAfterDisconnect = keepAfter,
)
@Test
fun `extra off, null device - cancel`() {
decideExtraNotificationAction(null, settings(useExtra = false, keepAfter = false)) shouldBe
ExtraNotificationAction.Cancel
}
@Test
fun `extra off, live device - cancel`() {
decideExtraNotificationAction(device(isLive = true), settings(useExtra = false, keepAfter = false)) shouldBe
ExtraNotificationAction.Cancel
}
@Test
fun `extra off, cached-only device, keepAfter true - cancel (extra off wins)`() {
decideExtraNotificationAction(device(isLive = false), settings(useExtra = false, keepAfter = true)) shouldBe
ExtraNotificationAction.Cancel
}
@Test
fun `extra on, live device, keepAfter false - post`() {
val live = device(isLive = true)
decideExtraNotificationAction(live, settings(useExtra = true, keepAfter = false)) shouldBe
ExtraNotificationAction.Post(live)
}
@Test
fun `extra on, live device, keepAfter true - post`() {
val live = device(isLive = true)
decideExtraNotificationAction(live, settings(useExtra = true, keepAfter = true)) shouldBe
ExtraNotificationAction.Post(live)
}
@Test
fun `extra on, cached-only device, keepAfter false - cancel`() {
decideExtraNotificationAction(device(isLive = false), settings(useExtra = true, keepAfter = false)) shouldBe
ExtraNotificationAction.Cancel
}
@Test
fun `extra on, cached-only device, keepAfter true - keep existing`() {
decideExtraNotificationAction(device(isLive = false), settings(useExtra = true, keepAfter = true)) shouldBe
ExtraNotificationAction.KeepExisting
}
@Test
fun `extra on, null device, keepAfter false - cancel`() {
decideExtraNotificationAction(null, settings(useExtra = true, keepAfter = false)) shouldBe
ExtraNotificationAction.Cancel
}
@Test
fun `extra on, null device, keepAfter true - keep existing`() {
decideExtraNotificationAction(null, settings(useExtra = true, keepAfter = true)) shouldBe
ExtraNotificationAction.KeepExisting
}
}