Skip to content

fix: adapt hooks to HyperOS 3.3 (Android 17) - #1669

Open
mexusbg wants to merge 10 commits into
ReChronoRain:mainfrom
mexusbg:fix/hyperos-3.3-android-17-compat
Open

fix: adapt hooks to HyperOS 3.3 (Android 17)#1669
mexusbg wants to merge 10 commits into
ReChronoRain:mainfrom
mexusbg:fix/hyperos-3.3-android-17-compat

Conversation

@mexusbg

@mexusbg mexusbg commented Aug 5, 2026

Copy link
Copy Markdown

Problem

On HyperOS 3.3 / Android 17 (SDK 37) the module was unusable: the app exited on launch, and once the version gate was lifted a number of SystemUI features were broken or rendered twice.

Most of it traces back to one change: HyperOS 3.3's SystemUI is built with R8 constructor inlining, so several hooked classes have no <init> left in the dex — the constructor body is folded into its caller as new-instance + iput:

.method public final getMiuiOperatorConfig(I)L…$OperatorConfig;
    new-instance v13, L…$OperatorConfig;
    iput-boolean v6, v13, …->hideVolte:Z      ← no invoke-direct <init>

Constructor-based hooks therefore fail — loudly for explicit lookups, and silently for hookAllConstructors, which matches zero targets without logging anything. That silent case is what made the duplicated dual-row signal hard to spot.

Approach

Each rule moves to an equivalent non-constructor entry point. Every new branch is selected by runtime capability detection (declaredConstructors.isNotEmpty(), field/method-name fallbacks, declared-type probes), always attempting the original path first, so behaviour on older releases is unchanged rather than gated on an SDK number.

Fixed

SystemUI

Rule Cause Fix
HideVoWiFiIcon, MobileTypeSingle2Hook OperatorConfig ctor inlined into getMiuiOperatorConfig; constructors[0] threw ArrayIndexOutOfBoundsException(length=0) hook that method's result
MobilePublicHookV MiuiCellularIconVM has no ctor, so SIM 2's isVisible was never replaced and the dual row was drawn once per mobile view handle in MiuiMobileIconBinder#bind; resolve subId via subscriptionId
MobileTypeSingle2Hook same, plus bind passes a MiuiMobileIconVMImpl wrapper whose flows are cold ChannelFlowTransformLatest unwrap to the inner VM
StateFlowHelper ReadonlyStateFlow.$$delegate_0 is declared StateFlow again, so the exact-type MutableStateFlow lookup missed fall back to StateFlow
JavaAdapter alwaysCollectFlow(Flow, Consumer) now returns void; casting to a non-null Job threw NPE and collection never started use the static overload with applicationScope when the instance method is void
StatusBarIcon block lists are public static final and reject reflective writes, aborting init() lists are already mutated in place, so the write-back is no longer fatal
PluginFactory mComponentName renamed to componentName; the throw was swallowed into "Failed to create plugin context.", silently disabling every miui.systemui.plugin feature try both names; NewPluginHelperKt now logs the throwable

Launcher

WorkspacePadding picked its class with versionCode < 600000000 ? NEW : OLD, inverted with respect to the @Version(min = 600000000) mapping used by the other home rules, so recent launchers resolved a class that no longer declares getWorkspaceCellPadding*. It now picks whichever class actually declares the getters instead of trusting a version code, and handles the Initinit rename.

Graceful skips

These targets are simply absent from the ROM, so they now skip with a debug line instead of throwing:

  • RemoveSIMLockSuccessDialogcom.miui.simlock is not in the global Security Center build
  • DisableThemeAdNew — the built-in ad model was replaced by third-party SDKs
  • AllowThirdTheme, DisableUploadAppListNew — DexKit anchor strings are gone; optionalMember skips a miss instead of throwing
  • VariousThirdAppsandroid.os.Build's final fields cannot be written reflectively on Android 17 (clearing Field.accessFlags does not help either, as finality is enforced below that mirror), so it reports an unsupported-platform warning rather than an error

Also adds Android 17 / HyperOS 3.3 to the supported version list as SUPPORT_PARTIAL.

Behaviour changes worth reviewing

  • StateFlowHelper.resolveDelegateFlow and JavaAdapter.startCollect return null on lookup failure instead of throwing, so callers no-op rather than aborting. JavaAdapter.alwaysCollectFlow is consequently KotlinJob?.
  • AllowThirdTheme / DisableUploadAppListNew no longer surface "Skip hook because initDexKit failed"; the hook is skipped just as before, only quietly.

Testing

Verified on a Xiaomi nezha_eea (2512BPNDAG), Android 17 / HyperOS OS3.0.332.0.XPAEUXM, SDK 37, LSPosed (KernelSU).

  • module launches; VoLTE/VoWiFi hiding confirmed via MiuiOperatorCustomizedPolicy dumping hideVolte=true, hideVowifi=true for every slot
  • dual-row signal renders once, with per-SIM levels cross-checked against dumpsys telephony.registry (miuiLevel → icon level) and confirmed to track each SIM independently
  • launcher, Security Center, Theme Store, Camera, Settings, Gallery, Notes restarted and swept: no Hook Failed, Class not found, Skip hook or plugin-context errors remain
  • assembleDebug passes

Not tested on OS 2.x / OS 3.0 hardware — the compatibility argument there rests on the old path being attempted first in every branch, which is worth a second pair of eyes.

HyperOS 3.3's SystemUI is built with R8 constructor inlining, so several
hooked classes have no <init> left in the dex (the constructor body is
folded into the caller as new-instance plus iput). Constructor-based hooks
therefore fail: loudly for explicit lookups, and silently for
hookAllConstructors, which matches zero targets without logging anything.

Rules are switched to equivalent non-constructor entry points, and every
new branch is chosen by runtime capability detection with the old path
tried first, so behaviour on older releases is unchanged.

SystemUI:
- HideVoWiFiIcon / MobileTypeSingle2Hook: OperatorConfig's constructor was
  inlined into MiuiOperatorCustomizedPolicy#getMiuiOperatorConfig; hook that
  method's result instead of constructors[0], which threw
  ArrayIndexOutOfBoundsException(length=0).
- MobilePublicHookV: MiuiCellularIconVM has no constructor, so the second
  SIM's isVisible was never replaced and the dual-row signal was drawn once
  per mobile view (rendered twice). Handle it in MiuiMobileIconBinder#bind,
  and resolve subId via subscriptionId, which MobileIconInteractorImpl uses.
- MobileTypeSingle2Hook: same bind-based fallback, unwrapping the
  MiuiMobileIconVMImpl wrapper to the inner MiuiCellularIconVM whose flows
  are still ReadonlyStateFlow.
- StateFlowHelper: ReadonlyStateFlow.$$delegate_0 is declared StateFlow
  again, so the exact-type lookup for MutableStateFlow no longer matched.
- JavaAdapter: alwaysCollectFlow(Flow, Consumer) now returns void, so the
  previous cast to a non-null Job threw NullPointerException and collection
  was never established. Call the static overload with applicationScope when
  the instance method is void so a cancellable Job is still available.
- StatusBarIcon: RIGHT_BLOCK_LIST / CONTROL_CENTER_BLOCK_LIST are
  public static final and reject reflective writes. The lists are already
  mutated in place, so the write-back is no longer fatal.
- PluginFactory: mComponentName was renamed to componentName. Reading only
  the old name threw, and the caller swallowed it into "Failed to create
  plugin context.", silently disabling every miui.systemui.plugin feature.
  NewPluginHelperKt now logs the throwable so this is diagnosable.

Launcher:
- WorkspacePadding: the DeviceConfig/DeviceConfigs choice was inverted with
  respect to the @Version(min = 600000000) mapping used by the other home
  rules, so recent launchers resolved a class without the padding getters.
  Pick whichever class actually declares them instead of trusting a version
  code, and handle the Init -> init rename.

Graceful skips where the target no longer exists in the ROM:
- RemoveSIMLockSuccessDialog: com.miui.simlock is absent from the global
  Security Center build.
- DisableThemeAdNew: the built-in ad model was replaced by third-party SDKs.
- AllowThirdTheme / DisableUploadAppListNew: DexKit anchor strings are gone;
  use optionalMember so a miss is skipped instead of throwing.
- VariousThirdApps: android.os.Build's final fields cannot be written
  reflectively on Android 17, so report it as an unsupported-platform
  warning rather than an error.

Also add Android 17 / HyperOS 3.3 to the supported version list.
@codacy-production

codacy-production Bot commented Aug 5, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 44 complexity

Metric Results
Complexity 44

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

mexusbg added 9 commits August 5, 2026 18:22
getOpenWithApps() builds the "Selected apps" list for both the clean
share menu and clean open menu pickers. Three of its four queries pass
the "HyperCeiler" bypass extra so CleanShareMenu / CleanOpenMenu skip
filtering, but the ACTION_SEND query did not.

CleanShareMenu strips every already selected package from ACTION_SEND
resolution, so the picker asked the same question through its own hook.
Any app whose only matching filter is ACTION_SEND vanished from the
picker as soon as it was selected: it could no longer be reviewed or
deselected, and the saved configuration looked lost even though the
share menu was being cleaned correctly.

Pass the bypass extra on the ACTION_SEND query as well. On a HyperOS 3.3
device with 23 selected packages the picker went from 136 to 144 entries,
with the eight recovered apps correctly shown as checked.
Codacy reported 26 new issues on the HyperOS 3.3 compatibility changes.
No behaviour changes; verified on a HyperOS 3.3 device after rebuilding
(single dual-row signal stack, one 5G label with Wi-Fi off, VoLTE hidden,
no hook failures in the LSPosed module log).

CompareObjectsWithEquals (High): isFinalFieldRejection walked the cause
chain and guarded against a self-referential cause with a reference
comparison. Bound the walk to MAX_CAUSE_DEPTH instead, which removes the
comparison and covers longer cycles too.

LabeledExpression / NestedBlockDepth: guard clauses inside hook lambdas
used labelled returns. Move each lambda body into a private function that
takes the value it needs, so plain returns work: applyOnBind in
MobilePublicHookV and MobileTypeSingle2Hook, bindConstructedResult for
the constructAndBind intercept, and applyOperatorConfig now accepting a
nullable config. applyToViewModel also accepts a nullable interactor so
the constructor hook no longer needs a labelled return, and the two
forEach loops that used return@forEach became for loops with continue.
The repeated flow-proxy wiring in applyToViewModel moved into
installDataSimProxy, which brings the nesting back under the limit.

StringLiteralDuplication: extract the repeated reflection field and view
id names into private constants.

CommentOverPrivateFunction / CommentOverPrivateProperty: the explanations
above private members stay, as line comments rather than KDoc.

UndocumentedPublicFunction: document JavaAdapter.alwaysCollectFlow,
including what a null return means.

UnnecessaryFullyQualifiedName: use the imported BaseHook name.
PMD's FieldDeclarationsShouldBeAtStartOfClass flagged the constant added
in the previous commit, which sat next to the method that uses it instead
of at the top of the class.
settings.gradle.kts requires credentials for the GitHub Packages Maven
repository on every build, but the only step that exports GIT_ACTOR and
GIT_TOKEN is "Create Sign File", which is gated on pushes to main. Every
pull request build therefore failed during settings evaluation with
"Missing GitHub credentials", before compiling anything.

The package is published to maven.pkg.github.com/ReChronoRain/HyperCeiler,
which is this same repository, so the automatic GITHUB_TOKEN can read it
once the job declares packages: read. That token is also available to pull
requests from forks, where the GIT_TOKEN secret is withheld by design, so
contributor builds work as well.

Pushes to main are unchanged and keep using the GIT_ACTOR / GIT_TOKEN
secrets exported by "Create Sign File".
The option had no effect on HyperOS 3. HideNavigationBar, which implements
it, is only registered in HomePhoneOld / HomePadOld (maxOSVersion = 2.0F)
and SystemUIV (maxSdk = 35), so on an OS 3 device neither the OS 3 launcher
class nor SystemUIB ever loads it.

Registering the existing hook for OS 3 is not the answer. It drives
NavStubView#mHideGestureLine, but that field feeds isImmersive,
getHotSpaceHeight, isNeedAdjustTouchArea and canPerformQuickSwitch, and
NavStubView#setHideGestureLine relayouts the gesture window. It does hide
the line, at the cost of resizing the gesture hot space, which breaks swipe
navigation and the long press that starts Circle to Search.

Hide the line by drawing instead. SystemUI paints it from
color/navigation_bar_home_handle_{light,dark}_color, so replacing both with
a fully transparent colour leaves the handle at its stock size while making
it invisible, and every touch region keeps the geometry the system computed.

dimen/navigation_handle_radius is deliberately left alone: forcing it to
zero also hides the line, but it feeds hit testing too, and a zero sized
handle stops Circle to Search.

Registered in SystemUIB only, so behaviour on older releases is unchanged,
and skipped when "customize gesture line" is on so a user's own thickness
and colour are not overwritten.

Verified on HyperOS 3.3 (Android 17): line hidden, swipe navigation and
Circle to Search both still working.
HyperOS 3 replaced the fragment based status bar with the HomeStatusBar
view binder pipeline, so MiuiCollapsedStatusBarFragment no longer exists
and the onViewCreated hook never fired. Fall back to
MiuiPhoneStatusBarView#onFinishInflate, which is the same view the
fragment used to expose as its root, and keep the fragment hook for
builds that still have it.
- DisableThermal: ThermalManagerService moved to
  com.android.server.power.thermal and the dispatch method is now
  postEventListenerLocked; hook both names and keep the old class as a
  fallback.
- AnimDurationRatio: DeviceLevelUtils moved from
  com.miui.home.launcher.common to com.miui.home.common.utils, and
  loadClass() aborted the whole hook when it was absent.
- MoreNotificationSettings: the modal controller is exposed as
  IModalController, so look that up before the old ModalController.
- AddAppManagerEntry: ManageApplications moved into the
  manageapplications subpackage; use the public
  ACTION_MANAGE_APPLICATIONS_SETTINGS intent instead of a class name.
…ckend

Android 17 replaced PermissionManagerServiceImpl with the access based
AppIdPermissionPolicy, and the method gained a leading MutateStateScope
parameter, so the package state is now argument 1 rather than argument 0.
Pick the class and the argument index together and keep the legacy pair
for older releases.
miui_channel_notification_settings.xml calls the badge checkbox
"setting_badge", so removeDefaultPrefs hid a key the filter never matched
and the row stayed missing while importance and allow_keyguard came back.
Match both keys so older builds keep working.

Also drop the IModalController lookup added earlier: MiuiNotificationMenuRow
no longer declares onClickInfoItem on HyperOS 3, so that handler is
unreachable and the change had no effect.
@mexusbg
mexusbg force-pushed the fix/hyperos-3.3-android-17-compat branch from b96100b to 2c51309 Compare August 6, 2026 22:04
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