package com.genymobile.scrcpy; 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 serviceManager.getClipboardManager().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); } } } } }); } 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() { CharSequence s = serviceManager.getClipboardManager().getText(); if (s == null) { return null; } return s.toString(); } public boolean setClipboardText(String text) { isSettingClipboard.set(true); boolean ok = serviceManager.getClipboardManager().setText(text); isSettingClipboard.set(false); return ok; } /** * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ public 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(); } }