BatteryOptimizationScreen (the onboarding "Get mail the instant it arrives" opt-in) previously guided the user with text only before deep-linking to system settings. Per #150 there is no OEM-universal intent that lands exactly on the Battery sub-screen, so a short in-app visual of the general "tap Battery -> choose Unrestricted" path adds value regardless of how precisely the deep link lands.
This adds a lightweight, dependency-free looping illustration on that screen, above the (retained) guidance text and before the "Take me there" / "Not now" actions -- so the user sees it while still in LibreMail.
Implementation
New BatteryGuideAnimation.kt -- a stylized Compose illustration (a faux settings card with a "Battery" row and an "Unrestricted" option). A highlight/selection moves from the Battery row to the Unrestricted option while a "tap here" dot pulses, driven by rememberInfiniteTransition.
Approach / no new dependency (issue asked for a deliberate choice): built from Compose primitives + core Material icons only -- no Lottie, no AnimatedVectorDrawable, nothing added to libs.versions.toml. It is deliberately generic (not a recording of any one OEM's real UI, which varies per #150).
Reduced motion:rememberReducedMotion() reads Settings.Global.ANIMATOR_DURATION_SCALE; when animations are off the same card renders at rest (no infinite transition).
TalkBack: the whole illustration exposes a single contentDescription mirroring the retained onboarding_battery_guidance text -- additive, not a replacement for the accessible path.
Deep link unchanged: still batterySettingsIntent() (ACTION_APPLICATION_DETAILS_SETTINGS app-details, per #150 / BatteryOptimizationManager); the ON_RESUME re-check and Take-me-there / Not-now flow are untouched. The column is now scrollable so the extra content never pushes the actions off-screen.
Logging: PII-free AppLog breadcrumbs at shown (with unrestricted / reducedMotion booleans), opening settings, skipped, plus a warn on intent-launch failure.
Placement
On the existing BatteryOptimizationScreen step (where the issue asked), only in the "offered" (not-yet-unrestricted) state, between the body text and the guidance/buttons.
Tests
Robolectric JVM (BatteryGuideAnimationJvmTest, +extended BatteryOptimizationScreenJvmTest): animated variant across both steps (hand-driven clock so the infinite transition can't hang waitForIdle), static reduced-motion variant, the default-arg path, and the isReducedMotion decision; the screen shows the guide in the offered state and not in the "done" state, and the enable button still fires the settings intent.
Instrumented (BatteryOptimizationStepTest): asserts the guide is shown in the onboarding flow and scrolls to the actions; disables device animations (as CI's emulator-runner does) so the static path renders on-device.
Gate
Local fast gate green: assembleDebug, testDebugUnitTest, jacocoTestCoverageVerification (0.84 floor), compileDebugAndroidTestKotlin, lintDebug, ktlintCheck, detekt. Full multi-API E2E matrix left to CI.
## What & why
Closes #174.
`BatteryOptimizationScreen` (the onboarding "Get mail the instant it arrives" opt-in) previously guided the user with **text only** before deep-linking to system settings. Per #150 there is no OEM-universal intent that lands exactly on the Battery sub-screen, so a short in-app visual of the general "tap Battery -> choose Unrestricted" path adds value regardless of how precisely the deep link lands.
This adds a lightweight, **dependency-free** looping illustration on that screen, above the (retained) guidance text and before the "Take me there" / "Not now" actions -- so the user sees it while still in LibreMail.
## Implementation
- **New `BatteryGuideAnimation.kt`** -- a stylized Compose illustration (a faux settings card with a "Battery" row and an "Unrestricted" option). A highlight/selection moves from the Battery row to the Unrestricted option while a "tap here" dot pulses, driven by `rememberInfiniteTransition`.
- **Approach / no new dependency** (issue asked for a deliberate choice): built from Compose primitives + core Material icons only -- **no Lottie, no `AnimatedVectorDrawable`**, nothing added to `libs.versions.toml`. It is deliberately **generic** (not a recording of any one OEM's real UI, which varies per #150).
- **Reduced motion:** `rememberReducedMotion()` reads `Settings.Global.ANIMATOR_DURATION_SCALE`; when animations are off the same card renders **at rest** (no infinite transition).
- **TalkBack:** the whole illustration exposes a single `contentDescription` mirroring the retained `onboarding_battery_guidance` text -- additive, not a replacement for the accessible path.
- **Deep link unchanged:** still `batterySettingsIntent()` (`ACTION_APPLICATION_DETAILS_SETTINGS` app-details, per #150 / `BatteryOptimizationManager`); the `ON_RESUME` re-check and Take-me-there / Not-now flow are untouched. The column is now scrollable so the extra content never pushes the actions off-screen.
- **Logging:** PII-free `AppLog` breadcrumbs at *shown* (with `unrestricted` / `reducedMotion` booleans), *opening settings*, *skipped*, plus a warn on intent-launch failure.
## Placement
On the existing `BatteryOptimizationScreen` step (where the issue asked), only in the "offered" (not-yet-unrestricted) state, between the body text and the guidance/buttons.
## Tests
- **Robolectric JVM** (`BatteryGuideAnimationJvmTest`, +extended `BatteryOptimizationScreenJvmTest`): animated variant across both steps (hand-driven clock so the infinite transition can't hang `waitForIdle`), static reduced-motion variant, the default-arg path, and the `isReducedMotion` decision; the screen shows the guide in the offered state and not in the "done" state, and the enable button still fires the settings intent.
- **Instrumented** (`BatteryOptimizationStepTest`): asserts the guide is shown in the onboarding flow and scrolls to the actions; disables device animations (as CI's emulator-runner does) so the static path renders on-device.
## Gate
Local fast gate green: `assembleDebug`, `testDebugUnitTest`, `jacocoTestCoverageVerification` (0.84 floor), `compileDebugAndroidTestKotlin`, `lintDebug`, `ktlintCheck`, `detekt`. Full multi-API E2E matrix left to CI.
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.
What & why
Closes #174.
BatteryOptimizationScreen(the onboarding "Get mail the instant it arrives" opt-in) previously guided the user with text only before deep-linking to system settings. Per #150 there is no OEM-universal intent that lands exactly on the Battery sub-screen, so a short in-app visual of the general "tap Battery -> choose Unrestricted" path adds value regardless of how precisely the deep link lands.This adds a lightweight, dependency-free looping illustration on that screen, above the (retained) guidance text and before the "Take me there" / "Not now" actions -- so the user sees it while still in LibreMail.
Implementation
BatteryGuideAnimation.kt-- a stylized Compose illustration (a faux settings card with a "Battery" row and an "Unrestricted" option). A highlight/selection moves from the Battery row to the Unrestricted option while a "tap here" dot pulses, driven byrememberInfiniteTransition.AnimatedVectorDrawable, nothing added tolibs.versions.toml. It is deliberately generic (not a recording of any one OEM's real UI, which varies per #150).rememberReducedMotion()readsSettings.Global.ANIMATOR_DURATION_SCALE; when animations are off the same card renders at rest (no infinite transition).contentDescriptionmirroring the retainedonboarding_battery_guidancetext -- additive, not a replacement for the accessible path.batterySettingsIntent()(ACTION_APPLICATION_DETAILS_SETTINGSapp-details, per #150 /BatteryOptimizationManager); theON_RESUMEre-check and Take-me-there / Not-now flow are untouched. The column is now scrollable so the extra content never pushes the actions off-screen.AppLogbreadcrumbs at shown (withunrestricted/reducedMotionbooleans), opening settings, skipped, plus a warn on intent-launch failure.Placement
On the existing
BatteryOptimizationScreenstep (where the issue asked), only in the "offered" (not-yet-unrestricted) state, between the body text and the guidance/buttons.Tests
BatteryGuideAnimationJvmTest, +extendedBatteryOptimizationScreenJvmTest): animated variant across both steps (hand-driven clock so the infinite transition can't hangwaitForIdle), static reduced-motion variant, the default-arg path, and theisReducedMotiondecision; the screen shows the guide in the offered state and not in the "done" state, and the enable button still fires the settings intent.BatteryOptimizationStepTest): asserts the guide is shown in the onboarding flow and scrolls to the actions; disables device animations (as CI's emulator-runner does) so the static path renders on-device.Gate
Local fast gate green:
assembleDebug,testDebugUnitTest,jacocoTestCoverageVerification(0.84 floor),compileDebugAndroidTestKotlin,lintDebug,ktlintCheck,detekt. Full multi-API E2E matrix left to CI.Merge Queue Status
2026-07-08 13:58 UTC· Rule:default· triggered by merge protections2026-07-08 13:58 UTC· on draft WIP: merge queue: checking main (6802b60) and [#439 + #435 + #438] together (#441)2026-07-08 15:19 UTC· atb038c3bdae5e557c1044cd1ad5d52e534f3ecc2d· mergeThis pull request spent 1 hour 20 minutes 48 seconds in the queue, including 1 hour 19 minutes 41 seconds running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed