FullCharge: Arm the reconnect gesture at the limit without waiting for the battery status - #46
Merged
Merged
Conversation
…r the battery status The gesture armed only when the charging-policy hardware state, a non-CHARGING battery status and an in-band percent held at once. After any write of the limit the phone is physically topping back up, so the battery status stays CHARGING for the ~10-12s Pixel HAL transition and nothing armed. An unplug in that window opened no reconnect window and the replug had nothing to consume, so a gesture right after "Restore now" silently did nothing. Every path that writes the limit while already in the band was affected - session restore, boot recovery, the widget's persistent-policy buttons - not just restore. Arming now also accepts a second path: the policy state is reported and the battery has already reached a *verified* limit percent. Reaching the limit replaces the dropped battery status as the signal that separates sitting at the limit from climbing through the band, so a replug at 76% under an 80% limit still arms nothing. The percent is sourced from GestureBasis.limitPercent(), which answers only from a verified readback; GestureBasis.evidence() is deliberately not used, as its journal fallback would arm the default basis off a limit Amply merely remembers writing. Accepting a CHARGING reading needs a counterweight, because the hardware state lags a policy change in both directions: leaving an 80% limit at 80% briefly still reports the old policy state, which would latch a hold that no longer exists and survive - the steady-plugged branch has never dropped a latch - until the battery left the band. That branch now retires a limit-hold basis on positive proof the limit is not holding: current flowing while no policy state is reported, from a readable broadcast. It is confined to the steady-plugged branch because a replug legitimately reads CHARGING before the policy state is re-reported, and dropping there would destroy the carried basis the reconnect window exists to preserve. Verified on a Pixel 9 Pro: re-arming after a restore went from ~10.5s to 108ms, and the full replay (gesture, Restore now, gesture again within the window) starts a second session where it previously did nothing. The climbing case (76% under an 80% limit) stays idle.
d4rken
marked this pull request as ready for review
August 3, 2026 16:28
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
The reconnect gesture no longer goes blind for ~10 seconds after your charge limit is written.
Previously, tapping Restore now and then immediately performing the gesture again did nothing — the phone is physically topping back up right after the limit is re-applied, and the gesture waited for charging to visibly stop before it would arm. On a Pixel 9 Pro that wait was ~10.5 seconds, which reads as the gesture simply being broken.
The gesture now arms as soon as the hardware confirms the limit is active and the battery has reached it. Re-arming after a restore is effectively immediate.
This affected every path that writes the limit while the battery is already near it — restoring a session, boot recovery, and the widget's persistent-policy buttons — not just "Restore now".
Unchanged: an accidental unplug/replug while the battery is still climbing below your limit still does nothing.
Technical Context
Root cause.
QuickFullChargeGesturearmed only whenplugged,chargingStatus == 4,batteryStatus != CHARGINGandpercent in 75..90held simultaneously. After a limit write the battery status staysCHARGINGfor the ~10–12 s Pixel HAL transition, so nothing armed; the unplug edge then went toIdleinstead ofAwaitingReconnectand the replug had no window to consume.From the reported logcat:
Fix. A second arming path for the same
LIMIT_HOLDbasis: policy state reported, percent in band, andpercent >= verifiedLimitPercent. Reaching the verified limit is what replaces the dropped battery-status check — it is what distinguishes sitting at the limit from climbing through the band. Both paths are kept; neither subsumes the other (drift to 79 % under an 80 % limit is held but not settled; the window after a write is settled but not held).Why not
GestureBasis.evidence(). It falls back tolastPersistentPolicywhen the hardware reading is inconclusive, which would arm the default basis off a limit Amply merely remembers writing — the class of bug #28 fixed for the opt-in any-level basis.GestureBasis.limitPercent()has no journal fallback by construction; its KDoc now records that it is an arming input, not just notification copy.The counterweight (review focus). Accepting a
CHARGINGreading is not free: the hardware state lags a policy change in both directions. Switching away from an 80 % limit at 80 % briefly still reports the old policy state, which would latch a hold that no longer exists — and the steady-plugged branch has never dropped a latch, so it survived until the battery left the 75–90 % band.setPersistentPolicy()resets the gesture and immediately re-evaluates against exactly that stale reading. The steady-plugged branch now retires a limit-hold basis on positive proof: current flowing while no policy state is reported, from a readable broadcast.Two guards on that rule, both load-bearing:
CHARGINGbefore the policy state is re-reported; dropping there would destroy the carried basis the reconnect window exists to preserve.percent >= 0. An unreadable percent marks the whole broadcast read as failed, so its charging status proves nothing either. Caught by the pre-existingan unreadable percent does not retire a limit-hold basistest.Verification. 795 unit tests pass on both flavors; the gesture suite went 43 → 45 cases, covering the logged regression, the climbing case, out-of-band, unverified limit, adaptive, drift, the stale-policy-state transition, and the replug carry-over.
Device-verified on two devices.
Pixel 9 Pro (caiman, Android 16) — the reported scenario end to end:
The negative case was checked with a simulated level: from a cleared latch, 76 % still charging under an 80 % limit stays
IDLE.Pixel 8 (shiba, Android 17 / API 37) — covers the drop rule, which the 9 Pro run predates. Switching the persistent policy to Adaptive reproduced the whole mechanism on hardware:
Without the rule that latch would have survived until the battery left the 75–90 % band. The basic gesture still triggers normally there (unplug → replug at Δ3.58 s →
TRIGGER), and after a restore the gesture stayed armed for over a minute whilebatteryStatuswas stillCHARGING— armed solely via the new path.Which of the two signals lags varies per transition rather than per device: when the battery status settles first, the settled path gives no advantage, because the verified limit percent is itself derived from the policy state. The fix removes the wait whenever the policy state arrives first, and never arms on anything weaker.