This file provides guidance to AI coding assistants when working with code in this repository.
Amply is an experimental Android controller for OEM battery charge-protection modes. Its primary action temporarily allows a full charge, then restores the user's protective policy at 100%, on unplug, or at a safety 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)
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
lineagesettings provider, keys charging_control_enabled/_mode/_charging_limit) is manufacturer-agnostic —
it matches every LineageOS build that ships the provider (plus the system user), but control is gated on
enforcement evidence for that build (HAL enforcement is per-build, and a read-back proves only that the ROM
stored the value): a device stays a candidate with controls off until a maintainer qualified its codename or the
user explicitly enables control on an unconfirmed build, and loses them for good if the battery is seen charging
past the cap. Observation can only ever refute a cap, never confirm one — no passively observable signal tells
a cap hold from a thermal or weak-supply pause. Reads are unprivileged
(ContentResolver), writes require Shizuku (the shell UID holds lineageos.permission.WRITE_SETTINGS, which
WRITE_SECURE_SETTINGS does not cover). GrapheneOS charge limit
(global battery_charge_limit, binary FixedLimit(80)/Unrestricted) is gated to GrapheneOS identity (its
app.grapheneos.* core packages; no property/feature/fingerprint marker exists) plus the system user —
reads AND writes require Shizuku: GrapheneOS marks the key @Protected, denying it to all third-party
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+), unqualified Xiaomi devices,
non-ColorOS-15 Oplus devices, and LineageOS builds without the settings provider remain diagnostics-only. See the
qualification
ledger (.claude/skills/device-qualification/) for the verified devices and mappings.
Package: eu.darken.amply. License: GPL-3.0-or-later. Status: pre-launch (current version in VERSION).
Single Gradle module (:app), flavors and build types are declared in app/build.gradle.kts. Two non-obvious
constraints:
- Java: build/test toolchain needs JDK 21 (Robolectric requires it to emulate Android SDK 36); compiled
bytecode still targets Java 17 (
compileOptions/jvmTargetinapp/build.gradle.kts). - Every variant shares the single applicationId
eu.darken.amply— no build-type suffixes. Because signing certificates differ, installed variants are mutually exclusive on a device; switching requires an uninstall.
Under app/src/main/java/eu/darken/amply/:
charging/core— policies, device capability checks, OEM adapters, WSS, Shizuku access (access/shizuku,adapter)charging/core/enforcement— the observed-enforcement gate: verdict engine, durable evidence, monitor watchercharging/core/qualification— the guided qualification run: an active cut → resume → cut challenge that proves the charging hardware obeys a cap, where the enforcement gate can only ever refute onefullcharge/core— temporary sessions, boot recovery, reconnect gesturemain/ui— activity, onboarding, dashboard, settings, setup guide,qualification,tile,widgetdiagnostics/core+diagnostics/ui— "Help add support" contribution wizard: read-only multi-mode setting discovery + on-device privacy reviewcommon— shared DataStore owner (AppDataStore) and cross-feature primitivescommon/datastore— thecreateValue()settings DSL every preference facade is built on (DataStoreValue)common/serialization— the singleJsonplusChargePolicySerializer, for JSON-backed setting recordscommon/theming— brand, Material You, mode, contrast preferencescommon/settings— reusable hierarchical settings rows/sectionscommon/debug/logging— opt-in debug sessions and logging backends
AIDL boundary: app/src/main/aidl/eu/darken/amply/charging/core/access/shizuku/IChargingControlService.aidl.
version.properties+VERSION— versioning source of truth (parsed by the buildSrcProjectConfigPlugin) and its drift mirror; bump viatools/release/bump.sh, never by handbuildSrc/—ProjectConfig(packageName/SDKs/version) plus shared build helpersapp/build.gradle.kts— flavors, build types, signing wiring, dependenciesbuild.gradle.kts(root) — plugin versions (AGP, KSP, Kotlin Compose, Hilt).github/workflows/code-checks.yml— CI (builds + tests + lint for both flavors)app/src/main/res/values/strings.xml— extracted user-facing strings (system-surfaced text)
Always-loaded topic guidance lives in .claude/rules/:
architecture.md— data flow,ChargeObservation, session/recovery, reconnect gesture, pitfallsprivileged-access.md— Shizuku/WSS access paths, capability gates, AIDL safety boundary (read before touching control code)build-commands.md— gradle build/test/lint commands, flavors, build typescode-style.md— Kotlin/Compose conventions, logging, DataStoretesting.md— JUnit 5 + Kotest conventions (JUnit 4 only for Robolectric)commit-guidelines.md— commit/PR format and prefixeslocalization.md— string extraction conventions and the current gapagent-instructions.md— sub-agent usage and working principles
Loaded on demand, as skills (.claude/skills/) — there are no nested CLAUDE.md files in this repo, all
guidance lives under .claude/:
oem-adapters— per-OEM adapter detail (keys, value domains, write ordering, session overrides)device-qualification— physical qualification protocol, verified-device ledger, per-OEM known gapsrelease— versioning,bump.sh, signing, release workflows, store metadata + screenshots
Amply has no arbitrary shell API. The Shizuku user service executes argument-separated commands, validates
namespaces and values, and allowlists every writable setting. The temporary override relies on a specialUse
foreground service because dormant apps cannot reliably receive power-disconnect broadcasts. Never widen the writable
allowlist, bypass the capability gate, or introduce a shell-string execution path. See rules/privileged-access.md.