diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt index 726063be..0b763803 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt @@ -183,18 +183,23 @@ data class PodDevice( ).minOrNull() } - // Charging — AAP preferred, BLE fallback, then cached + // Charging — AAP preferred, BLE fallback, then cached. The cached fallback only applies while + // the device is live (connected): charging is a real-time claim ("power is flowing now") that + // we can't stand behind once the device is out of range, so we suppress it after full + // disconnect rather than showing a frozen charging state on the dimmed "cached N ago" card. + // The raw charging bits are still persisted in `cached` for callers that want the last-known + // value directly (device.cached?.is…Charging). val isLeftPodCharging: Boolean? - get() = aap?.isLeftCharging ?: (ble as? HasChargeDetectionDual)?.isLeftPodCharging ?: cached?.isLeftCharging + get() = aap?.isLeftCharging ?: (ble as? HasChargeDetectionDual)?.isLeftPodCharging ?: cached?.isLeftCharging?.takeIf { isLive } val isRightPodCharging: Boolean? - get() = aap?.isRightCharging ?: (ble as? HasChargeDetectionDual)?.isRightPodCharging ?: cached?.isRightCharging + get() = aap?.isRightCharging ?: (ble as? HasChargeDetectionDual)?.isRightPodCharging ?: cached?.isRightCharging?.takeIf { isLive } val isCaseCharging: Boolean? - get() = aap?.isCaseCharging ?: (ble as? HasCase)?.isCaseCharging ?: cached?.isCaseCharging + get() = aap?.isCaseCharging ?: (ble as? HasCase)?.isCaseCharging ?: cached?.isCaseCharging?.takeIf { isLive } val isHeadsetBeingCharged: Boolean? - get() = aap?.isHeadsetCharging ?: (ble as? HasChargeDetection)?.isHeadsetBeingCharged ?: cached?.isHeadsetCharging + get() = aap?.isHeadsetCharging ?: (ble as? HasChargeDetection)?.isHeadsetBeingCharged ?: cached?.isHeadsetCharging?.takeIf { isLive } // Full per-slot charging state — AAP only (BLE + cache don't carry CHARGING_OPTIMIZED). // Null means "no live AAP reading", which lets callers avoid showing a stale Optimized chip diff --git a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotificationViewFactory.kt b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotificationViewFactory.kt index 812d3ed3..d535c334 100644 --- a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotificationViewFactory.kt +++ b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotificationViewFactory.kt @@ -71,12 +71,10 @@ class MonitorNotificationViewFactory @Inject constructor( if (device.hasEarDetection) { setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE) } - if (device.isHeadsetBeingCharged != null) { - setViewVisibility( - R.id.headphones_charging, - if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE - ) - } + setViewVisibility( + R.id.headphones_charging, + if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE + ) } private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews( @@ -151,12 +149,10 @@ class MonitorNotificationViewFactory @Inject constructor( if (device.hasEarDetection) { setViewVisibility(R.id.headphones_worn, if (device.isBeingWorn == true) View.VISIBLE else View.GONE) } - if (device.isHeadsetBeingCharged != null) { - setViewVisibility( - R.id.headphones_charging, - if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE - ) - } + setViewVisibility( + R.id.headphones_charging, + if (device.isHeadsetBeingCharged == true) View.VISIBLE else View.GONE + ) } private fun createUnknownDeviceBig(device: PodDevice): RemoteViews = RemoteViews( diff --git a/app/src/test/java/eu/darken/capod/monitor/core/PodDeviceCacheTest.kt b/app/src/test/java/eu/darken/capod/monitor/core/PodDeviceCacheTest.kt index 1ed635e9..69146723 100644 --- a/app/src/test/java/eu/darken/capod/monitor/core/PodDeviceCacheTest.kt +++ b/app/src/test/java/eu/darken/capod/monitor/core/PodDeviceCacheTest.kt @@ -91,10 +91,37 @@ class PodDeviceCacheTest : BaseTest() { } @Test - fun `charging falls back to cache`() { - val device = PodDevice(profileId = "test-profile", ble = null, aap = null, cached = cachedState) - device.isLeftPodCharging shouldBe false - device.isCaseCharging shouldBe true + fun `charging is suppressed when not live even if cached`() { + // Charging is a real-time claim we can't stand behind once the device is out of range. + // A cached-only (non-live) device must report null for every charging slot, so the + // dimmed "cached N ago" card shows no charging chip. Uses a cache with all four slots + // populated to prove each getter suppresses independently. + val fullChargingCache = cachedState.copy( + isLeftCharging = true, + isRightCharging = true, + isCaseCharging = true, + isHeadsetCharging = true, + ) + val device = PodDevice(profileId = "test-profile", ble = null, aap = null, cached = fullChargingCache) + device.isLive shouldBe false + device.isLeftPodCharging.shouldBeNull() + device.isRightPodCharging.shouldBeNull() + device.isCaseCharging.shouldBeNull() + device.isHeadsetBeingCharged.shouldBeNull() + } + + @Test + fun `charging still uses cache fallback while live`() { + // While a device is connected (here: an AAP session that is READY but hasn't sent a + // per-slot charging reading yet) the cached charging value is still shown, since the + // device is present and a fresh reading will correct it momentarily. + val aap = AapPodState( + connectionState = AapPodState.ConnectionState.READY, + lastMessageAt = fiveMinAgo, + ) + val device = PodDevice(profileId = "test-profile", ble = null, aap = aap, cached = cachedState) + device.isLive shouldBe true + device.isCaseCharging shouldBe true // AAP has no case charging -> cache fallback (live) } @Test