Skip to content

Commit

Permalink
Listen to display changed events
Browse files Browse the repository at this point in the history
Replace RotationWatcher and DisplayFoldListener by a single
DisplayListener, which is notified whenever the display size or dpi
changes.

However, the DisplayListener mechanism is broken in the first versions
of Android 14 (it is fixed in android-14.0.0_r29 by commit [1]), so
continue to use the old mechanism specifically for Android 14 (where
DisplayListener may be broken), until we receive the first
"display changed" event (which proves that it works).

[1]: <https://android.googlesource.com/platform/frameworks/base.git/+/5653c6b5875df599307c3e6bfae32fb2fc17ca1f%5E%21/>

Fixes #161 <#161>
Fixes #1918 <#1918>
Fixes #4152 <#4152>
Fixes #5362 comment <#5362 (comment)>
Refs #4469 <#4469>
PR #5415 <#5415>

Co-authored-by: Simon Chan <[email protected]>
  • Loading branch information
rom1v and yume-chan committed Oct 30, 2024
1 parent 3300efa commit 9730168
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ public int hashCode() {

@Override
public String toString() {
return "Size{" + "width=" + width + ", height=" + height + '}';
return "Size{" + width + 'x' + height + '}';
}
}
164 changes: 133 additions & 31 deletions server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;

import android.graphics.Rect;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
Expand All @@ -29,9 +32,19 @@ public class ScreenCapture extends SurfaceCapture {
private DisplayInfo displayInfo;
private ScreenInfo screenInfo;

// Source display size (before resizing/crop) for the current session
private Size sessionDisplaySize;

private IBinder display;
private VirtualDisplay virtualDisplay;

private DisplayManager.DisplayListenerHandle displayListenerHandle;
private HandlerThread handlerThread;

// On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really
// detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from
// DisplayListener (which proves that it works).
private boolean displayListenerWorks; // only accessed from the display listener thread
private IRotationWatcher rotationWatcher;
private IDisplayFoldListener displayFoldListener;

Expand All @@ -45,39 +58,57 @@ public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSi

@Override
public void init() {
if (displayId == 0) {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
registerDisplayListenerFallbacks();
}

if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
displayFoldListener = new IDisplayFoldListener.Stub() {

private boolean first = true;

@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (first) {
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
first = false;
return;
handlerThread = new HandlerThread("DisplayListener");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")");
}
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
if (!displayListenerWorks) {
// On the first display listener event, we know it works, we can unregister the fallbacks
displayListenerWorks = true;
unregisterDisplayListenerFallbacks();
}
}
if (this.displayId == displayId) {
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (di == null) {
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
// We can't compare with the current size, so reset unconditionally
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
}
setSessionDisplaySize(null);
requestReset();
} else {
Size size = di.getSize();

if (ScreenCapture.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
// The field is hidden on purpose, to read it with synchronization
@SuppressWarnings("checkstyle:HiddenField")
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized

requestReset();
// .equals() also works if sessionDisplaySize == null
if (!size.equals(sessionDisplaySize)) {
// Reset only if the size is different
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size);
}
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
// considers that the current size is the requested size (to avoid a duplicate requestReset())
setSessionDisplaySize(size);
requestReset();
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()");
}
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}
}
}, handler);
}

@Override
Expand All @@ -92,6 +123,7 @@ public void prepare() throws ConfigurationException {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}

setSessionDisplaySize(displayInfo.getSize());
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation);
}

Expand Down Expand Up @@ -146,12 +178,19 @@ public void start(Surface surface) {

@Override
public void release() {
if (rotationWatcher != null) {
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
unregisterDisplayListenerFallbacks();
}
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);

handlerThread.quitSafely();
handlerThread = null;

// displayListenerHandle may be null if registration failed
if (displayListenerHandle != null) {
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
displayListenerHandle = null;
}

if (display != null) {
SurfaceControl.destroyDisplay(display);
display = null;
Expand Down Expand Up @@ -191,4 +230,67 @@ private static void setDisplaySurface(IBinder display, Surface surface, int orie
SurfaceControl.closeTransaction();
}
}

private synchronized Size getSessionDisplaySize() {
return sessionDisplaySize;
}

private synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
this.sessionDisplaySize = sessionDisplaySize;
}

private void registerDisplayListenerFallbacks() {
if (displayId == 0) {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")");
}
requestReset();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}

// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
displayFoldListener = new IDisplayFoldListener.Stub() {

private boolean first = true;

@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (first) {
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
first = false;
return;
}

if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")");
}

if (ScreenCapture.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
requestReset();
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}

private void unregisterDisplayListenerFallbacks() {
synchronized (this) {
if (rotationWatcher != null) {
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
rotationWatcher = null;
}
if (displayFoldListener != null) {
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
displayFoldListener = null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,40 @@
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.display.VirtualDisplay;
import android.os.Handler;
import android.view.Display;
import android.view.Surface;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class DisplayManager {

// android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2;

public interface DisplayListener {
/**
* Called whenever the properties of a logical {@link android.view.Display},
* such as size and density, have changed.
*
* @param displayId The id of the logical display that changed.
*/
void onDisplayChanged(int displayId);
}

public static final class DisplayListenerHandle {
private final Object displayListenerProxy;
private DisplayListenerHandle(Object displayListenerProxy) {
this.displayListenerProxy = displayListenerProxy;
}
}

private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod;
Expand Down Expand Up @@ -158,4 +181,50 @@ public boolean requestDisplayPower(int displayId, boolean on) {
return false;
}
}

public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) {
try {
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
Object displayListenerProxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[] {displayListenerClass},
(proxy, method, args) -> {
if ("onDisplayChanged".equals(method.getName())) {
listener.onDisplayChanged((int) args[0]);
}
return null;
});
try {
manager.getClass()
.getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class, String.class)
.invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED, FakeContext.PACKAGE_NAME);
} catch (NoSuchMethodException e) {
try {
manager.getClass()
.getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class)
.invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED);
} catch (NoSuchMethodException e2) {
manager.getClass()
.getMethod("registerDisplayListener", displayListenerClass, Handler.class)
.invoke(manager, displayListenerProxy, handler);
}
}

return new DisplayListenerHandle(displayListenerProxy);
} catch (Exception e) {
// Rotation and screen size won't be updated, not a fatal error
Ln.e("Could not register display listener", e);
}

return null;
}

public void unregisterDisplayListener(DisplayListenerHandle listener) {
try {
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy);
} catch (Exception e) {
Ln.e("Could not unregister display listener", e);
}
}
}

0 comments on commit 9730168

Please sign in to comment.