From 280a20e0e004cd7c990bcaed6ae53324259ba81a Mon Sep 17 00:00:00 2001 From: nothingstopsme Date: Tue, 10 Dec 2019 00:18:47 +0800 Subject: [PATCH] Adding the support of KeyToTouch, which allows users to initiate an touch event via pressing some key on a keyboard. Following are the list of changes associated with this function: 1. A xml file "key-to-touch-map.xml" containing user-defined groups of mappings 2. The starting procedure is modified so that "key-to-touch-map.xml" is adb-pushed along with the executable "scrcpy-server"; then the scrcpy server will read that xml to prepare the KeyToTouch utility. 3. A new control message CONTROL_MSG_TYPE_SWITCH_KEY_MAPPING_GROUP (10) 4. Two shortcut commands "Ctrl+"Shift"+"," and "Ctrl"+"Shift"+".", which send CONTROL_MSG_TYPE_SWITCH_KEY_MAPPING_GROUP to the server for switching between key mapping groups 5. A new command-line option "--key-event-only". Under this option, scrcpy will treat all allowed keyboard inputs as key events rather than text events. --- Makefile.CrossWindows | 2 + app/src/control_msg.c | 3 + app/src/control_msg.h | 4 + app/src/event_converter.c | 31 +++++- app/src/event_converter.h | 3 +- app/src/input_manager.c | 50 +++++++-- app/src/input_manager.h | 8 +- app/src/main.c | 20 +++- app/src/scrcpy.c | 4 +- app/src/scrcpy.h | 4 +- app/src/server.c | 62 +++++++++-- server/key-to-touch-map.xml | 93 ++++++++++++++++ server/meson.build | 7 ++ .../com/genymobile/scrcpy/ControlMessage.java | 10 +- .../scrcpy/ControlMessageReader.java | 12 +++ .../com/genymobile/scrcpy/Controller.java | 72 ++++++++++++- .../com/genymobile/scrcpy/KeyToTouchMap.java | 93 ++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 14 ++- .../scrcpy/key2touch/MappingGroup.java | 100 ++++++++++++++++++ .../scrcpy/key2touch/TouchEvent.java | 21 ++++ 20 files changed, 580 insertions(+), 33 deletions(-) create mode 100644 server/key-to-touch-map.xml create mode 100644 server/src/main/java/com/genymobile/scrcpy/KeyToTouchMap.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/key2touch/MappingGroup.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/key2touch/TouchEvent.java diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 2b30dcb5..154ef8f6 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -98,6 +98,7 @@ build-win64-noconsole: prepare-deps-win64 dist-win32: build-server build-win32 build-win32-noconsole mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/key-to-touch-map.xml "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -113,6 +114,7 @@ dist-win32: build-server build-win32 build-win32-noconsole dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/key-to-touch-map.xml "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fda16025..daaa76af 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -74,6 +74,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; + case CONTROL_MSG_TYPE_SWITCH_KEY_MAPPING_GROUP: + buf[1] = msg->key_mapping_group_switch.direction; + return 2; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2f319d9d..b79c762d 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -28,6 +28,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + CONTROL_MSG_TYPE_SWITCH_KEY_MAPPING_GROUP, }; enum screen_power_mode { @@ -65,6 +66,9 @@ struct control_msg { struct { enum screen_power_mode mode; } set_screen_power_mode; + struct { + int32_t direction; + } key_mapping_group_switch; }; }; diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 80ead615..b7de2530 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -76,7 +76,7 @@ convert_meta_state(SDL_Keymod mod) { bool convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text) { + enum key_input_mode mode) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); @@ -94,7 +94,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_UP, AKEYCODE_DPAD_UP); } - if (prefer_text) { + if (mode == KEY_TEXT_PREFERRED) { // do not forward alpha and space key events return false; } @@ -131,6 +131,33 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_y, AKEYCODE_Y); MAP(SDLK_z, AKEYCODE_Z); MAP(SDLK_SPACE, AKEYCODE_SPACE); + } + + if(mode != KEY_EVENT_ONLY) + return false; + + switch (from) { + MAP(SDLK_SLASH, AKEYCODE_SLASH); + MAP(SDLK_PERIOD, AKEYCODE_PERIOD); + MAP(SDLK_COMMA, AKEYCODE_COMMA); + MAP(SDLK_SEMICOLON, AKEYCODE_SEMICOLON); + MAP(SDLK_QUOTE, AKEYCODE_APOSTROPHE); + MAP(SDLK_LEFTBRACKET, AKEYCODE_LEFT_BRACKET); + MAP(SDLK_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET); + MAP(SDLK_BACKSLASH, AKEYCODE_BACKSLASH); + MAP(SDLK_MINUS, AKEYCODE_MINUS); + MAP(SDLK_EQUALS, AKEYCODE_EQUALS); + MAP(SDLK_0, AKEYCODE_0); + MAP(SDLK_1, AKEYCODE_1); + MAP(SDLK_2, AKEYCODE_2); + MAP(SDLK_3, AKEYCODE_3); + MAP(SDLK_4, AKEYCODE_4); + MAP(SDLK_5, AKEYCODE_5); + MAP(SDLK_6, AKEYCODE_6); + MAP(SDLK_7, AKEYCODE_7); + MAP(SDLK_8, AKEYCODE_8); + MAP(SDLK_9, AKEYCODE_9); + MAP(SDLK_BACKQUOTE, AKEYCODE_GRAVE); FAIL; } } diff --git a/app/src/event_converter.h b/app/src/event_converter.h index c41887e1..31a649d8 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -6,6 +6,7 @@ #include "config.h" #include "control_msg.h" +#include "input_manager.h" bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); @@ -15,7 +16,7 @@ convert_meta_state(SDL_Keymod mod); bool convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text); + enum key_input_mode mode); enum android_motionevent_buttons convert_mouse_buttons(uint32_t state); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 60879005..a8af1164 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -97,6 +97,17 @@ action_menu(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); } +static void +switch_key_mapping_group(struct controller *controller, bool isForward) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SWITCH_KEY_MAPPING_GROUP; + msg.key_mapping_group_switch.direction = (int32_t) isForward; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'switch key mapping group forwards/backwards'"); + } +} + // turn the screen on if it was off, press BACK otherwise static void press_back_or_turn_screen_on(struct controller *controller) { @@ -214,13 +225,22 @@ clipboard_paste(struct controller *controller) { void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { - if (!im->prefer_text) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - assert(event->text[1] == '\0'); - // letters and space are handled as raw key event - return; - } + switch (im->_key_input_mode) + { + case KEY_EVENT_ONLY: + return; + case KEY_COMBINED: + { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + SDL_assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } + break; + } + default: + break; } struct control_msg msg; @@ -238,7 +258,7 @@ input_manager_process_text_input(struct input_manager *im, static bool convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, - bool prefer_text) { + enum key_input_mode mode) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -247,7 +267,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, uint16_t mod = from->keysym.mod; if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, - prefer_text)) { + mode)) { return false; } @@ -388,6 +408,16 @@ input_manager_process_key(struct input_manager *im, } } return; + case SDLK_COMMA: + if (control && cmd && shift && !repeat && down) { + switch_key_mapping_group(controller, false); + } + return; + case SDLK_PERIOD: + if (control && cmd && shift && !repeat && down) { + switch_key_mapping_group(controller, true); + } + return; } return; @@ -398,7 +428,7 @@ input_manager_process_key(struct input_manager *im, } struct control_msg msg; - if (convert_input_key(event, &msg, im->prefer_text)) { + if (convert_input_key(event, &msg, im->_key_input_mode)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 43fc0eeb..d41dd055 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -10,11 +10,17 @@ #include "video_buffer.h" #include "screen.h" +enum key_input_mode { + KEY_COMBINED = 0, + KEY_TEXT_PREFERRED = 1, + KEY_EVENT_ONLY = 2, +}; + struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; - bool prefer_text; + enum key_input_mode _key_input_mode; }; void diff --git a/app/src/main.c b/app/src/main.c index 78bdcfd2..3224038d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -78,6 +78,11 @@ static void usage(const char *arg0) { " special character, but breaks the expected behavior of alpha\n" " keys in games (typically WASD).\n" "\n" + " --key-event-only\n" + " Typing only generates key events rather than text events.\n" + " The resulting behaviour is opposite to the one induced by\n" + " --prefer-text.\n" + "\n" " --push-target path\n" " Set the target directory for pushing files to the device by\n" " drag & drop. It is passed as-is to \"adb push\".\n" @@ -193,6 +198,14 @@ static void usage(const char *arg0) { " " CTRL_OR_CMD "+i\n" " enable/disable FPS counter (print frames/second in logs)\n" "\n" + " " CTRL_OR_CMD "+Shfit+.\n" + " switching to the next key mapping group defined in the xml\n" + " (if provided).\n" + "\n" + " " CTRL_OR_CMD "+Shfit+,\n" + " switching to the previous key mapping group defined in the xml\n" + " (if provided).\n" + "\n" " Drag & drop APK file\n" " install APK from computer\n" "\n", @@ -401,6 +414,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_BORDERLESS 1011 #define OPT_MAX_FPS 1012 +#define OPT_KEY_EVENT_ONLY 1013 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -424,6 +438,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, + {"key-event-only", no_argument, NULL, OPT_KEY_EVENT_ONLY}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-x", required_argument, NULL, OPT_WINDOW_X}, @@ -541,7 +556,10 @@ parse_args(struct args *args, int argc, char *argv[]) { opts->push_target = optarg; break; case OPT_PREFER_TEXT: - opts->prefer_text = true; + opts->_key_input_mode = KEY_TEXT_PREFERRED; + break; + case OPT_KEY_EVENT_ONLY: + opts->_key_input_mode = KEY_EVENT_ONLY; break; default: // getopt prints the error message on stderr diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17be1ed4..302b300d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,7 +42,7 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, - .prefer_text = false, // initialized later + ._key_input_mode = KEY_COMBINED, // initialized later }; // init SDL and set appropriate hints @@ -418,7 +418,7 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } - input_manager.prefer_text = options->prefer_text; + input_manager._key_input_mode = options->_key_input_mode; ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8723f29f..1e93a4a5 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -30,7 +30,7 @@ struct scrcpy_options { bool display; bool turn_screen_off; bool render_expired_frames; - bool prefer_text; + enum key_input_mode _key_input_mode; bool window_borderless; }; @@ -56,7 +56,7 @@ struct scrcpy_options { .display = true, \ .turn_screen_off = false, \ .render_expired_frames = false, \ - .prefer_text = false, \ + ._key_input_mode = KEY_COMBINED, \ .window_borderless = false, \ } diff --git a/app/src/server.c b/app/src/server.c index 90eb4c69..5c8d7352 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -13,24 +13,30 @@ #include "util/net.h" #define SOCKET_NAME "scrcpy" -#define SERVER_FILENAME "scrcpy-server" +#define SERVER_FILENAME "scrcpy-server.jar" +#define KEY_TO_TOUCH_MAP_FILENAME "key-to-touch-map.xml" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define DEVICE_PATH_PREFIX "/data/local/tmp/" +#define DEVICE_SERVER_PATH DEVICE_PATH_PREFIX SERVER_FILENAME +#define DEVICE_XML_PATH DEVICE_PATH_PREFIX KEY_TO_TOUCH_MAP_FILENAME -static const char * -get_server_path(void) { +// the returned bool is used to indicate whether callers should be responsible for freeing the memory holding the path string +static bool +get_server_path(const char **ptr) { const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); if (server_path_env) { LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); // if the envvar is set, use it - return server_path_env; + *ptr = server_path_env; + return false; } #ifndef PORTABLE LOGD("Using server: " DEFAULT_SERVER_PATH); // the absolute path is hardcoded - return DEFAULT_SERVER_PATH; + *ptr = DEFAULT_SERVER_PATH; + return false; #else // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); @@ -38,7 +44,8 @@ get_server_path(void) { LOGE("Could not get executable path, " "using " SERVER_FILENAME " from current directory"); // not found, use current directory - return SERVER_FILENAME; + *ptr = SERVER_FILENAME; + return false; } char *dir = dirname(executable_path); size_t dirlen = strlen(dir); @@ -50,7 +57,8 @@ get_server_path(void) { LOGE("Could not alloc server path string, " "using " SERVER_FILENAME " from current directory"); SDL_free(executable_path); - return SERVER_FILENAME; + *ptr = SERVER_FILENAME; + return false; } memcpy(server_path, dir, dirlen); @@ -61,13 +69,47 @@ get_server_path(void) { SDL_free(executable_path); LOGD("Using server (portable): %s", server_path); - return server_path; + *ptr = server_path; + return true; #endif } static bool push_server(const char *serial) { - process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH); + + const char *server_path = NULL; + bool is_allocated = get_server_path(&server_path); + const char *beginningOfFilename = strrchr(server_path, '/'); + if(beginningOfFilename == NULL) + beginningOfFilename = strrchr(server_path, '\\'); + + size_t prefix_len = 0; + size_t xml_path_len = strlen(KEY_TO_TOUCH_MAP_FILENAME) + 1; // extra one for storing a null character + char *xml_path = NULL; + if(beginningOfFilename != NULL) + { + prefix_len = beginningOfFilename - server_path + 1; + xml_path_len = prefix_len + xml_path_len; + } + + xml_path = SDL_malloc(xml_path_len); + if(xml_path == NULL) + LOGE("Fail to allocate buffer for \"%s\". Skipping the push of it", KEY_TO_TOUCH_MAP_FILENAME); + else + { + strcpy(strncpy(xml_path, server_path, prefix_len) + prefix_len, KEY_TO_TOUCH_MAP_FILENAME); + + process_t process = adb_push(serial, xml_path, DEVICE_XML_PATH); + process_check_success(process, "adb push"); + + SDL_free(xml_path); + } + + + process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); + if(is_allocated && server_path != NULL) + SDL_free(server_path); + return process_check_success(process, "adb push"); } diff --git a/server/key-to-touch-map.xml b/server/key-to-touch-map.xml new file mode 100644 index 00000000..d20f8e58 --- /dev/null +++ b/server/key-to-touch-map.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/meson.build b/server/meson.build index 4ba481d5..3df723af 100644 --- a/server/meson.build +++ b/server/meson.build @@ -21,3 +21,10 @@ else install: true, install_dir: 'share/scrcpy') endif + +custom_target('copy_xml', + input: 'key-to-touch-map.xml', + output: 'key-to-touch-map.xml', + command: ['cp', '@INPUT@', '@OUTPUT@'], + install: true, + install_dir: 'share/scrcpy') diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 615773fb..465733ae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -15,11 +15,12 @@ public final class ControlMessage { 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_SWITCH_KEY_MAPPING_GROUP = 10; private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* or the direction of switching private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* private long pointerId; @@ -74,6 +75,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createSwitchKeyMappingGroup(int direction) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_SWITCH_KEY_MAPPING_GROUP; + msg.action = direction; + return msg; + } + /** * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants */ diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2f8b5177..148a904a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -13,6 +13,7 @@ public class ControlMessageReader { private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + private static final int SWITCH_KEY_MAPPING_GROUP_PAYLOAD_LENGTH = 1; public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; @@ -72,6 +73,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: msg = parseSetScreenPowerMode(); break; + case ControlMessage.TYPE_SWITCH_KEY_MAPPING_GROUP: + msg = parseSwitchKeyMappingGroup(); + break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: @@ -155,6 +159,14 @@ public class ControlMessageReader { return ControlMessage.createSetClipboard(text); } + private ControlMessage parseSwitchKeyMappingGroup() { + if (buffer.remaining() < SWITCH_KEY_MAPPING_GROUP_PAYLOAD_LENGTH) { + return null; + } + int direction = buffer.get(); + return ControlMessage.createSwitchKeyMappingGroup(direction); + } + private ControlMessage parseSetScreenPowerMode() { if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { return null; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 51b13627..d13c531e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.key2touch.TouchEvent; import com.genymobile.scrcpy.wrappers.InputManager; import android.os.SystemClock; @@ -21,6 +22,10 @@ public class Controller { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + private int lastKeyCodeAction = -1; + private int lastKeyCode = -1; + private long lastKeyCodeTimestamp = 0; + private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; @@ -106,13 +111,68 @@ public class Controller { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: device.setScreenPowerMode(msg.getAction()); break; + case ControlMessage.TYPE_SWITCH_KEY_MAPPING_GROUP: + if(msg.getAction() == 0) + KeyToTouchMap.instance.previousGroup(); + else + KeyToTouchMap.instance.nextGroup(); + break; default: // do nothing } } + private boolean injectKeycode(int action, int keycode, int metaState) { - return injectKeyEvent(action, keycode, 0, metaState); + int key = charMap.get(keycode, metaState); + boolean keyRepeating = false; + if(lastKeyCode == key && lastKeyCodeAction == action && action == KeyEvent.ACTION_DOWN) + keyRepeating = true; + + lastKeyCode = key; + lastKeyCodeAction = action; + + TouchEvent touchEvent = KeyToTouchMap.instance.lookup(key); + if(touchEvent == null) { + lastKeyCodeTimestamp = SystemClock.uptimeMillis(); + return injectKeyEvent(action, keycode, 0, metaState); + } + else + { + if(keyRepeating) + { + if(!touchEvent.repeating) + return false; + else + { + if(SystemClock.uptimeMillis() < lastKeyCodeTimestamp + touchEvent.repeatingInterval) + return false; + } + + + } + + + switch (action) { + case KeyEvent.ACTION_DOWN: { + action = MotionEvent.ACTION_DOWN; + break; + } + case KeyEvent.ACTION_UP: { + action = MotionEvent.ACTION_UP; + break; + } + default: + return false; + } + + + if(keyRepeating) + injectTouch(MotionEvent.ACTION_UP, touchEvent.pointerId, touchEvent.point, touchEvent.pressure, touchEvent.buttons, lastKeyCodeTimestamp+1); + + lastKeyCodeTimestamp = SystemClock.uptimeMillis(); + return injectTouch(action, touchEvent.pointerId, touchEvent.point, touchEvent.pressure, touchEvent.buttons, lastKeyCodeTimestamp); + } } private boolean injectChar(char c) { @@ -137,25 +197,33 @@ public class Controller { Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); continue; } + successCount++; } return successCount; } private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { - long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); if (point == null) { // ignore event return false; } + long now = SystemClock.uptimeMillis(); + + return injectTouch(action, pointerId, point, pressure, buttons, now); + } + + private boolean injectTouch(int action, long pointerId, Point point, float pressure, int buttons, long now) { + int pointerIndex = pointersState.getPointerIndex(pointerId); if (pointerIndex == -1) { Ln.w("Too many pointers for touch event"); return false; } + Pointer pointer = pointersState.get(pointerIndex); pointer.setPoint(point); pointer.setPressure(pressure); diff --git a/server/src/main/java/com/genymobile/scrcpy/KeyToTouchMap.java b/server/src/main/java/com/genymobile/scrcpy/KeyToTouchMap.java new file mode 100644 index 00000000..98223ea4 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/KeyToTouchMap.java @@ -0,0 +1,93 @@ +package com.genymobile.scrcpy; + +import android.util.Xml; + +import com.genymobile.scrcpy.key2touch.MappingGroup; +import com.genymobile.scrcpy.key2touch.TouchEvent; + +import org.xmlpull.v1.XmlPullParser; + +import java.io.FileInputStream; +import java.util.ArrayList; + +/** + * + */ +public final class KeyToTouchMap { + + + public static final KeyToTouchMap instance = new KeyToTouchMap(); + + + private final ArrayList groupList = new ArrayList(); + private int currentGroup = 0; + + + private KeyToTouchMap() { + groupList.add(new MappingGroup("None")); + currentGroup = 0; + } + + public void nextGroup() + { + ++currentGroup; + if(currentGroup >= groupList.size()) + currentGroup = 0; + + Ln.i("KeyToTouchMap: Current group " + groupList.get(currentGroup).name); + } + + public void previousGroup() + { + --currentGroup; + if(currentGroup < 0) + currentGroup = groupList.size() - 1; + + Ln.i("KeyToTouchMap: Current group " + groupList.get(currentGroup).name); + } + + public TouchEvent lookup(int keyCode) + { + MappingGroup group = groupList.get(currentGroup); + return group.lookup(keyCode); + + } + + public void parse(String inputFilePath) + { + MappingGroup noneGroup = groupList.get(0); + groupList.clear(); + groupList.add(noneGroup); + currentGroup = 0; + try(FileInputStream input = new FileInputStream(inputFilePath)) + { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(input, null); + parser.nextTag(); + + while (parser.nextTag() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + if(parser.getName().compareTo(MappingGroup.GROUP_TAG) != 0) + MappingGroup.skip(parser); + + try { + groupList.add(new MappingGroup(parser)); + } + catch (Exception parsingException) + { + Ln.e("Fail to parse MappingGroup", parsingException); + MappingGroup.skip(parser); + } + } + } + catch (Exception parsingException) + { + Ln.e(String.format("Fail to parse %s", inputFilePath), parsingException); + } + + } + +} + diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 87aeacac..cfe2ac90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -10,6 +10,7 @@ import java.io.IOException; public final class Server { private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + private static final String KEY_TO_TOUCH_MAP_PATH = "/data/local/tmp/key-to-touch-map.xml"; private Server() { // not instantiable @@ -131,7 +132,12 @@ public final class Server { try { new File(SERVER_PATH).delete(); } catch (Exception e) { - Ln.e("Could not unlink server", e); + Ln.e(String.format("Could not unlink %s", SERVER_PATH), e); + } + try { + new File(KEY_TO_TOUCH_MAP_PATH).delete(); + } catch (Exception e) { + Ln.e(String.format("Could not unlink %s", KEY_TO_TOUCH_MAP_PATH), e); } } @@ -157,6 +163,12 @@ public final class Server { } }); + + if(new File(KEY_TO_TOUCH_MAP_PATH).exists()) + KeyToTouchMap.instance.parse(KEY_TO_TOUCH_MAP_PATH); + + + unlinkSelf(); Options options = createOptions(args); scrcpy(options); diff --git a/server/src/main/java/com/genymobile/scrcpy/key2touch/MappingGroup.java b/server/src/main/java/com/genymobile/scrcpy/key2touch/MappingGroup.java new file mode 100644 index 00000000..ac6899a7 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/key2touch/MappingGroup.java @@ -0,0 +1,100 @@ +package com.genymobile.scrcpy.key2touch; + +import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.Point; + +import org.xmlpull.v1.XmlPullParser; + +import java.util.HashMap; + +public final class MappingGroup { + + public static final String GROUP_TAG = "Group"; + private static final String MAPPING_TAG = "Mapping"; + private static final String NAME_ATTR = "name"; + private static final String KEY_ATTR = "key"; + private static final String X_ATTR = "x"; + private static final String Y_ATTR = "y"; + private static final String REPEATING_ATTR = "repeating"; + + + public final String name; + private HashMap map = new HashMap(); + + private boolean parseMapping(XmlPullParser parser) + { + if (parser.getName().compareTo(MAPPING_TAG) != 0) { + return false; + } + + try { + String keyStr = parser.getAttributeValue(null, KEY_ATTR); + int key = -1; + if (keyStr != null && keyStr.toLowerCase().startsWith("\\u")) { + key = Integer.valueOf(keyStr.substring(2), 16); + } else + throw new NumberFormatException("The format is invalid: " + keyStr); + + + int x = Integer.valueOf(parser.getAttributeValue(null, X_ATTR)); + int y = Integer.valueOf(parser.getAttributeValue(null, Y_ATTR)); + boolean repeating = Boolean.parseBoolean(parser.getAttributeValue(null, REPEATING_ATTR)); + Ln.d("key = " + Character.toChars(key)[0] + ", x = " + x + ", y = " + y + ", repeating = " + repeating); + map.put(key, new TouchEvent(new Point(x, y), repeating)); + parser.nextTag(); + + } + catch (Exception parsingException) + { + Ln.e(String.format("Error while parsing %s", this.name), parsingException); + return false; + } + return true; + } + + public MappingGroup(String name) + { + this.name = name; + } + + public MappingGroup(XmlPullParser parser) throws Exception + { + name = parser.getAttributeValue(null, NAME_ATTR); + while (parser.nextTag() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + if(!parseMapping(parser)) + skip(parser); + + } + + } + + + + public TouchEvent lookup(int keyCode) + { + return map.get(keyCode); + } + + + + public static void skip(XmlPullParser parser) throws Exception + { + + int depth = 1; + while (depth != 0) { + switch (parser.next()) { + case XmlPullParser.END_TAG: + Ln.w(String.format("MappingGroup skipping: %s", parser.getName())); + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; + } + } + + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/key2touch/TouchEvent.java b/server/src/main/java/com/genymobile/scrcpy/key2touch/TouchEvent.java new file mode 100644 index 00000000..feeafdec --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/key2touch/TouchEvent.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.key2touch; + +import com.genymobile.scrcpy.Point; + +public final class TouchEvent { + + public final Point point; + public final boolean repeating; + + public static final int buttons = 1; + public static final float pressure = 1.0f; + public static final long pointerId = -2; + public static final long repeatingInterval = 100; + + public TouchEvent(Point point, boolean repeating) + { + this.point = point; + this.repeating = repeating; + + } +}