Imported from YifePlayte/WOMMO (
AGENTS.md). Install upstream withnpx skills add YifePlayte/WOMMO. Copyright stays with the author.
AGENTS.md
Project Context
WOMMO is a personal LSPosed module (API 102) that patches MIUI/HyperOS system components. One APK ships two independent hook families:
- Java/Kotlin hooks under
app/src/main/java/com/yifeplayte/wommo/hook/for regular Android processes that have dex code, built on ezhooktool. - Native hooks under
app/src/main/cpp/for the HyperOS 4 Flutter launcher (com.miui.home), whose business logic is a Dart AOT snapshot (libapp.so). That APK contains no dex, so Java hooks cannot reach it.
When a feature exists on both launcher generations, both implementations share one preference key: the Java hook covers the legacy launcher, the native hook covers the Flutter one, and the settings UI switch is the same.
Repository Layout
app/src/main/java/com/yifeplayte/wommo/
App.kt XposedService binding, UI process
hook/MainHook.kt the only XposedModule; lifecycle
hook/hooks/Base*.kt hook base classes
hook/hooks/singlepackage/ one group object per host package
hook/hooks/singlepackage/<group>/ BaseHook implementations (lowercase)
hook/hooks/multipackage/*.kt BaseMultiHook implementations
hook/hooks/subpackage/ BaseSubPackage + BaseSubHook groups
hook/utils/ Log, DexKit, preferences, fields...
activity/sections/*.kt settings UI; keys match hook keys
utils/ Build, Clazz, Object, Terminal...
app/src/main/cpp/
classhelper/ pre-existing JNI helper library
wommo_native.cpp native_init entry; hook dispatch
dart_image.{h,cpp} image lookup, pattern scan, patch
lsposed_hook_backend.* LSPosed native API wrapper
log.h WOMMO_LOGW / WOMMO_LOGE only
hooks/hooks.h native feature hook declarations
hooks/*_hook.cpp one file per native feature
app/src/main/resources/META-INF/xposed/ module.prop, java_init.list,
native_init.list, scope.list
classhelper is a separate pre-existing JNI helper. Do not rename it, merge it
into wommo_native, or change its exported symbols; Kotlin code loads it by
name (utils/Object.kt).
Java/Dex Hook System
Entry and lifecycle
META-INF/xposed/java_init.listdeclarescom.yifeplayte.wommo.hook.MainHookas the module entry.MainHooklifecycle:onModuleLoaded: setEzReflect.logger = Log, callEzXposed.initOnModuleLoaded(this, param), then install hooks onEzXposed.onTargetReady.onPackageLoaded/onPackageReady: initializeEzXposedfor the host package;onPackageReadyalso initializes DexKit with the host APK path for every package exceptsystem.onSystemServerStarting: system_server support.onHotReloading/onHotReloaded: delegate toEzXposed.handleHotReloading/handleHotReloadedWithTargetReady; after a reload the hooks are re-installed so fresh preference values are read.
installHooks()iterates the three reflection registries (singlePackagesHooked,multiPackagesHooked,subPackagesHooked), callsinit()on each, then closes DexKit.PACKAGE_NAME_HOOKEDis the union of all registry package names. It decides which packages receiveonPackageLoaded/onPackageReadyand powers the "restart all scope" dialog (killall <pkg>via libsu for every hooked package exceptandroid).- The settings UI calls
reloadAllTargets()after a switch change, which asksXposedService.hotReloadModuleto reload every running target process.
Hook kinds
| Base | Scope | Pick when |
|---|---|---|
BaseHook |
one package | normal single-process feature |
BasePackage |
one package | group object; owns the BaseHook list |
BaseMultiHook |
several packages | the same logic is installed per package through hooks: Map<String, () -> Unit> |
BaseSubHook |
one sub-package | feature lives in a plugin with its own ClassLoader |
BaseSubPackage |
one sub-package | group object; resolves the plugin ClassLoader |
- Every hook has a
key(snake_case feature identifier).isEnabledreads the remote preferencegetBoolean(key, false), so a hook installs only when the user enabled it. init()is single-shot (isInitguard) and wrapshook()inrunCatching, logging failures instead of crashing the host.
Registry scanning and directory conventions
ClassScanner.scanObjectOf<T>(packageName)reflects the module's own dex elements and returns every singleton (INSTANCEfield) assignable toTwhose class lives directly inpackageName(inner classes are skipped).BasePackagegroups hooks from the packagejavaClass.packageName + "." + javaClass.simpleName.lowercase(). Example:singlepackage/Home.kt(object Home : BasePackage("com.miui.home")) owns the hooks insinglepackage/home/.- The same convention applies to
BaseSubPackage. Group object names are CamelCase, their hook packages are all-lowercase. - A new group object only needs to exist;
MainHookdiscovers it.
Adding a Java hook
- Choose the group:
singlepackage(one package),multipackage(several), or asubpackageplugin. Reuse an existing group object when one covers the host package; otherwise add aBasePackage/BaseMultiHook/BaseSubPackage. - Add
object <Feature> : BaseHook()(orBaseSubHook) in the group's lowercase package, overridekeyandhook(). - Implement with the ezhooktool DSL:
loadClass(...),findMethod { name(...); paramCount(...); ... },.createHook { before { it.args[...] = ... } / after { ... } },returnConstant(...). - Add a UI switch/slider in the matching
activity/sections/*.ktwith the same key (SPSwitch/SPSlider) and a string resource inres/values. - When reimplementing an existing feature (for example on a new launcher generation), reuse the existing key so the same switch keeps working.
Tooling
- ezhooktool core helpers used everywhere:
loadClass,findMethod,findAllMethods,createHook/createHooks,callMethod,getFieldOrNull,putField,putStaticField,paramsAssignableFrom,notAbstract. - DexKit (
hook/utils/DexKit.kt) locates obfuscated members when names are unavailable:dexKitBridge.findClass { matcher { usingStrings = listOf(...) } }/findMethod { ... }, then.getInstance()/.getMethodInstance()which applyEzXposed.safeClassLoader..single()throws on ambiguity, so prefer stable markers (unique strings) and log clearly on failure. hook/utils/AdditionalFields.ktreplaces XposedHelpers additional instance fields with a weak identity map. Use it instead of holding strong references.utils/Object.ktloadslibclasshelperand exposesinvokeSuper*Methodfor calling super implementations from hooks.utils/Clazz.ktcan force static final fields throughUnsafewhen reflection refuses; only use it when a normal hook cannot work.hook/utils/Log.ktwrites to logcat (tagWOMMO) and to the libxposed log; useLog.i/w/efrom hooks.utils/Build.ktexposesIS_HYPER_OS,HYPER_OS_VERSION,IS_TABLET,IS_INTERNATIONAL_BUILDfor version-gated paths (seeSystemUIPlugin.initClassLoader()for the branching pattern).- Settings live in the remote preferences file
config(XSharedPreferenceson the hook side,SharedPreferences.mSPon the UI side). Hook code must read them atinit()time (or after hot reload), not cache them forever.
Native Hook System (Flutter MiuiHome)
Entry and lifecycle
META-INF/xposed/native_init.listdeclareslibwommo_native.so, the single native entry. Do not add one.soper feature.- LSPosed's HYOS entry initializes in the root spawner (cmdline
usap64, exe/system_ext/bin/hyos_spawner) before the launcher child is forked.native_initaccepts the spawner family and the launcher process, but business hooks only install from thelibapp.soload callback insidecom.miui.home. - Image verification is handle-bound and fail closed (see
AcquireLauncherImageinwommo_native.cpp):- the process must be exactly
com.miui.home(spawner family only passesnative_init, never the library callback); - the LSPosed-reported library handle must export
_kDartIsolateSnapshotInstructions; dladdron that symbol must report a mapping path that is either/data/app/.../base.apk!/lib/arm64-v8a/libapp.soor/product/priv-app/MiuiHome/.../MiuiHome.apk!/lib/arm64-v8a/libapp.so;AcquireImageAtBasemust find the same ELF image again by load bias so the segments are pinned to the verified handle. Never fall back to a looselibapp.soname match for patching.
- the process must be exactly
Patch persistence: madvise guard
- HyperOS memory cleanup can issue
madvise(MADV_DONTNEED)over the mapped launcher image. That drops the modified copy-on-write code pages and the kernel re-reads the original bytes, silently reverting every patch. lsposed_hook_backend.cpphooks libc'smadvisethrough LSPosed'shookFuncand removes registered hook pages fromMADV_DONTNEEDranges (GuardedMadvise). Page registration happens before the patch is written to close the concurrent-cleanup race.dart::PatchCodecallsProtectHookRangebefore touching the code. A range that cannot be protected is not patched: the guard is a hard prerequisite, not a best effort.- Verify the hook is live by reading the
madviseentry in/proc/<pid>/mem: the original bionic prologue (bti c; mov x8, #__NR_madvise; svc #0) is replaced by aldr x17 / br x17trampoline.
Adding a feature hook
- Create
app/src/main/cpp/hooks/<feature>_hook.cppwith an idempotent installer takingconst wommo::dart::Image&. - Declare it in
hooks/hooks.h. - Register
{ "<feature>", &wommo::hooks::Install<Feature>Hook }inkDartHooks[]inwommo_native.cpp. - Add the source to
WOMMO_NATIVE_SOURCESincpp/CMakeLists.txt. - Installers must stay idempotent across the repeated
libapp.soload callbacks: skip the fingerprint scan once the original hook pointer is set, otherwise the rescan fails because the function now starts with the hook jump. - When a feature needs symbols from another launcher library (for example
the lazily loaded
librust_maml_sdk.so), do not load it from thelibapp.socallback. Add a per-library installer (seeInstallBackHomeRatioSdkHooks) and call it fromOnLibraryLoadedwith the LSPosed handle;dlopen(RTLD_NOLOAD)fails inside that callback. Protect every hooked address withProtectHookRangefirst.
Dart runtime hooks (runtime values)
Some features need values that only exist while Dart code runs (gesture
geometry, live window rects). Dart AOT uses x15 as its frame/stack
pointer, so a Dart function must never be replaced by a plain C function:
the C compiler clobbers x15 and the caller's frame is corrupted. Use a
naked assembly trampoline instead (back_home_ratio_hook.cpp is the
reference):
- save
x0..x18,x30andd0..d7on the realsp, call a C recorder with the registers it needs (passx28too when the recorder reads compressed pointers), restore every register andbrto the LSPosedoriginalpointer; - to capture a return value,
blrthe original first, keepx0on the stack, feed it to the recorder and restore it beforeret; - read Dart fields at
offset + 7, rebuild compressed pointers aslow32 | (x28 << 32)and treat boxed values as objects. Validate every captured value (range, finiteness, freshness timestamp) and fail closed; object layouts are not guaranteed across launcher builds.
Patching rules
- Locate code by instruction fingerprint, never by launcher version offsets. A fingerprint must match exactly once in every supported libapp.so build. Missing or ambiguous matches fail closed: log a warning and keep the original launcher behavior.
- Pool-page operands (
add x17, x27, #page,ldr ..., [x17, #imm]) differ between builds. Use wildcardPatternWordentries for them and keep the rest exact. - Patch the smallest possible unit. Prefer bypassing the one failing branch
(
nop/retat a verified site) over rewriting shared behavior. Do not force data fields that other code paths depend on (for example, do not setisMIUIWidgetglobally just to pass the drag-to-PA gate). - Write through
dart::PatchCode: it manages mprotect, restores RX, clears the instruction cache, and reads the words back. Never patch without read-back verification. - Version-specific offsets discovered during reverse engineering are evidence, not constants. Keep them in local notes; only fingerprints belong in code.
- Known gap: the native hooks install unconditionally. Wiring them to the shared module preference keys (the same keys the Java hooks and UI use) is pending.
Logging
HyperOS logd filters INFO/DEBUG from the launcher process. Use WOMMO_LOGW /
WOMMO_LOGE (tag WommoNative) for everything. Never use android.util.Log
from native code; keep diagnostics in logcat under the module tag or in the
LSPosed logs.
Feature hooks mute their runtime logs by default so animation hot paths never
pay for IO: each hook file defines a local WOMMO_<FEATURE>_LOGW/LOGE macro
that expands to ((void)0) under a commented
// #define WOMMO_<FEATURE>_DEBUG line. Uncomment that line to get the logs
back when debugging on device. The entry and backend (wommo_native.cpp,
lsposed_hook_backend.cpp) keep their one-shot startup WARN logs because the
deploy workflow checks them.
Device and Testing Workflow
Target device: Redmi 25102RKBEC (myron), HyperOS 4 / Android 17, 4K pages,
LSPosed 2.2.0 (API 102).
Build (debug is the iteration target, release for delivery):
./gradlew :app:assembleDebug
Deploy Java hooks: adb install -r <apk>, then restart the affected scope
processes (the in-app "restart all scope" dialog runs killall per hooked
package, or do it manually with adb/root). Verify with adb logcat | grep WOMMO and the module log.
Deploy native hooks:
adb install -r app/build/outputs/apk/debug/WOMMO-*.apk- Kill the desktop's HYOS spawner so a fresh one loads the new APK. Find it by
checking that
readlink /proc/<pid>/exeis/system_ext/bin/hyos_spawner;kill -TERM <pid>and the launcher restarts automatically. - If the hook logs are missing, the launcher was forked from a spawner that
did not load the module. Kill the launcher process (
pidof com.miui.home) so it refork from a module-loaded spawner. - Verify:
adb logcat -d | grep WommoNativeand check the reported file offset. Then verify the patched words in/proc/<pid>/memas root. - Confirm the launcher is stable (no new tombstone) before visible tests. User-visible behavior must be confirmed on the device by the user.
Cross-version testing (native): keep both launcher APKs (system
/product/priv-app/MiuiHome/MiuiHome.apk and the current /data/app update).
adb install -r <apk> switches versions. Every new fingerprint must resolve
uniquely and patch correctly on both before the change is considered done.
Meta field offsets must not be hardcoded (they differ per launcher build).
Gesture paths (recents/back-home) only run on a fast diagonal swipe from the
bottom edge (input swipe <x> 2560 <x2> 1950 <150-400>); a slow or straight
swipe and a HOME key event return home without engaging the close animation.
The Flutter launcher exposes no uiautomator hierarchy (null root node), so
locate grid icons by tapping and reading mCurrentFocus instead of
uiautomator dump.
LSPosed scope is controlled only through:
su -c '/data/adb/modules/zygisk_lsposed/lspctl scope add|remove|list <module> <pkg> --user=0'
Recovery: Launcher Safe Mode
A crash loop makes the launcher start in safe mode (com.miui.home:safe_mode
and libapp.so is not loaded). Fix it by reinstalling the launcher APK with
adb install -r <MiuiHome.apk>; do not delete data, files, or properties.
Keep the previous known-good MiuiHome APK on hand before every native test.
Reverse Engineering Toolkit
libapp.sohas a.gnu_debugdatasection: XZ-compressed ELF containing the full Dart symbol table. Decompress and usereadelf -sWfor function names and offsets.- Dart AOT facts:
x27is the object pool pointer,x28 << 32supplies the high bits when decompressing 32-bit pointers,x22holds the null object, and the bool singletons arex22+0x20(true) andx22+0x30(false), so a bool condition often appears astbnz/tbz wN, #4.x15is the Dart frame/stack pointer (see the Dart runtime hooks section). - Maml icons and widgets are rendered by
librust_maml_sdk.so: external commands go through the exportedsend_command(id, ...)and variables throughput_variable_number(id, ...), so those symbols are the native boundary for anything maml-related. - The applied icon theme (
/data/system/theme/icons) also carries maml fancy icons underlayer_animating_icons/<pkg>/<n>/{fancy,quiet}/; built-in dynamic icons live in/system/media/theme/default/dynamicicons. Copy the device file before analysing it, local copies can lag behind. - To observe the pool or objects at runtime, read the target process memory as
root (
/proc/<pid>/mem); a small static NDK scanner is enough. - Strings in the APK are authoritative for user-visible behavior (toasts, log messages); Dart symbol names are authoritative for logic.
- jadx is useless for the Flutter launcher (no dex) but remains the right tool for any legacy launcher APK and for the module's own Java side.
Code and Commit Conventions
- Language: code comments and commit messages in English.
- Commits: a single short English line describing the change, matching the
existing
git logstyle. Commit only when asked; never push unless asked. - This project is GPL-3.0 with no per-file license headers. Do not import SPDX/license headers from reference projects; if code is adapted, keep a short attribution comment instead of a foreign license identifier.
- Do not add comments to code unless they explain a non-obvious contract (for example why a fingerprint is shaped the way it is).
Operating Boundaries
- Never write Android system properties, even with already-present values.
- Never modify files under
/data/adb/modulesor install helpers under/system/bin. - Keep the static scope minimal; do not add new packages to
scope.listfor a feature unless its process genuinely must be hooked. - A native patch must never be enabled unless its fingerprint and target range were both verified; fail closed and look like stock behavior otherwise.
- Keep the Java and native hook families independent: a failure in one must not affect the other, and new features should be added to whichever family owns the target process.
