diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index e933f60e..226126b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -13,6 +13,7 @@ import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.hardware.display.VirtualDisplay; @@ -199,6 +200,10 @@ public class NewDisplayCapture extends SurfaceCapture { ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); } + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + ServiceManager.getWindowManager().setDisplayWindowingMode(virtualDisplayId, WindowManager.WINDOWING_MODE_FREEFORM); + } + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 7ba5cc06..97cb4352 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,11 +4,15 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; +import android.os.Binder; import android.os.Build; +import android.os.IBinder; import android.os.IInterface; import android.view.IDisplayWindowListener; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; public final class WindowManager { @@ -18,6 +22,12 @@ public final class WindowManager { public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; public static final int DISPLAY_IME_POLICY_HIDE = 2; + // android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM + public static final int WINDOWING_MODE_FREEFORM = 5; + + // android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER + private static final int FEATURE_DEFAULT_TASK_CONTAINER = 1; + private final IInterface manager; private Method getRotationMethod; @@ -264,4 +274,102 @@ public final class WindowManager { Ln.e("Could not invoke method", e); } } + + /** + * Set the windowing mode of a display's default task area via WindowContainerTransaction. + * + * This is required for Android desktop mode to operate in freeform windowing mode on virtual displays. + */ + @TargetApi(AndroidVersions.API_34_ANDROID_14) + @SuppressWarnings("unchecked") + public void setDisplayWindowingMode(int displayId, int windowingMode) { + // A Binder token with the correct AIDL interface descriptor, so that any system server + // callbacks during registration are properly identified and silently handled + IBinder organizerBinder = new Binder() { + { + attachInterface(null, "android.window.IDisplayAreaOrganizer"); + } + + @Override + protected boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) + throws android.os.RemoteException { + if (super.onTransact(code, data, reply, flags)) { + return true; + } + // Silently accept any organizer callback transactions + return true; + } + }; + Object organizerProxy = null; + Object daoController = null; + try { + // Get the IWindowOrganizerController via ServiceManager + IInterface windowOrganizerController = ServiceManager.getService("window_organizer", + "android.window.IWindowOrganizerController"); + if (windowOrganizerController == null) { + Ln.w("window_organizer service not available"); + return; + } + + // Get the IDisplayAreaOrganizerController + daoController = windowOrganizerController.getClass().getMethod("getDisplayAreaOrganizerController").invoke(windowOrganizerController); + + // Create a no-op IDisplayAreaOrganizer proxy for registration; callbacks are ignored since we unregister immediately + Class idaoClass = Class.forName("android.window.IDisplayAreaOrganizer"); + organizerProxy = Proxy.newProxyInstance( + ClassLoader.getSystemClassLoader(), + new Class[]{idaoClass}, + (proxy, method, args) -> { + if ("asBinder".equals(method.getName())) { + return organizerBinder; + } + return null; + } + ); + + // Register the organizer to get display area tokens + Object parceledList = daoController.getClass() + .getMethod("registerOrganizer", idaoClass, int.class) + .invoke(daoController, organizerProxy, FEATURE_DEFAULT_TASK_CONTAINER); + + List displayAreaInfos = (List) parceledList.getClass().getMethod("getList").invoke(parceledList); + + // Find the display area token matching our virtual display + Object targetToken = null; + for (Object info : displayAreaInfos) { + Object displayAreaInfo = info.getClass().getMethod("getDisplayAreaInfo").invoke(info); + int daDisplayId = displayAreaInfo.getClass().getDeclaredField("displayId").getInt(displayAreaInfo); + if (daDisplayId == displayId) { + targetToken = displayAreaInfo.getClass().getDeclaredField("token").get(displayAreaInfo); + break; + } + } + + if (targetToken != null) { + // Create a WindowContainerTransaction to set the windowing mode + Class wctClass = Class.forName("android.window.WindowContainerTransaction"); + Object wct = wctClass.getDeclaredConstructor().newInstance(); + + Class tokenClass = Class.forName("android.window.WindowContainerToken"); + wctClass.getMethod("setWindowingMode", tokenClass, int.class).invoke(wct, targetToken, windowingMode); + + // Apply the transaction + windowOrganizerController.getClass().getMethod("applyTransaction", wctClass).invoke(windowOrganizerController, wct); + } else { + Ln.w("Could not find display area for display " + displayId); + } + } catch (Exception e) { + Ln.w("Could not set windowing mode for display " + displayId, e); + } finally { + // Always unregister the organizer + if (organizerProxy != null && daoController != null) { + try { + Class idaoClass = Class.forName("android.window.IDisplayAreaOrganizer"); + daoController.getClass().getMethod("unregisterOrganizer", idaoClass).invoke(daoController, organizerProxy); + } catch (Exception e) { + Ln.w("Could not unregister display area organizer", e); + } + } + } + } }