From df8f666ee4aa5bd3591610a9f7f841b5a5a7bc97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 21 Oct 2024 18:25:08 +0200 Subject: [PATCH] Listen to display changed events Replace RotationWatcher and DisplayFoldListener by a single DisplayListener, which is notified whenever the display size or dpi changes. Still use the old mechanism specifically for Android 14, where DisplayListener may be broken (it is fixed in recent Android 14 upgrades), until we receive the first DisplayListener event (which proves that it works). Fixes #161 Fixes #1918 Fixes #4152 Fixes #5362 comment Refs #4469 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../com/genymobile/scrcpy/device/Size.java | 2 +- .../scrcpy/video/ScreenCapture.java | 157 ++++++++++++++---- .../scrcpy/wrappers/DisplayManager.java | 69 ++++++++ 3 files changed, 195 insertions(+), 33 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 230fd29eb9..558deb00b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -52,6 +52,6 @@ public int hashCode() { @Override public String toString() { - return "Size{" + "width=" + width + ", height=" + height + '}'; + return "Size{" + width + 'x' + height + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 7e5169096e..f435e7f7d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -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; @@ -29,9 +32,18 @@ public class ScreenCapture extends SurfaceCapture { private DisplayInfo displayInfo; private ScreenInfo screenInfo; + private Size sessionSize; + private IBinder display; private VirtualDisplay virtualDisplay; + private DisplayManager.DisplayListenerHandle displayListenerHandle; + private HandlerThread handlerThread; + + // On Android 14, the DisplayListener may be broken (it never send events). This is fixed in recent Android 14 upgrades, but we can't really know. + // 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; @@ -45,39 +57,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(): " + getSessionSize() + " -> (unknown)"); } + setSessionSize(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 sessionSize = getSessionSize(); // synchronized - requestReset(); + // .equals() also works if currentSize == null + if (!size.equals(sessionSize)) { + // Reset only if the size is different + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: requestReset(): " + sessionSize + " -> " + 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()) + setSessionSize(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 @@ -93,6 +123,7 @@ public void prepare() throws ConfigurationException { } screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); + setSessionSize(screenInfo.getVideoSize()); } @Override @@ -146,12 +177,11 @@ public void start(Surface surface) { @Override public void release() { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); - } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + unregisterDisplayListenerFallbacks(); } + handlerThread.quitSafely(); + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -191,4 +221,67 @@ private static void setDisplaySurface(IBinder display, Surface surface, int orie SurfaceControl.closeTransaction(); } } + + private synchronized Size getSessionSize() { + return sessionSize; + } + + private synchronized void setSessionSize(Size sessionSize) { + this.sessionSize = sessionSize; + } + + 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; + } + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index c8c405bb4b..8f5086d827 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -9,17 +9,40 @@ import android.annotation.SuppressLint; 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; @@ -137,4 +160,50 @@ public VirtualDisplay createNewVirtualDisplay(String name, int width, int height android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); } + + 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); + } + } }