Charging: Detect LineageOS via the system feature, not a denied property - #45
Merged
Conversation
LineageOS was never detected in the app process, so every LineageOS device was routed to an OEM adapter. All five ro.lineage.* properties are SELinux-labelled custom_version_prop, which untrusted_app cannot read, and SystemProperties.get returns an empty string on denial rather than throwing — so SystemPropertyReader's runCatching never fired, nothing was logged, and LineageOsDetector silently returned null. getprop over adb had always masked this by running as the shell UID. That made QUALIFIED_CODENAMES dead code (a qualified codename could never activate), hid the contribution card, and pointed "open battery settings" at Battery Saver instead of Battery. Detection now uses the app-readable org.lineageos.android system feature via DeviceInfo.isLineageOs; the version property is kept as a secondary OR-input for derivatives that relabel it, and for diagnostics. Also in the LineageOS contribution path: - The guided settings-diff wizard is withheld (AdapterSupport .guidedCaptureUseful). Its capture set excludes the lineagesettings provider, so a run there always diffs to empty and cannot be delivered — users were walked into a dead end after entering details and capturing twice. Both entry points and the destination itself are gated; the destination waits for adapterResolved so a saved-state restore cannot flash the wizard on the permissive default. - The direct report carries a dumpsys lineagehealth probe (new zero-argument AIDL op; constant binary and argument, read-only, no write allowlist involved). It is an observation, never a verdict: provider selection branches on the configured mode before capability, and Toggle also accepts MODE_LIMIT and enforces the cap itself, so no value disqualifies a device and none proves enforcement. - The raw dump is reduced to PROVIDER|mode inside the user service, so the user's charging schedule (StartTime/TargetTime) and battery level never cross Binder. - Withholding the wizard removed these users' only Shizuku grant entry point, which the probe needs; the card now offers it, branching on BackendStatus so a stopped server opens Shizuku instead of firing a request that returns false immediately. Fixes found alongside: the wizard's bottom bar lacked a navigation-bar inset and sat under the 3-button nav bar, and the unsupported card interpolated Build.MANUFACTURER, telling a LineageOS Pixel user that "Google devices" are unmapped. Verified on Pixel 6 (oriole) / LineageOS 23.2 / Android 16: adapter selection is lineageos-lab, the report reads is_lineageos=true, lineage_cc_provider=DEADLINE, lineage_cc_mode=1, lineage_cc_limit_mechanism=NOT_OBSERVED, and every field degrades to unknown with Shizuku stopped. Regression tests drive DeviceInfo.current against a real PackageManager, the seam the previous unit tests bypassed by injecting lineageOsVersion directly.
#44 landed on the same three files with a related change: the metadata-only report is now gated on ChargingState.hasSupportLead. The two interact. #44's fallback without a lead is "the card keeps the wizard", and this branch withholds the wizard on LineageOS — together those could have left that card with no contribution path at all. They do not: hasSupportLead includes adapterMatched (the Lineage lab adapter matches once detection works) and hasLineageSettingsProvider (verified present on oriole), so the metadata path stays offered while the wizard is withheld. Documented on the parameter, with a preview for that combination since neither existing preview rendered it. Also corrected hasSupportLead itself: it counted `device.lineageOsVersion != null` as a lead, which is dead on every real LineageOS build for exactly the reason this branch exists. Now isLineageOs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Amply could not recognise LineageOS at all, so LineageOS phones were treated as if they were running their manufacturer's stock software. On a LineageOS Pixel that meant being told the 80% limit is "supported on Pixel 6a and newer phones", being sent to Battery Saver instead of the Battery screen that actually holds LineageOS's own Charging control, and never being offered the "help add support" card. Amply now recognises LineageOS and says the right things: it points at LineageOS's own charging control, and describes the device as a LineageOS device rather than a Google one.
For people who want to help add support, the LineageOS path is reworked. The guided step-by-step wizard is no longer offered there — on LineageOS it could never finish, because the settings it watches are not the ones LineageOS uses, so it always ended with nothing to send after several minutes of work. Instead the "send device info" report now includes which charge-control method the ROM uses, which is the useful part. Connecting Shizuku is offered directly on the card so that detail can be included; without it everything still works, the report just says "unknown".
No change for any other device. This does not make LineageOS charge control work — no LineageOS device is enabled by it.
Technical Context
Root cause. Every
ro.lineage.*property is SELinux-labelledcustom_version_prop, whichuntrusted_appcannot read, andSystemProperties.getreturns an empty string on denial instead of throwing. SoSystemPropertyReader'srunCatchingnever fired, nothing was logged,LineageOsDetector.detect()returned null, and both Lineage adapters skipped their probe.getpropover adb had always masked this because it runs as the shell UID. Confirmed byavc: denied { read } … tcontext=u:object_r:custom_version_prop:s0 … app=eu.darken.amply.Detection now keys on the
org.lineageos.androidsystem feature — app-readable with no<queries>entry and no permission, and declared upstream from LineageOS 15.1 through 23.2.lineageOsVersionstays as an OR-input (derivatives that relabel the property) and for diagnostics. Fingerprint sniffing is not an alternative: LineageOS spoofsBuild.FINGERPRINTto stock (google/oriole/oriole:16/…/release-keys).Why CI never caught it.
AdapterRegistrySelectionTestinjectedlineageOsVersionstraight intoDeviceInfo, so every gate looked correct while the real read returned nothing.DeviceInfoLineageDetectionTestnow drivesDeviceInfo.current(context)against a realPackageManagerthrough the registry.The probe is an observation, not a verdict. Reviewing this twice produced two wrong inferences worth recording so they are not reintroduced:
MODE_AUTO/MANUALreturnDeadlinebeforeLimitis consulted — soDeadlineproves nothing.Togglealso acceptsMODE_LIMITand enforcestargetPctitself by cutting charging, so binding it is a capable mechanism, not a rejection.isHALModeSupportedadditionally swallowsRemoteExceptionintofalse.Hence
LineageLimitMechanismhas no negative case (NATIVE_LIMIT/FRAMEWORK_TOGGLE/NOT_OBSERVED/UNKNOWN), and a test enumerates the constants so one cannot be added back. EvenNATIVE_LIMITis not qualification — oriole boundLimiton LineageOS 20 and still charged past the cap.Safety boundary. The new AIDL op takes no arguments: constant binary, constant argument, read-only, no write-allowlist involvement, nothing caller-supplied to validate. The raw dump is reduced to
PROVIDER|modeinside the user service — it containsStartTime/TargetTime(the user's charging schedule, i.e. their sleep window) and battery level, which must not reach a public report. Reviewed by Codex over four passes; no injection path and no route by which raw text reaches the app process or a log.Review guidance. Worth the closest look: the mechanism inference in
LineageHealthProbe.ktagainst upstreamChargingControlController.getProviderForModeandToggle.java; theguidedCaptureUsefulgating (both entry points plus the destination, which waits onadapterResolvedso a saved-state restore cannot flash the wizard on the permissive default); and theBackendStatusbranching on the card, since requesting permission is a no-op while the Shizuku server is stopped.Verified on Pixel 6 (oriole) / LineageOS 23.2 / Android 16. Adapter selection
lineageos-lab; report readsis_lineageos=true,lineageos_version=none,lineage_cc_provider=DEADLINE,lineage_cc_mode=1,lineage_cc_limit_mechanism=NOT_OBSERVED; all fields degrade to unknown with Shizuku stopped, where the button correctly reads "Open Shizuku" and launches it. Lineage settings unchanged throughout; no crashes. The device itself remains NO-GO on both LineageOS builds — that verdict rests on themode=3write reading back as1and the LOS 20 charge-past observation, not on this probe.Also fixed alongside: the contribution wizard's bottom bar had no navigation-bar inset and sat under the 3-button nav bar (~31px of Back/Next tappable), and the unsupported card interpolated
Build.MANUFACTURER, telling a LineageOS Pixel user that "Google devices" are unmapped.