Issue #150 asked for the onboarding "Take me there" battery step to land as close as
possible to the per-app Unrestricted / Optimized / Restricted screen instead of the
generic app-info page — without using the restricted REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
permission (Play rejection risk, see #17).
Spike findings
I checked the actual AOSP Settings source (frameworks/baseSettings.java), not just the
reference docs, for every public, non-@hideSettings.ACTION_* constant mentioning battery
or power. Conclusion: no public, non-hidden action opens the exact
Unrestricted/Optimized/Restricted screen for a specific package.
Settings.ACTION_VIEW_ADVANCED_POWER_USAGE_DETAILwould — it's package-scoped
(package: data URI) and looks like exactly the per-app battery-usage screen — but it's @hide in the platform source, i.e. not part of the public SDK. Using its literal action
string would mean depending on a private, unversioned implementation detail — the exact
fragility this class already avoids by not using the restricted permission. Ruled out.
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS is public and needs no permission,
but it is not package-scoped: it opens the system-wide "Battery Optimization" app list
(default filter hides already-optimized apps, so the user has to switch to "All apps" to
even find LibreMail), and tapping an app there shows a legacy two-state
Optimize/"Don't optimize" dialog that predates the "Restricted" option. That's a worse
landing than app-details for one specific, already-known app, so it's used only as a
fallback, not the primary target.
Settings.ACTION_APPLICATION_DETAILS_SETTINGS (today's intent), scoped to our package,
remains the closest safe, public, package-scoped option: on stock Android/Pixel/AOSP it
lands one tap ("Battery") away from the target screen.
What changed
BatteryOptimizationManager.settingsIntent() now tries a small, verified fallback chain
instead of unconditionally returning one intent:
Settings.ACTION_APPLICATION_DETAILS_SETTINGS scoped to our package (primary — unchanged
destination from before this change).
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS (fallback, only reached if #1
somehow doesn't resolve on a given device).
Each candidate is checked with Intent.resolveActivity(); the first that resolves wins, and
if somehow neither does, the last candidate is still returned so the caller never gets a
dead-end intent. On Pixel/AOSP and every real device I'm aware of, #1 always resolves, so the landing screen is unchanged from before — the honest outcome of this spike is that
today's app-details intent was already the closest safe option available; see the class KDoc
in BatteryOptimizationManager.kt for the full investigation notes and OEM caveats (OEM skins
that rename/relocate/drop the "Battery" entry from app-details have no public replacement
either, short of hardcoding fragile OEM component names, which this deliberately avoids).
The selection/fallback logic is extracted into a small Android-free helper
(firstResolvableOrElseLast) so it's directly unit-tested without touching real Android intent
resolution; a new instrumented test checks the real candidate intents/order against a real PackageManager.
No restricted permission or hidden/private API is used anywhere in this change.
Test plan
assembleDebug + testDebugUnitTest + lintDebug + ktlintCheck + detekt + compileDebugAndroidTestKotlin all green locally (JDK 21).
New JVM unit test (BatteryOptimizationManagerTest, 6 cases) covers the fallback-selection
logic: first-resolving-candidate wins, falls back to the last candidate rather than
dead-ending, and rejects an empty candidate list.
New instrumented test (BatteryOptimizationManagerIntentTest) checks the real candidate
intents/order/package-scoping against a real PackageManager.
Existing instrumented BatteryOptimizationStepTest (Espresso-Intents) needed no changes
and continues to assert the "Take me there" button launches ACTION_APPLICATION_DETAILS_SETTINGS
scoped to our package — still true, since that's the primary candidate and always resolves
on a real/emulated device.
## Summary
Issue #150 asked for the onboarding "Take me there" battery step to land as close as
possible to the per-app **Unrestricted / Optimized / Restricted** screen instead of the
generic app-info page — without using the restricted `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`
permission (Play rejection risk, see #17).
### Spike findings
I checked the actual AOSP `Settings` source (`frameworks/base` `Settings.java`), not just the
reference docs, for every public, non-`@hide` `Settings.ACTION_*` constant mentioning battery
or power. Conclusion: **no public, non-hidden action opens the exact
Unrestricted/Optimized/Restricted screen for a specific package.**
- `Settings.ACTION_VIEW_ADVANCED_POWER_USAGE_DETAIL` *would* — it's package-scoped
(`package:` data URI) and looks like exactly the per-app battery-usage screen — but it's
`@hide` in the platform source, i.e. not part of the public SDK. Using its literal action
string would mean depending on a private, unversioned implementation detail — the exact
fragility this class already avoids by not using the restricted permission. Ruled out.
- `Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS` is public and needs no permission,
but it is **not** package-scoped: it opens the system-wide "Battery Optimization" app list
(default filter hides already-optimized apps, so the user has to switch to "All apps" to
even find LibreMail), and tapping an app there shows a legacy two-state
Optimize/"Don't optimize" dialog that predates the "Restricted" option. That's a worse
landing than app-details for one specific, already-known app, so it's used only as a
fallback, not the primary target.
- `Settings.ACTION_APPLICATION_DETAILS_SETTINGS` (today's intent), scoped to our package,
remains the closest safe, public, package-scoped option: on stock Android/Pixel/AOSP it
lands **one tap ("Battery") away** from the target screen.
### What changed
`BatteryOptimizationManager.settingsIntent()` now tries a small, verified fallback chain
instead of unconditionally returning one intent:
1. **`Settings.ACTION_APPLICATION_DETAILS_SETTINGS`** scoped to our package (primary — unchanged
destination from before this change).
2. **`Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS`** (fallback, only reached if #1
somehow doesn't resolve on a given device).
Each candidate is checked with `Intent.resolveActivity()`; the first that resolves wins, and
if somehow neither does, the last candidate is still returned so the caller never gets a
dead-end intent. On Pixel/AOSP and every real device I'm aware of, #1 always resolves, so
**the landing screen is unchanged from before** — the honest outcome of this spike is that
today's app-details intent was already the closest safe option available; see the class KDoc
in `BatteryOptimizationManager.kt` for the full investigation notes and OEM caveats (OEM skins
that rename/relocate/drop the "Battery" entry from app-details have no public replacement
either, short of hardcoding fragile OEM component names, which this deliberately avoids).
The selection/fallback logic is extracted into a small Android-free helper
(`firstResolvableOrElseLast`) so it's directly unit-tested without touching real Android intent
resolution; a new instrumented test checks the real candidate intents/order against a real
`PackageManager`.
No restricted permission or hidden/private API is used anywhere in this change.
## Test plan
- [x] `assembleDebug` + `testDebugUnitTest` + `lintDebug` + `ktlintCheck` + `detekt` +
`compileDebugAndroidTestKotlin` all green locally (JDK 21).
- [x] New JVM unit test (`BatteryOptimizationManagerTest`, 6 cases) covers the fallback-selection
logic: first-resolving-candidate wins, falls back to the last candidate rather than
dead-ending, and rejects an empty candidate list.
- [x] New instrumented test (`BatteryOptimizationManagerIntentTest`) checks the real candidate
intents/order/package-scoping against a real `PackageManager`.
- [x] Existing instrumented `BatteryOptimizationStepTest` (Espresso-Intents) needed no changes
and continues to assert the "Take me there" button launches `ACTION_APPLICATION_DETAILS_SETTINGS`
scoped to our package — still true, since that's the primary candidate and always resolves
on a real/emulated device.
- [ ] Emulator E2E left to CI.
Closes #150
🤖 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.
Summary
Issue #150 asked for the onboarding "Take me there" battery step to land as close as
possible to the per-app Unrestricted / Optimized / Restricted screen instead of the
generic app-info page — without using the restricted
REQUEST_IGNORE_BATTERY_OPTIMIZATIONSpermission (Play rejection risk, see #17).
Spike findings
I checked the actual AOSP
Settingssource (frameworks/baseSettings.java), not just thereference docs, for every public, non-
@hideSettings.ACTION_*constant mentioning batteryor power. Conclusion: no public, non-hidden action opens the exact
Unrestricted/Optimized/Restricted screen for a specific package.
Settings.ACTION_VIEW_ADVANCED_POWER_USAGE_DETAILwould — it's package-scoped(
package:data URI) and looks like exactly the per-app battery-usage screen — but it's@hidein the platform source, i.e. not part of the public SDK. Using its literal actionstring would mean depending on a private, unversioned implementation detail — the exact
fragility this class already avoids by not using the restricted permission. Ruled out.
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGSis public and needs no permission,but it is not package-scoped: it opens the system-wide "Battery Optimization" app list
(default filter hides already-optimized apps, so the user has to switch to "All apps" to
even find LibreMail), and tapping an app there shows a legacy two-state
Optimize/"Don't optimize" dialog that predates the "Restricted" option. That's a worse
landing than app-details for one specific, already-known app, so it's used only as a
fallback, not the primary target.
Settings.ACTION_APPLICATION_DETAILS_SETTINGS(today's intent), scoped to our package,remains the closest safe, public, package-scoped option: on stock Android/Pixel/AOSP it
lands one tap ("Battery") away from the target screen.
What changed
BatteryOptimizationManager.settingsIntent()now tries a small, verified fallback chaininstead of unconditionally returning one intent:
Settings.ACTION_APPLICATION_DETAILS_SETTINGSscoped to our package (primary — unchangeddestination from before this change).
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS(fallback, only reached if #1somehow doesn't resolve on a given device).
Each candidate is checked with
Intent.resolveActivity(); the first that resolves wins, andif somehow neither does, the last candidate is still returned so the caller never gets a
dead-end intent. On Pixel/AOSP and every real device I'm aware of, #1 always resolves, so
the landing screen is unchanged from before — the honest outcome of this spike is that
today's app-details intent was already the closest safe option available; see the class KDoc
in
BatteryOptimizationManager.ktfor the full investigation notes and OEM caveats (OEM skinsthat rename/relocate/drop the "Battery" entry from app-details have no public replacement
either, short of hardcoding fragile OEM component names, which this deliberately avoids).
The selection/fallback logic is extracted into a small Android-free helper
(
firstResolvableOrElseLast) so it's directly unit-tested without touching real Android intentresolution; a new instrumented test checks the real candidate intents/order against a real
PackageManager.No restricted permission or hidden/private API is used anywhere in this change.
Test plan
assembleDebug+testDebugUnitTest+lintDebug+ktlintCheck+detekt+compileDebugAndroidTestKotlinall green locally (JDK 21).BatteryOptimizationManagerTest, 6 cases) covers the fallback-selectionlogic: first-resolving-candidate wins, falls back to the last candidate rather than
dead-ending, and rejects an empty candidate list.
BatteryOptimizationManagerIntentTest) checks the real candidateintents/order/package-scoping against a real
PackageManager.BatteryOptimizationStepTest(Espresso-Intents) needed no changesand continues to assert the "Take me there" button launches
ACTION_APPLICATION_DETAILS_SETTINGSscoped to our package — still true, since that's the primary candidate and always resolves
on a real/emulated device.
Closes #150
🤖 Generated with Claude Code