Implements #49 (part of the onboarding epic #9): a guided, F-Droid-safe way for users to allow LibreMail Unrestricted background/battery usage, so IMAP IDLE push (IdleService) and periodic WorkManager sync aren't throttled or killed by Doze.
Approach
Deep-links to the system app-details screen (where Battery → Unrestricted lives) rather than firing the restricted REQUEST_IGNORE_BATTERY_OPTIMIZATIONS dialog. No new manifest permission — safe on Google Play (#17) and F-Droid (#16), consistent with the project's F-Droid-first posture.
BatteryOptimizationManager — reads PowerManager.isIgnoringBatteryOptimizations and builds the app-details intent.
Onboarding step (BatteryOptimizationScreen) — shown after the first account is added; Take me there deep-links, Not now is non-blocking; re-checks status on resume and flips to a success state. Skipped entirely when already unrestricted or already handled.
Advanced Settings row — shows current status (Unrestricted vs. Optimized) and re-opens the system screen; the recovery path for users who skipped onboarding or are on aggressive-OEM devices.
battery_prompt_handled flag persisted in the settings DataStore (kept out of AppSettings — it's onboarding state, not a user preference).
E2E (BatteryOptimizationStepTest, Espresso-Intents): step offered → Not now → inbox (+ flag set); Take me there fires ACTION_APPLICATION_DETAILS_SETTINGS for this package; skip-when-handled. All 3 pass locally on API 37; the two touched existing E2E classes still pass.
Full local gate green: assembleDebug, testDebugUnitTest, compileDebugAndroidTestKotlin, lintDebug, detekt, ktlintCheck.
Notes
At minSdk 29 the "supported" gate is always true (Doze allowlist is API 23+); kept explicit for correctness if the floor ever drops.
The API can't distinguish Restricted from Optimized (isIgnoringBatteryOptimizations is false for both), so the "not unrestricted" copy covers both — noted inline.
Adds espresso-intents (reusing the existing espresso version) to androidTest deps.
Implements #49 (part of the onboarding epic #9): a guided, F-Droid-safe way for users to allow LibreMail **Unrestricted** background/battery usage, so IMAP IDLE push (`IdleService`) and periodic WorkManager sync aren't throttled or killed by Doze.
## Approach
Deep-links to the system **app-details** screen (where Battery → Unrestricted lives) rather than firing the restricted `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` dialog. No new manifest permission — safe on Google Play (#17) and F-Droid (#16), consistent with the project's F-Droid-first posture.
## What's included
- **`BatteryPromptDecision`** — pure, unit-tested gate: `supported && !alreadyUnrestricted && !alreadyHandled`.
- **`BatteryOptimizationManager`** — reads `PowerManager.isIgnoringBatteryOptimizations` and builds the app-details intent.
- **Onboarding step** (`BatteryOptimizationScreen`) — shown after the first account is added; **Take me there** deep-links, **Not now** is non-blocking; re-checks status on resume and flips to a success state. Skipped entirely when already unrestricted or already handled.
- **Advanced Settings row** — shows current status (Unrestricted vs. Optimized) and re-opens the system screen; the recovery path for users who skipped onboarding or are on aggressive-OEM devices.
- **`battery_prompt_handled`** flag persisted in the settings DataStore (kept out of `AppSettings` — it's onboarding state, not a user preference).
## Testing
- **Unit:** `BatteryPromptDecisionTest` (4) + `OnboardingViewModelTest` (6).
- **E2E** (`BatteryOptimizationStepTest`, Espresso-Intents): step offered → **Not now** → inbox (+ flag set); **Take me there** fires `ACTION_APPLICATION_DETAILS_SETTINGS` for this package; skip-when-handled. All 3 pass locally on API 37; the two touched existing E2E classes still pass.
- Full local gate green: `assembleDebug`, `testDebugUnitTest`, `compileDebugAndroidTestKotlin`, `lintDebug`, `detekt`, `ktlintCheck`.
## Notes
- At minSdk 29 the "supported" gate is always true (Doze allowlist is API 23+); kept explicit for correctness if the floor ever drops.
- The API can't distinguish **Restricted** from **Optimized** (`isIgnoringBatteryOptimizations` is false for both), so the "not unrestricted" copy covers both — noted inline.
- Adds `espresso-intents` (reusing the existing espresso version) to androidTest deps.
Closes #49
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Implements #49 (part of the onboarding epic #9): a guided, F-Droid-safe way for users to allow LibreMail Unrestricted background/battery usage, so IMAP IDLE push (
IdleService) and periodic WorkManager sync aren't throttled or killed by Doze.Approach
Deep-links to the system app-details screen (where Battery → Unrestricted lives) rather than firing the restricted
REQUEST_IGNORE_BATTERY_OPTIMIZATIONSdialog. No new manifest permission — safe on Google Play (#17) and F-Droid (#16), consistent with the project's F-Droid-first posture.What's included
BatteryPromptDecision— pure, unit-tested gate:supported && !alreadyUnrestricted && !alreadyHandled.BatteryOptimizationManager— readsPowerManager.isIgnoringBatteryOptimizationsand builds the app-details intent.BatteryOptimizationScreen) — shown after the first account is added; Take me there deep-links, Not now is non-blocking; re-checks status on resume and flips to a success state. Skipped entirely when already unrestricted or already handled.battery_prompt_handledflag persisted in the settings DataStore (kept out ofAppSettings— it's onboarding state, not a user preference).Testing
BatteryPromptDecisionTest(4) +OnboardingViewModelTest(6).BatteryOptimizationStepTest, Espresso-Intents): step offered → Not now → inbox (+ flag set); Take me there firesACTION_APPLICATION_DETAILS_SETTINGSfor this package; skip-when-handled. All 3 pass locally on API 37; the two touched existing E2E classes still pass.assembleDebug,testDebugUnitTest,compileDebugAndroidTestKotlin,lintDebug,detekt,ktlintCheck.Notes
isIgnoringBatteryOptimizationsis false for both), so the "not unrestricted" copy covers both — noted inline.espresso-intents(reusing the existing espresso version) to androidTest deps.Closes #49
🤖 Generated with Claude Code