Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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;
Expand Down Expand Up @@ -199,6 +200,10 @@ public void startNew(Surface surface) {
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);
Expand Down
108 changes: 108 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
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 {

Expand All @@ -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;

Expand Down Expand Up @@ -264,4 +274,102 @@ public void setDisplayImePolicy(int displayId, int displayImePolicy) {
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<Object> displayAreaInfos = (List<Object>) 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);
}
}
}
}
}