diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1dfcab2b..7e856664 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -182,7 +182,7 @@ Possible values are "disabled", "sdk", "uhid" and "aoa": - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. -For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: +For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS @@ -644,6 +644,10 @@ Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= .B MOD+Shift+v Inject computer clipboard text as a sequence of key events +.TP +.B MOD+k +Open keyboard settings on the device (for HID keyboard only) + .TP .B MOD+i Enable/disable FPS counter (print frames/second in logs) diff --git a/app/src/cli.c b/app/src/cli.c index 59cd5699..c1c68e92 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -377,8 +377,9 @@ static const struct sc_option options[] = { "For \"uhid\" and \"aoa\", the keyboard layout must be " "configured (once and for all) on the device, via Settings -> " "System -> Languages and input -> Physical keyboard. This " - "settings page can be started directly: `adb shell am start -a " - "android.settings.HARD_KEYBOARD_SETTINGS`.\n" + "settings page can be started directly using the shortcut " + "MOD+k (except in OTG mode) or by executing: `adb shell am " + "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" "This option is only available when a HID keyboard is enabled " "(or a physical keyboard is connected).\n" "Also see --mouse.", @@ -965,6 +966,10 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Shift+v" }, .text = "Inject computer clipboard text as a sequence of key events", }, + { + .shortcuts = { "MOD+k" }, + .text = "Open keyboard settings on the device (for HID keyboard only)", + }, { .shortcuts = { "MOD+i" }, .text = "Enable/disable FPS counter (print frames/second in logs)", diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 88575b4e..b3da5fe5 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -161,6 +161,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: + case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: // no additional data return 1; default: @@ -270,6 +271,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } break; } + case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + LOG_CMSG("open hard keyboard settings"); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 550168c2..cd1340ef 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -40,6 +40,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, + SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; enum sc_screen_power_mode { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7186186f..f26c4164 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -318,6 +318,18 @@ rotate_device(struct sc_input_manager *im) { } } +static void +open_hard_keyboard_settings(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request opening hard keyboard settings"); + } +} + static void apply_orientation_transform(struct sc_input_manager *im, enum sc_orientation transform) { @@ -550,6 +562,13 @@ sc_input_manager_process_key(struct sc_input_manager *im, rotate_device(im); } return; + case SDLK_k: + if (control && !shift && !repeat && down + && im->kp && im->kp->hid) { + // Only if the current keyboard is hid + open_hard_keyboard_settings(im); + } + return; } return; diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 726f65a9..00b7f92a 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -340,5 +340,6 @@ sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, // Key injection and clipboard synchronization are serialized kb->key_processor.async_paste = false; + kb->key_processor.hid = false; kb->key_processor.ops = &ops; } diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 8c51b11d..96374413 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -23,6 +23,13 @@ struct sc_key_processor { */ bool async_paste; + /** + * Set by the implementation to indicate that the keyboard is HID. In + * practice, it is used to react on a shortcut to open the hard keyboard + * settings only if the keyboard is HID. + */ + bool hid; + const struct sc_key_processor_ops *ops; }; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index f537bc29..515a3fd9 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -137,6 +137,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, // Clipboard synchronization is requested over the same control socket, so // there is no need for a specific synchronization mechanism kb->key_processor.async_paste = false; + kb->key_processor.hid = true; kb->key_processor.ops = &ops; static const struct sc_uhid_receiver_ops uhid_receiver_ops = { diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index b69d6cd8..736c97b0 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -94,6 +94,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { // events are sent over AOA, so it must wait for clipboard synchronization // to be acknowledged by the device before injecting Ctrl+v. kb->key_processor.async_paste = true; + kb->key_processor.hid = true; kb->key_processor.ops = &ops; return true; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 0ab61153..7a978f2b 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -370,6 +370,21 @@ static void test_serialize_uhid_input(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_open_hard_keyboard(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 1); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -390,5 +405,6 @@ int main(int argc, char *argv[]) { test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); + test_serialize_open_hard_keyboard(); return 0; } diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 21bccbd9..8c402855 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -48,6 +48,7 @@ _[Super] is typically the Windows or Cmd key._ | Cut to clipboard⁵ | MOD+x | Synchronize clipboards and paste⁵ | MOD+v | Inject computer clipboard text | MOD+Shift+v + | Open keyboard settings (HID keyboard only) | MOD+k | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 74bf5610..bcbacb4b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -19,6 +19,7 @@ public final class ControlMessage { public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; + public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; public static final long SEQUENCE_INVALID = 0; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 24aa73c0..1761d228 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -86,6 +86,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: + case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: msg = ControlMessage.createEmpty(type); break; case ControlMessage.TYPE_UHID_CREATE: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index fd320d3f..87faf8ba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,7 +1,9 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.InputManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; @@ -208,6 +210,9 @@ public class Controller implements AsyncProcessor { case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); break; + case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + openHardKeyboardSettings(); + break; default: // do nothing } @@ -446,4 +451,9 @@ public class Controller implements AsyncProcessor { return ok; } + + private void openHardKeyboardSettings() { + Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); + ServiceManager.getActivityManager().startActivity(intent); + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 7cc67c3e..0c8086f7 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -366,6 +366,22 @@ public class ControlMessageReaderTest { Assert.assertArrayEquals(data, event.getData()); } + @Test + public void testParseOpenHardKeyboardSettings() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader();