diff --git a/.gitignore b/.gitignore index ef0bccc..430b431 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ .claude/ aapt2-build/ -tools/aapt-cmake-build/ +tools/ +*.iml +.idea/ +.gradle/ +build/ +app/build/ +local.properties +*.apk +*.aab +*.jks +*.keystore diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..3c03e87 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("com.android.application") +} + +android { + namespace = "com.miracast.enabler" + compileSdk = 34 + + defaultConfig { + applicationId = "com.miracast.enabler" + minSdk = 31 + targetSdk = 34 + versionCode = 200 + versionName = "2.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + compileOnly("de.robv.android.xposed:api:82") + 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 new file mode 100644 index 0000000..6543fac --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init new file mode 100644 index 0000000..32c6d23 --- /dev/null +++ b/app/src/main/assets/xposed_init @@ -0,0 +1 @@ +com.miracast.enabler.MainHook diff --git a/app/src/main/java/com/miracast/enabler/MainHook.java b/app/src/main/java/com/miracast/enabler/MainHook.java new file mode 100644 index 0000000..df254f4 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/MainHook.java @@ -0,0 +1,37 @@ +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 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 { + + private static final String TAG = "MiracastEnabler"; + + @Override + public void initZygote(StartupParam startupParam) { + XposedBridge.log(TAG + ": initZygote"); + FrameworkResourceHook.hookZygote(); + } + + @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; + } + } +} diff --git a/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java b/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java new file mode 100644 index 0000000..d0bb223 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/hooks/DisplayManagerHook.java @@ -0,0 +1,78 @@ +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; + +/** + * 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); + } + + /** + * 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) { + 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"); + } catch (Throwable t) { + XposedBridge.log(TAG + ": failed to hook getFeatureState"); + XposedBridge.log(t); + } + } + + /** + * 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) { + 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"); + } catch (Throwable t) { + // Controller class name may differ or not exist — non-fatal + XposedBridge.log(TAG + ": WifiDisplayController hook skipped: " + t.getMessage()); + } + } +} diff --git a/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java b/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java new file mode 100644 index 0000000..ab3d3df --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/hooks/FrameworkResourceHook.java @@ -0,0 +1,52 @@ +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); + } + } +} diff --git a/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java b/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java new file mode 100644 index 0000000..54bad19 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/hooks/MediaRouterHook.java @@ -0,0 +1,114 @@ +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; + +/** + * 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); + } + + /** + * 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) { + 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"); + } catch (Throwable t) { + XposedBridge.log(TAG + ": failed to hook getFeatureState in Settings"); + XposedBridge.log(t); + } + } + + /** + * 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 + String[] controllerClasses = { + "com.android.settings.wfd.WifiDisplayPreferenceController", + "com.android.settings.display.WifiDisplayPreferenceController", + }; + + 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()"); + return; + } catch (Throwable ignored) { + } + } + + XposedBridge.log(TAG + ": no WifiDisplaySettings class found to hook (non-fatal)"); + } +} diff --git a/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java b/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java new file mode 100644 index 0000000..be978e1 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/hooks/SystemServerHook.java @@ -0,0 +1,110 @@ +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; + +/** + * 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); + } + + private static void hookSystemFeature(XC_LoadPackage.LoadPackageParam lpparam) { + 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"); + } catch (Throwable t) { + XposedBridge.log(TAG + ": failed to hook hasSystemFeature"); + XposedBridge.log(t); + } + } + + private static void hookPermissions(XC_LoadPackage.LoadPackageParam lpparam) { + // Try multiple permission check paths — the class name varies by Android version + String[] permClasses = { + "com.android.server.pm.permission.PermissionManagerServiceImpl", + "com.android.server.pm.permission.PermissionManagerService", + }; + + 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); + 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"); + } catch (Throwable t) { + XposedBridge.log(TAG + ": failed to hook permissions"); + XposedBridge.log(t); + } + } + + private static boolean isWfdPermission(String perm) { + return "android.permission.CONFIGURE_WIFI_DISPLAY".equals(perm) + || "android.permission.CONTROL_WIFI_DISPLAY".equals(perm); + } +} diff --git a/app/src/main/java/com/miracast/enabler/tile/MiracastTileService.java b/app/src/main/java/com/miracast/enabler/tile/MiracastTileService.java new file mode 100644 index 0000000..a327704 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/tile/MiracastTileService.java @@ -0,0 +1,81 @@ +package com.miracast.enabler.tile; + +import android.content.Context; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; + +import com.miracast.enabler.util.WfdManager; + +/** + * Quick Settings tile for Miracast. + * + * Tap behavior: + * - If disconnected: opens the Cast settings screen (where WFD sinks appear) + * - If connected: disconnects the current WFD session + * + * The tile state reflects the current WFD connection status. + */ +public class MiracastTileService extends TileService { + + private WfdManager wfdManager; + + @Override + public void onCreate() { + super.onCreate(); + wfdManager = new WfdManager(this); + } + + @Override + public void onStartListening() { + super.onStartListening(); + updateTileState(); + } + + @Override + public void onClick() { + super.onClick(); + + int state = wfdManager.getConnectionState(); + if (state == WfdManager.STATE_CONNECTED) { + wfdManager.disconnect(); + updateTileState(); + } else { + // Open Cast settings where WFD sinks will now appear + Intent intent = new Intent("android.settings.CAST_SETTINGS"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityAndCollapse(intent); + } + } + + @Override + public void onStopListening() { + super.onStopListening(); + } + + private void updateTileState() { + Tile tile = getQsTile(); + if (tile == null) return; + + int state = wfdManager.getConnectionState(); + switch (state) { + case WfdManager.STATE_CONNECTED: + tile.setState(Tile.STATE_ACTIVE); + String displayName = wfdManager.getActiveDisplayName(); + tile.setSubtitle(displayName != null ? displayName : "Connected"); + break; + case WfdManager.STATE_CONNECTING: + tile.setState(Tile.STATE_ACTIVE); + tile.setSubtitle("Connecting..."); + break; + default: + tile.setState(Tile.STATE_INACTIVE); + tile.setSubtitle(null); + break; + } + + tile.updateTile(); + } +} diff --git a/app/src/main/java/com/miracast/enabler/ui/SettingsActivity.java b/app/src/main/java/com/miracast/enabler/ui/SettingsActivity.java new file mode 100644 index 0000000..e9b5995 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/ui/SettingsActivity.java @@ -0,0 +1,73 @@ +package com.miracast.enabler.ui; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import com.miracast.enabler.R; + +public class SettingsActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(android.R.id.content, new SettingsFragment()) + .commit(); + } + } + + public static class SettingsFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + // World-readable so XSharedPreferences can read from hook processes + getPreferenceManager().setSharedPreferencesMode(Context.MODE_WORLD_READABLE); + setPreferencesFromResource(R.xml.preferences, rootKey); + + setupDisplaySource(); + setupDeviceInfo(); + } + + private void setupDisplaySource() { + // Only show display source option on foldable devices + PreferenceCategory displayCategory = findPreference("display_category"); + if (displayCategory == null) return; + + String device = Build.DEVICE; + boolean isFoldable = "rango".equals(device) + || "comet".equals(device) + || "cometl".equals(device) + || "felix".equals(device); + + if (!isFoldable) { + getPreferenceScreen().removePreference(displayCategory); + } + } + + private void setupDeviceInfo() { + Preference devicePref = findPreference("status_device"); + if (devicePref != null) { + String info = Build.MODEL + " (" + Build.DEVICE + ")" + + "\nAndroid " + Build.VERSION.RELEASE + + " (API " + Build.VERSION.SDK_INT + ")" + + "\nSoC: " + Build.SOC_MODEL; + devicePref.setSummary(info); + } + + Preference hookPref = findPreference("status_hooks"); + if (hookPref != null) { + hookPref.setSummary("Reboot after enabling module in LSPosed"); + } + } + } +} diff --git a/app/src/main/java/com/miracast/enabler/util/WfdManager.java b/app/src/main/java/com/miracast/enabler/util/WfdManager.java new file mode 100644 index 0000000..dd62f21 --- /dev/null +++ b/app/src/main/java/com/miracast/enabler/util/WfdManager.java @@ -0,0 +1,159 @@ +package com.miracast.enabler.util; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.util.Log; + +import java.lang.reflect.Method; + +/** + * Wrapper around Android's hidden DisplayManager Wi-Fi Display APIs. + * Uses reflection since these methods are @hide. + * + * Key hidden methods on DisplayManager: + * - connectWifiDisplay(String address) + * - disconnectWifiDisplay() + * - startWifiDisplayScan() + * - stopWifiDisplayScan() + * - getWifiDisplayStatus() -> WifiDisplayStatus + * + * WifiDisplayStatus has: + * - getFeatureState() -> int (0=unavail, 1=disabled, 2=off, 3=on) + * - getActiveDisplay() -> WifiDisplay (or null) + * - getAvailableDisplays() -> WifiDisplay[] + * - getScanState() -> int (0=not scanning, 1=scanning) + * - getActiveDisplayState() -> int (0=disconnected, 1=connecting, 2=connected) + */ +public class WfdManager { + + private static final String TAG = "MiracastEnabler/Wfd"; + + public static final int STATE_DISCONNECTED = 0; + public static final int STATE_CONNECTING = 1; + public static final int STATE_CONNECTED = 2; + + private final DisplayManager displayManager; + + public WfdManager(Context context) { + displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + } + + public void startScan() { + try { + Method m = DisplayManager.class.getMethod("startWifiDisplayScan"); + m.invoke(displayManager); + } catch (Throwable t) { + Log.e(TAG, "startWifiDisplayScan failed", t); + } + } + + public void stopScan() { + try { + Method m = DisplayManager.class.getMethod("stopWifiDisplayScan"); + m.invoke(displayManager); + } catch (Throwable t) { + Log.e(TAG, "stopWifiDisplayScan failed", t); + } + } + + public void connect(String address) { + try { + Method m = DisplayManager.class.getMethod("connectWifiDisplay", String.class); + m.invoke(displayManager, address); + } catch (Throwable t) { + Log.e(TAG, "connectWifiDisplay failed", t); + } + } + + public void disconnect() { + try { + Method m = DisplayManager.class.getMethod("disconnectWifiDisplay"); + m.invoke(displayManager); + } catch (Throwable t) { + Log.e(TAG, "disconnectWifiDisplay failed", t); + } + } + + /** + * Returns the current WFD connection state: + * STATE_DISCONNECTED (0), STATE_CONNECTING (1), or STATE_CONNECTED (2) + */ + public int getConnectionState() { + try { + Object status = getWifiDisplayStatus(); + if (status == null) return STATE_DISCONNECTED; + Method m = status.getClass().getMethod("getActiveDisplayState"); + return (int) m.invoke(status); + } catch (Throwable t) { + Log.e(TAG, "getConnectionState failed", t); + return STATE_DISCONNECTED; + } + } + + /** + * Returns the friendly name of the currently connected display, or null. + */ + public String getActiveDisplayName() { + try { + Object status = getWifiDisplayStatus(); + if (status == null) return null; + Method getActive = status.getClass().getMethod("getActiveDisplay"); + Object display = getActive.invoke(status); + if (display == null) return null; + Method getName = display.getClass().getMethod("getFriendlyDisplayName"); + return (String) getName.invoke(display); + } catch (Throwable t) { + Log.e(TAG, "getActiveDisplayName failed", t); + return null; + } + } + + /** + * Returns an array of available WifiDisplay objects, or null. + */ + public Object[] getAvailableDisplays() { + try { + Object status = getWifiDisplayStatus(); + if (status == null) return null; + Method m = status.getClass().getMethod("getAvailableDisplays"); + return (Object[]) m.invoke(status); + } catch (Throwable t) { + Log.e(TAG, "getAvailableDisplays failed", t); + return null; + } + } + + /** + * Get the device address from a WifiDisplay object. + */ + public static String getDisplayAddress(Object wifiDisplay) { + try { + Method m = wifiDisplay.getClass().getMethod("getDeviceAddress"); + return (String) m.invoke(wifiDisplay); + } catch (Throwable t) { + return null; + } + } + + /** + * Get the friendly name from a WifiDisplay object. + */ + public static String getDisplayName(Object wifiDisplay) { + try { + Method m = wifiDisplay.getClass().getMethod("getFriendlyDisplayName"); + return (String) m.invoke(wifiDisplay); + } catch (Throwable t) { + return null; + } + } + + private Object getWifiDisplayStatus() { + try { + Method m = DisplayManager.class.getMethod("getWifiDisplayStatus"); + return m.invoke(displayManager); + } catch (Throwable t) { + Log.e(TAG, "getWifiDisplayStatus failed", t); + return null; + } + } +} diff --git a/app/src/main/res/drawable/ic_cast.xml b/app/src/main/res/drawable/ic_cast.xml new file mode 100644 index 0000000..9f8869e --- /dev/null +++ b/app/src/main/res/drawable/ic_cast.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..db3373b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.xml b/app/src/main/res/mipmap-hdpi/ic_launcher.xml new file mode 100644 index 0000000..a8a8fa5 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..936f22b --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #1565C0 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c691abe --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,65 @@ + + + Miracast Enabler + Miracast + Scanning… + Connected + Disconnected + + + General + Module enabled + Enable Miracast Wi-Fi Display hooks + Resolution + Maximum output resolution + + Security + HDCP + Enable HDCP content protection (disable for better compatibility) + + Display Source + Display source + Select which display to mirror (foldable devices) + + Advanced + Force GPU composition + Fix black/green screen on Tensor devices + Single-channel concurrency + Prevent Wi-Fi latency spikes during Miracast + P2P GO intent + Wi-Fi Direct Group Owner intent (0-15, higher = prefer GO) + + Status + Hook status + Device info + + + Auto + 1920x1080 60fps + 1920x1080 30fps + 1280x720 60fps + 1280x720 30fps + + + 0 + 5 + 7 + 3 + 4 + + + + Inner display + Outer display + + + 0 + 3 + + + + android + com.android.systemui + com.android.settings + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml new file mode 100644 index 0000000..dea949a --- /dev/null +++ b/app/src/main/res/xml/preferences.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..06a72c3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("com.android.application") version "8.2.2" apply false +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f991a87 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048m diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..4844ffe Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a595206 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..17a9170 --- /dev/null +++ b/gradlew @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +if $JAVACMD --add-opens java.base/java.lang=ALL-UNNAMED -version ; then + DEFAULT_JVM_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED $DEFAULT_JVM_OPTS" +fi + +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..ef34b74 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "MiracastEnabler" +include(":app")