IMAP IDLE (push email) shuts down and temporarily reverts to pulling every 15 minutes at or below 20% battery #90

Closed
opened 2026-07-02 02:11:39 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-02 02:11:39 +00:00 (Migrated from github.com)

Observed behavior

Push email (the IdleService foreground service holding a long-lived IMAP IDLE connection per account)
stops delivering instantly at or below ~20% battery, and mail only arrives via the 15-minute periodic
SyncScheduler fallback (SyncScheduler.kt:27-33) until battery recovers.

What the code does today (confirmed by reading, not yet reproduced on-device)

There is no battery-aware logic anywhere in the push path: IdleService (IdleService.kt) and
IdlePushManager have zero reference to battery state, PowerManager, or BatteryManager. The only
battery-related code in the app is BatteryOptimizationManager (push/BatteryOptimizationManager.kt),
and it's only consulted at onboarding time (#49) and for the Settings status row
(SettingsViewModel.kt) — IdleService never calls it. So this reversion isn't app logic choosing to
back off at 20%; it's an emergent side effect of the OS (or OEM battery manager) throttling or killing the
long-lived socket/foreground service, and the app can't currently tell the difference between "IDLE is
fine" and "IDLE just silently stopped" — watchAccount's reconnect loop (IdleService.kt:93-111) keeps
retrying with backoff, but if the OS is refusing background network to a low-battery app, those retries
just keep failing quietly.

Two plausible OS-level mechanisms (not distinguished yet):

  1. Android's system Battery Saver, which restricts background network access once enabled (historically
    auto-enables around thresholds like this, and is user-configurable).
  2. OEM-specific aggressive battery management (some manufacturers kill background sockets or foreground
    services below a battery threshold, independent of stock Android Doze) — the class of issue
    dontkillmyapp.com catalogs. BatteryOptimizationManager's own doc comment already notes it can't
    distinguish "Optimized" from "Restricted" (push/BatteryOptimizationManager.kt:24-26), i.e. even a
    user who didn't complete the #49 prompt may be on a more restrictive setting than assumed.

Suggested approach

  1. Investigate on a real device at ~20% battery (with logging around watchAccount's reconnect
    attempts) to confirm which mechanism is actually responsible — this can't be verified in a sandbox.
  2. Regardless of root cause, the persistent "Watching for new mail" notification
    (IdleService.kt:120-142) never changes text/state today, so a user has no visible sign they've
    silently fallen back to 15-minute polling. Making that notification (or a Settings status row)
    honestly reflect a disconnected/reconnecting state turns a confusing silent degradation into a visible,
    explained one.
  3. If it's OS Battery Saver specifically, PowerManager.isPowerSaveMode() /
    ACTION_POWER_SAVE_MODE_CHANGED can detect it proactively.
  4. If it's the OEM "Restricted" state BatteryOptimizationManager already can't distinguish, consider
    surfacing OEM-specific guidance (à la dontkillmyapp.com) from Settings, alongside the existing
    battery-allowlist prompt.

Acceptance criteria

  • Root cause confirmed on a real device.
  • User gets some visible indication when push has degraded to polling (not silent).
  • Fix or mitigation implemented per whatever the investigation finds.

Relates to #89 (the other battery-threshold behavior) — likely worth a shared battery-state utility; see
the note there.

## Observed behavior Push email (the `IdleService` foreground service holding a long-lived IMAP IDLE connection per account) stops delivering instantly at or below ~20% battery, and mail only arrives via the 15-minute periodic `SyncScheduler` fallback (`SyncScheduler.kt:27-33`) until battery recovers. ## What the code does today (confirmed by reading, not yet reproduced on-device) There is **no battery-aware logic anywhere in the push path**: `IdleService` (`IdleService.kt`) and `IdlePushManager` have zero reference to battery state, `PowerManager`, or `BatteryManager`. The only battery-related code in the app is `BatteryOptimizationManager` (`push/BatteryOptimizationManager.kt`), and it's only consulted at onboarding time (#49) and for the Settings status row (`SettingsViewModel.kt`) — `IdleService` never calls it. So this reversion isn't app logic choosing to back off at 20%; it's an emergent side effect of the OS (or OEM battery manager) throttling or killing the long-lived socket/foreground service, and the app can't currently tell the difference between "IDLE is fine" and "IDLE just silently stopped" — `watchAccount`'s reconnect loop (`IdleService.kt:93-111`) keeps retrying with backoff, but if the OS is refusing background network to a low-battery app, those retries just keep failing quietly. Two plausible OS-level mechanisms (not distinguished yet): 1. Android's system Battery Saver, which restricts background network access once enabled (historically auto-enables around thresholds like this, and is user-configurable). 2. OEM-specific aggressive battery management (some manufacturers kill background sockets or foreground services below a battery threshold, independent of stock Android Doze) — the class of issue dontkillmyapp.com catalogs. `BatteryOptimizationManager`'s own doc comment already notes it can't distinguish "Optimized" from "Restricted" (`push/BatteryOptimizationManager.kt:24-26`), i.e. even a user who didn't complete the #49 prompt may be on a more restrictive setting than assumed. ## Suggested approach 1. **Investigate on a real device** at ~20% battery (with logging around `watchAccount`'s reconnect attempts) to confirm which mechanism is actually responsible — this can't be verified in a sandbox. 2. Regardless of root cause, the persistent "Watching for new mail" notification (`IdleService.kt:120-142`) never changes text/state today, so a user has no visible sign they've silently fallen back to 15-minute polling. Making that notification (or a Settings status row) honestly reflect a disconnected/reconnecting state turns a confusing silent degradation into a visible, explained one. 3. If it's OS Battery Saver specifically, `PowerManager.isPowerSaveMode()` / `ACTION_POWER_SAVE_MODE_CHANGED` can detect it proactively. 4. If it's the OEM "Restricted" state `BatteryOptimizationManager` already can't distinguish, consider surfacing OEM-specific guidance (à la dontkillmyapp.com) from Settings, alongside the existing battery-allowlist prompt. ## Acceptance criteria - [ ] Root cause confirmed on a real device. - [ ] User gets some visible indication when push has degraded to polling (not silent). - [ ] Fix or mitigation implemented per whatever the investigation finds. Relates to #89 (the other battery-threshold behavior) — likely worth a shared battery-state utility; see the note there.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#90