You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Charging: Add GrapheneOS charge-limit support (#56)
GrapheneOS backs "Limit to 80%" with the world-readable key `global
battery_charge_limit` (0/1, WSS-writable, no Shizuku) but samples it
only at plug-session start: external writes apply at the next
unplug/replug. A new adapter capability (policyLatchesAtPlug) carries
everything: a condition-based pending-until-replug verification state
(no settling clock, resolves only on evidence), a persisted 30s
disconnect grace window in the session engine so the user's deliberate
replug latches the override instead of triggering restore-on-disconnect,
and "unplug and replug" cues across dashboard/widget/tile/notifications.
Identity resolves core app.grapheneos.* system packages (FLAG_SYSTEM
required; no property/feature/fingerprint marker exists); the adapter
precedes the Pixel adapter, whose Google+Pixel probe would otherwise
swallow the device. Remote qualification via issue #49 (Pixel 9 Pro XL
komodo, GrapheneOS 2026080501/A17): enforcement at 80% with hardware
state 4 observed by the reporter; open items are recorded in the
qualification ledger and all fail closed. Existing adapters are
bit-for-bit unchanged. Includes a cross-model review hardening pass.
Closes#49.
Copy file name to clipboardExpand all lines: .claude/CLAUDE.md
+10-5Lines changed: 10 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ Amply is an **experimental Android controller for OEM battery charge-protection
8
8
temporarily allows a full charge, then restores the user's protective policy at 100%, on unplug, or at a safety
9
9
timeout.
10
10
11
-
Several control adapters exist — four OEM adapters plus a custom-ROM (LineageOS) adapter. **Pixel charging optimization** is capability-gated to Pixel 6a and newer phones on
11
+
Several control adapters exist — four OEM adapters plus two custom-ROM adapters (LineageOS, GrapheneOS). **Pixel charging optimization** is capability-gated to Pixel 6a and newer phones on
12
12
Android 15+ when Google's charging-optimization controller is present. **Samsung battery protection** (global
13
13
`protect_battery` keys) is gated to verified One UI generations — One UI 8 multi-mode, and the legacy One UI 4/5
14
14
toggle — on the system user. **Xiaomi charging protection** (secure `security_pc_secure_protect_mode_key`,
@@ -19,10 +19,15 @@ binary Adaptive/Unrestricted) is gated to the HyperOS 2.x ROM (`ro.mi.os.version
19
19
`lineagesettings` provider, keys `charging_control_enabled`/`_mode`/`_charging_limit`) is manufacturer-agnostic —
20
20
gated to a **physically-qualified device-codename allowlist** (HAL enforcement is per-device) plus the provider and
21
21
system user; **reads are unprivileged (ContentResolver), writes require Shizuku** (the shell UID holds
22
-
`lineageos.permission.WRITE_SETTINGS`, which `WRITE_SECURE_SETTINGS` does not cover). Other Pixels, Samsung on
23
-
unverified One UI versions (6/7, 9+), non-HyperOS-2 Xiaomi devices, non-ColorOS-15 Oplus devices, and unqualified
24
-
LineageOS builds remain diagnostics-only. See the qualification ledger (`.claude/skills/device-qualification/`) for
25
-
the verified devices and mappings.
22
+
`lineageos.permission.WRITE_SETTINGS`, which `WRITE_SECURE_SETTINGS` does not cover). **GrapheneOS charge limit**
23
+
(world-readable `global battery_charge_limit`, binary FixedLimit(80)/Unrestricted, WSS-writable — no Shizuku) is
24
+
gated to GrapheneOS identity (its `app.grapheneos.*` core packages; no property/feature/fingerprint marker exists)
25
+
plus key presence and the system user; the ROM **latches the key at plug-session start** (`policyLatchesAtPlug`),
26
+
so external writes take effect at the next unplug→replug — handled by a pending-until-replug verification state
27
+
and a 30s session grace window; the reconnect gesture is unsupported there. Other Pixels, Samsung on
Copy file name to clipboardExpand all lines: .claude/skills/device-qualification/SKILL.md
+24Lines changed: 24 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,6 +50,7 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
50
50
| 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 |
51
51
| Xiaomi | Xiaomi 13T `2306EPN60G` HyperOS 2.0 (`ro.mi.os.version.code=2`) |**Partial** — mapping/readback/session verified; the adaptive 80% hold could not be triggered, so daemon-level hardware enforcement is **not yet demonstrated**| Read matrix, both-direction writes, session at 100%, unknown-value refusal, R8 beta | 2026-07-21 |
52
52
| 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 |
53
+
| 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 |
53
54
54
55
## Known gaps
55
56
@@ -114,6 +115,29 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
114
115
(`google/oriole/oriole:16/…/release-keys`), so fingerprint sniffing is not a fallback. Otherwise clean on this
115
116
ROM: install/launch/onboarding/dashboard/settings with no crashes, honest "Unsupported device" reporting, live
116
117
battery monitoring across simulated plug/level transitions, and the charge alarm firing at threshold.
118
+
-**GrapheneOS** — landed **live** on remote qualification (issue #49; the only OEM row not tested on maintainer
119
+
hardware). Open items, all failing closed:
120
+
-**`app.grapheneos.*` package visibility from app context is unverified** — `<queries>` package entries are
121
+
specified platform behavior (not SELinux-fragile like `ro.lineage.*`), but GrapheneOS hardens aggressively. If
122
+
the packages are hidden, `isGrapheneOs` is false and the device falls to the Pixel adapter as
123
+
matched/diagnostics-only — no unsafe write path, but support silently vanishes; the first tester report of
124
+
"still unsupported" on the test build should check `is_grapheneos=` in the device report.
125
+
-**App-context WSS write unverified** — the tester's writes ran as shell UID; an Amply-originated
126
+
`Settings.Global.putString` under granted WSS is expected to behave identically (same namespace rules) but has
127
+
not been observed. The read-back-equality check catches a silently-failing write.
128
+
-**Factory-absent key state unknown** — the tester's device had the key present while off; whether a
129
+
never-toggled install exposes it is unverified. Absent → gate fails closed → diagnostics + contribution wizard
130
+
(`adapter_detail_grapheneos_no_key`), and `read()` refuses (`unrecognizedValue`) so a session never clobbers it.
131
+
-**State 4 below the limit unverified** — evidence was sampled at the 80% hold; if the ROM reports 4 only while
132
+
holding, a FixedLimit pending clears late (at the hold) instead of instantly. Cosmetic.
133
+
-**A plugged restore configures but cannot enforce** — restore-at-100%, the 24h safety timeout,
134
+
manual restore, and a plugged boot recovery all write the protective value while a plug session
135
+
is running; the ROM won't enforce it until the next replug, and no code path can change that
136
+
(mid-session writes are ignored by design). Amply's state is correct — config protective,
137
+
session/recovery closed, pending-until-replug hint shown — and the exposure is one charge cycle,
138
+
bounded by the plug session the user is already in. Deliberately NOT treated as a defect.
139
+
-**Wireless charging and secondary users**: NOT RUN (gated to system user).
140
+
- Sessions/boot-recovery/R8 smoke on real GrapheneOS hardware: pending the test build posted to issue #49.
117
141
-**Xiaomi** — adaptive hardware enforcement of external writes unconfirmed; treat the adapter as provisional until
0 commit comments