Listen to display changed events
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]: <5653c6b587
%5E%21/>
Fixes #161 <https://github.com/Genymobile/scrcpy/issues/161>
Fixes #1918 <https://github.com/Genymobile/scrcpy/issues/1918>
Fixes #4152 <https://github.com/Genymobile/scrcpy/issues/4152>
Fixes #5362 comment <https://github.com/Genymobile/scrcpy/issues/5362#issuecomment-2416219316>
Refs #4469 <https://github.com/Genymobile/scrcpy/pull/4469>
PR #5415 <https://github.com/Genymobile/scrcpy/pull/5415>
Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
This commit is contained in:
parent
04a3e6fb06
commit
e26bdb07a2
@ -52,6 +52,6 @@ public final class Size {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Size{" + "width=" + width + ", height=" + height + '}';
|
return "Size{" + width + 'x' + height + '}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,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;
|
||||||
@ -29,9 +32,19 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
private DisplayInfo displayInfo;
|
private DisplayInfo displayInfo;
|
||||||
private ScreenInfo screenInfo;
|
private ScreenInfo screenInfo;
|
||||||
|
|
||||||
|
// Source display size (before resizing/crop) for the current session
|
||||||
|
private Size sessionDisplaySize;
|
||||||
|
|
||||||
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 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 IRotationWatcher rotationWatcher;
|
||||||
private IDisplayFoldListener displayFoldListener;
|
private IDisplayFoldListener displayFoldListener;
|
||||||
|
|
||||||
@ -45,39 +58,57 @@ 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();
|
||||||
|
Handler handler = new Handler(handlerThread.getLooper());
|
||||||
private boolean first = true;
|
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
|
||||||
|
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
|
||||||
@Override
|
Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")");
|
||||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
}
|
||||||
if (first) {
|
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
||||||
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
|
if (!displayListenerWorks) {
|
||||||
first = false;
|
// On the first display listener event, we know it works, we can unregister the fallbacks
|
||||||
return;
|
displayListenerWorks = true;
|
||||||
}
|
unregisterDisplayListenerFallbacks();
|
||||||
|
|
||||||
if (ScreenCapture.this.displayId != displayId) {
|
|
||||||
// Ignore events related to other display ids
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestReset();
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
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();
|
||||||
|
|
||||||
|
// The field is hidden on purpose, to read it with synchronization
|
||||||
|
@SuppressWarnings("checkstyle:HiddenField")
|
||||||
|
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
|
||||||
|
|
||||||
|
// .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()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -92,6 +123,7 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
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);
|
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +178,19 @@ 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();
|
||||||
|
handlerThread = null;
|
||||||
|
|
||||||
|
// displayListenerHandle may be null if registration failed
|
||||||
|
if (displayListenerHandle != null) {
|
||||||
|
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
|
||||||
|
displayListenerHandle = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display != null) {
|
if (display != null) {
|
||||||
SurfaceControl.destroyDisplay(display);
|
SurfaceControl.destroyDisplay(display);
|
||||||
display = null;
|
display = null;
|
||||||
@ -191,4 +230,67 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
SurfaceControl.closeTransaction();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,40 @@ import android.annotation.SuppressLint;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
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;
|
||||||
private Method requestDisplayPowerMethod;
|
private Method requestDisplayPowerMethod;
|
||||||
@ -158,4 +181,50 @@ public final class DisplayManager {
|
|||||||
return false;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user