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.

معلومات المصدر

المستودع
d4rken-org/amply
آخر نشاط في المصدر
٢٣ سبتمبر ٢٠٢٦ في ١٦:١٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٤٩
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub