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 version 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).

Refs #4469 <https://github.com/Genymobile/scrcpy/pull/4469>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
This commit is contained in:
Romain Vimont 2024-10-21 18:25:08 +02:00
parent 05c80a036f
commit 3174ce2d5a
2 changed files with 140 additions and 26 deletions

View File

@ -6,12 +6,15 @@ import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
import android.view.IDisplayFoldListener; import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
@ -31,6 +34,13 @@ public class ScreenCapture extends SurfaceCapture {
private IBinder display; private IBinder display;
private VirtualDisplay virtualDisplay; 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 IRotationWatcher rotationWatcher;
private IDisplayFoldListener displayFoldListener; private IDisplayFoldListener displayFoldListener;
@ -44,30 +54,25 @@ public class ScreenCapture extends SurfaceCapture {
@Override @Override
public void init() { public void init() {
if (displayId == 0) { if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
rotationWatcher = new IRotationWatcher.Stub() { registerDisplayListenerFallbacks();
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
} }
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { handlerThread = new HandlerThread("DisplayListener");
displayFoldListener = new IDisplayFoldListener.Stub() { handlerThread.start();
@Override Handler handler = new Handler(handlerThread.getLooper());
public void onDisplayFoldChanged(int displayId, boolean folded) { displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
if (ScreenCapture.this.displayId != displayId) { if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
// Ignore events related to other display ids if (!displayListenerWorks) {
return; // On the first display listener event, we know it works, we can unregister the fallbacks
} displayListenerWorks = true;
unregisterDisplayListenerFallbacks();
requestReset();
} }
}; }
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); if (this.displayId == displayId) {
} requestReset();
}
}, handler);
} }
@Override @Override
@ -137,12 +142,11 @@ public class ScreenCapture extends SurfaceCapture {
@Override @Override
public void release() { public void release() {
if (rotationWatcher != null) { if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); unregisterDisplayListenerFallbacks();
}
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
} }
handlerThread.quitSafely();
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
if (display != null) { if (display != null) {
SurfaceControl.destroyDisplay(display); SurfaceControl.destroyDisplay(display);
display = null; display = null;
@ -182,4 +186,45 @@ public class ScreenCapture extends SurfaceCapture {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }
} }
private void registerDisplayListenerFallbacks() {
if (displayId == 0) {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
Ln.i("=== 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() {
@Override
public void onDisplayFoldChanged(int displayId, boolean 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;
}
}
}
} }

View File

@ -9,17 +9,40 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.os.Handler;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class DisplayManager { 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 final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method createVirtualDisplayMethod; private Method createVirtualDisplayMethod;
@ -137,4 +160,50 @@ public final class DisplayManager {
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); 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);
}
}
} }