Skip to main content

device-qualification

Amply's physical qualification protocol for OEM charge-control adapters, the ledger of verified devices (Pixel/Samsung/Xiaomi/OnePlus), and known gaps per OEM. Use when qualifying a new device or ROM, widening a capability gate, adding a codename to an allowlist, or recording device test results.

Datos de origen

Repositorio
d4rken-org/amply
Última actividad en el origen
23 de septiembre de 2026 a las 16:12
Idioma detectado de SKILL.md
inglés
Estrellas
43
Forks
0

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
device-qualification
description
Amply's physical qualification protocol for OEM charge-control adapters, the ledger of verified devices (Pixel/Samsung/Xiaomi/OnePlus), and known gaps per OEM. Use when qualifying a new device or ROM, widening a capability gate, adding a codename to an allowlist, or recording device test results.
# Device Qualification & Ledger Companion to `.claude/rules/privileged-access.md`, which holds the always-loaded safety boundary and the capability gates themselves. **Never widen a gate without adding a row to "Verified devices" below.** ## Qualifying a new device / OEM Adding or widening a live adapter requires physical qualification, not just a settings mapping: 1. **Characterize** — record model/codename, build fingerprint, OS + OEM-component versions, and the native charge-protection options. Diff `settings list {secure,system,global}` before/after each native UI state to isolate the exact key(s) and value domain. Note whether a key is **absent** in factory state (absent ≠ off). 2. **Validate hardware** — drive the raw key transitions unplugged, and charging below/near/above the limit, wired **and** wireless. The gate passes only if writes move the **real charging hardware** (battery status / sysfs `charging_policy` / current), not just the Settings UI. 3. **Validate access tiers** — WSS-only (writes apply, hidden reads stay truthfully "requested", grant survives reboot), Shizuku-only (authoritative readback, denial + binder-death + restart), and both (WSS write → Shizuku readback). An external native change must never be claimed as verified. 4. **Validate sessions** — full charge, early disconnect, manual restore, arm + safety timeouts, process death, force-stop, notification denial, real reboot + deferred `BOOT_COMPLETED` redelivery. Confirm exact restore of the prior value **including a previously-absent key**, and that an unknown/malformed value **refuses** the session without mutating the setting. Also confirm the interrupted-session surface both ways: a **force-stop that leaves a restore owed** must, on next open, restore the limit *and* show the dashboard interruption card, while a **reboot** must restore via boot recovery and show **no** card (the boot-count guard). A device that holds at its limit often reports `NOT_CHARGING`, which ends a session immediately — drive the lifecycle with `adb shell dumpsys battery set status 2 / set level N` (then `dumpsys battery reset`) so the session behaves as it would mid-charge. 5. **Minified build** — repeat the smoke path on an R8 `foss` beta (past runs caught R8-only startup/reflection breakage that debug builds hid). Record every scenario as **PASS / FAIL / NOT RUN / BLOCKED** — an untestable hardware condition is not a pass. Never commit device serials or user data. Requalify after a relevant OS / OEM-component update. **Go / no-go**: *Go* only if writes reliably control the hardware and restore safely (enable just the tested model/OS row). *Shizuku-only* if control needs an extra safe op invokable through the typed service. *No-go* → keep the device diagnostics-only. ## The in-app guided run is not a ledger row `charging/core/qualification/` lets a user run the cut → resume → cut challenge on their own device. A pass records `EnforcementStatus.SELF_QUALIFIED`, which enables control **for that user, on that exact build**, and produces a report (`qualification_schema=1`) carrying `Build.DEVICE` — the codename an allowlist entry needs and the field the contribution wizard omits. **A self-qualified pass never becomes a "Verified devices" row on its own, and never writes into a `QUALIFIED_CODENAMES` allowlist.** It is one run, on one unit, usually unattended, with no wireless leg, no access-tier matrix, no session/boot-recovery coverage and no R8 build — i.e. step 2 of the protocol partially, and none of steps 3-5. What it *is* good for is the thing that used to take days of email: it settles the does-the-hardware-obey question with a machine-collected, timestamped result instead of contributor prose, so a maintainer row becomes a matter of covering the remaining steps rather than starting from nothing. Treat an incoming qualification report the way the `tanzanite` enforcement evidence was treated: strong evidence for one question, explicitly scoped, recorded with its gaps named. ## Verified devices (physically tested) The gate's supported *scope* (see Capability Gates) is broader than what has been physically tested below. Widen a gate only after adding a row here. Detailed run narratives live in each adapter's landing commit. | OEM | Tested device / build | Hardware evidence | Coverage | Landed | |---|---|---|---|---| | Pixel | Pixel 8 `shiba` A17/API37; Pixel 9 Pro `caiman` A16/API36; Pixel 7a `lynx` A16/API36 | Full — sysfs `charging_policy` follows writes (~11–12s) | Access tiers, sessions, boot recovery, wireless hold, at-threshold, reconnect gesture, natural 100%, interrupted-session detection (7a) | 2026-07-15/-19/-20/-25 | | 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 | | 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 | | 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, 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 | | 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 | | 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 | | 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 | ## Known gaps - **LineageOS** — the live adapter now **matches every LineageOS build that ships the `lineagesettings` provider**; `QUALIFIED_CODENAMES` still ships **empty** but is only a maintainer fast path, no longer what makes the adapter reachable. Control is gated on **per-build enforcement evidence** instead: a device is a candidate with controls off until the user explicitly enables control on an unconfirmed build, and loses control permanently for that build if the battery is observed charging past the cap. A codename still goes into `QUALIFIED_CODENAMES` only after full qualification (real charging cessation at the limit, wired + wireless, below/at/above threshold) plus a "Verified devices" row — that list now buys skipping the opt-in, not access itself. - **Observation can only refute, never confirm** (established 2026-08-17, see the oriole re-observation below): no passively observable signal distinguishes a cap hold from a thermal or weak-supply pause. Both present as plugged + `BATTERY_STATUS_NOT_CHARGING` + a static level. `EXTRA_CHARGING_STATUS` does **not** break the tie — it is session-scoped, measured still reading `4` while the device charged ten points *below* its cap. A guided two-cap cut/resume/cut challenge (raise the cap, verify charging resumes and re-stops at the new threshold) is the known way to earn real confirmation and is deliberately not implemented. - **Pixel 6 (oriole) on LineageOS 23.2 / Android build `BP4A.251205.006` — re-observed 2026-08-17. The 2026-07-22 LOS 23.2 NO-GO below does NOT hold on this build.** `dumpsys lineagehealth` binds `ccprovider.Limit` (so the HAL *does* advertise the LIMIT mode bit again), `charging_control_mode` reads back as `3` and is **not** coerced to `1`, and with `enabled=1 / mode=3 / limit=70` the device sat at exactly 70 % on AC for ~4 h: `status: 4` (NOT_CHARGING), `Charging state: 4`, `Charging policy: 1`, voltage 4072 mV. **Causal check**: raising `charging_control_charging_limit` to `80` resumed charging within 20 s — `status: 2`, voltage 4072 → 4183 mV, temperature 294 → 311, charge counter 2908000 → 2910000 — i.e. the setting demonstrably drives the charging hardware, which is stronger evidence than an observed plateau. The limit was restored to `70` and re-verified by read-back; `enabled`/`mode` were never written. **This is NOT a GO and oriole is NOT in `QUALIFIED_CODENAMES`**: only part of step 2 was run — wired only, no wireless, no below/at/above sweep, no hold observed at the raised cap, and none of steps 3-5 (access tiers, sessions, boot recovery, R8). It supersedes the "HAL dropped LIMIT" claim for *this build only*. **Unexplained later observation, recorded so this row does not overclaim**: a few hours after the run above, the device was found at **78 %** with `charging_control_charging_limit` still reading `70` — i.e. a level above the cap. It is **not attributable** and must not be read either as enforcement failing or as anything else: by then the phone had left this run's control entirely — physically unplugged and moved, and its system clock force-set to `Tue Jul 21 02:01 CEST` (a month in the past, at 02:00) by another workflow, which is a scheduled/night-charge experiment signature. Any of that could produce the rise. The controlled observations above stand as recorded; this one is logged only so a later reader does not find it and conclude the row was written selectively. Re-check under controlled conditions before treating either as settled. **Why it matters beyond oriole**: same device, same Lineage major version, different build, opposite HAL capability. That is the concrete case for HAL capability being **build-scoped, not codename-scoped**, and it is why enforcement evidence is keyed on a composite build identity (fingerprint + incremental + build time + provider package version) rather than a codename — LineageOS spoofs `Build.FINGERPRINT` to stock, so the fingerprint alone cannot carry it. - **Pixel 6 (oriole) on LineageOS 20.0 / Android 13 — tested 2026-07-22, result NO-GO (no hardware enforcement).** The *software chain is fully validated* — both raw (shell-UID `content query`/`content insert`) **and through the app's real Shizuku backend end-to-end** (R8 debug build + Shizuku granted): tapping 80 % wrote the trio via `writeLineageSetting` and read back `Verified(FixedLimit(80), SHIZUKU)`. LineageOS's `ChargingControlController` **observes the external write** and updates its live config (`dumpsys lineagehealth` → `mConfigEnabled:true, mConfigMode:3, mConfigLimit:80`); the `vendor.lineage.health.IChargingControl/default` HAL is registered and its service runs. The refuse-don't-clobber decode was also confirmed live: a native `mode=2` (CUSTOM) state read as `Unknown(unrecognized)` ("Policy not verified"), never overwritten. And the diagnostics-only path was smoke-tested (empty allowlist → `lineageos-lab` → "Detected for diagnostics only", no crash). **But the hard percent limit did not cut charging** — with `limit=80` set, the battery charged 92→95 %+ at ~1.2 A (`charge_stage=Inactive`, `charge_limit` sysfs empty, `mChargingStopReason:0`). On Pixel the charge hardware is driven by Google's adaptive/`charge_deadline` mechanism, and this Lineage build's HAL does not map the %-cap to a real cutoff — the `mIsLimitSet:false` device-dependent class of gap. So oriole stays **out** of the allowlist. This is a clean validation of the conservative gate: an "any LineageOS device" gate would have claimed 80 % protection while the phone charged to 100 %. (Qualifying a device with a *working* charge-control HAL remains open; the adapter's provider/observer mechanism is proven, only per-device HAL enforcement varies.) - Also confirmed on oriole: `charging_control_*` keys are **absent in factory state** (validates the conservative "absent → unrecognized" decode). Still open: the provider's change-notification URI form (per-key vs table) against the registered `observedSettingUris`, to be checked on a HAL-enforcing device. - **Pixel 6 (oriole) on LineageOS 23.2 / Android 16 — retested 2026-07-22 after an anti-rollback firmware bump, result NO-GO (HAL no longer offers LIMIT mode at all).** A *sharper* root cause than the LOS 20 run: the `vendor.lineage.health.IChargingControl` HAL reports `getSupportedMode()` **without the LIMIT bit**, so `ChargingControlController.isChargingModeSupported(LIMIT)` is false and LineageOS **coerces `charging_control_mode=3` → `1` (AUTO)** — verified live: every raw shell-UID `content insert` of `mode=3` (even while `enabled=0`) read back as `1`. Enabling then logs `LineageHealth: No alarm found, auto charging control has no effect` and `Setting charge deadline: … 73353`; the active provider is `ccprovider.Deadline` (Google Adaptive Charging — time/alarm-based, **no fixed-percent cap**). `charging_policy`/`charge_stage` sysfs never changed. So oriole is NO-GO on **both** builds for different reasons: LOS 20 *accepted* LIMIT but the HAL silently didn't cut; LOS 23.2's HAL dropped LIMIT and falls back to the Deadline mechanism. Consequence for the adapter (unchanged — correct by design): SYNC_READBACK `apply(FixedLimit)` writes `mode=3`, reads back `mode=1 ≠ 3` → decodes `Unknown(unrecognizedValue=true)` → `apply` returns false, so it **refuses without a false claim of control**. Reinforces the allowlist bar: a device must both expose the LIMIT mode bit **and** actually cut charging. `charging_control_*` again **absent in factory state**. - **Pixel 6 (oriole) on LineageOS 23.2-20260720-NIGHTLY / Android 16 — app-level compatibility pass 2026-08-03.** HAL qualification **not re-run** (same build as the run above, verdict stands). This pass found a defect that made the whole Lineage path unreachable in production: **Amply never detected LineageOS at all.** All five `ro.lineage.*` properties are labelled `u:object_r:custom_version_prop:s0`, which SELinux denies to `untrusted_app` (`avc: denied { read } … tcontext=custom_version_prop … app=eu.darken.amply`). `SystemProperties.get` returns `""` on denial instead of throwing, so `SystemPropertyReader`'s `runCatching` never fired, nothing was logged, and `LineageOsDetector.detect()` silently returned null — `getprop` over adb had always worked because it runs as `shell`. Both Lineage adapters skipped and selection fell through to `google-pixel-lab-v1`, which (a) made `QUALIFIED_CODENAMES` dead — a qualified codename could never activate, (b) hid the "Help add support" wizard (`contributionWanted` defaults false on the Pixel adapter, true on the lab adapter), and (c) pointed "open battery settings" at Battery Saver, since the Pixel intent targets the absent Google Settings-Intelligence component and never tries `POWER_USAGE_SUMMARY` (which *does* resolve on LOS). Fixed by gating on the app-readable `org.lineageos.android` system feature (`DeviceInfo.isLineageOs`); the version property is kept as a **secondary identity signal** (OR-ed in, normally null on real hardware). The same run added a `dumpsys lineagehealth` probe to the device-support report. **It is an observation, never a verdict, and never qualifies a device.** Two reasons: selection is mode-dependent (upstream picks Deadline before Limit for `MODE_AUTO`/`MODE_MANUAL`, so this device's `Provider: Deadline` at `Mode: 1` merely means nothing was learned), and there is no negative case at all — `Toggle` also accepts `MODE_LIMIT` and enforces the cap itself, so binding it is a capable mechanism rather than a rejection. Even `NATIVE_LIMIT` proves nothing: oriole bound `Limit` on LOS 20 and still charged past the cap. Only physical observation of the charge current qualifies a device. Oriole's NO-GO rests on the `mode=3` write reading back as `1` plus the
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub