Skip to content

Commit 190c306

Browse files
authored
Merge pull request #95 from d4rken-org/worktree-graphene-latch-claim-scope
Charging: Scope the GrapheneOS replug-latch claim to its qualifying device
2 parents db572ce + 195f2a0 commit 190c306

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

.claude/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"debugbadger@claude-code-cafe": true,
44
"jvm-tools@claude-code-cafe": true,
55
"devtools@claude-code-cafe": true,
6-
"support-investigator@claude-code-cafe": true
6+
"support@claude-code-cafe": true,
7+
"frontend-design@claude-plugins-official": true
78
}
89
}

.claude/skills/device-qualification/SKILL.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
6767
| Samsung | Galaxy Tab A9+ SM-X210 One UI 8.0; Galaxy S20 FE SM-G781B One UI 4.1 | Full — sync readback + HAL enforcement | Modern multi-mode + legacy toggle, session E2E, native-change cancel, reboot recovery, R8 beta | 2026-07-21 |
6868
| Xiaomi | Xiaomi 13T `2306EPN60G` (`aristotle`) HyperOS 2.0 (`ro.mi.os.version.code=2`); 2026-08-16 re-run on `OS2.0.216.0.VMFEUXM` / Android 15 | **Partial, now characterized** — external shell-UID writes provably drive the daemon identically to native UI taps (`getProtectMode` → `checkUiModeProtect` → `setEnable`, ~80 ms, both directions, no hidden UI-only flag). The adaptive 80% hold is still **unobserved**: charged 59→100% with no plateau, `getNightChargingState` = 0 on all 140 evaluations. Cause identified — the gate is a *learned* charging-routine model (`key_ave_night_charge_start_minutes`, `…_sd`, `key_enter_night_charge_times`), not a clock window (forced clock to 02:30 did not open it). **BLOCKED on an unused test device, not FAILED.** No hardware hold signal (`Charging state`/`policy` = 0/0) | Read matrix, both-direction writes, session at 100%, unknown-value refusal, R8 beta (2026-07-21). Added 2026-08-16: external-vs-UI write-path equivalence, native UI reflects external key, learned-schedule root cause, clock-forcing negative | 2026-07-21 / 2026-08-16 |
6969
| OnePlus (Oplus) | OnePlus Nord CE4 Lite `CPH2621` ColorOS 15 (`ro.build.version.oplusrom=V15.0.0`) | Full — enforcement directly observable (device holds at 80%); external writes stick | Two mutually-exclusive `system` keys (Charging limit / Smart charging), WSS-only write rejected + Shizuku write succeeds for all three policies, WSS-only UX (controls disabled + Shizuku-required banner) | 2026-07-21 |
70-
| GrapheneOS | Pixel 9 Pro XL `komodo`, GrapheneOS 2026080501 / Android 17 — **REMOTE qualification via issue #49** (tester-run protocol, not maintainer hardware) | **Enforcement observed**: held at 80% with shield, `dumpsys battery` status=4/Charging state=4/policy=2 (limit on) vs 2/1/1 (off); shell-UID writes move the Settings UI live, **latch at plug-session start** — mid-session writes have no hardware effect until unplug→replug, replug reliably applies the current value | Key isolation (`settings list` diff → single `global battery_charge_limit` 0/1), write→UI both directions, mid-session no-op both directions, replug latch both directions, hardware signal both states. **NOT run**: app-context access tiers (WSS write from Amply, `app.grapheneos.*` package visibility), sessions/boot recovery, wireless, factory-absent key state, secondary user | 2026-08-12 |
70+
| GrapheneOS | Pixel 9 Pro XL `komodo`, GrapheneOS 2026080501 / Android 17 — **REMOTE qualification via issue #49** (tester-run protocol, not maintainer hardware) | **Enforcement observed**: held at 80% with shield, `dumpsys battery` status=4/Charging state=4/policy=2 (limit on) vs 2/1/1 (off); shell-UID writes move the Settings UI live, **latch at plug-session start** — mid-session writes have no hardware effect until unplug→replug, and on this device/build the replug applied the written value in both directions. **The replug half does not generalize**: refuted on `frankel` (Pixel 10, 2026081301) — see the known-gaps entry | Key isolation (`settings list` diff → single `global battery_charge_limit` 0/1), write→UI both directions, mid-session no-op both directions, replug latch both directions, hardware signal both states. **NOT run**: app-context access tiers (WSS write from Amply, `app.grapheneos.*` package visibility), sessions/boot recovery, wireless, factory-absent key state, secondary user | 2026-08-12 |
7171
| Xiaomi (HyperOS 3) | Redmi Note 14 `24117RN76G` (`tanzanite`), HyperOS 3.0.302 / Android 16 (`ro.mi.os.version.code=3`) — **REMOTE qualification via issue #48** (contributor-run protocol, not maintainer hardware) | **Both-direction enforcement of EXTERNAL shell-UID writes observed** (the same write path as Amply's Shizuku service): `settings put … 2` below the cap → Battery protection active, Settings UI follows immediately, held at 80% for ~20 min under active use (voltage 4228 mV holding vs 4391 mV charging, charge counter 4283 vs 4341 corroborate; sysfs `current_now` permission-denied, so no current reading); `settings put … 0` mid-hold → charging resumes past 80 immediately. **No hardware hold signal**: `dumpsys battery` reports `status: 2` / `Charging state: 0` / `Charging policy: 0` in both states → read-back-only verification | Key mapping (three modes incl. `2` = Battery protection @80, cap fixed — no percent picker), external write → UI both directions, sustained hold, mid-hold release. Beta run 2026-08-14 added: app-context three-mode control (direct WSS), factory-absent key = Intelligent (confirmed). Session restore FAILED in that run (observer noise cancel — app bug, fixed in #65). Re-verified 2026-08-16 on v0.3.4-beta0: **session restore PASSES** — full-charge session ran to 100% and Battery protection was re-written automatically while still plugged. **NOT run**: Shizuku tier, boot recovery, wireless, R8, unplug-early restore | 2026-08-16 |
7272
| GrapheneOS (follow-up) | Same device, **0.3.2-beta0 on-device report via issue #49** | **Package detection VERIFIED from app context** (`is_grapheneos=true` with the FLAG_SYSTEM check); **unprivileged key read DENIED**`has_battery_charge_limit=false` while the very same report showed `battery_charging_status=4` (limit enforcing). Root cause in GrapheneOS source: the key is `@Protected(read = SYSTEM_UI, readWrite = SETTINGS)` (frameworks_base `c30c6393`); SettingsProvider throws SecurityException for all other packages **including WSS holders**, with the shell UID explicitly exempt ("ADB is used for testing", `e87c93a2`) — so the tester's earlier adb runs ARE the Shizuku-path evidence. Factory-absent semantics resolved from source: `BoolSetting(..., default false)` → absent = off | Detection + fail-closed probe verified live; adapter re-gated to Shizuku-only in response | 2026-08-13 |
7373
| GrapheneOS (2nd device) | Pixel 10 `frankel`, GrapheneOS 2026081301 / Android 17, **Amply 0.5.0-beta0 (foss/beta, R8-minified) on-device report via issue #49** | **No new hardware observation** — the reporter states the app works with Shizuku granted, but reported neither an observed 80% hold nor a session run, so this row adds no enforcement evidence. The `komodo` enforcement observation above remains the only one | **Amply's own Shizuku path verified on hardware for the first time**: with Shizuku set up the app reads the key and offers the control, where the 0.3.2-beta0 run without Shizuku showed "setting not present". Second device, and the first on a Pixel generation other than the 9 Pro XL all prior work used. R8 smoke clean (the `beta` build type minifies and shrinks resources), and `ReconnectSupport.NONE` renders correctly as an unavailable gesture rather than a dead control. **NOT run**: observed 80% hold through the app, full-charge session + restore, boot recovery, wireless, secondary user | 2026-08-23 |
@@ -180,7 +180,31 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
180180
2nd-device ledger row) shows Amply's own Shizuku service reading the key and offering the control on real
181181
hardware, under R8, so those two are closed. What that report does *not* cover, because the reporter did not
182182
run them: an 80% hold observed through the app rather than over adb, a full-charge session and its restore,
183-
and boot recovery. Every enforcement claim on this ROM still rests on the `komodo` adb runs.
183+
and boot recovery. Every enforcement claim on this ROM still rests on the `komodo` adb runs, and one of them
184+
(the replug latch) is refuted on `frankel` — see the next entry.
185+
- **External writes are not honoured on every build — REFUTED on `frankel`** (Pixel 10, GrapheneOS 2026081301 /
186+
Android 17; issue #49, 2026-08-25). With Amply out of the loop entirely, a bare
187+
`settings put global battery_charge_limit 0` made while unplugged, and confirmed off in the ROM's own Settings
188+
UI, was still ignored at the next plug-in: the device held at exactly 80% (level climbed 74→80, then
189+
NOT_CHARGING at 80 with power connected, no level above 80 all day, thermals ~33 °C so not a throttle; the
190+
app's package appears nowhere in the 52k-line capture). The same device's **native Settings toggle works
191+
normally**, so the divergence is the external-write channel, not the cap. Cause **undetermined**: `komodo` was
192+
qualified on 2026080501 and `frankel` observed on 2026081301, neither device tested on the other's build, so a
193+
ROM release change and a Pixel 10 generation difference fit the evidence equally. No public upstream report
194+
matches this direction (the known ones are the inverse — charging past the cap while the limit is on). One
195+
caveat on the evidence: the capture carries no settings-provider logging, so "the key read 0 at plug time"
196+
rests on the reporter's statement plus the ROM's UI, not on instrumentation.
197+
- **Blast radius**: every write on this adapter shares one path, so on a device behaving like this the
198+
dashboard/widget/tile persistent writes, session restore, and boot recovery are all hardware-level no-ops
199+
that read back as verified. Boot recovery's convergence loop returns DONE on the first tick for the same
200+
reason (a matching settings readback is treated as convergence). Nothing in the app can notice: this adapter
201+
does not set `enforcementEvidenceRequired`, and the passive verdict engine only detects a climb *past* a
202+
cap, which is the opposite direction from a ROM enforcing a cap that is configured off.
203+
- **The protective direction is UNTESTED, and it is the open safety question.** Both observed failures wrote
204+
the limit *off* while the ROM kept enforcing, which leaves the battery protected and merely makes the UI
205+
dishonest. Whether a write turning the limit *on* is equally ignored — which would mean Amply claims
206+
protection that is not enforced — has never been run by anyone. Asked of the reporter 2026-08-25. Do not
207+
widen, re-qualify, or relax anything on this ROM until it is answered.
184208
- **State 4 below the limit unverified** — evidence was sampled at the 80% hold; if the ROM reports 4 only while
185209
holding, a FixedLimit pending clears late (at the hold) instead of instantly. Cosmetic.
186210
- **A plugged restore configures but cannot enforce** — restore-at-100%, the 24h safety timeout,

0 commit comments

Comments
 (0)