diff --git a/README.md b/README.md index 677e7a1c..2e95cb03 100644 --- a/README.md +++ b/README.md @@ -425,6 +425,7 @@ Also see [issue #14]. | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` | Power on | _Right-click²_ | _Right-click²_ | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` + | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6cb062b5..47fd767a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -195,6 +195,10 @@ turn screen on .B Ctrl+o turn device screen off (keep mirroring) +.TP +.B Ctrl+r +rotate device screen + .TP .B Ctrl+n expand notification panel diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fda16025..45113139 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -78,6 +78,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_GET_CLIPBOARD: + case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2f319d9d..49a159a6 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -28,6 +28,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + CONTROL_MSG_TYPE_ROTATE_DEVICE, }; enum screen_power_mode { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 60879005..8c4c230a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -211,6 +211,16 @@ clipboard_paste(struct controller *controller) { } } +static void +rotate_device(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request device rotation"); + } +} + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { @@ -388,6 +398,11 @@ input_manager_process_key(struct input_manager *im, } } return; + case SDLK_r: + if (control && cmd && !shift && !repeat && down) { + rotate_device(controller); + } + return; } return; diff --git a/app/src/main.c b/app/src/main.c index 78bdcfd2..e9a2b9aa 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -175,6 +175,9 @@ static void usage(const char *arg0) { " " CTRL_OR_CMD "+o\n" " turn device screen off (keep mirroring)\n" "\n" + " " CTRL_OR_CMD "+r\n" + " rotate device screen\n" + "\n" " " CTRL_OR_CMD "+n\n" " expand notification panel\n" "\n" diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 83ab011f..d6f556f3 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -236,6 +236,21 @@ static void test_serialize_set_screen_power_mode(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_rotate_device(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); @@ -248,5 +263,6 @@ int main(void) { test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); + test_serialize_rotate_device(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 615773fb..195b04bf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -15,6 +15,7 @@ public final class ControlMessage { public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_SET_CLIPBOARD = 8; public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + public static final int TYPE_ROTATE_DEVICE = 10; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2f8b5177..726b5659 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -76,6 +76,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_GET_CLIPBOARD: + case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 51b13627..dc0fa67b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -106,6 +106,9 @@ public class Controller { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: device.setScreenPowerMode(msg.getAction()); break; + case ControlMessage.TYPE_ROTATE_DEVICE: + device.rotateDevice(); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 708b9516..9448098a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.os.Build; @@ -170,6 +171,27 @@ public final class Device { Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } + /** + * 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(); + } + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index b0e44278..cc687cd5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,28 +1,95 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import android.view.IRotationWatcher; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public final class WindowManager { private final IInterface manager; + private Method getRotationMethod; + private Method freezeRotationMethod; + private Method isRotationFrozenMethod; + private Method thawRotationMethod; public WindowManager(IInterface manager) { this.manager = manager; } - public int getRotation() { - try { + private Method getGetRotationMethod() throws NoSuchMethodException { + if (getRotationMethod == null) { Class cls = manager.getClass(); try { // method changed since this commit: // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 - return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager); + getRotationMethod = cls.getMethod("getDefaultDisplayRotation"); } catch (NoSuchMethodException e) { // old version - return (Integer) cls.getMethod("getRotation").invoke(manager); + getRotationMethod = cls.getMethod("getRotation"); } - } catch (Exception e) { - throw new AssertionError(e); + } + return getRotationMethod; + } + + private Method getFreezeRotationMethod() throws NoSuchMethodException { + if (freezeRotationMethod == null) { + freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); + } + return freezeRotationMethod; + } + + private Method getIsRotationFrozenMethod() throws NoSuchMethodException { + if (isRotationFrozenMethod == null) { + isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); + } + return isRotationFrozenMethod; + } + + private Method getThawRotationMethod() throws NoSuchMethodException { + if (thawRotationMethod == null) { + thawRotationMethod = manager.getClass().getMethod("thawRotation"); + } + return thawRotationMethod; + } + + public int getRotation() { + try { + Method method = getGetRotationMethod(); + return (int) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return 0; + } + } + + public void freezeRotation(int rotation) { + try { + Method method = getFreezeRotationMethod(); + method.invoke(manager, rotation); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public boolean isRotationFrozen() { + try { + Method method = getIsRotationFrozenMethod(); + return (boolean) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } + + public void thawRotation() { + try { + Method method = getThawRotationMethod(); + method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index ede759dc..5e663bb9 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -240,6 +240,22 @@ public class ControlMessageReaderTest { Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); } + @Test + public void testParseRotateDevice() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader();