Add shortcut to rotate screen
On Ctrl+r, disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). Closes #11 <https://github.com/Genymobile/scrcpy/issues/11>
This commit is contained in:
parent
bdd05b4a16
commit
eb0f339271
13 changed files with 157 additions and 6 deletions
|
@ -425,6 +425,7 @@ Also see [issue #14].
|
||||||
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||||
| Power on | _Right-click²_ | _Right-click²_
|
| Power on | _Right-click²_ | _Right-click²_
|
||||||
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
||||||
|
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
||||||
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
||||||
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||||
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
||||||
|
|
|
@ -195,6 +195,10 @@ turn screen on
|
||||||
.B Ctrl+o
|
.B Ctrl+o
|
||||||
turn device screen off (keep mirroring)
|
turn device screen off (keep mirroring)
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B Ctrl+r
|
||||||
|
rotate device screen
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+n
|
.B Ctrl+n
|
||||||
expand notification panel
|
expand notification panel
|
||||||
|
|
|
@ -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_EXPAND_NOTIFICATION_PANEL:
|
||||||
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||||
|
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||||
// no additional data
|
// no additional data
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -28,6 +28,7 @@ enum control_msg_type {
|
||||||
CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||||
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||||
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||||
|
CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum screen_power_mode {
|
enum screen_power_mode {
|
||||||
|
|
|
@ -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
|
void
|
||||||
input_manager_process_text_input(struct input_manager *im,
|
input_manager_process_text_input(struct input_manager *im,
|
||||||
const SDL_TextInputEvent *event) {
|
const SDL_TextInputEvent *event) {
|
||||||
|
@ -388,6 +398,11 @@ input_manager_process_key(struct input_manager *im,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case SDLK_r:
|
||||||
|
if (control && cmd && !shift && !repeat && down) {
|
||||||
|
rotate_device(controller);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -175,6 +175,9 @@ static void usage(const char *arg0) {
|
||||||
" " CTRL_OR_CMD "+o\n"
|
" " CTRL_OR_CMD "+o\n"
|
||||||
" turn device screen off (keep mirroring)\n"
|
" turn device screen off (keep mirroring)\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" " CTRL_OR_CMD "+r\n"
|
||||||
|
" rotate device screen\n"
|
||||||
|
"\n"
|
||||||
" " CTRL_OR_CMD "+n\n"
|
" " CTRL_OR_CMD "+n\n"
|
||||||
" expand notification panel\n"
|
" expand notification panel\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
|
@ -236,6 +236,21 @@ static void test_serialize_set_screen_power_mode(void) {
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
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) {
|
int main(void) {
|
||||||
test_serialize_inject_keycode();
|
test_serialize_inject_keycode();
|
||||||
test_serialize_inject_text();
|
test_serialize_inject_text();
|
||||||
|
@ -248,5 +263,6 @@ int main(void) {
|
||||||
test_serialize_get_clipboard();
|
test_serialize_get_clipboard();
|
||||||
test_serialize_set_clipboard();
|
test_serialize_set_clipboard();
|
||||||
test_serialize_set_screen_power_mode();
|
test_serialize_set_screen_power_mode();
|
||||||
|
test_serialize_rotate_device();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ public final class ControlMessage {
|
||||||
public static final int TYPE_GET_CLIPBOARD = 7;
|
public static final int TYPE_GET_CLIPBOARD = 7;
|
||||||
public static final int TYPE_SET_CLIPBOARD = 8;
|
public static final int TYPE_SET_CLIPBOARD = 8;
|
||||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
||||||
|
public static final int TYPE_ROTATE_DEVICE = 10;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
|
|
|
@ -76,6 +76,7 @@ public class ControlMessageReader {
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
msg = ControlMessage.createEmpty(type);
|
msg = ControlMessage.createEmpty(type);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -106,6 +106,9 @@ public class Controller {
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
device.setScreenPowerMode(msg.getAction());
|
device.setScreenPowerMode(msg.getAction());
|
||||||
break;
|
break;
|
||||||
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
|
device.rotateDevice();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
|
||||||
|
|
||||||
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 com.genymobile.scrcpy.wrappers.WindowManager;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -170,6 +171,27 @@ public final class Device {
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
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) {
|
static Rect flipRect(Rect crop) {
|
||||||
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,95 @@
|
||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public final class WindowManager {
|
public final class WindowManager {
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
|
private Method getRotationMethod;
|
||||||
|
private Method freezeRotationMethod;
|
||||||
|
private Method isRotationFrozenMethod;
|
||||||
|
private Method thawRotationMethod;
|
||||||
|
|
||||||
public WindowManager(IInterface manager) {
|
public WindowManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRotation() {
|
private Method getGetRotationMethod() throws NoSuchMethodException {
|
||||||
try {
|
if (getRotationMethod == null) {
|
||||||
Class<?> cls = manager.getClass();
|
Class<?> cls = manager.getClass();
|
||||||
try {
|
try {
|
||||||
// method changed since this commit:
|
// method changed since this commit:
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
|
// 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) {
|
} catch (NoSuchMethodException e) {
|
||||||
// old version
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -240,6 +240,22 @@ public class ControlMessageReaderTest {
|
||||||
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
|
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
|
@Test
|
||||||
public void testMultiEvents() throws IOException {
|
public void testMultiEvents() throws IOException {
|
||||||
ControlMessageReader reader = new ControlMessageReader();
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
Loading…
Reference in a new issue