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_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` |
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 <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 {
|
||||
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ public final class SurfaceControl {
|
|||
|
||||
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 {
|
||||
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);
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue