Recurring on-device crash loop captured from the maintainer's Pixel 10 Pro XL (Android 17 / SDK 37). After the dataSync foreground-service runtime cap from #302 fires and IdleService self-stops, the service is restarted — both by START_STICKY null-intent redelivery and by explicit startForegroundService calls — and onStartCommand unconditionally calls startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC) while the rolling-24h dataSync budget is still exhausted. The platform rejects that start with ForegroundServiceStartNotAllowedException: Time limit already exhausted for foreground service type dataSync; the exception is uncaught, the process crashes, and START_STICKY restarts it straight back into the same rejection → crash loop until the 24h window frees budget.
This is a direct follow-up to #302: #302 added the onTimeout → fallBackToPeriodicSync() clean stop, but nothing prevents the subsequent (re)start from attempting a now-illegal dataSync FGS start.
Evidence
Artifacts: 2 in-app DebugReports + dumpsys dropbox (10 entries) + a full system bugreport, all from the maintainer's device. Times are device-local (-0500).
Signature C — ForegroundServiceStartNotAllowedException: Time limit already exhausted for foreground service type dataSync at IdleService.startAsForeground ← IdleService.onStartCommand (:87), wrapped in java.lang.RuntimeException: Unable to start service ...IdleService.... 5 occurrences:
Signature B — RemoteServiceException$ForegroundServiceDidNotStopInTimeException: A foreground service of type dataSync did not stop within its timeout ...IdleService. 1 occurrence:
2026-07-04 04:24:08.039 — on a pre-#302 build (stack startAsForeground line 206). Mitigated on current main by the onTimeout handler; listed for completeness so the fix keeps the stop path within the grace window.
04:42:53.582Z I/IdleService: dataSync FGS runtime cap reached: pausing IMAP IDLE; mail arrives via 15-minute periodic sync
04:42:53.672Z E/CrashReporter: Uncaught exception: java.lang.RuntimeException (+90 ms)
i.e. within 90 ms of fallBackToPeriodicSync() stopping the FGS, a restart re-enters onStartCommand → startForeground → rejection → crash.
Device/build: Google Pixel 10 Pro XL, Android release 17, SDK 37; app 0.1.0 (versionCode 1); Gmail PASSWORD_IMAP; settings pushIdle=true, encryptCache=false. No org.libremail tombstones or ANR traces in the bugreport (crashes are pure Java RuntimeExceptions).
Separate cluster, not this ticket: 4× java.lang.UnsatisfiedLinkError ... SQLiteConnection.nativeOpen on 2026-07-03 09:02:22–57 (SQLCipher native lib not loaded when opening the encrypted cache). Flagged for its own ticket.
Root cause — app/src/main/kotlin/org/libremail/push/IdleService.kt (origin/main)
startAsForeground (line 231) calls ServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC) (lines 233–237) with no guard / try-catch.
When #302's cap handler fallBackToPeriodicSync() (line 171) stops the service, START_STICKY makes the platform recreate it with a null intent, and IdlePushManager.start() / LibreMailApplication.ensurePushStarted() also re-issue explicit startForegroundService calls (e.g. on MainActivity foreground). Each lands in onStartCommand → startForeground(dataSync) while the 24h budget is exhausted → ForegroundServiceStartNotAllowedException (uncaught) → crash; START_STICKY re-restarts → loop.
IdlePushManager.start() wraps its startForegroundService in runCatching, but that only guards the caller; the throwing startForeground runs later on the service's own main thread and is not covered by it.
Proposed fix (IdleService.kt)
Return START_NOT_STICKY from onStartCommand instead of START_STICKY. Push is app-managed — LibreMailApplication's settings/account collector and ensurePushStarted() deterministically (re)start the service whenever it should run — so the platform's sticky null-intent auto-restart is redundant and fires exactly in the states that can't legally start a dataSync FGS (background / exhausted budget). Removes the null-intent restart loop.
Guard the FGS start. Wrap startForeground in try/catch for ForegroundServiceStartNotAllowedException (API 31+; minSdk is 29, so gate with Build.VERSION.SDK_INT >= 31 or catch its IllegalStateException supertype). On rejection, do not rethrow — degrade exactly like the cap handler: syncScheduler.schedulePeriodicSync(), post/keep the degraded PushMode.POLLING notification, then stopSelf(). (The start arrived via startForegroundService, so we must promptly stopSelf() after a failed startForeground to avoid the "did not call startForeground in time" ANR.) Return START_NOT_STICKY on this path.
Optional hardening: record a timestamp when fallBackToPeriodicSync() runs and, while still within the cap window, have onStartCommand skip the startForeground attempt entirely (schedule periodic sync + stopSelf()), so we don't rely on catching the exception on every restart.
Keep the #302onTimeout stop path synchronous/fast so ForegroundServiceDidNotStopInTimeException stays mitigated.
Logging (PII-free AppLog, per DoD)
On caught rejection: AppLog.w(TAG, "dataSync FGS start rejected (runtime cap or background); staying on 15-minute periodic sync") — no account/email/intent contents.
If adding the cap-window skip: AppLog.i(TAG, "dataSync FGS cap still active; skipping foreground start, periodic sync covers mail").
Affected files / lines (origin/main)
app/src/main/kotlin/org/libremail/push/IdleService.kt: onStartCommand:86–:102 (start :87, return START_STICKY:101), startAsForeground:231–:238 (startForeground:233, DATA_SYNC type :237), cap handler fallBackToPeriodicSync:171–:188.
Context (no change required, but explains the restart triggers): app/src/main/kotlin/org/libremail/push/IdlePushManager.kt (start()runCatching only guards the caller), app/src/main/kotlin/org/libremail/LibreMailApplication.kt (:87–:108, the start() / ensurePushStarted() triggers).
Definition of done
Unit test (JVM/Robolectric): onStartCommand returns START_NOT_STICKY; a startForeground that throws ForegroundServiceStartNotAllowedException is caught → schedulePeriodicSync() invoked + stopSelf() + no exception propagates.
E2E/instrumented test: the degrade path (FGS start rejected → service stops cleanly, periodic-sync fallback scheduled, degraded notification shown), alongside the existing PushStatusNotificationInstrumentedTest.
Suggested priority: P1 — recurring crash loop on the target/preview top SDK (37) on a daily-driver device; it self-heals when the 24h window resets and periodic sync still delivers mail, so not P0. Coordinator finalizes.
## Summary
Recurring on-device crash **loop** captured from the maintainer's Pixel 10 Pro XL (Android 17 / SDK 37). After the `dataSync` foreground-service runtime cap from #302 fires and `IdleService` self-stops, the service is restarted — both by `START_STICKY` null-intent redelivery and by explicit `startForegroundService` calls — and `onStartCommand` unconditionally calls `startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC)` while the rolling-24h dataSync budget is still exhausted. The platform rejects that start with `ForegroundServiceStartNotAllowedException: Time limit already exhausted for foreground service type dataSync`; the exception is uncaught, the process crashes, and `START_STICKY` restarts it straight back into the same rejection → crash loop until the 24h window frees budget.
This is a direct **follow-up to #302**: #302 added the `onTimeout` → `fallBackToPeriodicSync()` clean stop, but nothing prevents the *subsequent (re)start* from attempting a now-illegal dataSync FGS start.
## Evidence
Artifacts: 2 in-app `DebugReport`s + `dumpsys dropbox` (10 entries) + a full system bugreport, all from the maintainer's device. Times are device-local (-0500).
**Signature C — `ForegroundServiceStartNotAllowedException: Time limit already exhausted for foreground service type dataSync`** at `IdleService.startAsForeground` ← `IdleService.onStartCommand` (`:87`), wrapped in `java.lang.RuntimeException: Unable to start service ...IdleService...`. 5 occurrences:
- 2026-07-04 04:24:08.821 — redelivered **null** intent (`START_STICKY`)
- 2026-07-04 05:53:09.878 — null intent
- 2026-07-04 06:00:01.695 — null intent
- 2026-07-04 23:42:53.704 — explicit **Intent** (`startForegroundService`) — DebugReport `28d3fa45`
- 2026-07-04 23:42:54.921 — explicit Intent — DebugReport `2c1fb3cc`
**Signature B — `RemoteServiceException$ForegroundServiceDidNotStopInTimeException: A foreground service of type dataSync did not stop within its timeout ...IdleService`.** 1 occurrence:
- 2026-07-04 04:24:08.039 — on a pre-#302 build (stack `startAsForeground` line 206). Mitigated on current `main` by the `onTimeout` handler; listed for completeness so the fix keeps the stop path within the grace window.
**Smoking gun** — DebugReport `28d3fa45` in-app log buffer:
```
04:42:53.582Z I/IdleService: dataSync FGS runtime cap reached: pausing IMAP IDLE; mail arrives via 15-minute periodic sync
04:42:53.672Z E/CrashReporter: Uncaught exception: java.lang.RuntimeException (+90 ms)
```
i.e. within 90 ms of `fallBackToPeriodicSync()` stopping the FGS, a restart re-enters `onStartCommand` → `startForeground` → rejection → crash.
Device/build: Google Pixel 10 Pro XL, Android release 17, SDK 37; app 0.1.0 (versionCode 1); Gmail `PASSWORD_IMAP`; settings `pushIdle=true`, `encryptCache=false`. No org.libremail tombstones or ANR traces in the bugreport (crashes are pure Java `RuntimeException`s).
> Separate cluster, **not** this ticket: 4× `java.lang.UnsatisfiedLinkError ... SQLiteConnection.nativeOpen` on 2026-07-03 09:02:22–57 (SQLCipher native lib not loaded when opening the encrypted cache). Flagged for its own ticket.
## Root cause — `app/src/main/kotlin/org/libremail/push/IdleService.kt` (origin/main)
- `onStartCommand` (line 86) calls `startAsForeground(shownMode)` unconditionally (line 87) and returns `START_STICKY` (line 101).
- `startAsForeground` (line 231) calls `ServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC)` (lines 233–237) with no guard / try-catch.
- When #302's cap handler `fallBackToPeriodicSync()` (line 171) stops the service, `START_STICKY` makes the platform recreate it with a null intent, and `IdlePushManager.start()` / `LibreMailApplication.ensurePushStarted()` also re-issue explicit `startForegroundService` calls (e.g. on MainActivity foreground). Each lands in `onStartCommand` → `startForeground(dataSync)` while the 24h budget is exhausted → `ForegroundServiceStartNotAllowedException` (uncaught) → crash; `START_STICKY` re-restarts → loop.
- `IdlePushManager.start()` wraps its `startForegroundService` in `runCatching`, but that only guards the **caller**; the throwing `startForeground` runs later on the service's own main thread and is not covered by it.
## Proposed fix (`IdleService.kt`)
1. **Return `START_NOT_STICKY`** from `onStartCommand` instead of `START_STICKY`. Push is app-managed — `LibreMailApplication`'s settings/account collector and `ensurePushStarted()` deterministically (re)start the service whenever it should run — so the platform's sticky null-intent auto-restart is redundant and fires exactly in the states that can't legally start a dataSync FGS (background / exhausted budget). Removes the null-intent restart loop.
2. **Guard the FGS start.** Wrap `startForeground` in try/catch for `ForegroundServiceStartNotAllowedException` (API 31+; `minSdk` is 29, so gate with `Build.VERSION.SDK_INT >= 31` or catch its `IllegalStateException` supertype). On rejection, do **not** rethrow — degrade exactly like the cap handler: `syncScheduler.schedulePeriodicSync()`, post/keep the degraded `PushMode.POLLING` notification, then `stopSelf()`. (The start arrived via `startForegroundService`, so we must promptly `stopSelf()` after a failed `startForeground` to avoid the "did not call startForeground in time" ANR.) Return `START_NOT_STICKY` on this path.
3. **Optional hardening:** record a timestamp when `fallBackToPeriodicSync()` runs and, while still within the cap window, have `onStartCommand` skip the `startForeground` attempt entirely (schedule periodic sync + `stopSelf()`), so we don't rely on catching the exception on every restart.
4. Keep the #302 `onTimeout` stop path synchronous/fast so `ForegroundServiceDidNotStopInTimeException` stays mitigated.
## Logging (PII-free `AppLog`, per DoD)
- On caught rejection: `AppLog.w(TAG, "dataSync FGS start rejected (runtime cap or background); staying on 15-minute periodic sync")` — no account/email/intent contents.
- If adding the cap-window skip: `AppLog.i(TAG, "dataSync FGS cap still active; skipping foreground start, periodic sync covers mail")`.
## Affected files / lines (origin/main)
- `app/src/main/kotlin/org/libremail/push/IdleService.kt`: `onStartCommand` `:86`–`:102` (start `:87`, `return START_STICKY` `:101`), `startAsForeground` `:231`–`:238` (`startForeground` `:233`, DATA_SYNC type `:237`), cap handler `fallBackToPeriodicSync` `:171`–`:188`.
- Context (no change required, but explains the restart triggers): `app/src/main/kotlin/org/libremail/push/IdlePushManager.kt` (`start()` `runCatching` only guards the caller), `app/src/main/kotlin/org/libremail/LibreMailApplication.kt` (`:87`–`:108`, the `start()` / `ensurePushStarted()` triggers).
## Definition of done
- **Unit test** (JVM/Robolectric): `onStartCommand` returns `START_NOT_STICKY`; a `startForeground` that throws `ForegroundServiceStartNotAllowedException` is caught → `schedulePeriodicSync()` invoked + `stopSelf()` + no exception propagates.
- **E2E/instrumented test**: the degrade path (FGS start rejected → service stops cleanly, periodic-sync fallback scheduled, degraded notification shown), alongside the existing `PushStatusNotificationInstrumentedTest`.
**Suggested priority: P1** — recurring crash loop on the target/preview top SDK (37) on a daily-driver device; it self-heals when the 24h window resets and periodic sync still delivers mail, so not P0. Coordinator finalizes.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Recurring on-device crash loop captured from the maintainer's Pixel 10 Pro XL (Android 17 / SDK 37). After the
dataSyncforeground-service runtime cap from #302 fires andIdleServiceself-stops, the service is restarted — both bySTART_STICKYnull-intent redelivery and by explicitstartForegroundServicecalls — andonStartCommandunconditionally callsstartForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC)while the rolling-24h dataSync budget is still exhausted. The platform rejects that start withForegroundServiceStartNotAllowedException: Time limit already exhausted for foreground service type dataSync; the exception is uncaught, the process crashes, andSTART_STICKYrestarts it straight back into the same rejection → crash loop until the 24h window frees budget.This is a direct follow-up to #302: #302 added the
onTimeout→fallBackToPeriodicSync()clean stop, but nothing prevents the subsequent (re)start from attempting a now-illegal dataSync FGS start.Evidence
Artifacts: 2 in-app
DebugReports +dumpsys dropbox(10 entries) + a full system bugreport, all from the maintainer's device. Times are device-local (-0500).Signature C —
ForegroundServiceStartNotAllowedException: Time limit already exhausted for foreground service type dataSyncatIdleService.startAsForeground←IdleService.onStartCommand(:87), wrapped injava.lang.RuntimeException: Unable to start service ...IdleService.... 5 occurrences:START_STICKY)startForegroundService) — DebugReport28d3fa452c1fb3ccSignature B —
RemoteServiceException$ForegroundServiceDidNotStopInTimeException: A foreground service of type dataSync did not stop within its timeout ...IdleService. 1 occurrence:startAsForegroundline 206). Mitigated on currentmainby theonTimeouthandler; listed for completeness so the fix keeps the stop path within the grace window.Smoking gun — DebugReport
28d3fa45in-app log buffer:i.e. within 90 ms of
fallBackToPeriodicSync()stopping the FGS, a restart re-entersonStartCommand→startForeground→ rejection → crash.Device/build: Google Pixel 10 Pro XL, Android release 17, SDK 37; app 0.1.0 (versionCode 1); Gmail
PASSWORD_IMAP; settingspushIdle=true,encryptCache=false. No org.libremail tombstones or ANR traces in the bugreport (crashes are pure JavaRuntimeExceptions).Root cause —
app/src/main/kotlin/org/libremail/push/IdleService.kt(origin/main)onStartCommand(line 86) callsstartAsForeground(shownMode)unconditionally (line 87) and returnsSTART_STICKY(line 101).startAsForeground(line 231) callsServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC)(lines 233–237) with no guard / try-catch.fallBackToPeriodicSync()(line 171) stops the service,START_STICKYmakes the platform recreate it with a null intent, andIdlePushManager.start()/LibreMailApplication.ensurePushStarted()also re-issue explicitstartForegroundServicecalls (e.g. on MainActivity foreground). Each lands inonStartCommand→startForeground(dataSync)while the 24h budget is exhausted →ForegroundServiceStartNotAllowedException(uncaught) → crash;START_STICKYre-restarts → loop.IdlePushManager.start()wraps itsstartForegroundServiceinrunCatching, but that only guards the caller; the throwingstartForegroundruns later on the service's own main thread and is not covered by it.Proposed fix (
IdleService.kt)START_NOT_STICKYfromonStartCommandinstead ofSTART_STICKY. Push is app-managed —LibreMailApplication's settings/account collector andensurePushStarted()deterministically (re)start the service whenever it should run — so the platform's sticky null-intent auto-restart is redundant and fires exactly in the states that can't legally start a dataSync FGS (background / exhausted budget). Removes the null-intent restart loop.startForegroundin try/catch forForegroundServiceStartNotAllowedException(API 31+;minSdkis 29, so gate withBuild.VERSION.SDK_INT >= 31or catch itsIllegalStateExceptionsupertype). On rejection, do not rethrow — degrade exactly like the cap handler:syncScheduler.schedulePeriodicSync(), post/keep the degradedPushMode.POLLINGnotification, thenstopSelf(). (The start arrived viastartForegroundService, so we must promptlystopSelf()after a failedstartForegroundto avoid the "did not call startForeground in time" ANR.) ReturnSTART_NOT_STICKYon this path.fallBackToPeriodicSync()runs and, while still within the cap window, haveonStartCommandskip thestartForegroundattempt entirely (schedule periodic sync +stopSelf()), so we don't rely on catching the exception on every restart.onTimeoutstop path synchronous/fast soForegroundServiceDidNotStopInTimeExceptionstays mitigated.Logging (PII-free
AppLog, per DoD)AppLog.w(TAG, "dataSync FGS start rejected (runtime cap or background); staying on 15-minute periodic sync")— no account/email/intent contents.AppLog.i(TAG, "dataSync FGS cap still active; skipping foreground start, periodic sync covers mail").Affected files / lines (origin/main)
app/src/main/kotlin/org/libremail/push/IdleService.kt:onStartCommand:86–:102(start:87,return START_STICKY:101),startAsForeground:231–:238(startForeground:233, DATA_SYNC type:237), cap handlerfallBackToPeriodicSync:171–:188.app/src/main/kotlin/org/libremail/push/IdlePushManager.kt(start()runCatchingonly guards the caller),app/src/main/kotlin/org/libremail/LibreMailApplication.kt(:87–:108, thestart()/ensurePushStarted()triggers).Definition of done
onStartCommandreturnsSTART_NOT_STICKY; astartForegroundthat throwsForegroundServiceStartNotAllowedExceptionis caught →schedulePeriodicSync()invoked +stopSelf()+ no exception propagates.PushStatusNotificationInstrumentedTest.Suggested priority: P1 — recurring crash loop on the target/preview top SDK (37) on a daily-driver device; it self-heals when the 24h window resets and periodic sync still delivers mail, so not P0. Coordinator finalizes.