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