diff --git a/.pc/.quilt_patches b/.pc/.quilt_patches new file mode 100644 index 00000000..6857a8d4 --- /dev/null +++ b/.pc/.quilt_patches @@ -0,0 +1 @@ +debian/patches diff --git a/.pc/.quilt_series b/.pc/.quilt_series new file mode 100644 index 00000000..c2067066 --- /dev/null +++ b/.pc/.quilt_series @@ -0,0 +1 @@ +series diff --git a/.pc/.version b/.pc/.version new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/.pc/.version @@ -0,0 +1 @@ +2 diff --git a/.pc/0001-No-Android-Q.patch/.timestamp b/.pc/0001-No-Android-Q.patch/.timestamp new file mode 100644 index 00000000..e69de29b diff --git a/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/Controller.java b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/Controller.java new file mode 100644 index 00000000..9100a9db --- /dev/null +++ b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -0,0 +1,271 @@ +package com.genymobile.scrcpy; + +import android.os.Build; +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class Controller { + + private static final int DEVICE_ID_VIRTUAL = -1; + + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + + private final Device device; + private final DesktopConnection connection; + private final DeviceMessageSender sender; + + private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + + private long lastTouchDown; + private final PointersState pointersState = new PointersState(); + private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; + private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + + private boolean keepPowerModeOff; + + public Controller(Device device, DesktopConnection connection) { + this.device = device; + this.connection = connection; + initPointers(); + sender = new DeviceMessageSender(connection); + } + + private void initPointers() { + for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { + MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); + props.toolType = MotionEvent.TOOL_TYPE_FINGER; + + MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + coords.orientation = 0; + coords.size = 1; + + pointerProperties[i] = props; + pointerCoords[i] = coords; + } + } + + public void control() throws IOException { + // on start, power on the device + if (!device.isScreenOn()) { + device.injectKeycode(KeyEvent.KEYCODE_POWER); + + // dirty hack + // After POWER is injected, the device is powered on asynchronously. + // To turn the device screen off while mirroring, the client will send a message that + // would be handled before the device is actually powered on, so its effect would + // be "canceled" once the device is turned back on. + // Adding this delay prevents to handle the message before the device is actually + // powered on. + SystemClock.sleep(500); + } + + while (true) { + handleEvent(); + } + } + + public DeviceMessageSender getSender() { + return sender; + } + + private void handleEvent() throws IOException { + ControlMessage msg = connection.receiveControlMessage(); + switch (msg.getType()) { + case ControlMessage.TYPE_INJECT_KEYCODE: + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); + } + break; + case ControlMessage.TYPE_INJECT_TEXT: + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } + break; + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } + break; + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } + break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(); + } + break; + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + device.expandNotificationPanel(); + break; + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + device.collapsePanels(); + break; + case ControlMessage.TYPE_GET_CLIPBOARD: + String clipboardText = device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } + break; + case ControlMessage.TYPE_SET_CLIPBOARD: + setClipboard(msg.getText(), msg.getPaste()); + break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + if (device.supportsInputEvents()) { + int mode = msg.getAction(); + boolean setPowerModeOk = Device.setScreenPowerMode(mode); + if (setPowerModeOk) { + keepPowerModeOff = mode == Device.POWER_MODE_OFF; + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + } + break; + case ControlMessage.TYPE_ROTATE_DEVICE: + device.rotateDevice(); + break; + default: + // do nothing + } + } + + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { + if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + schedulePowerModeOff(); + } + return device.injectKeyEvent(action, keycode, repeat, metaState); + } + + private boolean injectChar(char c) { + String decomposed = KeyComposition.decompose(c); + char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c}; + KeyEvent[] events = charMap.getEvents(chars); + if (events == null) { + return false; + } + for (KeyEvent event : events) { + if (!device.injectEvent(event)) { + return false; + } + } + return true; + } + + private int injectText(String text) { + int successCount = 0; + for (char c : text.toCharArray()) { + if (!injectChar(c)) { + Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); + continue; + } + successCount++; + } + return successCount; + } + + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { + long now = SystemClock.uptimeMillis(); + + Point point = device.getPhysicalPoint(position); + if (point == null) { + Ln.w("Ignore touch event, it was generated for a different device size"); + return false; + } + + int pointerIndex = pointersState.getPointerIndex(pointerId); + if (pointerIndex == -1) { + Ln.w("Too many pointers for touch event"); + return false; + } + Pointer pointer = pointersState.get(pointerIndex); + pointer.setPoint(point); + pointer.setPressure(pressure); + pointer.setUp(action == MotionEvent.ACTION_UP); + + int pointerCount = pointersState.update(pointerProperties, pointerCoords); + + if (pointerCount == 1) { + if (action == MotionEvent.ACTION_DOWN) { + lastTouchDown = now; + } + } else { + // secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex + if (action == MotionEvent.ACTION_UP) { + action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } else if (action == MotionEvent.ACTION_DOWN) { + action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } + } + + MotionEvent event = MotionEvent + .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, + InputDevice.SOURCE_TOUCHSCREEN, 0); + return device.injectEvent(event); + } + + private boolean injectScroll(Position position, int hScroll, int vScroll) { + long now = SystemClock.uptimeMillis(); + Point point = device.getPhysicalPoint(position); + if (point == null) { + // ignore event + return false; + } + + MotionEvent.PointerProperties props = pointerProperties[0]; + props.id = 0; + + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.x = point.getX(); + coords.y = point.getY(); + coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); + coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + + MotionEvent event = MotionEvent + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, + InputDevice.SOURCE_TOUCHSCREEN, 0); + return device.injectEvent(event); + } + + /** + * Schedule a call to set power mode to off after a small delay. + */ + private static void schedulePowerModeOff() { + EXECUTOR.schedule(new Runnable() { + @Override + public void run() { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); + } + }, 200, TimeUnit.MILLISECONDS); + } + + private boolean pressBackOrTurnScreenOn() { + int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; + if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) { + schedulePowerModeOff(); + } + return device.injectKeycode(keycode); + } + + private boolean setClipboard(String text, boolean paste) { + boolean ok = device.setClipboardText(text); + if (ok) { + Ln.i("Device clipboard set"); + } + + // On Android >= 7, also press the PASTE key if requested + if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + device.injectKeycode(KeyEvent.KEYCODE_PASTE); + } + + return ok; + } +} diff --git a/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/Device.java b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/Device.java new file mode 100644 index 00000000..f23dd056 --- /dev/null +++ b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -0,0 +1,276 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ClipboardManager; +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.InputManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.SurfaceControl; +import com.genymobile.scrcpy.wrappers.WindowManager; + +import android.content.IOnPrimaryClipChangedListener; +import android.graphics.Rect; +import android.os.Build; +import android.os.IBinder; +import android.os.SystemClock; +import android.view.IRotationWatcher; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import java.util.concurrent.atomic.AtomicBoolean; + +public final class Device { + + public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; + public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + + public interface RotationListener { + void onRotationChanged(int rotation); + } + + public interface ClipboardListener { + void onClipboardTextChanged(String text); + } + + private final ServiceManager serviceManager = new ServiceManager(); + + private ScreenInfo screenInfo; + private RotationListener rotationListener; + private ClipboardListener clipboardListener; + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + private final boolean supportsInputEvents; + + public Device(Options options) { + displayId = options.getDisplayId(); + DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + + serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + synchronized (Device.this) { + screenInfo = screenInfo.withDeviceRotation(rotation); + + // notify + if (rotationListener != null) { + rotationListener.onRotationChanged(rotation); + } + } + } + }, displayId); + + if (options.getControl()) { + // If control is enabled, synchronize Android clipboard to the computer automatically + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } + } + } + } + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } + } + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } + } + + public synchronized ScreenInfo getScreenInfo() { + return screenInfo; + } + + public int getLayerStack() { + return layerStack; + } + + public Point getPhysicalPoint(Position position) { + // it hides the field on purpose, to read it with a lock + @SuppressWarnings("checkstyle:HiddenField") + ScreenInfo screenInfo = getScreenInfo(); // read with synchronization + + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!unlockedVideoSize.equals(clientVideoSize)) { + // The client sends a click relative to a video with wrong dimensions, + // the device may have been rotated since the event was generated, so ignore the event + return null; + } + Rect contentRect = screenInfo.getContentRect(); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); + return new Point(convertedX, convertedY); + } + + public static String getDeviceName() { + return Build.MODEL; + } + + public boolean supportsInputEvents() { + return supportsInputEvents; + } + + public boolean injectEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + + return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); + } + + public boolean injectEvent(InputEvent event) { + return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } + + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + return injectEvent(event); + } + + public boolean injectKeycode(int keyCode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + } + + public boolean isScreenOn() { + return serviceManager.getPowerManager().isScreenOn(); + } + + public synchronized void setRotationListener(RotationListener rotationListener) { + this.rotationListener = rotationListener; + } + + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { + this.clipboardListener = clipboardListener; + } + + public void expandNotificationPanel() { + serviceManager.getStatusBarManager().expandNotificationsPanel(); + } + + public void collapsePanels() { + serviceManager.getStatusBarManager().collapsePanels(); + } + + public String getClipboardText() { + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager == null) { + return null; + } + CharSequence s = clipboardManager.getText(); + if (s == null) { + return null; + } + return s.toString(); + } + + public boolean setClipboardText(String text) { + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager == null) { + return false; + } + + String currentClipboard = getClipboardText(); + if (currentClipboard != null && currentClipboard.equals(text)) { + // The clipboard already contains the requested text. + // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause + // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this + // problem, do not explicitly set the clipboard text if it already contains the expected content. + return false; + } + + isSettingClipboard.set(true); + boolean ok = clipboardManager.setText(text); + isSettingClipboard.set(false); + return ok; + } + + /** + * @param mode one of the {@code POWER_MODE_*} constants + */ + public static boolean setScreenPowerMode(int mode) { + IBinder d = SurfaceControl.getBuiltInDisplay(); + if (d == null) { + Ln.e("Could not get built-in display"); + return false; + } + return SurfaceControl.setDisplayPowerMode(d, mode); + } + + /** + * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). + */ + public void rotateDevice() { + WindowManager wm = serviceManager.getWindowManager(); + + boolean accelerometerRotation = !wm.isRotationFrozen(); + + int currentRotation = wm.getRotation(); + int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 + String newRotationString = newRotation == 0 ? "portrait" : "landscape"; + + Ln.i("Device rotation requested: " + newRotationString); + wm.freezeRotation(newRotation); + + // restore auto-rotate if necessary + if (accelerometerRotation) { + wm.thawRotation(); + } + } + + public ContentProvider createSettingsProvider() { + return serviceManager.getActivityManager().createSettingsProvider(); + } +} diff --git a/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java new file mode 100644 index 00000000..e25b6e99 --- /dev/null +++ b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -0,0 +1,119 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.content.ClipData; +import android.content.IOnPrimaryClipChangedListener; +import android.os.Build; +import android.os.IInterface; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ClipboardManager { + private final IInterface manager; + private Method getPrimaryClipMethod; + private Method setPrimaryClipMethod; + private Method addPrimaryClipChangedListener; + + public ClipboardManager(IInterface manager) { + this.manager = manager; + } + + private Method getGetPrimaryClipMethod() throws NoSuchMethodException { + if (getPrimaryClipMethod == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } else { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + } + } + return getPrimaryClipMethod; + } + + private Method getSetPrimaryClipMethod() throws NoSuchMethodException { + if (setPrimaryClipMethod == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } else { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + } + } + return setPrimaryClipMethod; + } + + private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); + } + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + } + + private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) + throws InvocationTargetException, IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); + } else { + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + } + } + + public CharSequence getText() { + try { + Method method = getGetPrimaryClipMethod(); + ClipData clipData = getPrimaryClip(method, manager); + if (clipData == null || clipData.getItemCount() == 0) { + return null; + } + return clipData.getItemAt(0).getText(); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + public boolean setText(CharSequence text) { + try { + Method method = getSetPrimaryClipMethod(); + ClipData clipData = ClipData.newPlainText(null, text); + setPrimaryClip(method, manager, clipData); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } + + private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) + throws InvocationTargetException, IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); + } else { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + } + } + + private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { + if (addPrimaryClipChangedListener == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); + } else { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + } + } + return addPrimaryClipChangedListener; + } + + public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + try { + Method method = getAddPrimaryClipChangedListener(); + addPrimaryClipChangedListener(method, manager, listener); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } +} diff --git a/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java new file mode 100644 index 00000000..8fbb860b --- /dev/null +++ b/.pc/0001-No-Android-Q.patch/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -0,0 +1,142 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.os.Build; +import android.os.IBinder; +import android.view.Surface; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +@SuppressLint("PrivateApi") +public final class SurfaceControl { + + private static final Class CLASS; + + // see + public static final int POWER_MODE_OFF = 0; + public static final int POWER_MODE_NORMAL = 2; + + static { + try { + CLASS = Class.forName("android.view.SurfaceControl"); + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + } + + private static Method getBuiltInDisplayMethod; + private static Method setDisplayPowerModeMethod; + + private SurfaceControl() { + // only static methods + } + + public static void openTransaction() { + try { + CLASS.getMethod("openTransaction").invoke(null); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void closeTransaction() { + try { + CLASS.getMethod("closeTransaction").invoke(null); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) { + try { + CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class) + .invoke(null, displayToken, orientation, layerStackRect, displayRect); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { + try { + CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void setDisplaySurface(IBinder displayToken, Surface surface) { + try { + CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static IBinder createDisplay(String name, boolean secure) { + try { + return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { + if (getBuiltInDisplayMethod == null) { + // the method signature has changed in Android Q + // + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); + } else { + getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); + } + } + return getBuiltInDisplayMethod; + } + + public static IBinder getBuiltInDisplay() { + + try { + Method method = getGetBuiltInDisplayMethod(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // call getBuiltInDisplay(0) + return (IBinder) method.invoke(null, 0); + } + + // call getInternalDisplayToken() + return (IBinder) method.invoke(null); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { + if (setDisplayPowerModeMethod == null) { + setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); + } + return setDisplayPowerModeMethod; + } + + public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { + try { + Method method = getSetDisplayPowerModeMethod(); + method.invoke(null, displayToken, mode); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } + + public static void destroyDisplay(IBinder displayToken) { + try { + CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); + } catch (Exception e) { + throw new AssertionError(e); + } + } +} diff --git a/.pc/applied-patches b/.pc/applied-patches new file mode 100644 index 00000000..1a129b92 --- /dev/null +++ b/.pc/applied-patches @@ -0,0 +1 @@ +0001-No-Android-Q.patch diff --git a/debian/control b/debian/control index 58641127..bb4441ff 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: net Priority: optional Maintainer: Yangfl Build-Depends: - debhelper-compat (= 13), + debhelper-compat (= 13), quilt Build-Depends-Arch: meson, libavcodec-dev, diff --git a/debian/rules b/debian/rules index c80b4d38..c401f4ba 100755 --- a/debian/rules +++ b/debian/rules @@ -1,6 +1,7 @@ #!/usr/bin/make -f #export DH_VERBOSE = 1 +export QUILT_PATCHES=$(CURDIR)debian/patches export DEB_BUILD_MAINT_OPTIONS = hardening=+all @@ -33,8 +34,10 @@ clean: override_dh_auto_clean: dh_auto_clean rm -rf build_manual + quilt pop -a override_dh_auto_configure-arch: + quilt push -a dh_auto_configure -a -- -Dcompile_server=false override_dh_auto_build-indep: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 9100a9db..572a1b27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -262,8 +262,8 @@ public class Controller { } // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { - device.injectKeycode(KeyEvent.KEYCODE_PASTE); + if (paste && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) { + device.injectKeycode(279); } return ok; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index f23dd056..6be06524 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -110,7 +110,7 @@ public final class Device { } // main display or any display on Android >= Q - supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= 29; if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index e25b6e99..9566de64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -22,7 +22,7 @@ public class ClipboardManager { private Method getGetPrimaryClipMethod() throws NoSuchMethodException { if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); } else { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); @@ -33,7 +33,7 @@ public class ClipboardManager { private Method getSetPrimaryClipMethod() throws NoSuchMethodException { if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); } else { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); @@ -43,7 +43,7 @@ public class ClipboardManager { } private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); } return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); @@ -51,7 +51,7 @@ public class ClipboardManager { private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); } else { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); @@ -86,7 +86,7 @@ public class ClipboardManager { private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); } else { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); @@ -95,7 +95,7 @@ public class ClipboardManager { private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); } else { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 8fbb860b..de098347 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -88,7 +88,7 @@ public final class SurfaceControl { if (getBuiltInDisplayMethod == null) { // the method signature has changed in Android Q // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); @@ -101,7 +101,7 @@ public final class SurfaceControl { try { Method method = getGetBuiltInDisplayMethod(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < 29) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); }