From ba00ef0874cddae8b8d9e96c379ff72a152b93f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:41:24 +0000 Subject: [PATCH 1/5] Initial plan From 7199e79ed4aafb00063e846b0974f921b620a2b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:58:50 +0000 Subject: [PATCH 2/5] Add freeform windowing mode and unconditional flags for virtual display desktop mode - Add VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS unconditionally for API 34+ alongside OWN_FOCUS to ensure desktop mode compatibility - Add WindowContainerTransaction-based setDisplayWindowingMode() to WindowManager using reflection to set WINDOWING_MODE_FREEFORM on the virtual display's default task area after creation - Call setDisplayWindowingMode after virtual display creation on API 34+ Co-authored-by: lmichaelwar <20604470+lmichaelwar@users.noreply.github.com> --- .../scrcpy/video/NewDisplayCapture.java | 8 +- .../scrcpy/wrappers/WindowManager.java | 94 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) 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..fde90f27 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; @@ -187,7 +188,8 @@ public class NewDisplayCapture extends SurfaceCapture { | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS - | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; + | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP + | VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; } } virtualDisplay = ServiceManager.getDisplayManager() @@ -199,6 +201,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..6ae4a1f6 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,9 @@ 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; + private final IInterface manager; private Method getRotationMethod; @@ -264,4 +271,91 @@ 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) { + IBinder organizerBinder = new Binder(); + Object organizerProxy = null; + Object daoController = null; + try { + // Get the IWindowOrganizerController + Class serviceManagerClass = Class.forName("android.os.ServiceManager"); + Method getServiceMethod = serviceManagerClass.getDeclaredMethod("getService", String.class); + IBinder wocBinder = (IBinder) getServiceMethod.invoke(null, "window_organizer"); + if (wocBinder == null) { + Ln.w("window_organizer service not available"); + return; + } + + Class iWocStubClass = Class.forName("android.window.IWindowOrganizerController$Stub"); + Object windowOrganizerController = iWocStubClass.getDeclaredMethod("asInterface", IBinder.class).invoke(null, wocBinder); + + // Get the IDisplayAreaOrganizerController + daoController = windowOrganizerController.getClass().getMethod("getDisplayAreaOrganizerController").invoke(windowOrganizerController); + + // Create a no-op IDisplayAreaOrganizer proxy for registration + 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 for FEATURE_DEFAULT_TASK_CONTAINER (1) to get display area tokens + int featureDefaultTaskContainer = 1; + Object parceledList = daoController.getClass() + .getMethod("registerOrganizer", idaoClass, int.class) + .invoke(daoController, organizerProxy, featureDefaultTaskContainer); + + 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); + } + } + } + } } From a6bda88b146f682bb6aba4d5caaf1511129c8f18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 21:00:09 +0000 Subject: [PATCH 3/5] Address code review: extract constant, add documentation comments Co-authored-by: lmichaelwar <20604470+lmichaelwar@users.noreply.github.com> --- .../com/genymobile/scrcpy/wrappers/WindowManager.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 6ae4a1f6..e3ab4768 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -25,6 +25,9 @@ public final class WindowManager { // 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; @@ -280,6 +283,7 @@ public final class WindowManager { @TargetApi(AndroidVersions.API_34_ANDROID_14) @SuppressWarnings("unchecked") public void setDisplayWindowingMode(int displayId, int windowingMode) { + // A Binder token used to identify this organizer during registration/unregistration with the system server IBinder organizerBinder = new Binder(); Object organizerProxy = null; Object daoController = null; @@ -299,7 +303,7 @@ public final class WindowManager { // Get the IDisplayAreaOrganizerController daoController = windowOrganizerController.getClass().getMethod("getDisplayAreaOrganizerController").invoke(windowOrganizerController); - // Create a no-op IDisplayAreaOrganizer proxy for registration + // 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(), @@ -312,11 +316,10 @@ public final class WindowManager { } ); - // Register the organizer for FEATURE_DEFAULT_TASK_CONTAINER (1) to get display area tokens - int featureDefaultTaskContainer = 1; + // Register the organizer to get display area tokens Object parceledList = daoController.getClass() .getMethod("registerOrganizer", idaoClass, int.class) - .invoke(daoController, organizerProxy, featureDefaultTaskContainer); + .invoke(daoController, organizerProxy, FEATURE_DEFAULT_TASK_CONTAINER); List displayAreaInfos = (List) parceledList.getClass().getMethod("getList").invoke(parceledList); From 14ac8040f12d2b4375a6c77645285063515b552a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:36:09 +0000 Subject: [PATCH 4/5] Initial plan From 342444373d89d692b91f1927455d4deb8aa0c4fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:40:04 +0000 Subject: [PATCH 5/5] Address PR #1 review comments: reuse ServiceManager.getService(), fix Binder descriptor, respect vdSystemDecorations flag 1. WindowManager.java: Replace manual reflection for window_organizer service with ServiceManager.getService() to avoid duplicating reflection logic. 2. WindowManager.java: Attach correct AIDL interface descriptor to the organizer Binder and override onTransact to safely handle callbacks. 3. NewDisplayCapture.java: Remove unconditional SHOULD_SHOW_SYSTEM_DECORATIONS from API 34+ block so --no-vd-system-decorations remains effective. Co-authored-by: lmichaelwar <20604470+lmichaelwar@users.noreply.github.com> --- .../scrcpy/video/NewDisplayCapture.java | 3 +- .../scrcpy/wrappers/WindowManager.java | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) 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 fde90f27..226126b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -188,8 +188,7 @@ public class NewDisplayCapture extends SurfaceCapture { | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS - | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP - | VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; } } virtualDisplay = ServiceManager.getDisplayManager() 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 e3ab4768..97cb4352 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -283,23 +283,34 @@ public final class WindowManager { @TargetApi(AndroidVersions.API_34_ANDROID_14) @SuppressWarnings("unchecked") public void setDisplayWindowingMode(int displayId, int windowingMode) { - // A Binder token used to identify this organizer during registration/unregistration with the system server - IBinder organizerBinder = new Binder(); + // 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 - Class serviceManagerClass = Class.forName("android.os.ServiceManager"); - Method getServiceMethod = serviceManagerClass.getDeclaredMethod("getService", String.class); - IBinder wocBinder = (IBinder) getServiceMethod.invoke(null, "window_organizer"); - if (wocBinder == null) { + // 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; } - Class iWocStubClass = Class.forName("android.window.IWindowOrganizerController$Stub"); - Object windowOrganizerController = iWocStubClass.getDeclaredMethod("asInterface", IBinder.class).invoke(null, wocBinder); - // Get the IDisplayAreaOrganizerController daoController = windowOrganizerController.getClass().getMethod("getDisplayAreaOrganizerController").invoke(windowOrganizerController);