Skip to content

feat!: settings UI, configurable mapping window, modern API (v1.2.0) - #4

Open
Rillwyn wants to merge 1 commit into
Dhangofa:mainfrom
Rillwyn:feat/settings-ui-1.2.0
Open

Rillwyn wants to merge 1 commit into
Dhangofa:mainfrom
Rillwyn:feat/settings-ui-1.2.0

Conversation

@Rillwyn

@Rillwyn Rillwyn commented Sep 16, 2026

Copy link
Copy Markdown

What this does

Reworks BatteryRemapper into a module with a Material settings screen, and migrates it from the legacy Xposed API to the modern libxposed API (102).

The module previously had no UI: the 20%..80% → 0%..100% window was hard-coded, and there was no way to see whether the hook was actually running. This PR makes the window configurable, adds a status card that answers "is this working?", and carries several fixes that came out of log analysis on a real device (MIUI + KernelSU + LSPosed).

New

Settings screen

  • Material 3 screen with a two-thumb range slider, a live preview of the resulting mapping — computed with the same function the hook uses, so the preview cannot drift from reality — plus your current physical percentage mapped live.
  • Save / Reset are pinned to the bottom, not part of the scrolling content, so the primary action is always reachable on a normal phone.
  • Default is still physical 20%..80% → displayed 0%..100%, i.e. exactly the current behaviour.

Module status card

  • Framework name, version and API level.
  • Whether com.android.systemui is in the module's scope.
  • Whether the module is actually loaded in the System UI process, with its PID and load state (UP_TO_DATE / STALE / RELOADING / FAILED), refreshed while the screen is open.
  • Distinct states for "LSPosed not detected", "scope not enabled" and "waiting for System UI", instead of silently doing nothing.

Battery protection

  • The shutdown countdown trigger is configurable, expressed on the displayed scale (0%..50%, default 0%). The default reproduces the original behaviour: with the default window, a displayed 0% is a physical 20%.
  • The card spells out the physical equivalent of the chosen trigger, live as the window changes — with a window narrower than the full scale, a displayed-level threshold is otherwise impossible to reason about.
  • The countdown dialog carries both English and Chinese in one string: System UI raises it from its own process, where an in-app language choice does not apply.

Language

  • English and Simplified Chinese with an in-app switch (including "follow system"), stored as a per-app language, so it also appears under Settings ▸ Apps ▸ Battery Remapper ▸ Language.

Changed

  • Module registration follows the modern API: META-INF/xposed/java_init.list + scope.list + module.prop replace assets/xposed_init and the xposed meta-data; the module name and description come from android:label / android:description.
  • The hook is an interceptor chain (hook(...).intercept(chain -> ...)) instead of XC_MethodHook, and the entry class extends XposedModule.
  • Settings sharing uses LSPosed remote preferences (XposedInterface.getRemotePreferences in the hook, XposedService.getRemotePreferences in the UI). The legacy XSharedPreferences is unsupported by current LSPosed (the official comparison table marks it unsupported since 2.1.0).
  • Battery saver hysteresis is unchanged: forced on at 20% or below of the displayed level, off above 50%, always off while charging.
  • Mapping.remap() is a dependency-free function shared by the hook and the UI preview, and it is verified against the original formula (every level 0..100 identical at the default window).

Fixed

Every one of these was a silent failure mode, found by reading logs from a real device:

  1. The battery display updates immediately after saving. System UI only recomputes the percentage when an ACTION_BATTERY_CHANGED broadcast arrives, so a window that changes while the battery is steady stayed invisible until the next real battery event — which is why plugging in a charger appeared to "apply" it. Every path that changes the window now re-sends the current sticky battery intent, scoped to System UI itself.
  2. The shutdown countdown cancels reliably when the charger is connected. The module now watches the battery directly instead of relying on System UI to read a level, and cancellation is unconditional rather than driven by a flag that could disagree with what is on screen.
  3. No more stale module instances. The module no longer hot-reloads itself on save. XposedService.hotReloadModule() does not reload in place: it loads the module classes through a new classloader, and statics are per-classloader, so every save left a live ghost instance behind with its own configuration, its own battery receiver and its own dialog reference. A countdown could be armed by a trigger the user had already lowered. A JVM system property — the one piece of state every classloader shares — now carries an ownership token, and leftovers go inert and take their own dialog down.
  4. Framework hooks are deoptimised first (XposedInterface.deoptimize). ART inlines small framework methods such as ContextWrapper.attachBaseContext; without deoptimising, the hook installs successfully and then never runs, which silently disabled battery saver automation and the shutdown dialog.
  5. The status card survives an activity recreation (for example a language change): the framework service is held process-wide, because its bind callback is a one-shot event and the helper drains its cache after the first replay.
  6. The target's Context is recovered through ActivityThread.currentApplication() as a fallback, which is what the legacy AndroidAppHelper did — needed because a module loaded late never sees Application#onCreate().

Build

  • compileSdk 37, AGP 8.13.2, Gradle 8.13, Java 17. minSdk 29 / targetSdk 34 unchanged.
  • compileSdk 37 is required by the io.github.libxposed 102 artifacts (minCompileSdk 37). The existing workflow already installs platforms;android-37, so CI needs no change.
  • Added a .gitignore (build output was previously untracked noise) and an adaptive launcher icon, since the module is now a launchable app.

Breaking change

The module is built against the modern Xposed API and declares minApiVersion 102, so it needs an LSPosed build that supports it, and it will not install on frameworks that only provide the legacy API. minSdk stays at 29, so device compatibility is unaffected.

How to test

  1. Install, enable the module in LSPosed, keep com.android.systemui in scope, reboot.
  2. Open the app: the status card should read Active with System UI's PID.
  3. Change the mapping window and Save: the status bar percentage should change immediately (no reboot, no System UI restart).
  4. Set the countdown trigger and unplug the charger: the bilingual 30 second countdown should appear at or below the trigger, and connecting the charger should cancel it.
  5. Switch the language: the screen re-renders in the other language and the status card still reads Active.

Rework BatteryRemapper into a module with a Material settings screen, and migrate it
from the legacy Xposed API to the modern libxposed API (102).

New
- Material 3 settings screen: a two-thumb range slider with a live preview of the
  resulting mapping, and Save / Reset pinned to the bottom so the primary action is
  always reachable.
- Module status card: framework name, version and API level, whether
  com.android.systemui is in the module's scope, and whether the module is actually
  loaded in the System UI process (with PID and load state), refreshed while the
  screen is open.
- Configurable mapping window: any physical 0%..100% onto displayed 0%..100%, default
  20%..80% (the original behaviour).
- Configurable shutdown countdown trigger, expressed on the DISPLAYED scale (0%..50%,
  default 0%), with the physical equivalent shown live as the window changes.
- Bilingual settings screen (English + Simplified Chinese) with an in-app switch,
  stored as a per-app language so it also appears in the system app settings.
- The shutdown dialog text is bilingual and comes from the module's own resources.

Changed
- Module entry and metadata now follow the modern API: META-INF/xposed/java_init.list,
  scope.list and module.prop replace assets/xposed_init and the xposed meta-data;
  the module name and description come from android:label / android:description.
- The hook is an interceptor chain on Intent.getIntExtra instead of XC_MethodHook,
  and the module entry class extends XposedModule.
- Settings are shared with the hooked process through LSPosed remote preferences;
  the legacy XSharedPreferences is no longer supported by current LSPosed.
- Battery saver hysteresis is unchanged: forced on at 20% or below of the displayed
  level, off above 50%, always off while charging.

Fixed
- The battery display updates immediately after saving. System UI only recomputes the
  displayed percentage when an ACTION_BATTERY_CHANGED broadcast arrives, so a changed
  window now re-sends the current sticky battery intent, scoped to System UI itself.
- The shutdown countdown cancels reliably when the charger is connected: the module
  watches the battery directly instead of relying on System UI to read a level, and
  cancellation no longer depends on a flag that could disagree with what is on screen.
- The settings screen no longer reports the module as missing after the activity is
  recreated (for example on a language change); the framework service is held
  process-wide.
- Module hooks are deoptimised before being installed (XposedInterface.deoptimize).
  ART inlines small framework methods such as ContextWrapper.attachBaseContext into
  their callers, and without deoptimising, the hook installs and then never runs.
- The module no longer hot-reloads itself on save. XposedService.hotReloadModule()
  loads the module classes through a NEW classloader, so every save left a live ghost
  instance behind with its own configuration, its own battery receiver and its own
  dialog reference. A JVM system property now carries an ownership token, and any
  leftover instance goes inert.

Build
- compileSdk 37, AGP 8.13.2, Gradle 8.13, Java 17. minSdk 29 and targetSdk 34 are
  unchanged. The libxposed 102 artifacts require minCompileSdk 37, which the existing
  CI already installs.
- The module carries an adaptive launcher icon, since it is now a launchable app.

BREAKING CHANGE: the module is built against the modern Xposed API (libxposed 102) and
declares minApiVersion 102, so it requires an LSPosed build that supports the modern
API. It no longer installs on frameworks that only provide the legacy API.
@Rillwyn
Rillwyn force-pushed the feat/settings-ui-1.2.0 branch from 372b1e1 to 68f3e9b Compare September 16, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant