diff --git a/README.md b/README.md index ef1424ba..0ee6d79b 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,9 @@ you are interested, see [issue 14]. | click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) | | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | | click on `POWER` | `Ctrl`+`p` | - | turn screen on | _Right-click²_ | + | power on | _Right-click²_ | + | turn device screen off (keep mirroring)| `Ctrl`+`o` | + | turn device screen on | `Ctrl`+`Shift`+`o` | | expand notification panel | `Ctrl`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` | | copy device clipboard to computer | `Ctrl`+`c` | diff --git a/app/src/control_msg.c b/app/src/control_msg.c index ca1028b3..9c3d9849 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -55,6 +55,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { &buf[1]); return 1 + len; } + case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + buf[1] = msg->set_screen_power_mode.mode; + return 2; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index abc11f32..e7fdfc4c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -24,6 +24,13 @@ enum control_msg_type { CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, +}; + +enum screen_power_mode { + // see + SCREEN_POWER_MODE_OFF = 0, + SCREEN_POWER_MODE_NORMAL = 2, }; struct control_msg { @@ -50,6 +57,9 @@ struct control_msg { struct { char *text; // owned, to be freed by SDL_free() } set_clipboard; + struct { + enum screen_power_mode mode; + } set_screen_power_mode; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index a040913b..68648b11 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -159,6 +159,18 @@ set_device_clipboard(struct controller *controller) { } } +static void +set_screen_power_mode(struct controller *controller, + enum screen_power_mode mode) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = mode; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); + } +} + static void switch_fps_counter_state(struct video_buffer *vb) { mutex_lock(vb->mutex); @@ -263,6 +275,14 @@ input_manager_process_key(struct input_manager *input_manager, action_power(input_manager->controller, action); } return; + case SDLK_o: + if (control && ctrl && !meta && event->type == SDL_KEYDOWN) { + enum screen_power_mode mode = shift + ? SCREEN_POWER_MODE_NORMAL + : SCREEN_POWER_MODE_OFF; + set_screen_power_mode(input_manager->controller, mode); + } + return; case SDLK_DOWN: #ifdef __APPLE__ if (control && !ctrl && meta && !shift) { diff --git a/app/src/main.c b/app/src/main.c index be611025..d8568884 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -129,7 +129,13 @@ static void usage(const char *arg0) { " click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " turn screen on\n" + " power on\n" + "\n" + " Ctrl+o\n" + " turn device screen off (keep mirroring)\n" + "\n" + " Ctrl+Shift+o\n" + " turn device screen on\n" "\n" " Ctrl+n\n" " expand notification panel\n" diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 20f72505..c0c501f2 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -213,6 +213,25 @@ static void test_serialize_set_clipboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_set_screen_power_mode(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + .set_screen_power_mode = { + .mode = SCREEN_POWER_MODE_NORMAL, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 2); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SCREEN_POWER_MODE_NORMAL + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); @@ -224,5 +243,6 @@ int main(void) { test_serialize_collapse_notification_panel(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); + test_serialize_set_screen_power_mode(); 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 093e293f..3f2b8960 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.SurfaceControl; + /** * Union of all supported event types, identified by their {@code type}. */ @@ -14,11 +16,12 @@ public final class ControlMessage { public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; 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; private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* private Position position; @@ -69,6 +72,16 @@ public final class ControlMessage { return event; } + /** + * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants + */ + public static ControlMessage createSetScreenPowerMode(int mode) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_SET_SCREEN_POWER_MODE; + event.action = mode; + return event; + } + public static ControlMessage createEmpty(int type) { ControlMessage event = new ControlMessage(); event.type = type; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 965cd29a..8ced049d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -11,6 +11,7 @@ public class ControlMessageReader { private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; @@ -67,6 +68,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + msg = parseSetScreenPowerMode(); + break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: @@ -144,6 +148,14 @@ public class ControlMessageReader { return ControlMessage.createSetClipboard(text); } + private ControlMessage parseSetScreenPowerMode() { + if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { + return null; + } + int mode = buffer.get(); + return ControlMessage.createSetScreenPowerMode(mode); + } + private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index c9215f50..dead6d3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -97,6 +97,9 @@ public class Controller { case ControlMessage.TYPE_SET_CLIPBOARD: device.setClipboardText(msg.getText()); break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + device.setScreenPowerMode(msg.getAction()); + 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 f6219a93..538135d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,15 +1,20 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.os.Build; +import android.os.IBinder; import android.os.RemoteException; import android.view.IRotationWatcher; import android.view.InputEvent; 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); } @@ -152,6 +157,15 @@ public final class Device { Ln.i("Device clipboard set"); } + /** + * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + */ + public void setScreenPowerMode(int mode) { + IBinder d = SurfaceControl.getBuiltInDisplay(0); + SurfaceControl.setDisplayPowerMode(d, mode); + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + 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/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 85733867..bed21b3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -10,6 +10,10 @@ 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"); @@ -71,6 +75,22 @@ public final class SurfaceControl { } } + public static IBinder getBuiltInDisplay(int builtInDisplayId) { + try { + return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void setDisplayPowerMode(IBinder displayToken, int mode) { + try { + CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); + } catch (Exception e) { + throw new AssertionError(e); + } + } + public static void destroyDisplay(IBinder displayToken) { try { CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 788ee12e..df1db1a6 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -210,6 +210,24 @@ public class ControlMessageReaderTest { Assert.assertEquals("testé", event.getText()); } + @Test + public void testParseSetScreenPowerMode() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); + dos.writeByte(Device.POWER_MODE_NORMAL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); + Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader();