Widget: Make the quick charge-mode buttons configurable - #77
Merged
Conversation
The persistent-policy buttons on the reconnect notification and the widget were a fixed pair. Both now resolve their policies through one pure unit, which keeps the invariant those buttons need: no rendered policy is ever outside the adapter's supported list. An unsupported target would persist a recovery goal and cancel a running session before ChargingRepository rejects the write, leaving the device converging on something it can never apply. A device with two or fewer policies has nothing to choose between, so a stored selection is ignored there rather than allowed to hide one of them. The selections themselves live in FullChargeStore: one list for the notification, one per-widget-id map for the widget, each a single JSON record. The notification toggle applies the 1-3 bounds and the membership rule inside one DataStore transaction, so two toggles racing from the same rendered picker state cannot write an empty or oversized selection.
…ction The gesture notification hardcoded two actions: the adapter's protective default and "Always 100%". It now renders one action per resolved policy, with a per-slot request code (extras don't factor into PendingIntent equality, so a shared code would let a later action overwrite an earlier one's target) and a label per policy, including the new pause-at-full one. setPersistentPolicy() gains the matching refusal: alongside the existing "nothing can write" guard it now refuses a target the current adapter does not support. A notification action outlives the render that produced it, so a button built for a previous adapter selection must not persist a recovery target that never converges.
Adds a "Notification buttons" section to Settings > Charging, built on a reusable QuickActionPolicyPicker: one row per charge mode the adapter supports, one to three of them selectable. The bounds are expressed by disabling rows (the last selected one cannot be unchecked, unchecked ones go inert at three), which is presentation only - the store's transaction remains the guard. The section appears only where there is something to pick (more than two policies) on a device that can use the gesture, and its rows follow the gesture's own enabled state, since that notification is where the buttons live. The dashboard state carries the available policies and the resolved selection, computed only once adapter selection has actually run: the capability defaults are permissive, so acting on them earlier would briefly offer a choice the resolved adapter forbids. SettingsSwitchItem takes a nullable subtitle, for rows whose title already says everything.
Each widget instance can now carry its own set of charge-mode buttons, picked in a new AppWidget configuration activity (reconfigurable, and optional where the platform supports that). The 1x100% button stays fixed and always last. The rendering branches on adapterResolved, never on how many policies are in the list: before selection has run the widget keeps today's two buttons, whose actions resolve their policy at tap time. ProtectAction and AlwaysFullAction are kept unchanged for exactly that path, and because stale launcher RemoteViews dispatch them by class name across app updates. The configured buttons carry their target as a stable id, since the composition that rendered them can be minutes old. The configuration activity is the one deliberate exception to the single-Activity rule: the AppWidget host launches a configuration by component name. It is exported, so it validates that the id belongs to Amply's own provider before reading or writing anything, refreshes the charging state itself (nothing else has during a launcher-initiated cold start), and offers a confirming exit in every state - on API 26-30 a cancelled configuration discards the widget being placed, so the upgrade link must never be the only way out. A failed save keeps the result cancelled; a failed render does not, since the next broadcast re-renders anyway. Deleting a widget drops its stored selection so a recycled id cannot inherit one.
A lab adapter resolves with an empty supported-policy list, so the resolved branch produced an empty action list and the widget rendered neither persistent-policy button - on every non-gated device. Those states now fall back to the legacy rendering, whose buttons resolve their policy at tap time and are guarded by the canApply pre-check in setPersistentOrOpen and the service's supported-policy refusal. Fixes review finding F4.
…result Back during a confirming action finished the activity with the initial RESULT_CANCELED while the save (and the best-effort widget render) was still running - on API 26-30 the host then discards the widget the user had just placed, even though its configuration was already stored. The ViewModel now exposes completionInFlight, set synchronously before either completion path launches its coroutine, which makes a second completion a no-op and is cleared only where complete() stays on the picker for a retry. The activity installs an inert BackHandler for that window and the screen renders every control disabled from the same flag, which replaces the Ready-only `saving` field. Fixes review finding F1.
The provider declared no android:minResize*, so the launcher used minHeight (110dp, the classic 2-cell height) as the resize floor and the widget could never shrink below two rows. Its content is only ~90dp tall and centered, so a 2-cell instance shows large empty bands. minResizeHeight=40dp / minResizeWidth=110dp set the floors explicitly; minHeight and the target cells are unchanged, so default placement stays 4x2. Below 80dp of height the widget now renders with reduced paddings (outer 12dp -> 6dp, button row top 10dp -> 4dp) so the buttons are not pushed out of a 1-cell instance. The status line stays in compact mode: dropping information reads worse than slight clipping on an unusually tight grid.
…tons # Conflicts: # app/src/main/java/eu/darken/amply/fullcharge/core/ChargeSessionService.kt # app/src/main/res/values/strings.xml
The charge-conditions merge widened the smart cast on the resolved adapter default, so the assertion is now dead weight and warns at compile time.
d4rken
added a commit
that referenced
this pull request
Aug 17, 2026
Main landed the observed-enforcement gate (#79) and the quick-action buttons (#77) while this branch was waiting on CI. Two textual conflicts, both the same shape and both in test setup where main added ChargingRepository's evidenceStore/buildIdentity arguments on the exact line this branch added BatteryReader's unitCalibration argument. Both sides are wanted, so both were kept. A third break was semantic rather than textual, so git merged it cleanly and the compiler caught it: ChargingRepositoryRestoreGateTest arrived with main as a new file and constructs BatteryReader without the calibration argument this branch added. Same fix as the other five call sites. Verified after resolution: 1345 tests across both flavors. Checked specifically that main's adapter-gating rework did not invalidate this branch's UnmappedDeviceSettingsIntentTest, which asserts a device matches no adapter at all; LineageOS now matches on the provider rather than a codename allowlist, but a bare Robolectric device still selects nothing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Technical Context
supportedPolicies;setPersistentPolicyadditionally refuses out-of-support targets before persisting, because a stale button (built before an app/ROM update changed the adapter) would otherwise strand a recovery target the repository rejects on every convergence attempt.configuration_optionalonly exists on API 31+, so on 26–30 every state — locked, loading, error, unsupported — offers an explicit "Add widget" exit returningRESULT_OK(a CANCELED exit discards the widget there). The activity is exported by necessity and validates the passed id belongs to Amply's provider before touching state; back is held while a save/update completion is still owed its result.onDelete); the notification selection is a separate global value; both toggle paths are single DataStore transactions so racing taps can't write out-of-bounds selections.ProtectAction/AlwaysFullActioncallbacks, which stale launcher views may still dispatch by class name) whenever no adapter is resolved or the device offers fewer than two policies.