IdleService runs continuously as a FOREGROUND_SERVICE_TYPE_DATA_SYNC foreground service (push is on by default). With targetSdk 37, Android 14+'s dataSync FGS runtime cap (~6h per rolling 24h) calls Service.onTimeout(...) and then force-stops the service — throwing a system FGS-timeout exception — if it doesn't stop itself. IdleService overrode onStartCommand/onDestroy/onBind but not onTimeout, so after ~6 cumulative hours push silently died and the app hit the exception; on API 35+ the budget is cumulative and a restart can't recover it until the next 24h window. Override both onTimeout(startId) (deprecated, API 34) and onTimeout(startId, fgsType) (API 35+); both route to a clean shutdown that re-asserts the already-scheduled 15-minute periodic sync, swaps the persistent notification to a degraded "paused" text and DETACHes it so it survives, then stopForeground(DETACH) + stopSelf so we never leave a dataSync FGS running past its cap (the exact condition the platform kills on). This mirrors the existing low-battery PushMode.POLLING fallback. The push-status text choice is pulled into a pure PushStatusNotification.statusTextRes seam and unit-tested on the JVM; the built notification's new timed-out text is covered by PushStatusNotificationInstrumentedTest. Closes #302 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
1.8 KiB
Kotlin
52 lines
1.8 KiB
Kotlin
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package org.libremail.push
|
|
|
|
import org.junit.Assert.assertEquals
|
|
import org.junit.Test
|
|
import org.libremail.R
|
|
import org.libremail.data.sync.PushMode
|
|
|
|
/**
|
|
* JVM coverage of [PushStatusNotification.statusTextRes] — the pure "which status message for which
|
|
* push state" decision. [PushStatusNotification.build]'s `NotificationCompat` is a no-op stub in the
|
|
* unit-test `android.jar`, so the built `Notification` is asserted on-device in
|
|
* `PushStatusNotificationInstrumentedTest`; this verifies the text-selection branch — including the
|
|
* #302 dataSync FGS runtime-cap fallback — with no emulator.
|
|
*/
|
|
class PushStatusNotificationTest {
|
|
|
|
@Test
|
|
fun `idle shows the instant-delivery text`() {
|
|
assertEquals(
|
|
R.string.notif_push_status_text,
|
|
PushStatusNotification.statusTextRes(PushMode.IDLE, timedOut = false),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `polling shows the low-battery fallback text`() {
|
|
assertEquals(
|
|
R.string.notif_push_status_text_low_battery,
|
|
PushStatusNotification.statusTextRes(PushMode.POLLING, timedOut = false),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `a dataSync FGS timeout shows the paused fallback text`() {
|
|
assertEquals(
|
|
R.string.notif_push_status_text_timed_out,
|
|
PushStatusNotification.statusTextRes(PushMode.POLLING, timedOut = true),
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `the timeout text wins even while push is nominally idle`() {
|
|
// The platform delivers the runtime-cap timeout while the service is still in IDLE mode, so
|
|
// timedOut must take precedence over the mode (#302).
|
|
assertEquals(
|
|
R.string.notif_push_status_text_timed_out,
|
|
PushStatusNotification.statusTextRes(PushMode.IDLE, timedOut = true),
|
|
)
|
|
}
|
|
}
|