diff --git a/README.md b/README.md index 6a987a73a3..97259ceb0a 100644 --- a/README.md +++ b/README.md @@ -698,10 +698,10 @@ _[Super] is typically the Windows or Cmd key._ | Rotate display left | MOD+ _(left)_ | Rotate display right | MOD+ _(right)_ | Resize window to 1:1 (pixel-perfect) | MOD+g - | Resize window to remove black borders | MOD+w \| _Double-click¹_ + | Resize window to remove black borders | MOD+w \| _Left-Double-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ | Click on `BACK` | MOD+b \| _Right-click²_ - | Click on `APP_SWITCH` | MOD+s + | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ | Click on `MENU` (unlock screen) | MOD+m | Click on `VOLUME_UP` | MOD+ _(up)_ | Click on `VOLUME_DOWN` | MOD+ _(down)_ @@ -710,18 +710,27 @@ _[Super] is typically the Windows or Cmd key._ | Turn device screen off (keep mirroring) | MOD+o | Turn device screen on | MOD+Shift+o | Rotate device screen | MOD+r - | Expand notification panel | MOD+n - | Collapse notification panel | MOD+Shift+n - | Copy to clipboard³ | MOD+c - | Cut to clipboard³ | MOD+x - | Synchronize clipboards and paste³ | MOD+v + | Expand notification panel | MOD+n \| _5th-click³_ + | Expand settings panel | MOD+n+n \| _5th-double-click³_ + | Collapse panels | MOD+Shift+n + | Copy to clipboard⁴ | MOD+c + | Cut to clipboard⁴ | MOD+x + | Synchronize clipboards and paste⁴ | MOD+v | Inject computer clipboard text | MOD+Shift+v | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom | Ctrl+_click-and-move_ _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ -_³Only on Android >= 7._ +_³4th/5th mouse button, if your mouse has it_ +_⁴Only on Android >= 7._ + +Shortcuts with repeated keys are executted by releasing and pressing the key a second time. +For example, to execute "Expand settings panel": + +1. Press and keep pressing MOD. +2. Then double-press n. +3. Finally, release MOD. All Ctrl+_key_ shortcuts are forwarded to the device, so they are handled by the active application. diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 69e750140a..8908c5462a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -81,7 +81,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[1] = msg->set_screen_power_mode.mode; return 2; case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: - case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 8d9ab7d42d..c1099c794f 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -27,7 +27,8 @@ enum control_msg_type { CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, - CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_PANELS, CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b8a8c84602..9408661688 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -72,6 +72,10 @@ input_manager_init(struct input_manager *im, im->sdl_shortcut_mods.count = shortcut_mods->count; im->vfinger_down = false; + + im->last_keycode = SDLK_UNKNOWN; + im->last_mod = 0; + im->key_repeat = 0; } static void @@ -179,9 +183,19 @@ expand_notification_panel(struct controller *controller) { } static void -collapse_notification_panel(struct controller *controller) { +expand_settings_panel(struct controller *controller) { struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; + msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'expand settings panel'"); + } +} + +static void +collapse_panels(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); @@ -384,16 +398,27 @@ input_manager_process_key(struct input_manager *im, // control: indicates the state of the command-line option --no-control bool control = im->control; - bool smod = is_shortcut_mod(im, event->keysym.mod); - struct controller *controller = im->controller; SDL_Keycode keycode = event->keysym.sym; + uint16_t mod = event->keysym.mod; bool down = event->type == SDL_KEYDOWN; bool ctrl = event->keysym.mod & KMOD_CTRL; bool shift = event->keysym.mod & KMOD_SHIFT; bool repeat = event->repeat; + bool smod = is_shortcut_mod(im, mod); + + if (down && !repeat) { + if (keycode == im->last_keycode && mod == im->last_mod) { + ++im->key_repeat; + } else { + im->key_repeat = 0; + im->last_keycode = keycode; + im->last_mod = mod; + } + } + // The shortcut modifier is pressed if (smod) { int action = down ? ACTION_DOWN : ACTION_UP; @@ -498,9 +523,11 @@ input_manager_process_key(struct input_manager *im, case SDLK_n: if (control && !repeat && down) { if (shift) { - collapse_notification_panel(controller); - } else { + collapse_panels(controller); + } else if (im->key_repeat == 0) { expand_notification_panel(controller); + } else { + expand_settings_panel(controller); } } return; @@ -666,7 +693,11 @@ input_manager_process_mouse_button(struct input_manager *im, return; } if (control && event->button == SDL_BUTTON_X2 && down) { - expand_notification_panel(im->controller); + if (event->clicks < 2) { + expand_notification_panel(im->controller); + } else { + expand_settings_panel(im->controller); + } return; } if (control && event->button == SDL_BUTTON_RIGHT) { diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 160977b387..5c7e2b913f 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -33,6 +33,13 @@ struct input_manager { } sdl_shortcut_mods; bool vfinger_down; + + // Tracks the number of identical consecutive shortcut key down events. + // Not to be confused with event->repeat, which counts the number of + // system-generated repeated key presses. + unsigned key_repeat; + SDL_Keycode last_keycode; + uint16_t last_mod; }; void diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 4771ce1fe5..ef9247ca23 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -177,9 +177,9 @@ static void test_serialize_expand_notification_panel(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_collapse_notification_panel(void) { +static void test_serialize_expand_settings_panel(void) { struct control_msg msg = { - .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; @@ -187,7 +187,22 @@ static void test_serialize_collapse_notification_panel(void) { assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_collapse_panels(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -274,7 +289,8 @@ int main(int argc, char *argv[]) { test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); - test_serialize_collapse_notification_panel(); + test_serialize_expand_settings_panel(); + test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 44cb1b599f..f8edd53c11 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -11,11 +11,12 @@ public final class ControlMessage { public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - 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; - public static final int TYPE_ROTATE_DEVICE = 10; + public static final int TYPE_EXPAND_SETTINGS_PANEL = 6; + public static final int TYPE_COLLAPSE_PANELS = 7; + public static final int TYPE_GET_CLIPBOARD = 8; + public static final int TYPE_SET_CLIPBOARD = 9; + public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_ROTATE_DEVICE = 11; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 7ebecf76ac..e4ab840201 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -77,7 +77,8 @@ public ControlMessage next() { msg = parseSetScreenPowerMode(); break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 6af5ddf649..3760bbd4b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -107,7 +107,10 @@ private void handleEvent() throws IOException { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: Device.expandNotificationPanel(); break; - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + Device.expandSettingsPanel(); + break; + case ControlMessage.TYPE_COLLAPSE_PANELS: Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 624c9fa0d3..a63976fd20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -227,6 +227,10 @@ public static void expandNotificationPanel() { SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); } + public static void expandSettingsPanel() { + SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); + } + public static void collapsePanels() { SERVICE_MANAGER.getStatusBarManager().collapsePanels(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 71967c500c..93ed452875 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -14,7 +14,7 @@ public class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; - private boolean getContentProviderExternalMethodLegacy; + private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; public ActivityManager(IInterface manager) { @@ -29,7 +29,7 @@ private Method getGetContentProviderExternalMethod() throws NoSuchMethodExceptio } catch (NoSuchMethodException e) { // old version getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); - getContentProviderExternalMethodLegacy = true; + getContentProviderExternalMethodNewVersion = false; } } return getContentProviderExternalMethod; @@ -46,7 +46,7 @@ private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); Object[] args; - if (!getContentProviderExternalMethodLegacy) { + if (getContentProviderExternalMethodNewVersion) { // new version args = new Object[]{name, ServiceManager.USER_ID, token, null}; } else { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 6f8941bdf0..5b1e5f5ed8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -11,6 +11,8 @@ public class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; + private Method expandSettingsPanelMethod; + private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; public StatusBarManager(IInterface manager) { @@ -24,6 +26,20 @@ private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException return expandNotificationsPanelMethod; } + private Method getExpandSettingsPanel() throws NoSuchMethodException { + if (expandSettingsPanelMethod == null) { + try { + // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); + } catch (NoSuchMethodException e) { + // old version + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); + expandSettingsPanelMethodNewVersion = false; + } + } + return expandSettingsPanelMethod; + } + private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); @@ -40,6 +56,21 @@ public void expandNotificationsPanel() { } } + public void expandSettingsPanel() { + try { + Method method = getExpandSettingsPanel(); + if (expandSettingsPanelMethodNewVersion) { + // new version + method.invoke(manager, (Object) null); + } else { + // old version + method.invoke(manager); + } + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + public void collapsePanels() { try { Method method = getCollapsePanelsMethod(); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 6167ddebf0..da56848676 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -182,19 +182,35 @@ public void testParseExpandNotificationPanelEvent() throws IOException { } @Test - public void testParseCollapseNotificationPanelEvent() throws IOException { + public void testParseExpandSettingsPanelEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + } + + @Test + public void testParseCollapsePanelsEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); } @Test