Implement device screen off while mirroring

Add two shortcuts:
 - Ctrl+o to turn the device screen off while mirroring
 - Ctrl+Shift+o to turn it back on

On power on (either via the POWER key or BACK while screen is off), both
the device screen and the mirror are turned on.

<https://github.com/Genymobile/scrcpy/issues/175>
This commit is contained in:
Romain Vimont 2019-03-15 20:23:30 +01:00
parent 3ee9560ece
commit 12a3bb25d3
12 changed files with 144 additions and 3 deletions

View file

@ -284,7 +284,9 @@ you are interested, see [issue 14].
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) | | click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) |
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
| click on `POWER` | `Ctrl`+`p` | | 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` | | expand notification panel | `Ctrl`+`n` |
| collapse notification panel | `Ctrl`+`Shift`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` |
| copy device clipboard to computer | `Ctrl`+`c` | | copy device clipboard to computer | `Ctrl`+`c` |

View file

@ -55,6 +55,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
&buf[1]); &buf[1]);
return 1 + len; 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_BACK_OR_SCREEN_ON:
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:

View file

@ -24,6 +24,13 @@ enum control_msg_type {
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
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,
};
enum screen_power_mode {
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SCREEN_POWER_MODE_OFF = 0,
SCREEN_POWER_MODE_NORMAL = 2,
}; };
struct control_msg { struct control_msg {
@ -50,6 +57,9 @@ struct control_msg {
struct { struct {
char *text; // owned, to be freed by SDL_free() char *text; // owned, to be freed by SDL_free()
} set_clipboard; } set_clipboard;
struct {
enum screen_power_mode mode;
} set_screen_power_mode;
}; };
}; };

View file

@ -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 static void
switch_fps_counter_state(struct video_buffer *vb) { switch_fps_counter_state(struct video_buffer *vb) {
mutex_lock(vb->mutex); mutex_lock(vb->mutex);
@ -263,6 +275,14 @@ input_manager_process_key(struct input_manager *input_manager,
action_power(input_manager->controller, action); action_power(input_manager->controller, action);
} }
return; 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: case SDLK_DOWN:
#ifdef __APPLE__ #ifdef __APPLE__
if (control && !ctrl && meta && !shift) { if (control && !ctrl && meta && !shift) {

View file

@ -129,7 +129,13 @@ static void usage(const char *arg0) {
" click on POWER (turn screen on/off)\n" " click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click (when screen is off)\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" "\n"
" Ctrl+n\n" " Ctrl+n\n"
" expand notification panel\n" " expand notification panel\n"

View file

@ -213,6 +213,25 @@ static void test_serialize_set_clipboard(void) {
assert(!memcmp(buf, expected, sizeof(expected))); 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) { int main(void) {
test_serialize_inject_keycode(); test_serialize_inject_keycode();
test_serialize_inject_text(); test_serialize_inject_text();
@ -224,5 +243,6 @@ int main(void) {
test_serialize_collapse_notification_panel(); test_serialize_collapse_notification_panel();
test_serialize_get_clipboard(); test_serialize_get_clipboard();
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_screen_power_mode();
return 0; return 0;
} }

View file

@ -1,5 +1,7 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
/** /**
* Union of all supported event types, identified by their {@code type}. * 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_COLLAPSE_NOTIFICATION_PANEL = 6;
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;
private int type; private int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* 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 keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_*
private Position position; private Position position;
@ -69,6 +72,16 @@ public final class ControlMessage {
return event; 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) { public static ControlMessage createEmpty(int type) {
ControlMessage event = new ControlMessage(); ControlMessage event = new ControlMessage();
event.type = type; event.type = type;

View file

@ -11,6 +11,7 @@ public class ControlMessageReader {
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; 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 TEXT_MAX_LENGTH = 300;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
@ -67,6 +68,9 @@ public class ControlMessageReader {
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard(); msg = parseSetClipboard();
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode();
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
@ -144,6 +148,14 @@ public class ControlMessageReader {
return ControlMessage.createSetClipboard(text); 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) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = buffer.getInt();
int y = buffer.getInt(); int y = buffer.getInt();

View file

@ -97,6 +97,9 @@ public class Controller {
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText()); device.setClipboardText(msg.getText());
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
break;
default: default:
// do nothing // do nothing
} }

View file

@ -1,15 +1,20 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import android.view.InputEvent; import android.view.InputEvent;
public final class Device { 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 { public interface RotationListener {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
} }
@ -152,6 +157,15 @@ public final class Device {
Ln.i("Device clipboard set"); 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) { 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);
} }

View file

@ -10,6 +10,10 @@ public final class SurfaceControl {
private static final Class<?> CLASS; private static final Class<?> CLASS;
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
public static final int POWER_MODE_OFF = 0;
public static final int POWER_MODE_NORMAL = 2;
static { static {
try { try {
CLASS = Class.forName("android.view.SurfaceControl"); 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) { public static void destroyDisplay(IBinder displayToken) {
try { try {
CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);

View file

@ -210,6 +210,24 @@ public class ControlMessageReaderTest {
Assert.assertEquals("testé", event.getText()); 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 @Test
public void testMultiEvents() throws IOException { public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader(); ControlMessageReader reader = new ControlMessageReader();