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:
nothingstopsme 2019-12-10 00:18:47 +08:00
parent bdd05b4a16
commit 280a20e0e0
20 changed files with 580 additions and 33 deletions

View file

@ -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)/"

View file

@ -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:

View file

@ -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;
};
};

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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'");
}

View file

@ -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

View file

@ -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

View file

@ -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...");

View file

@ -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, \
}

View file

@ -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");
}

View 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>

View file

@ -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')

View file

@ -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
*/

View file

@ -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;

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}