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); + } + } }