| name | oem-adapters |
|---|---|
| description | Per-OEM charge-control adapter detail for Amply — the exact settings keys, value domains, write ordering, verification strategy, and session override of the Samsung, Xiaomi, OnePlus/ColorOS, LineageOS, and Pixel adapters. Use when reading or changing anything under charging/core/adapter, adding an adapter, or reasoning about how a policy maps to real settings values. |
Per-OEM adapter detail, for work under charging/core/adapter. The always-loaded companions are
.claude/rules/architecture.md (data flow, ChargeObservation, session/recovery) and
.claude/rules/privileged-access.md (safety boundary, capability gates) — read the latter before changing any
control code. The ledger of physically-verified devices lives in the device-qualification skill.
Two live adapters, gated by ro.build.version.oneui ranges plus protect_battery presence plus system user
(all in world-readable global namespace; only writes need WSS):
- Modern (One UI 8.x):
protect_battery0=off / 1=Maximum / 3=Standard(pause at 100%, resume 95%), plusbattery_protection_threshold80|85|90|95 (absent = 80, only valid ticks decode; malformed → Unknown). Policies: FixedLimit(80/85/90/95), PauseAtFull, Unrestricted. Session override = PauseAtFull (reaches 100% while keeping Samsung's own safety net). Threshold is written before mode. - Legacy (One UI 4.x/5.x):
protect_battery0/1 toggle, fixed 85% cap. Policies: FixedLimit(85), Unrestricted. Session override = Unrestricted.
Writes apply synchronously (VerificationStrategy.SYNC_READBACK): apply() requires read-back equality, no
pending-settle window, boot recovery converges on settings readback, and no reapply-inversion trick is needed.
The reconnect gesture is Pixel-only (reconnectGestureSupported). One UI 6/7 and 9+ fall through to the
diagnostics-only lab adapter. An external protect_battery=0 makes One UI forget the user's prior mode (it falls back
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).
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).
One live adapter (oplus-coloros15-v1) for the ColorOS/OxygenOS (Oplus) family — OnePlus, Oppo, Realme —
gated to ro.build.version.oplusrom == 15 (Oplus-exclusive property, so it doubles as the family signal) +
system user. Two mutually-exclusive system keys under Battery health: regular_charge_protection_switch_state
= "Charging limit" (fixed 80% cap → FixedLimit(80)) and smart_charge_protection_switch_state = "Smart charging"
(adaptive → Adaptive); neither on = Unrestricted; both on = Unknown/unrecognized. The OEM enforces exclusion and
keeps a _status mirror (Amply writes only _switch_state). SYNC_READBACK with read-back equality; session
override = Unrestricted; protective default = FixedLimit(80). Writes require Shizuku — the keys are system
namespace, which WRITE_SECURE_SETTINGS cannot write (reads are unprivileged); the adapter sets
preferShizukuForWrites. Unqualified Oplus versions fall to OnePlusLabAdapter. Enforcement is directly
observable (device holds at 80%). See the qualification ledger (device-qualification skill).
One live adapter (lineageos-chargingcontrol-v1) plus a LineageLabAdapter, for LineageOS's native Charging
Control. Manufacturer-agnostic — the ROM changes charging control regardless of the OEM hardware — so both are
registered first in AdapterRegistry, ahead of every OEM adapter; a LineageOS build on Samsung/Xiaomi/OnePlus/
Pixel hardware is handled by these, never the OEM lab adapters (stock devices are not isLineageOs and skip both).
Gate: DeviceInfo.isLineageOs + Build.DEVICE in a physically-qualified codename allowlist
(QUALIFIED_CODENAMES) + lineagesettings provider present + system user. Unqualified LineageOS builds fall to
LineageLabAdapter.
isLineageOs comes from the org.lineageos.android system feature, not ro.lineage.build.version: the
ro.lineage.* properties are SELinux-denied to untrusted_app (custom_version_prop) and read back empty, which
previously made every LineageOS device look like stock and fall through to an OEM adapter. See
rules/privileged-access.md.
The three keys live in the private content://lineagesettings/system provider (SettingNamespace.LINEAGE_SYSTEM),
NOT any AOSP settings namespace: charging_control_enabled (0/1), charging_control_mode (3=LIMIT), and
charging_control_charging_limit (the discrete ticks 70/75/80/85/90/95). A hard cap is enabled=1+mode=3+limit=N;
enabled=0 is Unrestricted. Writes are ordered limit→mode→enabled (the observable "on" flip last). Reads are
unprivileged (LineageSettingsClient via ContentResolver, shared by both backends); writes require Shizuku
(content insert; the shell UID holds lineageos.permission.WRITE_SETTINGS, which WRITE_SECURE_SETTINGS cannot
cover — preferShizukuForWrites, and the WSS auto-grant is skipped). SYNC_READBACK with read-back equality;
session override = Unrestricted; protective default = FixedLimit(80); reconnect gesture unsupported.
LineageOS's own ChargingControlController observes these keys and re-drives the vendor.lineage.health. IChargingControl HAL, so an external write is honored. But the HAL is device-dependent (the setting can flip
while charging never actually stops — the mIsLimitSet:false bug), which is why the gate is a qualified-codename
allowlist and control ships disabled until a codename is physically proven. read() returns Verified only for
states v1 can restore exactly (a supported fixed limit, or Unrestricted); AUTO/CUSTOM schedules, off-tick limits, and
an absent/malformed enabled decode to Unknown(unrecognizedValue=true) so a temporary session refuses rather than
clobbering the user's native choice. Verified devices + coverage: see the qualification ledger (device-qualification skill).
One live adapter (grapheneos-chargelimit-v1) for GrapheneOS's own "Limit to 80%" (Settings → Battery → Charging
optimization). ROM-identity adapter, ordered after the Lineage pair and BEFORE pixel in AdapterRegistry —
GrapheneOS ships only on Pixels, and the Pixel probe (any Google/Pixel*) would otherwise swallow the device as a
matched-but-diagnostics-only stock Pixel. Gate: DeviceInfo.isGrapheneOs (core app.grapheneos.* packages via
PackageManager <queries> — NO property/feature/fingerprint marker exists; verified empty on a real device) +
hasBatteryChargeLimit (world-readable key presence — the capability signal, deliberately NOT part of identity) +
system user. No lab adapter: a GrapheneOS build without the key stays on this adapter as diagnostics-only with
contributionWanted.
Single key global battery_charge_limit: 1 = fixed 80% cap (bypass charging; hard-wired, no threshold key) →
FixedLimit(80), 0 = off → Unrestricted; absent/other → Unknown(unrecognizedValue=true) (factory-absent
semantics unverified — refuse, don't guess). WSS-writable, no Shizuku. SYNC_READBACK with read-back equality;
session override = Unrestricted; protective default = FixedLimit(80); reapply == apply (no observer-poke — see
below); reconnect gesture unsupported (structurally: the gesture's override write lands strictly after the
replug broadcast, which the ROM has already sampled past).
The defining quirk: policyLatchesAtPlug = true. GrapheneOS samples the key only at plug-session start; an
external write updates the Settings UI live but has no hardware effect until the next unplug→replug (the native
toggle applies live because Settings pokes the charging service directly — so the session watcher's
cancel-without-restore on an observed external change stays correct). This drives the pending-until-replug
verification state and the session engine's 30s replug grace window (see rules/architecture.md). While enforcing,
the device reports stock-Pixel hardware state 4 (EXTRA_CHARGING_STATUS), which the adapter's own decodeHardware
maps to Verified(FixedLimit(80), BATTERY_HARDWARE) — deliberately not shared with the Pixel decode, which also
maps state 5 to an Adaptive profile this adapter cannot restore. Remote qualification (issue #49): see the
device-qualification skill.
Writes only two secure settings:
secure/adaptive_charging_enabledsecure/charge_optimization_mode
Ordering matters:
- Fixed 80%: adaptive
0, then mode1 - Unrestricted: mode
0, then adaptive0 - Adaptive: mode
0, then adaptive1
Google's Settings Intelligence worker applies external secure-setting changes asynchronously (measured
charging-HAL delay ≈ 11–12 s on tested Pixels). A same-package same-value write does not fire the settings
observer — re-writes briefly invert charge_optimization_mode before applying the target.