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:
parent
3ee9560ece
commit
12a3bb25d3
12 changed files with 144 additions and 3 deletions
|
@ -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` |
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue