mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-04-20 11:35:57 +00:00
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.
This commit is contained in:
parent
bdd05b4a16
commit
280a20e0e0
20 changed files with 580 additions and 33 deletions
|
@ -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)/"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...");
|
||||
|
|
|
@ -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, \
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
93
server/key-to-touch-map.xml
Normal file
93
server/key-to-touch-map.xml
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<KeyToTouch>
|
||||
<!--
|
||||
The following two groups are only for demonstration.
|
||||
Users can design their own mappings and add more groups for meeting their needs.
|
||||
|
||||
The "x" and "y" refer to the physical coordinates of the display of the connected device.
|
||||
Such information can be shown by turning on the option "System"->"Developer options"->"Pointer location"
|
||||
-->
|
||||
<Group name="Portrait_English">
|
||||
|
||||
<Mapping key="\u0009" x="589" y="1114" repeating="false" /> <!--tab-->
|
||||
|
||||
|
||||
<Mapping key="\u0020" x="350" y="1110" repeating="false" /> <!--space-->
|
||||
<Mapping key="\u002C" x="198" y="1114" repeating="false" /> <!--,-->
|
||||
<Mapping key="\u002E" x="508" y="1114" repeating="false" /> <!--.-->
|
||||
|
||||
|
||||
<Mapping key="\u0043" x="52" y="1000" repeating="false" /> <!--C-->
|
||||
|
||||
<Mapping key="\u0061" x="68" y="880" repeating="false" /> <!--a-->
|
||||
<Mapping key="\u0062" x="426" y="1005" repeating="false" /> <!--b-->
|
||||
<Mapping key="\u0063" x="293" y="1006" repeating="false" /> <!--c-->
|
||||
<Mapping key="\u0064" x="214" y="885" repeating="false" /> <!--d-->
|
||||
<Mapping key="\u0065" x="176" y="771" repeating="false" /> <!--e-->
|
||||
<Mapping key="\u0066" x="286" y="890" repeating="false" /> <!--f-->
|
||||
<Mapping key="\u0067" x="355" y="890" repeating="false" /> <!--g-->
|
||||
<Mapping key="\u0068" x="431" y="892" repeating="false" /> <!--h-->
|
||||
<Mapping key="\u0069" x="543" y="778" repeating="false" /> <!--i-->
|
||||
<Mapping key="\u006A" x="505" y="896" repeating="false" /> <!--j-->
|
||||
<Mapping key="\u006B" x="579" y="894" repeating="false" /> <!--k-->
|
||||
<Mapping key="\u006C" x="650" y="890" repeating="false" /> <!--l-->
|
||||
<Mapping key="\u006D" x="555" y="1001" repeating="false" /> <!--m-->
|
||||
<Mapping key="\u006E" x="491" y="990" repeating="false" /> <!--n-->
|
||||
<Mapping key="\u006F" x="610" y="774" repeating="false" /> <!--o-->
|
||||
<Mapping key="\u0070" x="684" y="781" repeating="false" /> <!--p-->
|
||||
<Mapping key="\u0071" x="35" y="778" repeating="false" /> <!--q-->
|
||||
<Mapping key="\u0072" x="248" y="779" repeating="false" /> <!--r-->
|
||||
<Mapping key="\u0073" x="142" y="885" repeating="false" /> <!--s-->
|
||||
<Mapping key="\u0074" x="322" y="785" repeating="false" /> <!--t-->
|
||||
<Mapping key="\u0075" x="470" y="776" repeating="false" /> <!--u-->
|
||||
<Mapping key="\u0076" x="360" y="1006" repeating="false" /> <!--v-->
|
||||
<Mapping key="\u0077" x="107" y="783" repeating="false" /> <!--w-->
|
||||
<Mapping key="\u0078" x="227" y="1006" repeating="false" /> <!--x-->
|
||||
<Mapping key="\u0079" x="396" y="781" repeating="false" /> <!--y-->
|
||||
<Mapping key="\u007A" x="163" y="1005" repeating="false" /> <!--z-->
|
||||
|
||||
|
||||
</Group>
|
||||
|
||||
<Group name="Landscape_English">
|
||||
|
||||
<Mapping key="\u0009" x="1062" y="665" repeating="false" /> <!--tab-->
|
||||
|
||||
|
||||
<Mapping key="\u0020" x="675" y="660" repeating="false" /> <!--space-->
|
||||
<Mapping key="\u002C" x="423" y="663" repeating="false" /> <!--,-->
|
||||
<Mapping key="\u002E" x="935" y="661" repeating="false" /> <!--.-->
|
||||
|
||||
<Mapping key="\u0043" x="182" y="562" repeating="false" /> <!--C-->
|
||||
|
||||
<Mapping key="\u0061" x="211" y="472" repeating="false" /> <!--a-->
|
||||
<Mapping key="\u0062" x="793" y="568" repeating="false" /> <!--b-->
|
||||
<Mapping key="\u0063" x="580" y="570" repeating="false" /> <!--c-->
|
||||
<Mapping key="\u0064" x="449" y="472" repeating="false" /> <!--d-->
|
||||
<Mapping key="\u0065" x="387" y="384" repeating="false" /> <!--e-->
|
||||
<Mapping key="\u0066" x="567" y="472" repeating="false" /> <!--f-->
|
||||
<Mapping key="\u0067" x="686" y="472" repeating="false" /> <!--g-->
|
||||
<Mapping key="\u0068" x="807" y="474" repeating="false" /> <!--h-->
|
||||
<Mapping key="\u0069" x="984" y="382" repeating="false" /> <!--i-->
|
||||
<Mapping key="\u006A" x="926" y="474" repeating="false" /> <!--j-->
|
||||
<Mapping key="\u006B" x="1045" y="474" repeating="false" /> <!--k-->
|
||||
<Mapping key="\u006C" x="1157" y="471" repeating="false" /> <!--l-->
|
||||
<Mapping key="\u006D" x="1007" y="568" repeating="false" /> <!--m-->
|
||||
<Mapping key="\u006E" x="905" y="568" repeating="false" /> <!--n-->
|
||||
<Mapping key="\u006F" x="1102" y="384" repeating="false" /> <!--o-->
|
||||
<Mapping key="\u0070" x="1224" y="387" repeating="false" /> <!--p-->
|
||||
<Mapping key="\u0071" x="155" y="378" repeating="false" /> <!--q-->
|
||||
<Mapping key="\u0072" x="512" y="384" repeating="false" /> <!--r-->
|
||||
<Mapping key="\u0073" x="330" y="476" repeating="false" /> <!--s-->
|
||||
<Mapping key="\u0074" x="629" y="385" repeating="false" /> <!--t-->
|
||||
<Mapping key="\u0075" x="865" y="382" repeating="false" /> <!--u-->
|
||||
<Mapping key="\u0076" x="686" y="570" repeating="false" /> <!--v-->
|
||||
<Mapping key="\u0077" x="272" y="384" repeating="false" /> <!--w-->
|
||||
<Mapping key="\u0078" x="471" y="570" repeating="false" /> <!--x-->
|
||||
<Mapping key="\u0079" x="744" y="382" repeating="false" /> <!--y-->
|
||||
<Mapping key="\u007A" x="368" y="568" repeating="false" /> <!--z-->
|
||||
|
||||
</Group>
|
||||
|
||||
|
||||
</KeyToTouch>
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<MappingGroup> groupList = new ArrayList<MappingGroup>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -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<Integer, TouchEvent> map = new HashMap<Integer, TouchEvent>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue