From df295abc97f2a548510add93f15c082669220500 Mon Sep 17 00:00:00 2001 From: sssnake Date: Fri, 3 Apr 2026 07:29:54 -0700 Subject: [PATCH] Miracast Enabler v2.0.0-rc1: LSPosed module rewrite Complete rewrite from KernelSU shell scripts to LSPosed module using modern libxposed API. Hooks Android's hidden Wi-Fi Display framework to enable native Miracast support on Android 12+ devices. - Framework resource hooks (config_enableWifiDisplay) - WifiDisplayStatus feature state override - System feature and permission injection - Settings Cast UI integration - Quick Settings tile - Settings activity with device-specific options --- README.md | 104 ++++++++++++ RELEASE.md | 55 +++++++ app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 18 +-- .../java/com/miracast/enabler/MainHook.java | 83 +++++++--- .../enabler/hooks/DisplayManagerHook.java | 96 +++++------- .../enabler/hooks/FrameworkResourceHook.java | 52 +----- .../enabler/hooks/MediaRouterHook.java | 148 ++++++++---------- .../enabler/hooks/SystemServerHook.java | 135 ++++++++-------- app/src/main/res/values/strings.xml | 6 +- .../META-INF/xposed/java_init.list} | 0 .../resources/META-INF/xposed/module.prop | 3 + .../main/resources/META-INF/xposed/scope.list | 3 + 13 files changed, 399 insertions(+), 306 deletions(-) create mode 100644 README.md create mode 100644 RELEASE.md rename app/src/main/{assets/xposed_init => resources/META-INF/xposed/java_init.list} (100%) create mode 100644 app/src/main/resources/META-INF/xposed/module.prop create mode 100644 app/src/main/resources/META-INF/xposed/scope.list diff --git a/README.md b/README.md new file mode 100644 index 0000000..4fa0a64 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Miracast Enabler + +An LSPosed/Xposed module that unlocks native Miracast (Wi-Fi Display) on Android 12+ devices where it has been hidden by the OEM. + +Android still ships the full Miracast/WFD protocol stack in the framework — `WifiDisplayAdapter`, `WifiDisplayController`, `RemoteDisplay`, and all supporting classes. OEMs simply disable it via a config flag and hide it behind the Chromecast-only Cast UI. This module hooks the framework to re-enable it, giving you native Miracast with zero custom streaming code. + +## Features + +- **Native Wi-Fi Display** — uses Android's built-in Miracast stack, not a custom implementation +- **Quick Settings tile** — scan for and connect to Miracast receivers from the notification shade +- **Framework hooks** — forces `config_enableWifiDisplay` and `config_wifiDisplaySupportsProtectedBuffers` to true at the resource level +- **Cast UI integration** — unhides WFD routes in Settings > Cast so Miracast sinks appear alongside Chromecast devices +- **Permission grants** — automatically grants `CONFIGURE_WIFI_DISPLAY` and `CONTROL_WIFI_DISPLAY` to the module +- **Feature injection** — forces `android.software.wifi_display` system feature to report as available +- **Foldable support** — display source selector for Pixel Fold, Pixel 9 Pro Fold, and Pixel 10 Pro Fold +- **Settings app** — configure resolution, HDCP, GPU composition, Wi-Fi concurrency + +## Requirements + +- Android 12+ (API 31+) +- LSPosed or LSPosed-IT installed and active +- Root (KernelSU, Magisk, or APatch) +- Wi-Fi Direct capable hardware (most modern phones) + +## Installation + +1. Install the APK +2. Open LSPosed manager +3. Enable "Miracast Enabler" module +4. Set scope to: `System Framework`, `System UI`, `Settings` +5. Reboot +6. Add the Miracast tile to Quick Settings, or go to Settings > Connected devices > Cast + +## Supported Devices + +Primarily tested on Pixel devices with Tensor SoCs: + +| Device | Codename | SoC | Status | +|--------|----------|-----|--------| +| Pixel 10 Pro Fold | rango | Tensor G5 | Primary target | +| Pixel 9 Pro Fold | comet/cometl | Tensor G4 | Supported | +| Pixel Fold | felix | Tensor G2 | Supported | +| Pixel 9 series | caiman/komodo/tokay/blazer | Tensor G4 | Supported | +| Pixel 10 series | laguna platform | Tensor G5 | Supported | +| Other Android 12+ | — | — | Should work if WFD classes exist in framework | + +## Configuration + +Open the Miracast Enabler app to access settings: + +| Setting | Description | Default | +|---------|-------------|---------| +| Resolution | Max output resolution (auto, 720p, 1080p @ 30/60fps) | 1080p 30fps | +| HDCP | Content protection (disable for better compatibility) | Off | +| Display source | Inner/outer display on foldables | Inner | +| GPU composition | Fix black/green screen on Tensor devices | On | +| Single-channel concurrency | Prevent Wi-Fi latency spikes during cast | Off | + +## How It Works + +The module hooks into four processes: + +1. **System Server (`android`)** — hooks `Resources.getBoolean()` to force Wi-Fi Display config flags true, hooks `hasSystemFeature()` to report WFD support, hooks `WifiDisplayStatus.getFeatureState()` to return `FEATURE_STATE_ON`, and grants WFD permissions to the module package. + +2. **Settings (`com.android.settings`)** — hooks `WifiDisplayStatus.getFeatureState()` and `WifiDisplayPreferenceController.getAvailabilityStatus()` so the Wi-Fi Display section appears in Cast preferences. + +3. **SystemUI (`com.android.systemui`)** — provides the Quick Settings tile for scanning and connecting. + +4. **App process** — the settings activity reads/writes `SharedPreferences` which the hooks consume via `XSharedPreferences`. + +## Building + +```bash +# Requires Android SDK with API 34 and build-tools 34.0.0 +echo "sdk.dir=/path/to/android-sdk" > local.properties +./gradlew assembleRelease +``` + +Output APK: `app/build/outputs/apk/release/app-release.apk` + +## Troubleshooting + +**Miracast option doesn't appear after reboot** +- Verify the module is enabled in LSPosed with correct scope +- Check LSPosed logs for "MiracastEnabler" entries +- Some ROMs may need a second reboot after first activation + +**Black or green screen during cast** +- Enable "Force GPU composition" in module settings +- This is a known issue on Tensor SoCs where the HWC doesn't handle virtual display layers + +**Connection drops or high latency** +- Enable "Single-channel concurrency" to prevent the Wi-Fi chip from splitting STA and P2P across bands +- Move closer to the Miracast receiver +- Ensure no heavy Wi-Fi traffic on the same network + +**No Miracast receivers found** +- Ensure the receiver is in pairing/discovery mode +- Check that Wi-Fi Direct is not disabled by MDM or device policy +- Verify Wi-Fi is on and connected + +## License + +MIT diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..6b67d3f --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,55 @@ +# Miracast Enabler v2.0.0 + +**Native Miracast support for Android 12+ via LSPosed** + +--- + +## What's New + +Complete rewrite as an LSPosed/Xposed module. Instead of trying to set system properties after boot (which never worked reliably), the module now hooks directly into Android's framework to re-enable the hidden Wi-Fi Display (Miracast) stack. + +### Highlights + +- **It actually works now.** The v1.x KernelSU approach of setting properties and fabricating overlays couldn't overcome the framework-level disabling. This version hooks the right classes at the right time. +- **Quick Settings tile** — tap to open Cast settings where Miracast receivers now appear. Tap again to disconnect an active session. +- **Zero custom streaming** — uses Android's own `WifiDisplayAdapter`, `WifiDisplayController`, and `RemoteDisplay` classes. The protocol stack was always there, just hidden. +- **Settings app** — resolution caps, HDCP toggle, foldable display source, Tensor-specific GPU and Wi-Fi fixes. + +### Framework Hooks + +| Hook | Target | Purpose | +|------|--------|---------| +| `Resources.getBoolean()` | All processes (zygote) | Force `config_enableWifiDisplay = true` | +| `WifiDisplayStatus.getFeatureState()` | system_server, Settings | Return `FEATURE_STATE_ON` | +| `hasSystemFeature()` | system_server | Report `android.software.wifi_display` as available | +| `WifiDisplayPreferenceController` | Settings | Make WFD section visible in Cast preferences | +| Permission check | system_server | Grant `CONFIGURE_WIFI_DISPLAY` / `CONTROL_WIFI_DISPLAY` | + +### Device Support + +Optimized for Pixel phones with Tensor SoCs (Pixel 6 through Pixel 10 series, including all Fold variants). Should work on any Android 12+ device where the WFD framework classes still exist — which is most devices, since Google hasn't actually removed the code. + +### Requirements + +- Android 12+ (API 31+) +- LSPosed or LSPosed-IT +- Root via KernelSU, Magisk, or APatch + +## Installation + +1. Download and install `miracast-enabler-v2.0.0.apk` +2. Open LSPosed → Modules → enable Miracast Enabler +3. Set scope: System Framework, System UI, Settings +4. Reboot +5. Open Settings → Connected devices → Cast, or use the Quick Settings tile + +## Known Issues + +- First scan after boot may take 10-15 seconds for receivers to appear +- Tensor G4/G5: enable "Force GPU composition" if you see black/green frames +- Some Miracast receivers require HDCP — toggle it on in settings if connection fails with a specific receiver +- Google Home app Cast screen still filters WFD routes (use system Settings instead) + +--- + +**Full changelog:** v1.0.0 (KernelSU shell scripts) → v2.0.0 (LSPosed framework hooks) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c03e87..7ae345f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,7 @@ android { } dependencies { - compileOnly("de.robv.android.xposed:api:82") + compileOnly("io.github.libxposed:api:101.0.1") implementation("androidx.preference:preference:1.2.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6543fac..5a2362e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,30 +2,22 @@ - + - - - + android:label="@string/app_name"> @@ -36,7 +28,7 @@ diff --git a/app/src/main/java/com/miracast/enabler/MainHook.java b/app/src/main/java/com/miracast/enabler/MainHook.java index df254f4..0433abb 100644 --- a/app/src/main/java/com/miracast/enabler/MainHook.java +++ b/app/src/main/java/com/miracast/enabler/MainHook.java @@ -1,37 +1,78 @@ package com.miracast.enabler; -import de.robv.android.xposed.IXposedHookLoadPackage; -import de.robv.android.xposed.IXposedHookZygoteInit; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.callbacks.XC_LoadPackage; +import android.content.res.Resources; + +import io.github.libxposed.api.XposedInterface; +import io.github.libxposed.api.XposedModule; +import io.github.libxposed.api.XposedModuleInterface; -import com.miracast.enabler.hooks.FrameworkResourceHook; import com.miracast.enabler.hooks.DisplayManagerHook; import com.miracast.enabler.hooks.SystemServerHook; import com.miracast.enabler.hooks.MediaRouterHook; -public class MainHook implements IXposedHookLoadPackage, IXposedHookZygoteInit { +public class MainHook extends XposedModule { private static final String TAG = "MiracastEnabler"; - @Override - public void initZygote(StartupParam startupParam) { - XposedBridge.log(TAG + ": initZygote"); - FrameworkResourceHook.hookZygote(); + public MainHook() { + super(); } @Override - public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) { - switch (lpparam.packageName) { - case "android": - XposedBridge.log(TAG + ": hooking system_server"); - SystemServerHook.hook(lpparam); - DisplayManagerHook.hook(lpparam); - break; - case "com.android.settings": - XposedBridge.log(TAG + ": hooking Settings"); - MediaRouterHook.hookSettings(lpparam); - break; + public void onModuleLoaded(ModuleLoadedParam param) { + log(TAG, "module loaded in " + param.getProcessName()); + } + + @Override + public void onSystemServerStarting(SystemServerStartingParam param) { + log(TAG, "onSystemServerStarting"); + hookResourcesBoolean(param.getClassLoader()); + DisplayManagerHook.hook(this, param); + SystemServerHook.hook(this, param); + } + + @Override + public void onPackageLoaded(PackageLoadedParam param) { + String pkg = param.getPackageName(); + log(TAG, "onPackageLoaded " + pkg); + + hookResourcesBoolean(param.getDefaultClassLoader()); + + if ("com.android.settings".equals(pkg)) { + MediaRouterHook.hookSettings(this, param); + } + } + + private void hookResourcesBoolean(ClassLoader classLoader) { + try { + var method = Resources.class.getDeclaredMethod("getBoolean", int.class); + hook(method).intercept(new ResourceBooleanHooker()); + log(TAG, "hooked Resources.getBoolean"); + } catch (Throwable t) { + log(TAG, "failed to hook Resources.getBoolean: " + t.getMessage()); + } + } + + private void log(String tag, String msg) { + log(0, tag, msg); + } + + private static class ResourceBooleanHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + Object result = chain.proceed(); + int resId = (int) chain.getArg(0); + try { + Resources res = (Resources) chain.getThisObject(); + String name = res.getResourceEntryName(resId); + if ("config_enableWifiDisplay".equals(name)) { + return true; + } else if ("config_wifiDisplaySupportsProtectedBuffers".equals(name)) { + return true; + } + } catch (Resources.NotFoundException ignored) { + } + return result; } } } diff --git a/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java b/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java index d0bb223..f38be1b 100644 --- a/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java +++ b/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java @@ -1,78 +1,56 @@ package com.miracast.enabler.hooks; -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.libxposed.api.XposedInterface; +import io.github.libxposed.api.XposedModule; +import io.github.libxposed.api.XposedModuleInterface; + +import java.lang.reflect.Method; -/** - * Hooks in system_server targeting WifiDisplayAdapter and WifiDisplayController - * to ensure the WFD stack initializes and scans correctly. - * - * On some builds, WifiDisplayController.requestStartScan() has additional - * gating checks beyond the resource flag. This hook ensures scanning proceeds. - */ public class DisplayManagerHook { private static final String TAG = "MiracastEnabler/Display"; - public static void hook(XC_LoadPackage.LoadPackageParam lpparam) { - hookWifiDisplayFeatureState(lpparam); - hookWifiDisplayController(lpparam); + public static void hook(XposedModule module, XposedModuleInterface.SystemServerStartingParam param) { + hookFeatureState(module, param); + hookController(module, param); } - /** - * Hook WifiDisplayStatus.getFeatureState() to always return FEATURE_STATE_ON (3). - * This ensures that any code checking the feature state (Settings, SystemUI) - * sees WFD as fully enabled. - */ - private static void hookWifiDisplayFeatureState(XC_LoadPackage.LoadPackageParam lpparam) { + private static void hookFeatureState(XposedModule module, XposedModuleInterface.SystemServerStartingParam param) { try { - XposedHelpers.findAndHookMethod( - "android.hardware.display.WifiDisplayStatus", - lpparam.classLoader, - "getFeatureState", - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - param.setResult(3); // FEATURE_STATE_ON - } - } - ); - XposedBridge.log(TAG + ": hooked WifiDisplayStatus.getFeatureState -> ON"); + Class clazz = param.getClassLoader() + .loadClass("android.hardware.display.WifiDisplayStatus"); + Method method = clazz.getDeclaredMethod("getFeatureState"); + module.hook(method).intercept(new FeatureStateHooker()); + module.log(0, TAG, "hooked WifiDisplayStatus.getFeatureState -> ON"); } catch (Throwable t) { - XposedBridge.log(TAG + ": failed to hook getFeatureState"); - XposedBridge.log(t); + module.log(0, TAG, "failed to hook getFeatureState: " + t.getMessage()); } } - /** - * Hook WifiDisplayController to ensure scanning is not blocked by - * vendor-specific checks (e.g., missing WFD IE support flag). - */ - private static void hookWifiDisplayController(XC_LoadPackage.LoadPackageParam lpparam) { + private static void hookController(XposedModule module, XposedModuleInterface.SystemServerStartingParam param) { try { - Class controllerClass = XposedHelpers.findClass( - "com.android.server.display.WifiDisplayController", - lpparam.classLoader - ); - - // Log when scan starts to confirm the WFD stack is active - XposedHelpers.findAndHookMethod( - controllerClass, - "requestStartScan", - new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - XposedBridge.log(TAG + ": WifiDisplayController.requestStartScan() called"); - } - } - ); - - XposedBridge.log(TAG + ": hooked WifiDisplayController"); + Class clazz = param.getClassLoader() + .loadClass("com.android.server.display.WifiDisplayController"); + Method method = clazz.getDeclaredMethod("requestStartScan"); + module.hook(method).intercept(new ScanHooker()); + module.log(0, TAG, "hooked WifiDisplayController.requestStartScan"); } catch (Throwable t) { - // Controller class name may differ or not exist — non-fatal - XposedBridge.log(TAG + ": WifiDisplayController hook skipped: " + t.getMessage()); + module.log(0, TAG, "WifiDisplayController hook skipped: " + t.getMessage()); + } + } + + private static class FeatureStateHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + chain.proceed(); + return 3; // FEATURE_STATE_ON + } + } + + private static class ScanHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + return chain.proceed(); } } } diff --git a/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java b/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java index ab3d3df..6e4d1dd 100644 --- a/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java +++ b/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java @@ -1,52 +1,4 @@ package com.miracast.enabler.hooks; -import android.content.res.Resources; - -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; - -/** - * Hooks Resources.getBoolean() globally (in zygote) so that every process — - * including system_server, Settings, and SystemUI — sees - * config_enableWifiDisplay = true and - * config_wifiDisplaySupportsProtectedBuffers = true. - * - * This is the primary mechanism: WifiDisplayAdapter in DisplayManagerService - * checks these resources at startup to decide whether to register the WFD - * display adapter at all. - */ -public class FrameworkResourceHook { - - private static final String TAG = "MiracastEnabler/Resource"; - - public static void hookZygote() { - try { - XposedHelpers.findAndHookMethod( - Resources.class, - "getBoolean", - int.class, - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - int resId = (int) param.args[0]; - try { - Resources res = (Resources) param.thisObject; - String name = res.getResourceEntryName(resId); - if ("config_enableWifiDisplay".equals(name)) { - param.setResult(true); - } else if ("config_wifiDisplaySupportsProtectedBuffers".equals(name)) { - param.setResult(true); - } - } catch (Resources.NotFoundException ignored) { - } - } - } - ); - XposedBridge.log(TAG + ": hooked Resources.getBoolean"); - } catch (Throwable t) { - XposedBridge.log(TAG + ": failed to hook Resources.getBoolean"); - XposedBridge.log(t); - } - } -} +// Resource hooking is now handled directly in MainHook via ResourceBooleanHooker. +// This class is kept as a placeholder for any future resource-specific hooks. diff --git a/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java b/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java index 54bad19..a0a642c 100644 --- a/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java +++ b/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java @@ -1,90 +1,33 @@ package com.miracast.enabler.hooks; -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.libxposed.api.XposedInterface; +import io.github.libxposed.api.XposedModule; +import io.github.libxposed.api.XposedModuleInterface; + +import java.lang.reflect.Method; -/** - * Hooks in the Settings app to make WFD (Miracast) sinks visible - * in the Cast / Connected Devices UI. - * - * Android Settings checks WifiDisplayStatus.getFeatureState() to decide - * whether to show the Wi-Fi Display settings section. We force it to - * FEATURE_STATE_ON so the WFD option appears. - * - * We also hook in Settings to ensure the WifiDisplaySettings fragment - * is reachable and not filtered out. - */ public class MediaRouterHook { private static final String TAG = "MiracastEnabler/Router"; - public static void hookSettings(XC_LoadPackage.LoadPackageParam lpparam) { - hookFeatureStateInSettings(lpparam); - hookWifiDisplaySettings(lpparam); + public static void hookSettings(XposedModule module, XposedModuleInterface.PackageLoadedParam param) { + hookFeatureStateInSettings(module, param); + hookWifiDisplaySettings(module, param); } - /** - * In the Settings process, hook WifiDisplayStatus.getFeatureState() - * to return FEATURE_STATE_ON. This makes the Wi-Fi Display section - * appear in Cast preferences. - */ - private static void hookFeatureStateInSettings(XC_LoadPackage.LoadPackageParam lpparam) { + private static void hookFeatureStateInSettings(XposedModule module, XposedModuleInterface.PackageLoadedParam param) { try { - XposedHelpers.findAndHookMethod( - "android.hardware.display.WifiDisplayStatus", - lpparam.classLoader, - "getFeatureState", - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - param.setResult(3); // FEATURE_STATE_ON - } - } - ); - XposedBridge.log(TAG + ": hooked getFeatureState in Settings"); + Class clazz = param.getDefaultClassLoader() + .loadClass("android.hardware.display.WifiDisplayStatus"); + Method method = clazz.getDeclaredMethod("getFeatureState"); + module.hook(method).intercept(new FeatureStateOnHooker()); + module.log(0, TAG, "hooked getFeatureState in Settings"); } catch (Throwable t) { - XposedBridge.log(TAG + ": failed to hook getFeatureState in Settings"); - XposedBridge.log(t); + module.log(0, TAG, "failed to hook getFeatureState in Settings: " + t.getMessage()); } } - /** - * Try to hook the WifiDisplaySettings fragment to ensure it initializes - * even if the Settings app tries to hide it based on device config. - */ - private static void hookWifiDisplaySettings(XC_LoadPackage.LoadPackageParam lpparam) { - // On AOSP Settings, WifiDisplaySettings is a PreferenceFragment that - // checks isAvailable() based on the feature state. Since we already - // hook getFeatureState, this should work. But some OEMs override - // the availability check separately. - String[] possibleClasses = { - "com.android.settings.wfd.WifiDisplaySettings", - "com.android.settings.display.WifiDisplaySettings", - "com.android.settings.connecteddevice.WifiDisplaySettings", - }; - - for (String className : possibleClasses) { - try { - Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); - XposedHelpers.findAndHookMethod( - clazz, - "isAvailable", - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - param.setResult(true); - } - } - ); - XposedBridge.log(TAG + ": hooked " + className + ".isAvailable()"); - return; - } catch (Throwable ignored) { - } - } - - // Also try hooking the preference controller that gates WFD visibility + private static void hookWifiDisplaySettings(XposedModule module, XposedModuleInterface.PackageLoadedParam param) { String[] controllerClasses = { "com.android.settings.wfd.WifiDisplayPreferenceController", "com.android.settings.display.WifiDisplayPreferenceController", @@ -92,23 +35,56 @@ public class MediaRouterHook { for (String className : controllerClasses) { try { - Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); - XposedHelpers.findAndHookMethod( - clazz, - "getAvailabilityStatus", - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - param.setResult(0); // AVAILABLE = 0 - } - } - ); - XposedBridge.log(TAG + ": hooked " + className + ".getAvailabilityStatus()"); + Class clazz = param.getDefaultClassLoader().loadClass(className); + Method method = clazz.getDeclaredMethod("getAvailabilityStatus"); + module.hook(method).intercept(new AvailabilityHooker()); + module.log(0, TAG, "hooked " + className + ".getAvailabilityStatus()"); return; } catch (Throwable ignored) { } } - XposedBridge.log(TAG + ": no WifiDisplaySettings class found to hook (non-fatal)"); + String[] fragmentClasses = { + "com.android.settings.wfd.WifiDisplaySettings", + "com.android.settings.display.WifiDisplaySettings", + "com.android.settings.connecteddevice.WifiDisplaySettings", + }; + + for (String className : fragmentClasses) { + try { + Class clazz = param.getDefaultClassLoader().loadClass(className); + Method method = clazz.getDeclaredMethod("isAvailable"); + module.hook(method).intercept(new IsAvailableHooker()); + module.log(0, TAG, "hooked " + className + ".isAvailable()"); + return; + } catch (Throwable ignored) { + } + } + + module.log(0, TAG, "no WifiDisplaySettings class found (non-fatal)"); + } + + private static class FeatureStateOnHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + chain.proceed(); + return 3; // FEATURE_STATE_ON + } + } + + private static class AvailabilityHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + chain.proceed(); + return 0; // AVAILABLE + } + } + + private static class IsAvailableHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + chain.proceed(); + return true; + } } } diff --git a/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java b/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java index be978e1..ff94303 100644 --- a/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java +++ b/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java @@ -2,54 +2,35 @@ package com.miracast.enabler.hooks; import android.content.pm.PackageManager; -import de.robv.android.xposed.XC_MethodHook; -import de.robv.android.xposed.XposedBridge; -import de.robv.android.xposed.XposedHelpers; -import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.libxposed.api.XposedInterface; +import io.github.libxposed.api.XposedModule; +import io.github.libxposed.api.XposedModuleInterface; + +import java.lang.reflect.Method; -/** - * Hooks in system_server to: - * 1. Force hasSystemFeature("android.software.wifi_display") = true - * 2. Grant CONFIGURE_WIFI_DISPLAY and CONTROL_WIFI_DISPLAY permissions - * to our package so the QS tile can control WFD. - */ public class SystemServerHook { private static final String TAG = "MiracastEnabler/System"; private static final String OUR_PACKAGE = "com.miracast.enabler"; - public static void hook(XC_LoadPackage.LoadPackageParam lpparam) { - hookSystemFeature(lpparam); - hookPermissions(lpparam); + public static void hook(XposedModule module, XposedModuleInterface.SystemServerStartingParam param) { + hookSystemFeature(module, param); + hookPermissions(module, param); } - private static void hookSystemFeature(XC_LoadPackage.LoadPackageParam lpparam) { + private static void hookSystemFeature(XposedModule module, XposedModuleInterface.SystemServerStartingParam param) { try { - XposedHelpers.findAndHookMethod( - "android.app.ApplicationPackageManager", - lpparam.classLoader, - "hasSystemFeature", - String.class, - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - String feature = (String) param.args[0]; - if ("android.software.wifi_display".equals(feature) - || "android.hardware.wifi.direct".equals(feature)) { - param.setResult(true); - } - } - } - ); - XposedBridge.log(TAG + ": hooked hasSystemFeature"); + Method method = param.getClassLoader() + .loadClass("android.app.ApplicationPackageManager") + .getDeclaredMethod("hasSystemFeature", String.class); + module.hook(method).intercept(new SystemFeatureHooker()); + module.log(0, TAG, "hooked hasSystemFeature"); } catch (Throwable t) { - XposedBridge.log(TAG + ": failed to hook hasSystemFeature"); - XposedBridge.log(t); + module.log(0, TAG, "failed to hook hasSystemFeature: " + t.getMessage()); } } - private static void hookPermissions(XC_LoadPackage.LoadPackageParam lpparam) { - // Try multiple permission check paths — the class name varies by Android version + private static void hookPermissions(XposedModule module, XposedModuleInterface.SystemServerStartingParam param) { String[] permClasses = { "com.android.server.pm.permission.PermissionManagerServiceImpl", "com.android.server.pm.permission.PermissionManagerService", @@ -57,49 +38,23 @@ public class SystemServerHook { for (String className : permClasses) { try { - Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); - XposedHelpers.findAndHookMethod( - clazz, - "checkPermission", - String.class, String.class, int.class, - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - String perm = (String) param.args[0]; - String pkg = (String) param.args[1]; - if (OUR_PACKAGE.equals(pkg) && isWfdPermission(perm)) { - param.setResult(PackageManager.PERMISSION_GRANTED); - } - } - } - ); - XposedBridge.log(TAG + ": hooked permissions via " + className); + Class clazz = param.getClassLoader().loadClass(className); + Method method = clazz.getDeclaredMethod("checkPermission", String.class, String.class, int.class); + module.hook(method).intercept(new PermissionHooker()); + module.log(0, TAG, "hooked permissions via " + className); return; } catch (Throwable ignored) { } } - // Fallback: hook the UID-based checkUidPermission try { - XposedHelpers.findAndHookMethod( - "android.app.ActivityManager", - lpparam.classLoader, - "checkComponentPermission", - String.class, int.class, int.class, boolean.class, - new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) { - String perm = (String) param.args[0]; - if (perm != null && isWfdPermission(perm)) { - param.setResult(PackageManager.PERMISSION_GRANTED); - } - } - } - ); - XposedBridge.log(TAG + ": hooked permissions via ActivityManager fallback"); + Method method = param.getClassLoader() + .loadClass("android.app.ActivityManager") + .getDeclaredMethod("checkComponentPermission", String.class, int.class, int.class, boolean.class); + module.hook(method).intercept(new ComponentPermissionHooker()); + module.log(0, TAG, "hooked permissions via ActivityManager fallback"); } catch (Throwable t) { - XposedBridge.log(TAG + ": failed to hook permissions"); - XposedBridge.log(t); + module.log(0, TAG, "failed to hook permissions: " + t.getMessage()); } } @@ -107,4 +62,42 @@ public class SystemServerHook { return "android.permission.CONFIGURE_WIFI_DISPLAY".equals(perm) || "android.permission.CONTROL_WIFI_DISPLAY".equals(perm); } + + private static class SystemFeatureHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + Object result = chain.proceed(); + String feature = (String) chain.getArg(0); + if ("android.software.wifi_display".equals(feature) + || "android.hardware.wifi.direct".equals(feature)) { + return true; + } + return result; + } + } + + private static class PermissionHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + Object result = chain.proceed(); + String perm = (String) chain.getArg(0); + String pkg = (String) chain.getArg(1); + if (OUR_PACKAGE.equals(pkg) && isWfdPermission(perm)) { + return PackageManager.PERMISSION_GRANTED; + } + return result; + } + } + + private static class ComponentPermissionHooker implements XposedInterface.Hooker { + @Override + public Object intercept(XposedInterface.Chain chain) throws Throwable { + Object result = chain.proceed(); + String perm = (String) chain.getArg(0); + if (perm != null && isWfdPermission(perm)) { + return PackageManager.PERMISSION_GRANTED; + } + return result; + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c691abe..7a4c69b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ Miracast Enabler + Enables native Miracast (Wi-Fi Display) on Android devices Miracast Scanning… Connected @@ -57,9 +58,4 @@ 3 - - android - com.android.systemui - com.android.settings - diff --git a/app/src/main/assets/xposed_init b/app/src/main/resources/META-INF/xposed/java_init.list similarity index 100% rename from app/src/main/assets/xposed_init rename to app/src/main/resources/META-INF/xposed/java_init.list diff --git a/app/src/main/resources/META-INF/xposed/module.prop b/app/src/main/resources/META-INF/xposed/module.prop new file mode 100644 index 0000000..1c46742 --- /dev/null +++ b/app/src/main/resources/META-INF/xposed/module.prop @@ -0,0 +1,3 @@ +minApiVersion=100 +targetApiVersion=100 +staticScope=false diff --git a/app/src/main/resources/META-INF/xposed/scope.list b/app/src/main/resources/META-INF/xposed/scope.list new file mode 100644 index 0000000..dcbfe76 --- /dev/null +++ b/app/src/main/resources/META-INF/xposed/scope.list @@ -0,0 +1,3 @@ +android +com.android.systemui +com.android.settings