Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ timeout.
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
Android 15+ when Google's charging-optimization controller is present. **Samsung battery protection** (global
`protect_battery` keys) is gated to verified One UI generations — One UI 8 multi-mode, and the legacy One UI 4/5
toggle — on the system user. **Xiaomi charging protection** (secure `security_pc_secure_protect_mode_key`,
binary Adaptive/Unrestricted) is gated to the HyperOS 2.x ROM (`ro.mi.os.version.code == 2`) on Xiaomi devices.
toggle — on the system user. **Xiaomi charging protection** (secure `security_pc_secure_protect_mode_key`)
has two adapters: binary Adaptive/Unrestricted gated to the HyperOS 2.x ROM (`ro.mi.os.version.code == 2`), and a
HyperOS 3 three-mode variant adding a FixedLimit(80) hard cap — gated to HyperOS 3 **plus a qualified-codename
allowlist** (mode `2` is not HyperOS-3-wide and cannot be probed at runtime).
**OnePlus/ColorOS charging protection** (mutually-exclusive `system` keys `regular_/smart_charge_protection_switch_state`
= FixedLimit(80)/Adaptive) is gated to ColorOS 15 (`ro.build.version.oplusrom == 15`) across the Oplus family
(OnePlus/Oppo/Realme) — **writes require Shizuku** (system namespace). **LineageOS charging control** (the private
Expand All @@ -26,7 +28,7 @@ system user; **reads are unprivileged (ContentResolver), writes require Shizuku*
packages including WSS holders, with only the shell UID exempt. The ROM **latches the key at plug-session start**
(`policyLatchesAtPlug`), so external writes take effect at the next unplug→replug — handled by a
pending-until-replug verification state and a 30s session grace window; the reconnect gesture is unsupported
there. Other Pixels, Samsung on unverified One UI versions (6/7, 9+), non-HyperOS-2 Xiaomi devices,
there. Other Pixels, Samsung on unverified One UI versions (6/7, 9+), unqualified Xiaomi devices,
non-ColorOS-15 Oplus devices, and unqualified LineageOS builds remain diagnostics-only. See the qualification
ledger (`.claude/skills/device-qualification/`) for the verified devices and mappings.

Expand Down
27 changes: 20 additions & 7 deletions .claude/rules/privileged-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,26 @@ without a qualified device; record results in the qualification ledger (`device-

### Xiaomi

Xiaomi control requires **all** of: Xiaomi manufacturer (covers Redmi/POCO, which report Xiaomi as
manufacturer), `ro.mi.os.version.code == 2` (HyperOS 2.x — the ROM feature is version-scoped, not
model-scoped), and the system user. Use `ro.mi.os.version.code`, NOT the frozen legacy
`ro.miui.ui.version.code`. Do not widen to HyperOS 3+ without qualifying a device; record results in the qualification ledger (`device-qualification` skill). The single key is per-user `secure`, applied synchronously. Two deliberate
assumptions: the feature is treated as present on any HyperOS 2 device (a device lacking it reads the key
absent → a harmless false claim of control), and daemon-level enforcement of external writes is pending
long-term observation (see Known gaps below).
Two live adapters over the same per-user `secure` key (applied synchronously). Use `ro.mi.os.version.code`, NOT
the frozen legacy `ro.miui.ui.version.code`.

**HyperOS 2** (`xiaomi-hyperos2-v1`) requires **all** of: Xiaomi manufacturer (covers Redmi/POCO, which report
Xiaomi as manufacturer), `ro.mi.os.version.code == 2` (the two-mode feature is version-scoped, not model-scoped),
and the system user. Two deliberate assumptions: the feature is treated as present on any HyperOS 2 device (a
device lacking it reads the key absent → a harmless false claim of control), and daemon-level enforcement of
external writes is pending long-term observation (see Known gaps below).

**HyperOS 3** (`xiaomi-hyperos3-v1`, adds mode `2` = Battery protection, hard cap 80%) requires **all** of:
Xiaomi manufacturer, `ro.mi.os.version.code == 3`, a **physically-qualified device codename**
(`XiaomiHyperOs3ChargingAdapter.QUALIFIED_CODENAMES`, ships with `tanzanite`), and the system user. The gate
CANNOT be version-only: mode `2` is model-/build-dependent within HyperOS 3 (a Poco F5 `marblein` on HyperOS
3.0.2 carries only the two old modes), the version property exposes no minor version, and no runtime probe for
mode-2 presence exists — the key is absent in factory state and reading it returns only the current value. Both-
direction hardware enforcement of external shell-UID writes was demonstrated on `tanzanite` (issue #48); no
hardware hold signal exists in `dumpsys battery`, so verification is read-back only. The boundary write domain
for the key is `{0,1,2}` **globally** — accepted on HyperOS 2 because no Amply code path emits `2` there and the
HyperOS 2 decode refuses it. Widen the codename allowlist only with a qualified device plus a ledger row
(`device-qualification` skill); unqualified HyperOS 3 devices fall to the lab adapter.

### GrapheneOS

Expand Down
36 changes: 34 additions & 2 deletions .claude/skills/device-qualification/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
| 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 |
| 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 |
| 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 |
| 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), external write → UI both directions, sustained hold, mid-hold release. **NOT run** (GrapheneOS-precedent landing; verify on the next beta via issue #48): app-context access tiers, sessions/boot recovery, factory-absent key state, wireless, R8 | 2026-08-14 |
| 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 |

## Known gaps
Expand Down Expand Up @@ -136,7 +137,8 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
- **Wireless charging and secondary users**: NOT RUN (gated to system user).
- **Xiaomi** — adaptive hardware enforcement of external writes unconfirmed; treat the adapter as provisional until
the 80% hold is physically observed.
- **HyperOS 3 candidate mapping (contribution report, 2026-08-07 — unqualified, stays diagnostics-only).** A
- **HyperOS 3 candidate mapping (contribution report, 2026-08-07 — unqualified at the time; since landed,
see the LANDED bullet below).** A
Redmi Note 14 `24117RN76G` (`tanzanite`, Android 16 / SDK 36, `ro.mi.os.version.code=3`, ROM
`3.0.302.0.WOGMIXM.C08`) reported via the contribution wizard that the **same key**
`secure/security_pc_secure_protect_mode_key` now carries **three** modes: `0` = Charge fully and
Expand All @@ -161,7 +163,22 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
**external** writes, the decisive question for Amply and open even on qualified HyperOS 2, remains
unproven. Follow-up runs requested in the issue #48 thread: sustained hold at 80 with `current_now`, and
the both-direction external-write test (adb `settings put` to `2` below the cap → hold; back to `0`
mid-hold → charging resumes past 80).
mid-hold → charging resumes past 80). Delivered 2026-08-14 — see the enforcement bullet below.
- **Both-direction external-write enforcement DEMONSTRATED (2026-08-14, issue #48, same `tanzanite`
device).** The contributor ran the requested protocol via on-device adb shell — the **shell UID, the same
write path Amply's Shizuku service uses**. (1) Below 80% plugged, UI manually set to "Charge fully",
`settings put secure security_pc_secure_protect_mode_key 2` → Battery Protection activated and the
Settings UI reflected it immediately, so the daemon reacts to external key writes, not just its own UI.
(2) Held at 80% for ~20 minutes plugged under active use without gaining a point; sysfs
`current_now` was permission-denied so there is no current reading, but the dumps corroborate the hold
indirectly (voltage 4228 mV at the hold vs 4391 mV after resume; charge counter 4283 vs 4341). (3)
`settings put … 0` mid-hold → charging resumed immediately, UI followed, level passed 80. Caveats: hold
evidence is level observation plus voltage/counter deltas, not a current measurement, and both dumps
show `status: 2` / `Charging state: 0` / `Charging policy: 0` — HyperOS 3 exposes **no hardware hold
signal**, so a future adapter gets readback-only verification (like Samsung, unlike Pixel/GrapheneOS).
This answers the decisive daemon-enforcement question for `tanzanite` mode `2`; the remaining items
(gate design, boundary widening, factory/absent-key semantics, sessions/access-tiers/R8) are tracked in
the landing bullet below.
- **Second HyperOS 3 data point (support mail, 2026-08-13): the hard-cap mode is NOT HyperOS-3-wide.** A
Poco F5 `23049PCD8I` (`marblein`, Android 15 / SDK 35, HyperOS 3.0.2, ROM `OS3.0.2.0.VMRINXM`) reported
via the contribution wizard only the two HyperOS-2-style modes on the same key (`1` = Intelligent
Expand All @@ -171,5 +188,20 @@ only after adding a row here. Detailed run narratives live in each adapter's lan
one row in the privacy review (mapping possibly incomplete), and the device was previously rooted with
FDE.ai driving the charge limit. Thread: support mail "Amply device-support discovery", 2026-08-13; the
contributor was asked whether the settings screen shows any third mode / 80% option.
- **LANDED 2026-08-14: `xiaomi-hyperos3-v1`, gated to a qualified-codename allowlist (`tanzanite` only) —
GrapheneOS-precedent landing** (remote enforcement qualification via issue #48, see the Verified devices
row; app-level items open, all failing closed, to be verified by the contributor on the next beta):
- **Absent-key decode unverified**: absent decodes as Intelligent/Adaptive, mirroring HyperOS 2's
factory-state assumption; HyperOS 3 factory semantics are unknown. Worst case if wrong: a session
restore writes `1` onto a factory state that was not Intelligent — bounded, no battery hazard.
Next-beta ask: `settings delete secure security_pc_secure_protect_mode_key`, then `settings get` +
observe which mode the native UI shows.
- **Sessions / boot recovery / access tiers (WSS-only vs Shizuku) / R8**: NOT RUN on HyperOS 3;
the mechanism is the shared session engine + the same `secure`-namespace write path qualified on
HyperOS 2, but on-device confirmation is pending the next beta.
- **Adaptive (mode `1`) enforcement undemonstrated** — identical provisional status as HyperOS 2 (the
top-level Xiaomi gap above); only mode `2` has demonstrated hardware enforcement.
- The gate cannot widen past the codename allowlist: record any new HyperOS 3 device here plus a
Verified-devices row before adding its codename to `XiaomiHyperOs3ChargingAdapter.QUALIFIED_CODENAMES`.
- **Pixel** — wireless at-threshold hold/charge-past and the widget under Shizuku-only remain unexercised (both share
the verified wired mechanism).
42 changes: 27 additions & 15 deletions .claude/skills/oem-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,33 @@ diagnostics-only lab adapter. An external `protect_battery=0` makes One UI forge
to the OEM default on re-enable), so Amply restores the exact prior policy itself rather than trusting Samsung's
bookkeeping. Verified devices + coverage: see the qualification ledger (`device-qualification` skill).

## Xiaomi Adapter

One live adapter (`xiaomi-hyperos2-v1`), gated to the HyperOS ROM version (the setting is a ROM
feature, not a per-model one): manufacturer Xiaomi (covers Redmi/POCO — they report Xiaomi as
manufacturer) + `ro.mi.os.version.code == 2` (HyperOS 2.x) + system user. Use `ro.mi.os.version.code`,
NOT the frozen legacy `ro.miui.ui.version.code`. Single key
`secure/security_pc_secure_protect_mode_key`: `0`=charge fully, `1`=Intelligent (heuristic 80% hold →
`ChargePolicy.Adaptive`), absent=Intelligent (factory state). No hard-cap mode exists **on HyperOS 2**; a
HyperOS 3 contribution report (2026-08-07) shows the same key with a third value `2`="Battery protection" —
a candidate hard-cap mode, unqualified (see the `device-qualification` skill). SYNC_READBACK
with read-back equality; session override = Unrestricted; protective default = Adaptive. HyperOS 1,
pre-HyperOS MIUI, and HyperOS 3 fall to `XiaomiLabAdapter` (diagnostics + contribution). Two
documented assumptions: the feature is treated as present on any HyperOS 2 device (a device lacking it
reads the key absent → a harmless false claim of control), and daemon-level enforcement of external
writes is pending long-term observation (see the `device-qualification` skill).
## Xiaomi Adapters

Two live adapters over the single key `secure/security_pc_secure_protect_mode_key` (both in
`XiaomiChargingAdapter.kt`). Use `ro.mi.os.version.code`, NOT the frozen legacy `ro.miui.ui.version.code`.
Manufacturer Xiaomi covers Redmi/POCO — they report Xiaomi as manufacturer.

- **HyperOS 2 (`xiaomi-hyperos2-v1`)** — gated to the HyperOS ROM version (the two-mode setting is a ROM
feature, not a per-model one): manufacturer Xiaomi + `ro.mi.os.version.code == 2` + system user. Values:
`0`=charge fully, `1`=Intelligent (heuristic 80% hold → `ChargePolicy.Adaptive`), absent=Intelligent
(factory state); `2` does not exist on HyperOS 2 and decodes `Unknown(unrecognizedValue)`. Session
override = Unrestricted; protective default = Adaptive. Two documented assumptions: the feature is treated
as present on any HyperOS 2 device (a device lacking it reads the key absent → a harmless false claim of
control), and daemon-level enforcement of external writes is pending long-term observation.
- **HyperOS 3 (`xiaomi-hyperos3-v1`)** — same key plus `2`="Battery protection" (hard cap →
`FixedLimit(80)`; both-direction enforcement of external shell-UID writes demonstrated on `tanzanite`,
issue #48). Gate: manufacturer Xiaomi + `ro.mi.os.version.code == 3` + **qualified-codename allowlist**
(`QUALIFIED_CODENAMES`, ships `tanzanite`) + system user. Version-only gating is impossible: mode `2` is
model-/build-dependent within HyperOS 3 (`marblein` on 3.0.2 has only 0/1), the property has no minor
version, and mode-2 presence cannot be probed (key absent in factory state). Session override =
Unrestricted; protective default = FixedLimit(80) — the only Xiaomi mode with demonstrated enforcement.
Absent=Intelligent mirrors HyperOS 2 but is **unverified on HyperOS 3** (pending the issue-#48
qualification run). No hardware decode: `dumpsys battery` exposes no hold signal on HyperOS 3.

Both are SYNC_READBACK with read-back equality; writes are WSS-capable (`secure` namespace). HyperOS 1,
pre-HyperOS MIUI, and unqualified HyperOS 3 devices fall to `XiaomiLabAdapter` (diagnostics + contribution).
The boundary write domain for the key is `{0,1,2}` globally (see `privileged-access.md`). Qualification
evidence: `device-qualification` skill.

## OnePlus / ColorOS Adapter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,12 @@ internal object SettingWritePolicy {
// Pixel charging optimization
"charge_optimization_mode" to setOf("0", "1"),
"adaptive_charging_enabled" to setOf("0", "1"),
// Xiaomi HyperOS charging protection
"security_pc_secure_protect_mode_key" to setOf("0", "1"),
// Xiaomi HyperOS charging protection. "2" = HyperOS 3 "Battery protection" (hard cap 80,
// qualified on tanzanite — issue #48 / ledger). The domain is global, so "2" is also
// boundary-writable on HyperOS 2 devices: accepted, no Amply code path emits it there
// (the HyperOS 2 adapter maps no policy to it and its decode refuses it) — the boundary
// owns the per-key domain, not per-ROM routing.
"security_pc_secure_protect_mode_key" to setOf("0", "1", "2"),
),
"global" to mapOf(
// GrapheneOS charge limit (1 = fixed 80% cap; the ROM latches it at plug-session start)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class AdapterRegistry @Inject constructor(
samsungLegacy: SamsungLegacyChargingAdapter,
samsungLab: SamsungLabAdapter,
xiaomi: XiaomiChargingAdapter,
xiaomiHyperOs3: XiaomiHyperOs3ChargingAdapter,
xiaomiLab: XiaomiLabAdapter,
onePlus: OnePlusChargingAdapter,
onePlusLab: OnePlusLabAdapter,
Expand All @@ -38,7 +39,7 @@ class AdapterRegistry @Inject constructor(
// Live adapters otherwise match only their verified scopes; same-OEM misses fall to the lab adapters.
private val adapters = listOf(
lineage, lineageLab, grapheneOs,
pixel, samsungModern, samsungLegacy, samsungLab, xiaomi, xiaomiLab, onePlus, onePlusLab,
pixel, samsungModern, samsungLegacy, samsungLab, xiaomi, xiaomiHyperOs3, xiaomiLab, onePlus, onePlusLab,
)

fun select(device: DeviceInfo = DeviceInfo.current(context)): AdapterSelection {
Expand Down
Loading