Onboarding: opt-in to unrestricted battery/background usage #49

Closed
opened 2026-07-01 15:26:49 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-01 15:26:49 +00:00 (Migrated from github.com)

Part of #9.

Context

Timely mail delivery depends on two mechanisms today, both of which Android's battery
management throttles:

  • Push: IdleService — a dataSync foreground service holding an IMAP IDLE connection
    per account for instant delivery (opt-in, settings.pushIdle, on by default; toggled under
    Advanced Settings).
  • Polling: SyncScheduler.schedulePeriodicSync() — a WorkManager periodic job at the
    15-minute floor with a CONNECTED network constraint.

Doze, App Standby buckets, and especially the per-app Restricted battery setting defer
those WorkManager jobs and let the OS kill the foreground service, so mail arrives late or only
when the app is next opened. IdlePushManager.start() already swallows
ForegroundServiceStartNotAllowedException when a background start is blocked — moving the app
onto the battery allowlist reduces how often the OS tears IDLE down in the first place.

There's no battery handling in the app today (no PowerManager /
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS anywhere; the manifest declares none).
MainActivity.NotificationPermissionEffect is the pattern to mirror for a one-time,
first-run prompt.

Two distinct Android surfaces, often conflated:

  1. Doze allowlist ("ignore battery optimizations"). Read with
    PowerManager.isIgnoringBatteryOptimizations(pkg). Changeable via the direct system dialog
    ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (needs the restricted
    REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission) or by deep-linking to the settings
    screen with no special permission.
  2. Per-app Unrestricted / Optimized / Restricted (App info → Battery, refined on Android
    12+). No API sets this — we can only deep-link to app details and guide the user. Note
    isIgnoringBatteryOptimizations returns true for Unrestricted and false for both
    Optimized and Restricted, so it can't single out the harmful "Restricted" state.

Chosen approach — guided deep-link (F-Droid-safe). We explain the benefit, then send the
user to the system battery screen to flip the app to Unrestricted; we do not fire the
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS dialog. Google Play restricts that permission to an
approved set of use cases (rejection risk — see #17), whereas the deep-link path needs no extra
permission and is safe on both Play and F-Droid (#16), consistent with the project's F-Droid-first
posture. (A one-tap direct dialog could later be added behind an F-Droid-only flavor if we want it.)

Scope

  • Gate the feature on "where supported" (API level) and current state: no-op when
    isIgnoringBatteryOptimizations is already true, so already-unrestricted users are never
    prompted.
  • Onboarding step shown after at least one account is added (so the "instant delivery"
    pitch is concrete): explains and encourages, with a primary Take me there action and a
    non-blocking Not now. Never blocks finishing onboarding.
  • On Take me there, deep-link to the system battery/allowlist screen
    (ACTION_APPLICATION_DETAILS_SETTINGS, or ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
    on return, re-check state and reflect it. No new manifest permission.
  • Don't nag: persist a "battery prompt handled/dismissed" flag in DataStore (mirror the
    booleans in SettingsRepository / AppSettings) so onboarding asks at most once.
  • Advanced Settings entry (next to Push mail (IMAP IDLE)): show current battery status
    (Unrestricted vs. optimized/restricted) and a button to (re)open the system screen — the
    recovery path for users who skipped onboarding or are on OEMs with aggressive battery
    managers. Consider surfacing it contextually when push is on but we're not allowlisted.
  • Onboarding + settings copy/strings; SPDX header on new files.
  • Unit-test the "should we prompt?" decision (supported API × not already unrestricted ×
    not already dismissed). The intent launch is thin — cover it in E2E/manual.

Acceptance criteria

  • On a supported version where the app is not allowlisted, onboarding surfaces the opt-in exactly
    once; Take me there opens the correct system screen; Not now continues onboarding and
    does not auto-re-prompt.
  • If already unrestricted, or on a version without the feature, no prompt appears.
  • Advanced Settings always reflects the current battery state and can re-open the system screen.
  • With the app set to Unrestricted, IDLE push / periodic sync keep running in the background
    across Doze windows (manual/E2E verification).

Open questions

  • The Restricted bucket (the real delivery-killer) is only reachable/detectable via the
    App-info Battery screen, not the Doze allowlist API. Is deep-linking there with instructions in
    scope, or is the Doze allowlist enough for our needs? (IDLE-as-foreground-service benefits mainly
    from the allowlist.)
  • OEM battery managers (Samsung, Xiaomi/MIUI, OnePlus, Huawei — dontkillmyapp.com) ignore the
    AOSP allowlist. Standard path + a help link now, per-OEM guidance later? (Suggest: out of scope
    here.)
  • Placement in the flow — a dedicated final onboarding step, or folded into the #30
    "add another? / first-account landing" juncture?

Relevant files

  • app/src/main/kotlin/org/libremail/push/IdleService.kt, push/IdlePushManager.kt (what
    benefits; already handles blocked background starts)
  • app/src/main/kotlin/org/libremail/data/sync/SyncScheduler.kt (periodic sync throttled under Doze)
  • app/src/main/kotlin/org/libremail/MainActivity.kt (NotificationPermissionEffect — one-time
    prompt pattern)
  • app/src/main/kotlin/org/libremail/data/settings/SettingsRepository.kt + AppSettings (add the
    "prompt handled" flag); ui/settings/SettingsScreen.kt (+ SettingsViewModel) for the Advanced
    row; res/values/strings.xml
  • onboarding scaffold + session state from #26; ui/LibreMailApp.kt

Dependencies

Part of the Onboarding & account setup epic (#9); sequence after #26 (scaffold), near #30
(post-add landing). Ties into #17 (Google Play compliance) and #16 (F-Droid compliance) via the
battery-optimization policy. Amplifies the existing push (IDLE) feature — pairs with
settings.pushIdle.

Part of #9. ## Context Timely mail delivery depends on two mechanisms today, both of which Android's battery management throttles: - **Push:** `IdleService` — a `dataSync` foreground service holding an IMAP IDLE connection per account for instant delivery (opt-in, `settings.pushIdle`, on by default; toggled under **Advanced Settings**). - **Polling:** `SyncScheduler.schedulePeriodicSync()` — a WorkManager periodic job at the 15-minute floor with a `CONNECTED` network constraint. Doze, App Standby buckets, and especially the per-app **Restricted** battery setting defer those WorkManager jobs and let the OS kill the foreground service, so mail arrives late or only when the app is next opened. `IdlePushManager.start()` already swallows `ForegroundServiceStartNotAllowedException` when a background start is blocked — moving the app onto the battery allowlist reduces how often the OS tears IDLE down in the first place. There's no battery handling in the app today (no `PowerManager` / `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` anywhere; the manifest declares none). `MainActivity.NotificationPermissionEffect` is the pattern to mirror for a one-time, first-run prompt. **Two distinct Android surfaces, often conflated:** 1. **Doze allowlist** ("ignore battery optimizations"). Read with `PowerManager.isIgnoringBatteryOptimizations(pkg)`. Changeable via the direct system dialog `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` (needs the restricted `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission) **or** by deep-linking to the settings screen with no special permission. 2. **Per-app Unrestricted / Optimized / Restricted** (App info → Battery, refined on Android 12+). **No API sets this** — we can only deep-link to app details and guide the user. Note `isIgnoringBatteryOptimizations` returns `true` for **Unrestricted** and `false` for *both* **Optimized** and **Restricted**, so it can't single out the harmful "Restricted" state. **Chosen approach — guided deep-link (F-Droid-safe).** We explain the benefit, then send the user to the system battery screen to flip the app to Unrestricted; we do **not** fire the `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` dialog. Google Play restricts that permission to an approved set of use cases (rejection risk — see #17), whereas the deep-link path needs no extra permission and is safe on both Play and F-Droid (#16), consistent with the project's F-Droid-first posture. (A one-tap direct dialog could later be added behind an F-Droid-only flavor if we want it.) ## Scope - [ ] Gate the feature on "where supported" (API level) **and** current state: no-op when `isIgnoringBatteryOptimizations` is already `true`, so already-unrestricted users are never prompted. - [ ] Onboarding step shown **after at least one account is added** (so the "instant delivery" pitch is concrete): explains and encourages, with a primary **Take me there** action and a non-blocking **Not now**. Never blocks finishing onboarding. - [ ] On **Take me there**, deep-link to the system battery/allowlist screen (`ACTION_APPLICATION_DETAILS_SETTINGS`, or `ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS`); on return, re-check state and reflect it. **No new manifest permission.** - [ ] Don't nag: persist a "battery prompt handled/dismissed" flag in DataStore (mirror the booleans in `SettingsRepository` / `AppSettings`) so onboarding asks at most once. - [ ] **Advanced Settings** entry (next to *Push mail (IMAP IDLE)*): show current battery status (Unrestricted vs. optimized/restricted) and a button to (re)open the system screen — the recovery path for users who skipped onboarding or are on OEMs with aggressive battery managers. Consider surfacing it contextually when push is on but we're not allowlisted. - [ ] Onboarding + settings copy/strings; SPDX header on new files. - [ ] Unit-test the "should we prompt?" decision (supported API × not already unrestricted × not already dismissed). The intent launch is thin — cover it in E2E/manual. ## Acceptance criteria - On a supported version where the app is not allowlisted, onboarding surfaces the opt-in exactly once; **Take me there** opens the correct system screen; **Not now** continues onboarding and does not auto-re-prompt. - If already unrestricted, or on a version without the feature, no prompt appears. - Advanced Settings always reflects the current battery state and can re-open the system screen. - With the app set to Unrestricted, IDLE push / periodic sync keep running in the background across Doze windows (manual/E2E verification). ## Open questions - The **Restricted** bucket (the real delivery-killer) is only reachable/detectable via the App-info Battery screen, not the Doze allowlist API. Is deep-linking there with instructions in scope, or is the Doze allowlist enough for our needs? (IDLE-as-foreground-service benefits mainly from the allowlist.) - **OEM battery managers** (Samsung, Xiaomi/MIUI, OnePlus, Huawei — dontkillmyapp.com) ignore the AOSP allowlist. Standard path + a help link now, per-OEM guidance later? (Suggest: out of scope here.) - **Placement** in the flow — a dedicated final onboarding step, or folded into the #30 "add another? / first-account landing" juncture? ## Relevant files - `app/src/main/kotlin/org/libremail/push/IdleService.kt`, `push/IdlePushManager.kt` (what benefits; already handles blocked background starts) - `app/src/main/kotlin/org/libremail/data/sync/SyncScheduler.kt` (periodic sync throttled under Doze) - `app/src/main/kotlin/org/libremail/MainActivity.kt` (`NotificationPermissionEffect` — one-time prompt pattern) - `app/src/main/kotlin/org/libremail/data/settings/SettingsRepository.kt` + `AppSettings` (add the "prompt handled" flag); `ui/settings/SettingsScreen.kt` (+ `SettingsViewModel`) for the Advanced row; `res/values/strings.xml` - onboarding scaffold + session state from #26; `ui/LibreMailApp.kt` ## Dependencies Part of the **Onboarding & account setup** epic (#9); sequence after #26 (scaffold), near #30 (post-add landing). Ties into #17 (Google Play compliance) and #16 (F-Droid compliance) via the battery-optimization policy. Amplifies the existing push (IDLE) feature — pairs with `settings.pushIdle`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#49