From cabb102a04153f9c237b18257d3269a058558c63 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Dec 2017 11:38:44 +0100 Subject: [PATCH] Implement keyboard/mouse control To control the device from the computer: - retrieve mouse and keyboard SDL events; - convert them to Android events; - serialize them; - send them on the same socket used by the video stream (but in the opposite direction); - deserialize the events on the Android side; - inject them using the InputManager. --- Makefile | 6 +- app/meson.build | 17 + app/src/android/input.h | 829 ++++++++++++++++++ app/src/android/keycodes.h | 745 ++++++++++++++++ app/src/control.c | 99 +++ app/src/control.h | 29 + app/src/controlevent.c | 91 ++ app/src/controlevent.h | 66 ++ app/src/convert.c | 214 +++++ app/src/convert.h | 20 + app/src/screen.c | 286 ++++-- app/src/strutil.h | 2 +- app/tests/test_control_event_queue.c | 94 ++ server/Makefile | 34 +- .../com/genymobile/scrcpy/ControlEvent.java | 102 +++ .../genymobile/scrcpy/ControlEventReader.java | 99 +++ .../genymobile/scrcpy/DesktopConnection.java | 28 +- .../src/com/genymobile/scrcpy/DeviceUtil.java | 5 + .../genymobile/scrcpy/EventController.java | 127 +++ .../com/genymobile/scrcpy/ScrCpyServer.java | 18 +- .../scrcpy/wrappers/InputManager.java | 34 + .../scrcpy/wrappers/ServiceManager.java | 4 + .../scrcpy/ControlEventReaderTest.java | 151 ++++ 23 files changed, 2999 insertions(+), 101 deletions(-) create mode 100644 app/src/android/input.h create mode 100644 app/src/android/keycodes.h create mode 100644 app/src/control.c create mode 100644 app/src/control.h create mode 100644 app/src/controlevent.c create mode 100644 app/src/controlevent.h create mode 100644 app/src/convert.c create mode 100644 app/src/convert.h create mode 100644 app/tests/test_control_event_queue.c create mode 100644 server/src/com/genymobile/scrcpy/ControlEvent.java create mode 100644 server/src/com/genymobile/scrcpy/ControlEventReader.java create mode 100644 server/src/com/genymobile/scrcpy/EventController.java create mode 100644 server/src/com/genymobile/scrcpy/wrappers/InputManager.java create mode 100644 server/tests/com/genymobile/scrcpy/ControlEventReaderTest.java diff --git a/Makefile b/Makefile index df9f5df5..0bb360a4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: default release clean build-app build-server dist +.PHONY: default release clean build-app build-server dist dist-zip sums test BUILD_DIR := build DIST := dist @@ -36,3 +36,7 @@ dist-zip: dist sums: cd "$(DIST)"; \ sha256sum *.zip > SHA256SUM.txt + +test: + +$(MAKE) -C server test + ninja -C "$(BUILD_DIR)" test diff --git a/app/meson.build b/app/meson.build index 59a58f47..f1cfa579 100644 --- a/app/meson.build +++ b/app/meson.build @@ -3,6 +3,9 @@ project('scrcpy-app', 'c') src = [ 'src/scrcpy.c', 'src/command.c', + 'src/control.c', + 'src/controlevent.c', + 'src/convert.c', 'src/decoder.c', 'src/frames.c', 'src/lockutil.c', @@ -26,3 +29,17 @@ dependencies = [ ] executable('scrcpy', src, dependencies: dependencies) + + +### TESTS + +tests = [ + ['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/controlevent.c']], +] + +src_dir = include_directories('src') + +foreach t : tests + exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies) + test(t[0], exe) +endforeach diff --git a/app/src/android/input.h b/app/src/android/input.h new file mode 100644 index 00000000..0d087f9b --- /dev/null +++ b/app/src/android/input.h @@ -0,0 +1,829 @@ +// copied from +// blob 08299899b6305a0fe74d7d2b8471b7cd0af49dc7 +// (and modified) +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _ANDROID_INPUT_H +#define _ANDROID_INPUT_H + +/** + * Meta key / modifer state. + */ +enum android_metastate { + /** No meta keys are pressed. */ + AMETA_NONE = 0, + + /** This mask is used to check whether one of the ALT meta keys is pressed. */ + AMETA_ALT_ON = 0x02, + + /** This mask is used to check whether the left ALT meta key is pressed. */ + AMETA_ALT_LEFT_ON = 0x10, + + /** This mask is used to check whether the right ALT meta key is pressed. */ + AMETA_ALT_RIGHT_ON = 0x20, + + /** This mask is used to check whether one of the SHIFT meta keys is pressed. */ + AMETA_SHIFT_ON = 0x01, + + /** This mask is used to check whether the left SHIFT meta key is pressed. */ + AMETA_SHIFT_LEFT_ON = 0x40, + + /** This mask is used to check whether the right SHIFT meta key is pressed. */ + AMETA_SHIFT_RIGHT_ON = 0x80, + + /** This mask is used to check whether the SYM meta key is pressed. */ + AMETA_SYM_ON = 0x04, + + /** This mask is used to check whether the FUNCTION meta key is pressed. */ + AMETA_FUNCTION_ON = 0x08, + + /** This mask is used to check whether one of the CTRL meta keys is pressed. */ + AMETA_CTRL_ON = 0x1000, + + /** This mask is used to check whether the left CTRL meta key is pressed. */ + AMETA_CTRL_LEFT_ON = 0x2000, + + /** This mask is used to check whether the right CTRL meta key is pressed. */ + AMETA_CTRL_RIGHT_ON = 0x4000, + + /** This mask is used to check whether one of the META meta keys is pressed. */ + AMETA_META_ON = 0x10000, + + /** This mask is used to check whether the left META meta key is pressed. */ + AMETA_META_LEFT_ON = 0x20000, + + /** This mask is used to check whether the right META meta key is pressed. */ + AMETA_META_RIGHT_ON = 0x40000, + + /** This mask is used to check whether the CAPS LOCK meta key is on. */ + AMETA_CAPS_LOCK_ON = 0x100000, + + /** This mask is used to check whether the NUM LOCK meta key is on. */ + AMETA_NUM_LOCK_ON = 0x200000, + + /** This mask is used to check whether the SCROLL LOCK meta key is on. */ + AMETA_SCROLL_LOCK_ON = 0x400000, +}; + +/** + * Input event types. + */ +enum android_input_event_type { + /** Indicates that the input event is a key event. */ + AINPUT_EVENT_TYPE_KEY = 1, + /** Indicates that the input event is a motion event. */ + AINPUT_EVENT_TYPE_MOTION = 2 +}; + +/** + * Key event actions. + */ +enum android_keyevent_action { + /** The key has been pressed down. */ + AKEY_EVENT_ACTION_DOWN = 0, + + /** The key has been released. */ + AKEY_EVENT_ACTION_UP = 1, + + /** + * Multiple duplicate key events have occurred in a row, or a + * complex string is being delivered. The repeat_count property + * of the key event contains the number of times the given key + * code should be executed. + */ + AKEY_EVENT_ACTION_MULTIPLE = 2 +}; + +/** + * Key event flags. + */ +enum android_keyevent_flags { + /** This mask is set if the device woke because of this key event. */ + AKEY_EVENT_FLAG_WOKE_HERE = 0x1, + + /** This mask is set if the key event was generated by a software keyboard. */ + AKEY_EVENT_FLAG_SOFT_KEYBOARD = 0x2, + + /** This mask is set if we don't want the key event to cause us to leave touch mode. */ + AKEY_EVENT_FLAG_KEEP_TOUCH_MODE = 0x4, + + /** + * This mask is set if an event was known to come from a trusted + * part of the system. That is, the event is known to come from + * the user, and could not have been spoofed by a third party + * component. + */ + AKEY_EVENT_FLAG_FROM_SYSTEM = 0x8, + + /** + * This mask is used for compatibility, to identify enter keys that are + * coming from an IME whose enter key has been auto-labelled "next" or + * "done". This allows TextView to dispatch these as normal enter keys + * for old applications, but still do the appropriate action when + * receiving them. + */ + AKEY_EVENT_FLAG_EDITOR_ACTION = 0x10, + + /** + * When associated with up key events, this indicates that the key press + * has been canceled. Typically this is used with virtual touch screen + * keys, where the user can slide from the virtual key area on to the + * display: in that case, the application will receive a canceled up + * event and should not perform the action normally associated with the + * key. Note that for this to work, the application can not perform an + * action for a key until it receives an up or the long press timeout has + * expired. + */ + AKEY_EVENT_FLAG_CANCELED = 0x20, + + /** + * This key event was generated by a virtual (on-screen) hard key area. + * Typically this is an area of the touchscreen, outside of the regular + * display, dedicated to "hardware" buttons. + */ + AKEY_EVENT_FLAG_VIRTUAL_HARD_KEY = 0x40, + + /** + * This flag is set for the first key repeat that occurs after the + * long press timeout. + */ + AKEY_EVENT_FLAG_LONG_PRESS = 0x80, + + /** + * Set when a key event has AKEY_EVENT_FLAG_CANCELED set because a long + * press action was executed while it was down. + */ + AKEY_EVENT_FLAG_CANCELED_LONG_PRESS = 0x100, + + /** + * Set for AKEY_EVENT_ACTION_UP when this event's key code is still being + * tracked from its initial down. That is, somebody requested that tracking + * started on the key down and a long press has not caused + * the tracking to be canceled. + */ + AKEY_EVENT_FLAG_TRACKING = 0x200, + + /** + * Set when a key event has been synthesized to implement default behavior + * for an event that the application did not handle. + * Fallback key events are generated by unhandled trackball motions + * (to emulate a directional keypad) and by certain unhandled key presses + * that are declared in the key map (such as special function numeric keypad + * keys when numlock is off). + */ + AKEY_EVENT_FLAG_FALLBACK = 0x400, +}; + +/** + * Bit shift for the action bits holding the pointer index as + * defined by AMOTION_EVENT_ACTION_POINTER_INDEX_MASK. + */ +#define AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT 8 + +/** Motion event actions */ +enum android_motionevent_action { + /** Bit mask of the parts of the action code that are the action itself. */ + AMOTION_EVENT_ACTION_MASK = 0xff, + + /** + * Bits in the action code that represent a pointer index, used with + * AMOTION_EVENT_ACTION_POINTER_DOWN and AMOTION_EVENT_ACTION_POINTER_UP. Shifting + * down by AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT provides the actual pointer + * index where the data for the pointer going up or down can be found. + */ + AMOTION_EVENT_ACTION_POINTER_INDEX_MASK = 0xff00, + + /** A pressed gesture has started, the motion contains the initial starting location. */ + AMOTION_EVENT_ACTION_DOWN = 0, + + /** + * A pressed gesture has finished, the motion contains the final release location + * as well as any intermediate points since the last down or move event. + */ + AMOTION_EVENT_ACTION_UP = 1, + + /** + * A change has happened during a press gesture (between AMOTION_EVENT_ACTION_DOWN and + * AMOTION_EVENT_ACTION_UP). The motion contains the most recent point, as well as + * any intermediate points since the last down or move event. + */ + AMOTION_EVENT_ACTION_MOVE = 2, + + /** + * The current gesture has been aborted. + * You will not receive any more points in it. You should treat this as + * an up event, but not perform any action that you normally would. + */ + AMOTION_EVENT_ACTION_CANCEL = 3, + + /** + * A movement has happened outside of the normal bounds of the UI element. + * This does not provide a full gesture, but only the initial location of the movement/touch. + */ + AMOTION_EVENT_ACTION_OUTSIDE = 4, + + /** + * A non-primary pointer has gone down. + * The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed. + */ + AMOTION_EVENT_ACTION_POINTER_DOWN = 5, + + /** + * A non-primary pointer has gone up. + * The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed. + */ + AMOTION_EVENT_ACTION_POINTER_UP = 6, + + /** + * A change happened but the pointer is not down (unlike AMOTION_EVENT_ACTION_MOVE). + * The motion contains the most recent point, as well as any intermediate points since + * the last hover move event. + */ + AMOTION_EVENT_ACTION_HOVER_MOVE = 7, + + /** + * The motion event contains relative vertical and/or horizontal scroll offsets. + * Use getAxisValue to retrieve the information from AMOTION_EVENT_AXIS_VSCROLL + * and AMOTION_EVENT_AXIS_HSCROLL. + * The pointer may or may not be down when this event is dispatched. + * This action is always delivered to the winder under the pointer, which + * may not be the window currently touched. + */ + AMOTION_EVENT_ACTION_SCROLL = 8, + + /** The pointer is not down but has entered the boundaries of a window or view. */ + AMOTION_EVENT_ACTION_HOVER_ENTER = 9, + + /** The pointer is not down but has exited the boundaries of a window or view. */ + AMOTION_EVENT_ACTION_HOVER_EXIT = 10, + + /* One or more buttons have been pressed. */ + AMOTION_EVENT_ACTION_BUTTON_PRESS = 11, + + /* One or more buttons have been released. */ + AMOTION_EVENT_ACTION_BUTTON_RELEASE = 12, +}; + +/** + * Motion event flags. + */ +enum android_motionevent_flags { + /** + * This flag indicates that the window that received this motion event is partly + * or wholly obscured by another visible window above it. This flag is set to true + * even if the event did not directly pass through the obscured area. + * A security sensitive application can check this flag to identify situations in which + * a malicious application may have covered up part of its content for the purpose + * of misleading the user or hijacking touches. An appropriate response might be + * to drop the suspect touches or to take additional precautions to confirm the user's + * actual intent. + */ + AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED = 0x1, +}; + +/** + * Motion event edge touch flags. + */ +enum android_motionevent_edge_touch_flags { + /** No edges intersected. */ + AMOTION_EVENT_EDGE_FLAG_NONE = 0, + + /** Flag indicating the motion event intersected the top edge of the screen. */ + AMOTION_EVENT_EDGE_FLAG_TOP = 0x01, + + /** Flag indicating the motion event intersected the bottom edge of the screen. */ + AMOTION_EVENT_EDGE_FLAG_BOTTOM = 0x02, + + /** Flag indicating the motion event intersected the left edge of the screen. */ + AMOTION_EVENT_EDGE_FLAG_LEFT = 0x04, + + /** Flag indicating the motion event intersected the right edge of the screen. */ + AMOTION_EVENT_EDGE_FLAG_RIGHT = 0x08 +}; + +/** + * Constants that identify each individual axis of a motion event. + * @anchor AMOTION_EVENT_AXIS + */ +enum android_motionevent_axis { + /** + * Axis constant: X axis of a motion event. + * + * - For a touch screen, reports the absolute X screen position of the center of + * the touch contact area. The units are display pixels. + * - For a touch pad, reports the absolute X surface position of the center of the touch + * contact area. The units are device-dependent. + * - For a mouse, reports the absolute X screen position of the mouse pointer. + * The units are display pixels. + * - For a trackball, reports the relative horizontal displacement of the trackball. + * The value is normalized to a range from -1.0 (left) to 1.0 (right). + * - For a joystick, reports the absolute X position of the joystick. + * The value is normalized to a range from -1.0 (left) to 1.0 (right). + */ + AMOTION_EVENT_AXIS_X = 0, + /** + * Axis constant: Y axis of a motion event. + * + * - For a touch screen, reports the absolute Y screen position of the center of + * the touch contact area. The units are display pixels. + * - For a touch pad, reports the absolute Y surface position of the center of the touch + * contact area. The units are device-dependent. + * - For a mouse, reports the absolute Y screen position of the mouse pointer. + * The units are display pixels. + * - For a trackball, reports the relative vertical displacement of the trackball. + * The value is normalized to a range from -1.0 (up) to 1.0 (down). + * - For a joystick, reports the absolute Y position of the joystick. + * The value is normalized to a range from -1.0 (up or far) to 1.0 (down or near). + */ + AMOTION_EVENT_AXIS_Y = 1, + /** + * Axis constant: Pressure axis of a motion event. + * + * - For a touch screen or touch pad, reports the approximate pressure applied to the surface + * by a finger or other tool. The value is normalized to a range from + * 0 (no pressure at all) to 1 (normal pressure), although values higher than 1 + * may be generated depending on the calibration of the input device. + * - For a trackball, the value is set to 1 if the trackball button is pressed + * or 0 otherwise. + * - For a mouse, the value is set to 1 if the primary mouse button is pressed + * or 0 otherwise. + */ + AMOTION_EVENT_AXIS_PRESSURE = 2, + /** + * Axis constant: Size axis of a motion event. + * + * - For a touch screen or touch pad, reports the approximate size of the contact area in + * relation to the maximum detectable size for the device. The value is normalized + * to a range from 0 (smallest detectable size) to 1 (largest detectable size), + * although it is not a linear scale. This value is of limited use. + * To obtain calibrated size information, see + * {@link AMOTION_EVENT_AXIS_TOUCH_MAJOR} or {@link AMOTION_EVENT_AXIS_TOOL_MAJOR}. + */ + AMOTION_EVENT_AXIS_SIZE = 3, + /** + * Axis constant: TouchMajor axis of a motion event. + * + * - For a touch screen, reports the length of the major axis of an ellipse that + * represents the touch area at the point of contact. + * The units are display pixels. + * - For a touch pad, reports the length of the major axis of an ellipse that + * represents the touch area at the point of contact. + * The units are device-dependent. + */ + AMOTION_EVENT_AXIS_TOUCH_MAJOR = 4, + /** + * Axis constant: TouchMinor axis of a motion event. + * + * - For a touch screen, reports the length of the minor axis of an ellipse that + * represents the touch area at the point of contact. + * The units are display pixels. + * - For a touch pad, reports the length of the minor axis of an ellipse that + * represents the touch area at the point of contact. + * The units are device-dependent. + * + * When the touch is circular, the major and minor axis lengths will be equal to one another. + */ + AMOTION_EVENT_AXIS_TOUCH_MINOR = 5, + /** + * Axis constant: ToolMajor axis of a motion event. + * + * - For a touch screen, reports the length of the major axis of an ellipse that + * represents the size of the approaching finger or tool used to make contact. + * - For a touch pad, reports the length of the major axis of an ellipse that + * represents the size of the approaching finger or tool used to make contact. + * The units are device-dependent. + * + * When the touch is circular, the major and minor axis lengths will be equal to one another. + * + * The tool size may be larger than the touch size since the tool may not be fully + * in contact with the touch sensor. + */ + AMOTION_EVENT_AXIS_TOOL_MAJOR = 6, + /** + * Axis constant: ToolMinor axis of a motion event. + * + * - For a touch screen, reports the length of the minor axis of an ellipse that + * represents the size of the approaching finger or tool used to make contact. + * - For a touch pad, reports the length of the minor axis of an ellipse that + * represents the size of the approaching finger or tool used to make contact. + * The units are device-dependent. + * + * When the touch is circular, the major and minor axis lengths will be equal to one another. + * + * The tool size may be larger than the touch size since the tool may not be fully + * in contact with the touch sensor. + */ + AMOTION_EVENT_AXIS_TOOL_MINOR = 7, + /** + * Axis constant: Orientation axis of a motion event. + * + * - For a touch screen or touch pad, reports the orientation of the finger + * or tool in radians relative to the vertical plane of the device. + * An angle of 0 radians indicates that the major axis of contact is oriented + * upwards, is perfectly circular or is of unknown orientation. A positive angle + * indicates that the major axis of contact is oriented to the right. A negative angle + * indicates that the major axis of contact is oriented to the left. + * The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians + * (finger pointing fully right). + * - For a stylus, the orientation indicates the direction in which the stylus + * is pointing in relation to the vertical axis of the current orientation of the screen. + * The range is from -PI radians to PI radians, where 0 is pointing up, + * -PI/2 radians is pointing left, -PI or PI radians is pointing down, and PI/2 radians + * is pointing right. See also {@link AMOTION_EVENT_AXIS_TILT}. + */ + AMOTION_EVENT_AXIS_ORIENTATION = 8, + /** + * Axis constant: Vertical Scroll axis of a motion event. + * + * - For a mouse, reports the relative movement of the vertical scroll wheel. + * The value is normalized to a range from -1.0 (down) to 1.0 (up). + * + * This axis should be used to scroll views vertically. + */ + AMOTION_EVENT_AXIS_VSCROLL = 9, + /** + * Axis constant: Horizontal Scroll axis of a motion event. + * + * - For a mouse, reports the relative movement of the horizontal scroll wheel. + * The value is normalized to a range from -1.0 (left) to 1.0 (right). + * + * This axis should be used to scroll views horizontally. + */ + AMOTION_EVENT_AXIS_HSCROLL = 10, + /** + * Axis constant: Z axis of a motion event. + * + * - For a joystick, reports the absolute Z position of the joystick. + * The value is normalized to a range from -1.0 (high) to 1.0 (low). + * On game pads with two analog joysticks, this axis is often reinterpreted + * to report the absolute X position of the second joystick instead. + */ + AMOTION_EVENT_AXIS_Z = 11, + /** + * Axis constant: X Rotation axis of a motion event. + * + * - For a joystick, reports the absolute rotation angle about the X axis. + * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). + */ + AMOTION_EVENT_AXIS_RX = 12, + /** + * Axis constant: Y Rotation axis of a motion event. + * + * - For a joystick, reports the absolute rotation angle about the Y axis. + * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). + */ + AMOTION_EVENT_AXIS_RY = 13, + /** + * Axis constant: Z Rotation axis of a motion event. + * + * - For a joystick, reports the absolute rotation angle about the Z axis. + * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). + * On game pads with two analog joysticks, this axis is often reinterpreted + * to report the absolute Y position of the second joystick instead. + */ + AMOTION_EVENT_AXIS_RZ = 14, + /** + * Axis constant: Hat X axis of a motion event. + * + * - For a joystick, reports the absolute X position of the directional hat control. + * The value is normalized to a range from -1.0 (left) to 1.0 (right). + */ + AMOTION_EVENT_AXIS_HAT_X = 15, + /** + * Axis constant: Hat Y axis of a motion event. + * + * - For a joystick, reports the absolute Y position of the directional hat control. + * The value is normalized to a range from -1.0 (up) to 1.0 (down). + */ + AMOTION_EVENT_AXIS_HAT_Y = 16, + /** + * Axis constant: Left Trigger axis of a motion event. + * + * - For a joystick, reports the absolute position of the left trigger control. + * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed). + */ + AMOTION_EVENT_AXIS_LTRIGGER = 17, + /** + * Axis constant: Right Trigger axis of a motion event. + * + * - For a joystick, reports the absolute position of the right trigger control. + * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed). + */ + AMOTION_EVENT_AXIS_RTRIGGER = 18, + /** + * Axis constant: Throttle axis of a motion event. + * + * - For a joystick, reports the absolute position of the throttle control. + * The value is normalized to a range from 0.0 (fully open) to 1.0 (fully closed). + */ + AMOTION_EVENT_AXIS_THROTTLE = 19, + /** + * Axis constant: Rudder axis of a motion event. + * + * - For a joystick, reports the absolute position of the rudder control. + * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right). + */ + AMOTION_EVENT_AXIS_RUDDER = 20, + /** + * Axis constant: Wheel axis of a motion event. + * + * - For a joystick, reports the absolute position of the steering wheel control. + * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right). + */ + AMOTION_EVENT_AXIS_WHEEL = 21, + /** + * Axis constant: Gas axis of a motion event. + * + * - For a joystick, reports the absolute position of the gas (accelerator) control. + * The value is normalized to a range from 0.0 (no acceleration) + * to 1.0 (maximum acceleration). + */ + AMOTION_EVENT_AXIS_GAS = 22, + /** + * Axis constant: Brake axis of a motion event. + * + * - For a joystick, reports the absolute position of the brake control. + * The value is normalized to a range from 0.0 (no braking) to 1.0 (maximum braking). + */ + AMOTION_EVENT_AXIS_BRAKE = 23, + /** + * Axis constant: Distance axis of a motion event. + * + * - For a stylus, reports the distance of the stylus from the screen. + * A value of 0.0 indicates direct contact and larger values indicate increasing + * distance from the surface. + */ + AMOTION_EVENT_AXIS_DISTANCE = 24, + /** + * Axis constant: Tilt axis of a motion event. + * + * - For a stylus, reports the tilt angle of the stylus in radians where + * 0 radians indicates that the stylus is being held perpendicular to the + * surface, and PI/2 radians indicates that the stylus is being held flat + * against the surface. + */ + AMOTION_EVENT_AXIS_TILT = 25, + /** + * Axis constant: Generic scroll axis of a motion event. + * + * - This is used for scroll axis motion events that can't be classified as strictly + * vertical or horizontal. The movement of a rotating scroller is an example of this. + */ + AMOTION_EVENT_AXIS_SCROLL = 26, + /** + * Axis constant: The movement of x position of a motion event. + * + * - For a mouse, reports a difference of x position between the previous position. + * This is useful when pointer is captured, in that case the mouse pointer doesn't + * change the location but this axis reports the difference which allows the app + * to see how the mouse is moved. + */ + AMOTION_EVENT_AXIS_RELATIVE_X = 27, + /** + * Axis constant: The movement of y position of a motion event. + * + * Same as {@link RELATIVE_X}, but for y position. + */ + AMOTION_EVENT_AXIS_RELATIVE_Y = 28, + /** + * Axis constant: Generic 1 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_1 = 32, + /** + * Axis constant: Generic 2 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_2 = 33, + /** + * Axis constant: Generic 3 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_3 = 34, + /** + * Axis constant: Generic 4 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_4 = 35, + /** + * Axis constant: Generic 5 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_5 = 36, + /** + * Axis constant: Generic 6 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_6 = 37, + /** + * Axis constant: Generic 7 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_7 = 38, + /** + * Axis constant: Generic 8 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_8 = 39, + /** + * Axis constant: Generic 9 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_9 = 40, + /** + * Axis constant: Generic 10 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_10 = 41, + /** + * Axis constant: Generic 11 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_11 = 42, + /** + * Axis constant: Generic 12 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_12 = 43, + /** + * Axis constant: Generic 13 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_13 = 44, + /** + * Axis constant: Generic 14 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_14 = 45, + /** + * Axis constant: Generic 15 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_15 = 46, + /** + * Axis constant: Generic 16 axis of a motion event. + * The interpretation of a generic axis is device-specific. + */ + AMOTION_EVENT_AXIS_GENERIC_16 = 47, + + // NOTE: If you add a new axis here you must also add it to several other files. + // Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list. +}; + +/** + * Constants that identify buttons that are associated with motion events. + * Refer to the documentation on the MotionEvent class for descriptions of each button. + */ +enum android_motionevent_buttons { + /** primary */ + AMOTION_EVENT_BUTTON_PRIMARY = 1 << 0, + /** secondary */ + AMOTION_EVENT_BUTTON_SECONDARY = 1 << 1, + /** tertiary */ + AMOTION_EVENT_BUTTON_TERTIARY = 1 << 2, + /** back */ + AMOTION_EVENT_BUTTON_BACK = 1 << 3, + /** forward */ + AMOTION_EVENT_BUTTON_FORWARD = 1 << 4, + AMOTION_EVENT_BUTTON_STYLUS_PRIMARY = 1 << 5, + AMOTION_EVENT_BUTTON_STYLUS_SECONDARY = 1 << 6, +}; + +/** + * Constants that identify tool types. + * Refer to the documentation on the MotionEvent class for descriptions of each tool type. + */ +enum android_motionevent_tool_type { + /** unknown */ + AMOTION_EVENT_TOOL_TYPE_UNKNOWN = 0, + /** finger */ + AMOTION_EVENT_TOOL_TYPE_FINGER = 1, + /** stylus */ + AMOTION_EVENT_TOOL_TYPE_STYLUS = 2, + /** mouse */ + AMOTION_EVENT_TOOL_TYPE_MOUSE = 3, + /** eraser */ + AMOTION_EVENT_TOOL_TYPE_ERASER = 4, +}; + +/** + * Input source masks. + * + * Refer to the documentation on android.view.InputDevice for more details about input sources + * and their correct interpretation. + */ +enum android_input_source_class { + /** mask */ + AINPUT_SOURCE_CLASS_MASK = 0x000000ff, + + /** none */ + AINPUT_SOURCE_CLASS_NONE = 0x00000000, + /** button */ + AINPUT_SOURCE_CLASS_BUTTON = 0x00000001, + /** pointer */ + AINPUT_SOURCE_CLASS_POINTER = 0x00000002, + /** navigation */ + AINPUT_SOURCE_CLASS_NAVIGATION = 0x00000004, + /** position */ + AINPUT_SOURCE_CLASS_POSITION = 0x00000008, + /** joystick */ + AINPUT_SOURCE_CLASS_JOYSTICK = 0x00000010, +}; + +/** + * Input sources. + */ +enum android_input_source { + /** unknown */ + AINPUT_SOURCE_UNKNOWN = 0x00000000, + + /** keyboard */ + AINPUT_SOURCE_KEYBOARD = 0x00000100 | AINPUT_SOURCE_CLASS_BUTTON, + /** dpad */ + AINPUT_SOURCE_DPAD = 0x00000200 | AINPUT_SOURCE_CLASS_BUTTON, + /** gamepad */ + AINPUT_SOURCE_GAMEPAD = 0x00000400 | AINPUT_SOURCE_CLASS_BUTTON, + /** touchscreen */ + AINPUT_SOURCE_TOUCHSCREEN = 0x00001000 | AINPUT_SOURCE_CLASS_POINTER, + /** mouse */ + AINPUT_SOURCE_MOUSE = 0x00002000 | AINPUT_SOURCE_CLASS_POINTER, + /** stylus */ + AINPUT_SOURCE_STYLUS = 0x00004000 | AINPUT_SOURCE_CLASS_POINTER, + /** bluetooth stylus */ + AINPUT_SOURCE_BLUETOOTH_STYLUS = 0x00008000 | AINPUT_SOURCE_STYLUS, + /** trackball */ + AINPUT_SOURCE_TRACKBALL = 0x00010000 | AINPUT_SOURCE_CLASS_NAVIGATION, + /** mouse relative */ + AINPUT_SOURCE_MOUSE_RELATIVE = 0x00020000 | AINPUT_SOURCE_CLASS_NAVIGATION, + /** touchpad */ + AINPUT_SOURCE_TOUCHPAD = 0x00100000 | AINPUT_SOURCE_CLASS_POSITION, + /** navigation */ + AINPUT_SOURCE_TOUCH_NAVIGATION = 0x00200000 | AINPUT_SOURCE_CLASS_NONE, + /** joystick */ + AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK, + /** rotary encoder */ + AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE, + + /** any */ + AINPUT_SOURCE_ANY = 0xffffff00, +}; + +/** + * Keyboard types. + * + * Refer to the documentation on android.view.InputDevice for more details. + */ +enum android_keyboard_type { + /** none */ + AINPUT_KEYBOARD_TYPE_NONE = 0, + /** non alphabetic */ + AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC = 1, + /** alphabetic */ + AINPUT_KEYBOARD_TYPE_ALPHABETIC = 2, +}; + +/** + * Constants used to retrieve information about the range of motion for a particular + * coordinate of a motion event. + * + * Refer to the documentation on android.view.InputDevice for more details about input sources + * and their correct interpretation. + * + * @deprecated These constants are deprecated. Use {@link AMOTION_EVENT_AXIS AMOTION_EVENT_AXIS_*} constants instead. + */ +enum android_motion_range { + /** x */ + AINPUT_MOTION_RANGE_X = AMOTION_EVENT_AXIS_X, + /** y */ + AINPUT_MOTION_RANGE_Y = AMOTION_EVENT_AXIS_Y, + /** pressure */ + AINPUT_MOTION_RANGE_PRESSURE = AMOTION_EVENT_AXIS_PRESSURE, + /** size */ + AINPUT_MOTION_RANGE_SIZE = AMOTION_EVENT_AXIS_SIZE, + /** touch major */ + AINPUT_MOTION_RANGE_TOUCH_MAJOR = AMOTION_EVENT_AXIS_TOUCH_MAJOR, + /** touch minor */ + AINPUT_MOTION_RANGE_TOUCH_MINOR = AMOTION_EVENT_AXIS_TOUCH_MINOR, + /** tool major */ + AINPUT_MOTION_RANGE_TOOL_MAJOR = AMOTION_EVENT_AXIS_TOOL_MAJOR, + /** tool minor */ + AINPUT_MOTION_RANGE_TOOL_MINOR = AMOTION_EVENT_AXIS_TOOL_MINOR, + /** orientation */ + AINPUT_MOTION_RANGE_ORIENTATION = AMOTION_EVENT_AXIS_ORIENTATION, +}; + +#endif // _ANDROID_INPUT_H diff --git a/app/src/android/keycodes.h b/app/src/android/keycodes.h new file mode 100644 index 00000000..60465a18 --- /dev/null +++ b/app/src/android/keycodes.h @@ -0,0 +1,745 @@ +// copied from +// blob 2164d6163e1646c22825e364cad4f3c47638effd +// (and modified) +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _ANDROID_KEYCODES_H +#define _ANDROID_KEYCODES_H + +/** + * Key codes. + */ +enum android_keycode { + /** Unknown key code. */ + AKEYCODE_UNKNOWN = 0, + /** Soft Left key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom left + * of the display. */ + AKEYCODE_SOFT_LEFT = 1, + /** Soft Right key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom right + * of the display. */ + AKEYCODE_SOFT_RIGHT = 2, + /** Home key. + * This key is handled by the framework and is never delivered to applications. */ + AKEYCODE_HOME = 3, + /** Back key. */ + AKEYCODE_BACK = 4, + /** Call key. */ + AKEYCODE_CALL = 5, + /** End Call key. */ + AKEYCODE_ENDCALL = 6, + /** '0' key. */ + AKEYCODE_0 = 7, + /** '1' key. */ + AKEYCODE_1 = 8, + /** '2' key. */ + AKEYCODE_2 = 9, + /** '3' key. */ + AKEYCODE_3 = 10, + /** '4' key. */ + AKEYCODE_4 = 11, + /** '5' key. */ + AKEYCODE_5 = 12, + /** '6' key. */ + AKEYCODE_6 = 13, + /** '7' key. */ + AKEYCODE_7 = 14, + /** '8' key. */ + AKEYCODE_8 = 15, + /** '9' key. */ + AKEYCODE_9 = 16, + /** '*' key. */ + AKEYCODE_STAR = 17, + /** '#' key. */ + AKEYCODE_POUND = 18, + /** Directional Pad Up key. + * May also be synthesized from trackball motions. */ + AKEYCODE_DPAD_UP = 19, + /** Directional Pad Down key. + * May also be synthesized from trackball motions. */ + AKEYCODE_DPAD_DOWN = 20, + /** Directional Pad Left key. + * May also be synthesized from trackball motions. */ + AKEYCODE_DPAD_LEFT = 21, + /** Directional Pad Right key. + * May also be synthesized from trackball motions. */ + AKEYCODE_DPAD_RIGHT = 22, + /** Directional Pad Center key. + * May also be synthesized from trackball motions. */ + AKEYCODE_DPAD_CENTER = 23, + /** Volume Up key. + * Adjusts the speaker volume up. */ + AKEYCODE_VOLUME_UP = 24, + /** Volume Down key. + * Adjusts the speaker volume down. */ + AKEYCODE_VOLUME_DOWN = 25, + /** Power key. */ + AKEYCODE_POWER = 26, + /** Camera key. + * Used to launch a camera application or take pictures. */ + AKEYCODE_CAMERA = 27, + /** Clear key. */ + AKEYCODE_CLEAR = 28, + /** 'A' key. */ + AKEYCODE_A = 29, + /** 'B' key. */ + AKEYCODE_B = 30, + /** 'C' key. */ + AKEYCODE_C = 31, + /** 'D' key. */ + AKEYCODE_D = 32, + /** 'E' key. */ + AKEYCODE_E = 33, + /** 'F' key. */ + AKEYCODE_F = 34, + /** 'G' key. */ + AKEYCODE_G = 35, + /** 'H' key. */ + AKEYCODE_H = 36, + /** 'I' key. */ + AKEYCODE_I = 37, + /** 'J' key. */ + AKEYCODE_J = 38, + /** 'K' key. */ + AKEYCODE_K = 39, + /** 'L' key. */ + AKEYCODE_L = 40, + /** 'M' key. */ + AKEYCODE_M = 41, + /** 'N' key. */ + AKEYCODE_N = 42, + /** 'O' key. */ + AKEYCODE_O = 43, + /** 'P' key. */ + AKEYCODE_P = 44, + /** 'Q' key. */ + AKEYCODE_Q = 45, + /** 'R' key. */ + AKEYCODE_R = 46, + /** 'S' key. */ + AKEYCODE_S = 47, + /** 'T' key. */ + AKEYCODE_T = 48, + /** 'U' key. */ + AKEYCODE_U = 49, + /** 'V' key. */ + AKEYCODE_V = 50, + /** 'W' key. */ + AKEYCODE_W = 51, + /** 'X' key. */ + AKEYCODE_X = 52, + /** 'Y' key. */ + AKEYCODE_Y = 53, + /** 'Z' key. */ + AKEYCODE_Z = 54, + /** ',' key. */ + AKEYCODE_COMMA = 55, + /** '.' key. */ + AKEYCODE_PERIOD = 56, + /** Left Alt modifier key. */ + AKEYCODE_ALT_LEFT = 57, + /** Right Alt modifier key. */ + AKEYCODE_ALT_RIGHT = 58, + /** Left Shift modifier key. */ + AKEYCODE_SHIFT_LEFT = 59, + /** Right Shift modifier key. */ + AKEYCODE_SHIFT_RIGHT = 60, + /** Tab key. */ + AKEYCODE_TAB = 61, + /** Space key. */ + AKEYCODE_SPACE = 62, + /** Symbol modifier key. + * Used to enter alternate symbols. */ + AKEYCODE_SYM = 63, + /** Explorer special function key. + * Used to launch a browser application. */ + AKEYCODE_EXPLORER = 64, + /** Envelope special function key. + * Used to launch a mail application. */ + AKEYCODE_ENVELOPE = 65, + /** Enter key. */ + AKEYCODE_ENTER = 66, + /** Backspace key. + * Deletes characters before the insertion point, unlike {@link AKEYCODE_FORWARD_DEL}. */ + AKEYCODE_DEL = 67, + /** '`' (backtick) key. */ + AKEYCODE_GRAVE = 68, + /** '-'. */ + AKEYCODE_MINUS = 69, + /** '=' key. */ + AKEYCODE_EQUALS = 70, + /** '[' key. */ + AKEYCODE_LEFT_BRACKET = 71, + /** ']' key. */ + AKEYCODE_RIGHT_BRACKET = 72, + /** '\' key. */ + AKEYCODE_BACKSLASH = 73, + /** ';' key. */ + AKEYCODE_SEMICOLON = 74, + /** ''' (apostrophe) key. */ + AKEYCODE_APOSTROPHE = 75, + /** '/' key. */ + AKEYCODE_SLASH = 76, + /** '@' key. */ + AKEYCODE_AT = 77, + /** Number modifier key. + * Used to enter numeric symbols. + * This key is not {@link AKEYCODE_NUM_LOCK}; it is more like {@link AKEYCODE_ALT_LEFT}. */ + AKEYCODE_NUM = 78, + /** Headset Hook key. + * Used to hang up calls and stop media. */ + AKEYCODE_HEADSETHOOK = 79, + /** Camera Focus key. + * Used to focus the camera. */ + AKEYCODE_FOCUS = 80, + /** '+' key. */ + AKEYCODE_PLUS = 81, + /** Menu key. */ + AKEYCODE_MENU = 82, + /** Notification key. */ + AKEYCODE_NOTIFICATION = 83, + /** Search key. */ + AKEYCODE_SEARCH = 84, + /** Play/Pause media key. */ + AKEYCODE_MEDIA_PLAY_PAUSE= 85, + /** Stop media key. */ + AKEYCODE_MEDIA_STOP = 86, + /** Play Next media key. */ + AKEYCODE_MEDIA_NEXT = 87, + /** Play Previous media key. */ + AKEYCODE_MEDIA_PREVIOUS = 88, + /** Rewind media key. */ + AKEYCODE_MEDIA_REWIND = 89, + /** Fast Forward media key. */ + AKEYCODE_MEDIA_FAST_FORWARD = 90, + /** Mute key. + * Mutes the microphone, unlike {@link AKEYCODE_VOLUME_MUTE}. */ + AKEYCODE_MUTE = 91, + /** Page Up key. */ + AKEYCODE_PAGE_UP = 92, + /** Page Down key. */ + AKEYCODE_PAGE_DOWN = 93, + /** Picture Symbols modifier key. + * Used to switch symbol sets (Emoji, Kao-moji). */ + AKEYCODE_PICTSYMBOLS = 94, + /** Switch Charset modifier key. + * Used to switch character sets (Kanji, Katakana). */ + AKEYCODE_SWITCH_CHARSET = 95, + /** A Button key. + * On a game controller, the A button should be either the button labeled A + * or the first button on the bottom row of controller buttons. */ + AKEYCODE_BUTTON_A = 96, + /** B Button key. + * On a game controller, the B button should be either the button labeled B + * or the second button on the bottom row of controller buttons. */ + AKEYCODE_BUTTON_B = 97, + /** C Button key. + * On a game controller, the C button should be either the button labeled C + * or the third button on the bottom row of controller buttons. */ + AKEYCODE_BUTTON_C = 98, + /** X Button key. + * On a game controller, the X button should be either the button labeled X + * or the first button on the upper row of controller buttons. */ + AKEYCODE_BUTTON_X = 99, + /** Y Button key. + * On a game controller, the Y button should be either the button labeled Y + * or the second button on the upper row of controller buttons. */ + AKEYCODE_BUTTON_Y = 100, + /** Z Button key. + * On a game controller, the Z button should be either the button labeled Z + * or the third button on the upper row of controller buttons. */ + AKEYCODE_BUTTON_Z = 101, + /** L1 Button key. + * On a game controller, the L1 button should be either the button labeled L1 (or L) + * or the top left trigger button. */ + AKEYCODE_BUTTON_L1 = 102, + /** R1 Button key. + * On a game controller, the R1 button should be either the button labeled R1 (or R) + * or the top right trigger button. */ + AKEYCODE_BUTTON_R1 = 103, + /** L2 Button key. + * On a game controller, the L2 button should be either the button labeled L2 + * or the bottom left trigger button. */ + AKEYCODE_BUTTON_L2 = 104, + /** R2 Button key. + * On a game controller, the R2 button should be either the button labeled R2 + * or the bottom right trigger button. */ + AKEYCODE_BUTTON_R2 = 105, + /** Left Thumb Button key. + * On a game controller, the left thumb button indicates that the left (or only) + * joystick is pressed. */ + AKEYCODE_BUTTON_THUMBL = 106, + /** Right Thumb Button key. + * On a game controller, the right thumb button indicates that the right + * joystick is pressed. */ + AKEYCODE_BUTTON_THUMBR = 107, + /** Start Button key. + * On a game controller, the button labeled Start. */ + AKEYCODE_BUTTON_START = 108, + /** Select Button key. + * On a game controller, the button labeled Select. */ + AKEYCODE_BUTTON_SELECT = 109, + /** Mode Button key. + * On a game controller, the button labeled Mode. */ + AKEYCODE_BUTTON_MODE = 110, + /** Escape key. */ + AKEYCODE_ESCAPE = 111, + /** Forward Delete key. + * Deletes characters ahead of the insertion point, unlike {@link AKEYCODE_DEL}. */ + AKEYCODE_FORWARD_DEL = 112, + /** Left Control modifier key. */ + AKEYCODE_CTRL_LEFT = 113, + /** Right Control modifier key. */ + AKEYCODE_CTRL_RIGHT = 114, + /** Caps Lock key. */ + AKEYCODE_CAPS_LOCK = 115, + /** Scroll Lock key. */ + AKEYCODE_SCROLL_LOCK = 116, + /** Left Meta modifier key. */ + AKEYCODE_META_LEFT = 117, + /** Right Meta modifier key. */ + AKEYCODE_META_RIGHT = 118, + /** Function modifier key. */ + AKEYCODE_FUNCTION = 119, + /** System Request / Print Screen key. */ + AKEYCODE_SYSRQ = 120, + /** Break / Pause key. */ + AKEYCODE_BREAK = 121, + /** Home Movement key. + * Used for scrolling or moving the cursor around to the start of a line + * or to the top of a list. */ + AKEYCODE_MOVE_HOME = 122, + /** End Movement key. + * Used for scrolling or moving the cursor around to the end of a line + * or to the bottom of a list. */ + AKEYCODE_MOVE_END = 123, + /** Insert key. + * Toggles insert / overwrite edit mode. */ + AKEYCODE_INSERT = 124, + /** Forward key. + * Navigates forward in the history stack. Complement of {@link AKEYCODE_BACK}. */ + AKEYCODE_FORWARD = 125, + /** Play media key. */ + AKEYCODE_MEDIA_PLAY = 126, + /** Pause media key. */ + AKEYCODE_MEDIA_PAUSE = 127, + /** Close media key. + * May be used to close a CD tray, for example. */ + AKEYCODE_MEDIA_CLOSE = 128, + /** Eject media key. + * May be used to eject a CD tray, for example. */ + AKEYCODE_MEDIA_EJECT = 129, + /** Record media key. */ + AKEYCODE_MEDIA_RECORD = 130, + /** F1 key. */ + AKEYCODE_F1 = 131, + /** F2 key. */ + AKEYCODE_F2 = 132, + /** F3 key. */ + AKEYCODE_F3 = 133, + /** F4 key. */ + AKEYCODE_F4 = 134, + /** F5 key. */ + AKEYCODE_F5 = 135, + /** F6 key. */ + AKEYCODE_F6 = 136, + /** F7 key. */ + AKEYCODE_F7 = 137, + /** F8 key. */ + AKEYCODE_F8 = 138, + /** F9 key. */ + AKEYCODE_F9 = 139, + /** F10 key. */ + AKEYCODE_F10 = 140, + /** F11 key. */ + AKEYCODE_F11 = 141, + /** F12 key. */ + AKEYCODE_F12 = 142, + /** Num Lock key. + * This is the Num Lock key; it is different from {@link AKEYCODE_NUM}. + * This key alters the behavior of other keys on the numeric keypad. */ + AKEYCODE_NUM_LOCK = 143, + /** Numeric keypad '0' key. */ + AKEYCODE_NUMPAD_0 = 144, + /** Numeric keypad '1' key. */ + AKEYCODE_NUMPAD_1 = 145, + /** Numeric keypad '2' key. */ + AKEYCODE_NUMPAD_2 = 146, + /** Numeric keypad '3' key. */ + AKEYCODE_NUMPAD_3 = 147, + /** Numeric keypad '4' key. */ + AKEYCODE_NUMPAD_4 = 148, + /** Numeric keypad '5' key. */ + AKEYCODE_NUMPAD_5 = 149, + /** Numeric keypad '6' key. */ + AKEYCODE_NUMPAD_6 = 150, + /** Numeric keypad '7' key. */ + AKEYCODE_NUMPAD_7 = 151, + /** Numeric keypad '8' key. */ + AKEYCODE_NUMPAD_8 = 152, + /** Numeric keypad '9' key. */ + AKEYCODE_NUMPAD_9 = 153, + /** Numeric keypad '/' key (for division). */ + AKEYCODE_NUMPAD_DIVIDE = 154, + /** Numeric keypad '*' key (for multiplication). */ + AKEYCODE_NUMPAD_MULTIPLY = 155, + /** Numeric keypad '-' key (for subtraction). */ + AKEYCODE_NUMPAD_SUBTRACT = 156, + /** Numeric keypad '+' key (for addition). */ + AKEYCODE_NUMPAD_ADD = 157, + /** Numeric keypad '.' key (for decimals or digit grouping). */ + AKEYCODE_NUMPAD_DOT = 158, + /** Numeric keypad ',' key (for decimals or digit grouping). */ + AKEYCODE_NUMPAD_COMMA = 159, + /** Numeric keypad Enter key. */ + AKEYCODE_NUMPAD_ENTER = 160, + /** Numeric keypad '=' key. */ + AKEYCODE_NUMPAD_EQUALS = 161, + /** Numeric keypad '(' key. */ + AKEYCODE_NUMPAD_LEFT_PAREN = 162, + /** Numeric keypad ')' key. */ + AKEYCODE_NUMPAD_RIGHT_PAREN = 163, + /** Volume Mute key. + * Mutes the speaker, unlike {@link AKEYCODE_MUTE}. + * This key should normally be implemented as a toggle such that the first press + * mutes the speaker and the second press restores the original volume. */ + AKEYCODE_VOLUME_MUTE = 164, + /** Info key. + * Common on TV remotes to show additional information related to what is + * currently being viewed. */ + AKEYCODE_INFO = 165, + /** Channel up key. + * On TV remotes, increments the television channel. */ + AKEYCODE_CHANNEL_UP = 166, + /** Channel down key. + * On TV remotes, decrements the television channel. */ + AKEYCODE_CHANNEL_DOWN = 167, + /** Zoom in key. */ + AKEYCODE_ZOOM_IN = 168, + /** Zoom out key. */ + AKEYCODE_ZOOM_OUT = 169, + /** TV key. + * On TV remotes, switches to viewing live TV. */ + AKEYCODE_TV = 170, + /** Window key. + * On TV remotes, toggles picture-in-picture mode or other windowing functions. */ + AKEYCODE_WINDOW = 171, + /** Guide key. + * On TV remotes, shows a programming guide. */ + AKEYCODE_GUIDE = 172, + /** DVR key. + * On some TV remotes, switches to a DVR mode for recorded shows. */ + AKEYCODE_DVR = 173, + /** Bookmark key. + * On some TV remotes, bookmarks content or web pages. */ + AKEYCODE_BOOKMARK = 174, + /** Toggle captions key. + * Switches the mode for closed-captioning text, for example during television shows. */ + AKEYCODE_CAPTIONS = 175, + /** Settings key. + * Starts the system settings activity. */ + AKEYCODE_SETTINGS = 176, + /** TV power key. + * On TV remotes, toggles the power on a television screen. */ + AKEYCODE_TV_POWER = 177, + /** TV input key. + * On TV remotes, switches the input on a television screen. */ + AKEYCODE_TV_INPUT = 178, + /** Set-top-box power key. + * On TV remotes, toggles the power on an external Set-top-box. */ + AKEYCODE_STB_POWER = 179, + /** Set-top-box input key. + * On TV remotes, switches the input mode on an external Set-top-box. */ + AKEYCODE_STB_INPUT = 180, + /** A/V Receiver power key. + * On TV remotes, toggles the power on an external A/V Receiver. */ + AKEYCODE_AVR_POWER = 181, + /** A/V Receiver input key. + * On TV remotes, switches the input mode on an external A/V Receiver. */ + AKEYCODE_AVR_INPUT = 182, + /** Red "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + AKEYCODE_PROG_RED = 183, + /** Green "programmable" key. + * On TV remotes, actsas a contextual/programmable key. */ + AKEYCODE_PROG_GREEN = 184, + /** Yellow "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + AKEYCODE_PROG_YELLOW = 185, + /** Blue "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + AKEYCODE_PROG_BLUE = 186, + /** App switch key. + * Should bring up the application switcher dialog. */ + AKEYCODE_APP_SWITCH = 187, + /** Generic Game Pad Button #1.*/ + AKEYCODE_BUTTON_1 = 188, + /** Generic Game Pad Button #2.*/ + AKEYCODE_BUTTON_2 = 189, + /** Generic Game Pad Button #3.*/ + AKEYCODE_BUTTON_3 = 190, + /** Generic Game Pad Button #4.*/ + AKEYCODE_BUTTON_4 = 191, + /** Generic Game Pad Button #5.*/ + AKEYCODE_BUTTON_5 = 192, + /** Generic Game Pad Button #6.*/ + AKEYCODE_BUTTON_6 = 193, + /** Generic Game Pad Button #7.*/ + AKEYCODE_BUTTON_7 = 194, + /** Generic Game Pad Button #8.*/ + AKEYCODE_BUTTON_8 = 195, + /** Generic Game Pad Button #9.*/ + AKEYCODE_BUTTON_9 = 196, + /** Generic Game Pad Button #10.*/ + AKEYCODE_BUTTON_10 = 197, + /** Generic Game Pad Button #11.*/ + AKEYCODE_BUTTON_11 = 198, + /** Generic Game Pad Button #12.*/ + AKEYCODE_BUTTON_12 = 199, + /** Generic Game Pad Button #13.*/ + AKEYCODE_BUTTON_13 = 200, + /** Generic Game Pad Button #14.*/ + AKEYCODE_BUTTON_14 = 201, + /** Generic Game Pad Button #15.*/ + AKEYCODE_BUTTON_15 = 202, + /** Generic Game Pad Button #16.*/ + AKEYCODE_BUTTON_16 = 203, + /** Language Switch key. + * Toggles the current input language such as switching between English and Japanese on + * a QWERTY keyboard. On some devices, the same function may be performed by + * pressing Shift+Spacebar. */ + AKEYCODE_LANGUAGE_SWITCH = 204, + /** Manner Mode key. + * Toggles silent or vibrate mode on and off to make the device behave more politely + * in certain settings such as on a crowded train. On some devices, the key may only + * operate when long-pressed. */ + AKEYCODE_MANNER_MODE = 205, + /** 3D Mode key. + * Toggles the display between 2D and 3D mode. */ + AKEYCODE_3D_MODE = 206, + /** Contacts special function key. + * Used to launch an address book application. */ + AKEYCODE_CONTACTS = 207, + /** Calendar special function key. + * Used to launch a calendar application. */ + AKEYCODE_CALENDAR = 208, + /** Music special function key. + * Used to launch a music player application. */ + AKEYCODE_MUSIC = 209, + /** Calculator special function key. + * Used to launch a calculator application. */ + AKEYCODE_CALCULATOR = 210, + /** Japanese full-width / half-width key. */ + AKEYCODE_ZENKAKU_HANKAKU = 211, + /** Japanese alphanumeric key. */ + AKEYCODE_EISU = 212, + /** Japanese non-conversion key. */ + AKEYCODE_MUHENKAN = 213, + /** Japanese conversion key. */ + AKEYCODE_HENKAN = 214, + /** Japanese katakana / hiragana key. */ + AKEYCODE_KATAKANA_HIRAGANA = 215, + /** Japanese Yen key. */ + AKEYCODE_YEN = 216, + /** Japanese Ro key. */ + AKEYCODE_RO = 217, + /** Japanese kana key. */ + AKEYCODE_KANA = 218, + /** Assist key. + * Launches the global assist activity. Not delivered to applications. */ + AKEYCODE_ASSIST = 219, + /** Brightness Down key. + * Adjusts the screen brightness down. */ + AKEYCODE_BRIGHTNESS_DOWN = 220, + /** Brightness Up key. + * Adjusts the screen brightness up. */ + AKEYCODE_BRIGHTNESS_UP = 221, + /** Audio Track key. + * Switches the audio tracks. */ + AKEYCODE_MEDIA_AUDIO_TRACK = 222, + /** Sleep key. + * Puts the device to sleep. Behaves somewhat like {@link AKEYCODE_POWER} but it + * has no effect if the device is already asleep. */ + AKEYCODE_SLEEP = 223, + /** Wakeup key. + * Wakes up the device. Behaves somewhat like {@link AKEYCODE_POWER} but it + * has no effect if the device is already awake. */ + AKEYCODE_WAKEUP = 224, + /** Pairing key. + * Initiates peripheral pairing mode. Useful for pairing remote control + * devices or game controllers, especially if no other input mode is + * available. */ + AKEYCODE_PAIRING = 225, + /** Media Top Menu key. + * Goes to the top of media menu. */ + AKEYCODE_MEDIA_TOP_MENU = 226, + /** '11' key. */ + AKEYCODE_11 = 227, + /** '12' key. */ + AKEYCODE_12 = 228, + /** Last Channel key. + * Goes to the last viewed channel. */ + AKEYCODE_LAST_CHANNEL = 229, + /** TV data service key. + * Displays data services like weather, sports. */ + AKEYCODE_TV_DATA_SERVICE = 230, + /** Voice Assist key. + * Launches the global voice assist activity. Not delivered to applications. */ + AKEYCODE_VOICE_ASSIST = 231, + /** Radio key. + * Toggles TV service / Radio service. */ + AKEYCODE_TV_RADIO_SERVICE = 232, + /** Teletext key. + * Displays Teletext service. */ + AKEYCODE_TV_TELETEXT = 233, + /** Number entry key. + * Initiates to enter multi-digit channel nubmber when each digit key is assigned + * for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + * User Control Code. */ + AKEYCODE_TV_NUMBER_ENTRY = 234, + /** Analog Terrestrial key. + * Switches to analog terrestrial broadcast service. */ + AKEYCODE_TV_TERRESTRIAL_ANALOG = 235, + /** Digital Terrestrial key. + * Switches to digital terrestrial broadcast service. */ + AKEYCODE_TV_TERRESTRIAL_DIGITAL = 236, + /** Satellite key. + * Switches to digital satellite broadcast service. */ + AKEYCODE_TV_SATELLITE = 237, + /** BS key. + * Switches to BS digital satellite broadcasting service available in Japan. */ + AKEYCODE_TV_SATELLITE_BS = 238, + /** CS key. + * Switches to CS digital satellite broadcasting service available in Japan. */ + AKEYCODE_TV_SATELLITE_CS = 239, + /** BS/CS key. + * Toggles between BS and CS digital satellite services. */ + AKEYCODE_TV_SATELLITE_SERVICE = 240, + /** Toggle Network key. + * Toggles selecting broacast services. */ + AKEYCODE_TV_NETWORK = 241, + /** Antenna/Cable key. + * Toggles broadcast input source between antenna and cable. */ + AKEYCODE_TV_ANTENNA_CABLE = 242, + /** HDMI #1 key. + * Switches to HDMI input #1. */ + AKEYCODE_TV_INPUT_HDMI_1 = 243, + /** HDMI #2 key. + * Switches to HDMI input #2. */ + AKEYCODE_TV_INPUT_HDMI_2 = 244, + /** HDMI #3 key. + * Switches to HDMI input #3. */ + AKEYCODE_TV_INPUT_HDMI_3 = 245, + /** HDMI #4 key. + * Switches to HDMI input #4. */ + AKEYCODE_TV_INPUT_HDMI_4 = 246, + /** Composite #1 key. + * Switches to composite video input #1. */ + AKEYCODE_TV_INPUT_COMPOSITE_1 = 247, + /** Composite #2 key. + * Switches to composite video input #2. */ + AKEYCODE_TV_INPUT_COMPOSITE_2 = 248, + /** Component #1 key. + * Switches to component video input #1. */ + AKEYCODE_TV_INPUT_COMPONENT_1 = 249, + /** Component #2 key. + * Switches to component video input #2. */ + AKEYCODE_TV_INPUT_COMPONENT_2 = 250, + /** VGA #1 key. + * Switches to VGA (analog RGB) input #1. */ + AKEYCODE_TV_INPUT_VGA_1 = 251, + /** Audio description key. + * Toggles audio description off / on. */ + AKEYCODE_TV_AUDIO_DESCRIPTION = 252, + /** Audio description mixing volume up key. + * Louden audio description volume as compared with normal audio volume. */ + AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253, + /** Audio description mixing volume down key. + * Lessen audio description volume as compared with normal audio volume. */ + AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254, + /** Zoom mode key. + * Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) */ + AKEYCODE_TV_ZOOM_MODE = 255, + /** Contents menu key. + * Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + * Code */ + AKEYCODE_TV_CONTENTS_MENU = 256, + /** Media context menu key. + * Goes to the context menu of media contents. Corresponds to Media Context-sensitive + * Menu (0x11) of CEC User Control Code. */ + AKEYCODE_TV_MEDIA_CONTEXT_MENU = 257, + /** Timer programming key. + * Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + * CEC User Control Code. */ + AKEYCODE_TV_TIMER_PROGRAMMING = 258, + /** Help key. */ + AKEYCODE_HELP = 259, + AKEYCODE_NAVIGATE_PREVIOUS = 260, + AKEYCODE_NAVIGATE_NEXT = 261, + AKEYCODE_NAVIGATE_IN = 262, + AKEYCODE_NAVIGATE_OUT = 263, + /** Primary stem key for Wear + * Main power/reset button on watch. */ + AKEYCODE_STEM_PRIMARY = 264, + /** Generic stem key 1 for Wear */ + AKEYCODE_STEM_1 = 265, + /** Generic stem key 2 for Wear */ + AKEYCODE_STEM_2 = 266, + /** Generic stem key 3 for Wear */ + AKEYCODE_STEM_3 = 267, + /** Directional Pad Up-Left */ + AKEYCODE_DPAD_UP_LEFT = 268, + /** Directional Pad Down-Left */ + AKEYCODE_DPAD_DOWN_LEFT = 269, + /** Directional Pad Up-Right */ + AKEYCODE_DPAD_UP_RIGHT = 270, + /** Directional Pad Down-Right */ + AKEYCODE_DPAD_DOWN_RIGHT = 271, + /** Skip forward media key */ + AKEYCODE_MEDIA_SKIP_FORWARD = 272, + /** Skip backward media key */ + AKEYCODE_MEDIA_SKIP_BACKWARD = 273, + /** Step forward media key. + * Steps media forward one from at a time. */ + AKEYCODE_MEDIA_STEP_FORWARD = 274, + /** Step backward media key. + * Steps media backward one from at a time. */ + AKEYCODE_MEDIA_STEP_BACKWARD = 275, + /** Put device to sleep unless a wakelock is held. */ + AKEYCODE_SOFT_SLEEP = 276, + /** Cut key. */ + AKEYCODE_CUT = 277, + /** Copy key. */ + AKEYCODE_COPY = 278, + /** Paste key. */ + AKEYCODE_PASTE = 279, + /** fingerprint navigation key, up. */ + AKEYCODE_SYSTEM_NAVIGATION_UP = 280, + /** fingerprint navigation key, down. */ + AKEYCODE_SYSTEM_NAVIGATION_DOWN = 281, + /** fingerprint navigation key, left. */ + AKEYCODE_SYSTEM_NAVIGATION_LEFT = 282, + /** fingerprint navigation key, right. */ + AKEYCODE_SYSTEM_NAVIGATION_RIGHT = 283, + /** all apps */ + AKEYCODE_ALL_APPS = 284 +}; + +#endif // _ANDROID_KEYCODES_H diff --git a/app/src/control.c b/app/src/control.c new file mode 100644 index 00000000..0b072dff --- /dev/null +++ b/app/src/control.c @@ -0,0 +1,99 @@ +#include "control.h" + +#include "lockutil.h" + +SDL_bool controller_init(struct controller *controller, TCPsocket video_socket) { + if (!control_event_queue_init(&controller->queue)) { + return SDL_FALSE; + } + + if (!(controller->mutex = SDL_CreateMutex())) { + return SDL_FALSE; + } + + if (!(controller->event_cond = SDL_CreateCond())) { + SDL_DestroyMutex(controller->mutex); + return SDL_FALSE; + } + + controller->video_socket = video_socket; + controller->stopped = SDL_FALSE; + + return SDL_TRUE; +} + +void controller_destroy(struct controller *controller) { + SDL_DestroyCond(controller->event_cond); + SDL_DestroyMutex(controller->mutex); + control_event_queue_destroy(&controller->queue); +} + +SDL_bool controller_push_event(struct controller *controller, struct control_event *event) { + SDL_bool res; + mutex_lock(controller->mutex); + SDL_bool was_empty = control_event_queue_is_empty(&controller->queue); + res = control_event_queue_push(&controller->queue, event); + if (was_empty) { + cond_signal(controller->event_cond); + } + mutex_unlock(controller->mutex); + return res; +} + +static SDL_bool process_event(struct controller *controller, struct control_event *event) { + unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE]; + int length = control_event_serialize(event, serialized_event); + if (!length) { + return SDL_FALSE; + } + int w = SDLNet_TCP_Send(controller->video_socket, serialized_event, length); + return w == length; +} + +static int run_controller(void *data) { + struct controller *controller = data; + + mutex_lock(controller->mutex); + for (;;) { + while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) { + cond_wait(controller->event_cond, controller->mutex); + } + if (controller->stopped) { + // stop immediately, do not process further events + break; + } + struct control_event event; + while (control_event_queue_take(&controller->queue, &event)) { + if (!process_event(controller, &event)) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Cannot write event to socket"); + goto end; + } + } + } +end: + mutex_unlock(controller->mutex); + return 0; +} + +SDL_bool controller_start(struct controller *controller) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Starting controller thread"); + + controller->thread = SDL_CreateThread(run_controller, "controller", controller); + if (!controller->thread) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not start controller thread"); + return SDL_FALSE; + } + + return SDL_TRUE; +} + +void controller_stop(struct controller *controller) { + mutex_lock(controller->mutex); + controller->stopped = SDL_TRUE; + cond_signal(controller->event_cond); + mutex_unlock(controller->mutex); +} + +void controller_join(struct controller *controller) { + SDL_WaitThread(controller->thread, NULL); +} diff --git a/app/src/control.h b/app/src/control.h new file mode 100644 index 00000000..3efd14f9 --- /dev/null +++ b/app/src/control.h @@ -0,0 +1,29 @@ +#ifndef CONTROL_H +#define CONTROL_H + +#include "controlevent.h" + +#include +#include +#include + +struct controller { + TCPsocket video_socket; + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *event_cond; + SDL_bool stopped; + struct control_event_queue queue; +}; + +SDL_bool controller_init(struct controller *controller, TCPsocket video_socket); +void controller_destroy(struct controller *controller); + +SDL_bool controller_start(struct controller *controller); +void controller_stop(struct controller *controller); +void controller_join(struct controller *controller); + +// expose simple API to hide control_event_queue +SDL_bool controller_push_event(struct controller *controller, struct control_event *event); + +#endif diff --git a/app/src/controlevent.c b/app/src/controlevent.c new file mode 100644 index 00000000..fda49d11 --- /dev/null +++ b/app/src/controlevent.c @@ -0,0 +1,91 @@ +#include "controlevent.h" + +#include +#include + +#include "lockutil.h" + +static inline void write16(Uint8 *buf, Uint16 value) { + buf[0] = value >> 8; + buf[1] = value; +} + +static inline void write32(Uint8 *buf, Uint32 value) { + buf[0] = value >> 24; + buf[1] = value >> 16; + buf[2] = value >> 8; + buf[3] = value; +} + +int control_event_serialize(struct control_event *event, unsigned char *buf) { + buf[0] = event->type; + switch (event->type) { + case CONTROL_EVENT_TYPE_KEYCODE: + buf[1] = event->keycode_event.action; + write32(&buf[2], event->keycode_event.keycode); + write32(&buf[6], event->keycode_event.metastate); + return 10; + case CONTROL_EVENT_TYPE_TEXT: { + // write length (1 byte) + date (non nul-terminated) + size_t len = strlen(event->text_event.text); + if (len > TEXT_MAX_LENGTH) { + len = TEXT_MAX_LENGTH; + } + buf[1] = (Uint8) len; + memcpy(&buf[2], &event->text_event.text, len); + return 2 + len; + } + case CONTROL_EVENT_TYPE_MOUSE: + buf[1] = event->mouse_event.action; + write32(&buf[2], event->mouse_event.buttons); + write32(&buf[6], (Uint32) event->mouse_event.x); + write32(&buf[10], (Uint32) event->mouse_event.y); + return 14; + case CONTROL_EVENT_TYPE_SCROLL: + write32(&buf[1], (Uint32) event->scroll_event.x); + write32(&buf[5], (Uint32) event->scroll_event.y); + write32(&buf[9], (Uint32) event->scroll_event.hscroll); + write32(&buf[13], (Uint32) event->scroll_event.vscroll); + return 17; + default: + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unknown event type: %u\n", (unsigned) event->type); + return 0; + } +} + +SDL_bool control_event_queue_is_empty(struct control_event_queue *queue) { + return queue->head == queue->tail; +} + +SDL_bool control_event_queue_is_full(struct control_event_queue *queue) { + return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail; +} + +SDL_bool control_event_queue_init(struct control_event_queue *queue) { + queue->head = 0; + queue->tail = 0; + // the current implementation may not fail + return SDL_TRUE; +} + +void control_event_queue_destroy(struct control_event_queue *queue) { + // nothing to do in the current implementation +} + +SDL_bool control_event_queue_push(struct control_event_queue *queue, struct control_event *event) { + if (control_event_queue_is_full(queue)) { + return SDL_FALSE; + } + queue->data[queue->head] = *event; + queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE; + return SDL_TRUE; +} + +SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event) { + if (control_event_queue_is_empty(queue)) { + return SDL_FALSE; + } + *event = queue->data[queue->tail]; + queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE; + return SDL_TRUE; +} diff --git a/app/src/controlevent.h b/app/src/controlevent.h new file mode 100644 index 00000000..0c4d1f03 --- /dev/null +++ b/app/src/controlevent.h @@ -0,0 +1,66 @@ +#ifndef CONTROL_EVENT_H +#define CONTROL_EVENT_H + +#include +#include + +#include "android/input.h" +#include "android/keycodes.h" + +#define CONTROL_EVENT_QUEUE_SIZE 64 +#define SERIALIZED_EVENT_MAX_SIZE 33 +#define TEXT_MAX_LENGTH 31 + +enum control_event_type { + CONTROL_EVENT_TYPE_KEYCODE, + CONTROL_EVENT_TYPE_TEXT, + CONTROL_EVENT_TYPE_MOUSE, + CONTROL_EVENT_TYPE_SCROLL, +}; + +struct control_event { + enum control_event_type type; + union { + struct { + enum android_keyevent_action action; + enum android_keycode keycode; + enum android_metastate metastate; + } keycode_event; + struct { + char text[TEXT_MAX_LENGTH + 1]; // nul-terminated string + } text_event; + struct { + enum android_motionevent_action action; + enum android_motionevent_buttons buttons; + Sint32 x; + Sint32 y; + } mouse_event; + struct { + Sint32 x; + Sint32 y; + Sint32 hscroll; + Sint32 vscroll; + } scroll_event; + }; +}; + +struct control_event_queue { + struct control_event data[CONTROL_EVENT_QUEUE_SIZE]; + int head; + int tail; +}; + +// buf size must be at least SERIALIZED_EVENT_MAX_SIZE +int control_event_serialize(struct control_event *event, unsigned char *buf); + +SDL_bool control_event_queue_init(struct control_event_queue *queue); +void control_event_queue_destroy(struct control_event_queue *queue); + +SDL_bool control_event_queue_is_empty(struct control_event_queue *queue); +SDL_bool control_event_queue_is_full(struct control_event_queue *queue); + +// event is copied, the queue does not use the event after the function returns +SDL_bool control_event_queue_push(struct control_event_queue *queue, struct control_event *event); +SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event); + +#endif diff --git a/app/src/convert.c b/app/src/convert.c new file mode 100644 index 00000000..493112d5 --- /dev/null +++ b/app/src/convert.c @@ -0,0 +1,214 @@ +#include "convert.h" + +#define MAP(FROM, TO) case FROM: *to = TO; return SDL_TRUE +#define FAIL default: return SDL_FALSE + +static SDL_bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { + switch (from) { + MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); + MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); + FAIL; + } +} + +static enum android_metastate autocomplete_metastate(enum android_metastate metastate) { + // fill dependant flags + if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { + metastate |= AMETA_SHIFT_ON; + } + if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { + metastate |= AMETA_CTRL_ON; + } + if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { + metastate |= AMETA_ALT_ON; + } + if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { + metastate |= AMETA_META_ON; + } + + return metastate; +} + + +static enum android_metastate convert_meta_state(SDL_Keymod mod) { + enum android_metastate metastate = 0; + if (mod & KMOD_LSHIFT) { + metastate |= AMETA_SHIFT_LEFT_ON; + } + if (mod & KMOD_RSHIFT) { + metastate |= AMETA_SHIFT_RIGHT_ON; + } + if (mod & KMOD_LCTRL) { + metastate |= AMETA_CTRL_LEFT_ON; + } + if (mod & KMOD_RCTRL) { + metastate |= AMETA_CTRL_RIGHT_ON; + } + if (mod & KMOD_LALT) { + metastate |= AMETA_ALT_LEFT_ON; + } + if (mod & KMOD_RALT) { + metastate |= AMETA_ALT_RIGHT_ON; + } + if (mod & KMOD_LGUI) { // Windows key + metastate |= AMETA_META_LEFT_ON; + } + if (mod & KMOD_RGUI) { // Windows key + metastate |= AMETA_META_RIGHT_ON; + } + if (mod & KMOD_NUM) { + metastate |= AMETA_NUM_LOCK_ON; + } + if (mod & KMOD_CAPS) { + metastate |= AMETA_CAPS_LOCK_ON; + } + if (mod & KMOD_MODE) { // Alt Gr + // no mapping? + } + + // fill the dependent fields + return autocomplete_metastate(metastate); +} + +static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to) { + switch (from) { + MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT); + MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); + MAP(SDLK_LALT, AKEYCODE_ALT_LEFT); + MAP(SDLK_LGUI, AKEYCODE_META_LEFT); + MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT); + MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); + MAP(SDLK_RALT, AKEYCODE_ALT_RIGHT); + MAP(SDLK_RGUI, AKEYCODE_META_RIGHT); + MAP(SDLK_RETURN, AKEYCODE_ENTER); + MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); + MAP(SDLK_BACKSPACE, AKEYCODE_DEL); + MAP(SDLK_TAB, AKEYCODE_TAB); + MAP(SDLK_SPACE, AKEYCODE_SPACE); + MAP(SDLK_F1, AKEYCODE_F1); + MAP(SDLK_F2, AKEYCODE_F2); + MAP(SDLK_F3, AKEYCODE_F3); + MAP(SDLK_F4, AKEYCODE_F4); + MAP(SDLK_F5, AKEYCODE_F5); + MAP(SDLK_F6, AKEYCODE_F6); + MAP(SDLK_F7, AKEYCODE_F7); + MAP(SDLK_F8, AKEYCODE_F8); + MAP(SDLK_F9, AKEYCODE_F9); + MAP(SDLK_F10, AKEYCODE_F10); + MAP(SDLK_F11, AKEYCODE_F11); + MAP(SDLK_F12, AKEYCODE_F12); + MAP(SDLK_PRINTSCREEN, AKEYCODE_SYSRQ); + MAP(SDLK_SCROLLLOCK, AKEYCODE_SCROLL_LOCK); + MAP(SDLK_PAUSE, AKEYCODE_BREAK); + MAP(SDLK_INSERT, AKEYCODE_INSERT); + MAP(SDLK_HOME, AKEYCODE_HOME); + MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); + MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); + MAP(SDLK_END, AKEYCODE_MOVE_END); + MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); + MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); + MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); + MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); + MAP(SDLK_UP, AKEYCODE_DPAD_UP); + MAP(SDLK_NUMLOCKCLEAR, AKEYCODE_NUM_LOCK); + MAP(SDLK_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE); + MAP(SDLK_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY); + MAP(SDLK_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT); + MAP(SDLK_KP_PLUS, AKEYCODE_NUMPAD_ADD); + MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); + MAP(SDLK_KP_1, AKEYCODE_NUMPAD_1); + MAP(SDLK_KP_2, AKEYCODE_NUMPAD_2); + MAP(SDLK_KP_3, AKEYCODE_NUMPAD_3); + MAP(SDLK_KP_4, AKEYCODE_NUMPAD_4); + MAP(SDLK_KP_5, AKEYCODE_NUMPAD_5); + MAP(SDLK_KP_6, AKEYCODE_NUMPAD_6); + MAP(SDLK_KP_7, AKEYCODE_NUMPAD_7); + MAP(SDLK_KP_8, AKEYCODE_NUMPAD_8); + MAP(SDLK_KP_9, AKEYCODE_NUMPAD_9); + MAP(SDLK_KP_0, AKEYCODE_NUMPAD_0); + MAP(SDLK_KP_PERIOD, AKEYCODE_NUMPAD_DOT); + FAIL; + } +} + +static SDL_bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + +static enum android_motionevent_buttons convert_mouse_buttons(Uint32 state) { + enum android_motionevent_buttons buttons = 0; + if (state & SDL_BUTTON_LMASK) { + buttons |= AMOTION_EVENT_BUTTON_PRIMARY; + } + if (state & SDL_BUTTON_RMASK) { + buttons |= AMOTION_EVENT_BUTTON_SECONDARY; + } + if (state & SDL_BUTTON_MMASK) { + buttons |= AMOTION_EVENT_BUTTON_TERTIARY; + } + if (state & SDL_BUTTON_X1) { + buttons |= AMOTION_EVENT_BUTTON_BACK; + } + if (state & SDL_BUTTON_X2) { + buttons |= AMOTION_EVENT_BUTTON_FORWARD; + } + return buttons; +} + +SDL_bool input_key_from_sdl_to_android(SDL_KeyboardEvent *from, struct control_event *to) { + to->type = CONTROL_EVENT_TYPE_KEYCODE; + + if (!convert_keycode_action(from->type, &to->keycode_event.action)) { + return SDL_FALSE; + } + + if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode)) { + return SDL_FALSE; + } + + to->keycode_event.metastate = convert_meta_state(from->keysym.mod); + + return SDL_TRUE; +} + +SDL_bool mouse_button_from_sdl_to_android(SDL_MouseButtonEvent *from, struct control_event *to) { + to->type = CONTROL_EVENT_TYPE_MOUSE; + + if (!convert_mouse_action(from->type, &to->mouse_event.action)) { + return SDL_FALSE; + } + + to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); + to->mouse_event.x = from->x; + to->mouse_event.y = from->y; + + return SDL_TRUE; +} + +SDL_bool mouse_motion_from_sdl_to_android(SDL_MouseMotionEvent *from, struct control_event *to) { + to->type = CONTROL_EVENT_TYPE_MOUSE; + to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE; + to->mouse_event.buttons = convert_mouse_buttons(from->state); + to->mouse_event.x = from->x; + to->mouse_event.y = from->y; + + return SDL_TRUE; +} + +SDL_bool mouse_wheel_from_sdl_to_android(struct complete_mouse_wheel_event *from, struct control_event *to) { + to->type = CONTROL_EVENT_TYPE_SCROLL; + + to->scroll_event.x = from->x; + to->scroll_event.y = from->y; + + SDL_MouseWheelEvent *wheel = from->mouse_wheel_event; + int mul = wheel->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; + to->scroll_event.hscroll = mul * wheel->x; + to->scroll_event.vscroll = mul * wheel->y; + + return SDL_TRUE; +} diff --git a/app/src/convert.h b/app/src/convert.h new file mode 100644 index 00000000..03d3f3aa --- /dev/null +++ b/app/src/convert.h @@ -0,0 +1,20 @@ +#ifndef CONVERT_H +#define CONVERT_H + +#include +#include +#include "controlevent.h" + +// on Android, a scroll event requires the current mouse position +struct complete_mouse_wheel_event { + SDL_MouseWheelEvent *mouse_wheel_event; + Sint32 x; + Sint32 y; +}; + +SDL_bool input_key_from_sdl_to_android(SDL_KeyboardEvent *from, struct control_event *to); +SDL_bool mouse_button_from_sdl_to_android(SDL_MouseButtonEvent *from, struct control_event *to); +SDL_bool mouse_motion_from_sdl_to_android(SDL_MouseMotionEvent *from, struct control_event *to); +SDL_bool mouse_wheel_from_sdl_to_android(struct complete_mouse_wheel_event *from, struct control_event *to); + +#endif diff --git a/app/src/screen.c b/app/src/screen.c index 0047bcb7..efc756c1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -9,6 +9,8 @@ #include #include "command.h" +#include "control.h" +#include "convert.h" #include "decoder.h" #include "events.h" #include "frames.h" @@ -21,20 +23,40 @@ #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) -static struct frames frames; -static struct decoder decoder; - struct size { Uint16 width; Uint16 height; }; +static struct frames frames; +static struct decoder decoder; +static struct controller controller; + +static SDL_Window *window; +static SDL_Renderer *renderer; +static SDL_Texture *texture; +static struct size frame_size; +static SDL_bool texture_empty = SDL_TRUE; +static SDL_bool fullscreen = SDL_FALSE; + static long timestamp_ms(void) { struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec * 1000 + tv.tv_usec / 1000; } +static void count_frame(void) { + static long ts = 0; + static int nbframes = 0; + long now = timestamp_ms(); + ++nbframes; + if (now - ts > 1000) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%d fps", nbframes); + ts = now; + nbframes = 0; + } +} + static TCPsocket listen_on_port(Uint16 port) { IPaddress addr = { .host = INADDR_ANY, @@ -214,6 +236,155 @@ static int wait_for_success(process_t proc, const char *name) { return 0; } +static SDL_bool handle_new_frame(void) { + mutex_lock(frames.mutex); + AVFrame *frame = frames.rendering_frame; + frames.rendering_frame_consumed = SDL_TRUE; + if (!decoder.skip_frames) { + cond_signal(frames.rendering_frame_consumed_cond); + } + + struct size current_frame_size = {frame->width, frame->height}; + if (!prepare_for_frame(window, renderer, &texture, frame_size, current_frame_size)) { + return SDL_FALSE; + } + + frame_size = current_frame_size; + + update_texture(frame, texture); + mutex_unlock(frames.mutex); + + render(renderer, texture); + return SDL_TRUE; +} + +static void handle_text_input(SDL_TextInputEvent *event) { + struct control_event control_event; + control_event.type = CONTROL_EVENT_TYPE_TEXT; + strncpy(control_event.text_event.text, event->text, TEXT_MAX_LENGTH); + control_event.text_event.text[TEXT_MAX_LENGTH] = '\0'; + if (!controller_push_event(&controller, &control_event)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Cannot send text event"); + } +} + +static void handle_key(SDL_KeyboardEvent *event) { + SDL_Keycode keycode = event->keysym.sym; + SDL_bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); + SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); + SDL_bool repeat = event->repeat; + + // Capture Ctrl+x: optimal size + if (keycode == SDLK_x && !repeat && ctrl && !shift) { + if (event->type == SDL_KEYDOWN) { + struct size optimal_size = get_optimal_window_size(window, frame_size); + SDL_SetWindowSize(window, optimal_size.width, optimal_size.height); + } + return; + } + + // Capture Ctrl+f: switch fullscreen + if (keycode == SDLK_f && !repeat && ctrl && !shift) { + if (event->type == SDL_KEYDOWN) { + Uint32 new_mode = fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; + if (!SDL_SetWindowFullscreen(window, new_mode)) { + fullscreen = !fullscreen; + render(renderer, texture_empty ? NULL : texture); + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Could not switch fullscreen mode: %s", SDL_GetError()); + } + } + return; + } + + struct control_event control_event; + if (input_key_from_sdl_to_android(event, &control_event)) { + if (!controller_push_event(&controller, &control_event)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Cannot send control event"); + } + } +} + +static void handle_mouse_motion(SDL_MouseMotionEvent *event) { + struct control_event control_event; + if (mouse_motion_from_sdl_to_android(event, &control_event)) { + if (!controller_push_event(&controller, &control_event)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Cannot send mouse motion event"); + } + } +} + +static void handle_mouse_button(SDL_MouseButtonEvent *event) { + struct control_event control_event; + if (mouse_button_from_sdl_to_android(event, &control_event)) { + if (!controller_push_event(&controller, &control_event)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Cannot send mouse button event"); + } + } +} + +static void handle_mouse_wheel(struct complete_mouse_wheel_event *event) { + struct control_event control_event; + if (mouse_wheel_from_sdl_to_android(event, &control_event)) { + if (!controller_push_event(&controller, &control_event)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Cannot send wheel button event"); + } + } +} + +void event_loop(void) { + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case EVENT_DECODER_STOPPED: + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Video decoder stopped"); + case SDL_QUIT: + return; + case EVENT_NEW_FRAME: + if (!handle_new_frame()) { + return; + } + texture_empty = SDL_FALSE; + count_frame(); // display fps for debug + break; + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + render(renderer, texture_empty ? NULL : texture); + break; + } + break; + case SDL_TEXTINPUT: { + handle_text_input(&event.text); + break; + } + case SDL_KEYDOWN: + case SDL_KEYUP: + handle_key(&event.key); + break; + case SDL_MOUSEMOTION: + handle_mouse_motion(&event.motion); + break; + case SDL_MOUSEWHEEL: { + struct complete_mouse_wheel_event complete_event; + complete_event.mouse_wheel_event = &event.wheel; + int x; + int y; + SDL_GetMouseState(&x, &y); + complete_event.x = (Sint32) x; + complete_event.y = (Sint32) y; + handle_mouse_wheel(&complete_event); + break; + } + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + handle_mouse_button(&event.button); + break; + } + } +} + SDL_bool show_screen(const char *serial, Uint16 local_port) { SDL_bool ret = 0; @@ -258,7 +429,6 @@ SDL_bool show_screen(const char *serial, Uint16 local_port) { goto screen_finally_adb_reverse_remove; } - struct size frame_size; char device_name[DEVICE_NAME_FIELD_LENGTH]; // screenrecord does not send frames when the screen content does not change @@ -292,10 +462,24 @@ SDL_bool show_screen(const char *serial, Uint16 local_port) { goto screen_finally_destroy_frames; } + if (!controller_init(&controller, device_socket)) { + ret = SDL_FALSE; + SDLNet_TCP_Close(device_socket); + stop_server(server); + goto screen_finally_stop_decoder; + } + + if (!controller_start(&controller)) { + ret = SDL_FALSE; + SDLNet_TCP_Close(device_socket); + stop_server(server); + goto screen_finally_destroy_controller; + } + if (SDL_Init(SDL_INIT_VIDEO)) { SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "Could not initialize SDL: %s", SDL_GetError()); ret = SDL_FALSE; - goto screen_finally_stop_decoder; + goto screen_finally_stop_and_join_controller; } // FIXME it may crash in SDL_Quit in i965_dri.so // As a workaround, do not call SDL_Quit() (we are exiting anyway). @@ -307,7 +491,7 @@ SDL_bool show_screen(const char *serial, Uint16 local_port) { } struct size window_size = get_initial_optimal_size(frame_size); - SDL_Window *window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_size.width, window_size.height, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); if (!window) { SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not create window: %s", SDL_GetError()); @@ -315,7 +499,7 @@ SDL_bool show_screen(const char *serial, Uint16 local_port) { goto screen_finally_stop_decoder; } - SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if (!renderer) { SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Could not create renderer: %s", SDL_GetError()); ret = SDL_FALSE; @@ -329,7 +513,7 @@ SDL_bool show_screen(const char *serial, Uint16 local_port) { } SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height); - SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, frame_size.width, frame_size.height); + texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, frame_size.width, frame_size.height); if (!texture) { SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Could not create texture: %s", SDL_GetError()); ret = SDL_FALSE; @@ -339,87 +523,8 @@ SDL_bool show_screen(const char *serial, Uint16 local_port) { SDL_RenderClear(renderer); SDL_RenderPresent(renderer); - long ts = timestamp_ms(); - int nbframes = 0; + event_loop(); - SDL_bool texture_empty = SDL_TRUE; - SDL_bool fullscreen = SDL_FALSE; - SDL_Event event; - while (SDL_WaitEvent(&event)) { - switch (event.type) { - case EVENT_DECODER_STOPPED: - SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Video decoder stopped"); - case SDL_QUIT: - goto screen_quit; - case EVENT_NEW_FRAME: - mutex_lock(frames.mutex); - AVFrame *frame = frames.rendering_frame; - frames.rendering_frame_consumed = SDL_TRUE; - if (!decoder.skip_frames) { - cond_signal(frames.rendering_frame_consumed_cond); - } - - struct size current_frame_size = {frame->width, frame->height}; - if (!prepare_for_frame(window, renderer, &texture, frame_size, current_frame_size)) { - goto screen_quit; - } - - frame_size = current_frame_size; - - update_texture(frame, texture); - mutex_unlock(frames.mutex); - - texture_empty = SDL_FALSE; - - long now = timestamp_ms(); - ++nbframes; - if (now - ts > 1000) { - SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%d fps", nbframes); - ts = now; - nbframes = 0; - } - render(renderer, texture); - - break; - case SDL_WINDOWEVENT: - switch (event.window.event) { - case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: - render(renderer, texture_empty ? NULL : texture); - break; - } - break; - case SDL_KEYDOWN: { - SDL_bool ctrl = SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL); - SDL_bool shift = SDL_GetModState() & (KMOD_LSHIFT | KMOD_RSHIFT); - SDL_bool repeat = event.key.repeat; - switch (event.key.keysym.sym) { - case SDLK_x: - if (!repeat && ctrl && !shift) { - // Ctrl+x - struct size optimal_size = get_optimal_window_size(window, frame_size); - SDL_SetWindowSize(window, optimal_size.width, optimal_size.height); - } - break; - case SDLK_f: - if (!repeat && ctrl && !shift) { - // Ctrl+f - Uint32 new_mode = fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; - if (!SDL_SetWindowFullscreen(window, new_mode)) { - fullscreen = !fullscreen; - render(renderer, texture_empty ? NULL : texture); - } else { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Could not switch fullscreen mode: %s", SDL_GetError()); - } - } - break; - } - break; - } - } - } - -screen_quit: SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "quit..."); SDL_DestroyTexture(texture); screen_finally_destroy_renderer: @@ -429,6 +534,11 @@ screen_finally_destroy_renderer: //SDL_DestroyRenderer(renderer); screen_finally_destroy_window: //SDL_DestroyWindow(window); +screen_finally_stop_and_join_controller: + controller_stop(&controller); + controller_join(&controller); +screen_finally_destroy_controller: + controller_destroy(&controller); screen_finally_stop_decoder: SDLNet_TCP_Close(device_socket); // kill the server before decoder_join() to wake up the decoder diff --git a/app/src/strutil.h b/app/src/strutil.h index 76771781..c9971410 100644 --- a/app/src/strutil.h +++ b/app/src/strutil.h @@ -11,4 +11,4 @@ size_t xstrncpy(char *dest, const char *src, size_t n); // join tokens by sep into dst // returns the number of chars actually written (max n-1) if no trucation // occurred, or n if truncated -size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t); +size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); diff --git a/app/tests/test_control_event_queue.c b/app/tests/test_control_event_queue.c new file mode 100644 index 00000000..2f3fc994 --- /dev/null +++ b/app/tests/test_control_event_queue.c @@ -0,0 +1,94 @@ +#include + +#include "controlevent.h" + +static void test_control_event_queue_empty() { + struct control_event_queue queue; + SDL_bool init_ok = control_event_queue_init(&queue); + assert(init_ok); + + assert(control_event_queue_is_empty(&queue)); + + struct control_event dummy_event; + SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event); + assert(push_ok); + assert(!control_event_queue_is_empty(&queue)); + + SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); + assert(take_ok); + assert(control_event_queue_is_empty(&queue)); + + SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event); + assert(!take_empty_ok); // the queue is empty + + control_event_queue_destroy(&queue); +} + +static void test_control_event_queue_full() { + struct control_event_queue queue; + SDL_bool init_ok = control_event_queue_init(&queue); + assert(init_ok); + + assert(!control_event_queue_is_full(&queue)); + + struct control_event dummy_event; + // fill the queue + while (control_event_queue_push(&queue, &dummy_event)); + + SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); + assert(take_ok); + assert(!control_event_queue_is_full(&queue)); + + control_event_queue_destroy(&queue); +} + +static void test_control_event_queue_push_take() { + struct control_event_queue queue; + SDL_bool init_ok = control_event_queue_init(&queue); + assert(init_ok); + + struct control_event event = { + .type = CONTROL_EVENT_TYPE_KEYCODE, + .keycode_event = { + .action = AKEY_EVENT_ACTION_DOWN, + .keycode = AKEYCODE_ENTER, + .metastate = AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON, + }, + }; + + SDL_bool push1_ok = control_event_queue_push(&queue, &event); + assert(push1_ok); + + event = (struct control_event) { + .type = CONTROL_EVENT_TYPE_TEXT, + .text_event = { + .text = "abc", + }, + }; + + SDL_bool push2_ok = control_event_queue_push(&queue, &event); + assert(push2_ok); + + // overwrite event + SDL_bool take1_ok = control_event_queue_take(&queue, &event); + assert(take1_ok); + assert(event.type == CONTROL_EVENT_TYPE_KEYCODE); + assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN); + assert(event.keycode_event.keycode == AKEYCODE_ENTER); + assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON)); + + // overwrite event + SDL_bool take2_ok = control_event_queue_take(&queue, &event); + assert(take2_ok); + assert(event.type == CONTROL_EVENT_TYPE_TEXT); + assert(!strcmp(event.text_event.text, "abc")); + + control_event_queue_destroy(&queue); +} + +int main() { + test_control_event_queue_empty(); + test_control_event_queue_full(); + test_control_event_queue_push_take(); + return 0; +} diff --git a/server/Makefile b/server/Makefile index 2bc0384b..60dbbeac 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,9 +1,12 @@ -.PHONY: jar push run clean +.PHONY: jar push run clean compile compiletests test SRC_DIR := src GEN_DIR := gen CLS_DIR := classes CLS_DEX := classes.dex +TEST_SRC_DIR := tests +TEST_CLS_DIR := test_classes +TEST_LIBS := /usr/share/java/junit4.jar:/usr/share/java/hamcrest-core.jar BUILD_TOOLS := $(ANDROID_HOME)/build-tools/26.0.2 AIDL := $(BUILD_TOOLS)/aidl @@ -17,15 +20,24 @@ ANDROID_JAR := $(ANDROID_HOME)/platforms/android-26/android.jar AIDL_SRC := android/view/IRotationWatcher.aidl SRC := com/genymobile/scrcpy/ScrCpyServer.java \ + com/genymobile/scrcpy/ControlEvent.java \ + com/genymobile/scrcpy/ControlEventReader.java \ com/genymobile/scrcpy/DesktopConnection.java \ com/genymobile/scrcpy/DeviceUtil.java \ + com/genymobile/scrcpy/EventController.java \ com/genymobile/scrcpy/ScreenInfo.java \ com/genymobile/scrcpy/ScreenStreamer.java \ com/genymobile/scrcpy/ScreenStreamerSession.java \ com/genymobile/scrcpy/wrappers/DisplayManager.java \ + com/genymobile/scrcpy/wrappers/InputManager.java \ com/genymobile/scrcpy/wrappers/ServiceManager.java \ com/genymobile/scrcpy/wrappers/WindowManager.java \ +TEST_SRC := com/genymobile/scrcpy/ControlEventReaderTest.java \ + +# generate classnames from filepath +TEST_CLS := $(subst /,.,$(basename $(TEST_SRC))) + JAR := scrcpy-server.jar MAIN := com.genymobile.scrcpy.ScrCpyServer @@ -35,6 +47,7 @@ SRC_CLS := $(SRC:%.java=$(CLS_DIR)/%.class) CLS := $(AIDL_CLS) $(SRC_CLS) ALL_JAVA := $(AIDL_GEN) $(addprefix $(SRC_DIR)/,$(SRC)) +ALL_TESTS := $(addprefix $(TEST_SRC_DIR)/,$(TEST_SRC)) jar: $(JAR) @@ -42,13 +55,16 @@ $(AIDL_GEN): $(GEN_DIR)/%.java : $(SRC_DIR)/%.aidl mkdir -p $(GEN_DIR) "$(AIDL)" -o$(GEN_DIR) $(SRC_DIR)/$(AIDL_SRC) - -$(JAR): $(ALL_JAVA) - @mkdir -p $(CLS_DIR) +compile: $(ALL_JAVA) + @mkdir -p "$(CLS_DIR)" javac -source 1.7 -target 1.7 \ -cp "$(ANDROID_JAR)" \ -d "$(CLS_DIR)" -sourcepath $(SRC_DIR):$(GEN_DIR) \ $(ALL_JAVA) + +$(JAR): $(ALL_JAVA) + # we cannot track easily class dependencies, so execute compile only when jar is outdated + +$(MAKE) compile "$(DX)" --dex --output=$(CLS_DEX) $(CLS_DIR) jar cvf $(JAR) classes.dex @@ -59,4 +75,12 @@ run: push adb shell "CLASSPATH=/data/local/tmp/$(JAR) app_process /system/bin $(MAIN)" clean: - rm -rf $(CLS_DEX) $(CLS_DIR) $(GEN_DIR) $(JAR) + rm -rf $(CLS_DEX) $(CLS_DIR) $(GEN_DIR) $(JAR) $(TEST_CLS_DIR) + +compiletests: compile $(ALL_TESTS) + @mkdir -p "$(TEST_CLS_DIR)" + javac -cp "$(TEST_LIBS):$(ANDROID_JAR):$(CLS_DIR)" -d "$(TEST_CLS_DIR)" -sourcepath "$(TEST_SRC_DIR)" $(ALL_TESTS) + +test: + +$(MAKE) compiletests + java -cp "$(TEST_LIBS):$(ANDROID_JAR):$(CLS_DIR):$(TEST_CLS_DIR)" org.junit.runner.JUnitCore $(TEST_CLS) diff --git a/server/src/com/genymobile/scrcpy/ControlEvent.java b/server/src/com/genymobile/scrcpy/ControlEvent.java new file mode 100644 index 00000000..567a70f3 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ControlEvent.java @@ -0,0 +1,102 @@ +package com.genymobile.scrcpy; + +/** + * Union of all supported event types, identified by their {@code type}. + */ +public class ControlEvent { + + public static final int TYPE_KEYCODE = 0; + public static final int TYPE_TEXT = 1; + public static final int TYPE_MOUSE = 2; + public static final int TYPE_SCROLL = 3; + + private int type; + private String text; + private int metaState; // KeyEvent.META_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* + private int keycode; // KeyEvent.KEYCODE_* + private int buttons; // MotionEvent.BUTTON_* + private int x; + private int y; + private int hScroll; + private int vScroll; + + private ControlEvent() { + } + + public static ControlEvent createKeycodeControlEvent(int action, int keycode, int metaState) { + ControlEvent event = new ControlEvent(); + event.type = TYPE_KEYCODE; + event.action = action; + event.keycode = keycode; + event.metaState = metaState; + return event; + } + + public static ControlEvent createTextControlEvent(String text) { + ControlEvent event = new ControlEvent(); + event.type = TYPE_TEXT; + event.text = text; + return event; + } + + public static ControlEvent createMotionControlEvent(int action, int buttons, int x, int y) { + ControlEvent event = new ControlEvent(); + event.type = TYPE_MOUSE; + event.action = action; + event.buttons = buttons; + event.x = x; + event.y = y; + return event; + } + + public static ControlEvent createScrollControlEvent(int x, int y, int hScroll, int vScroll) { + ControlEvent event = new ControlEvent(); + event.type = TYPE_SCROLL; + event.x = x; + event.y = y; + event.hScroll = hScroll; + event.vScroll = vScroll; + return event; + } + + public int getType() { + return type; + } + + public String getText() { + return text; + } + + public int getMetaState() { + return metaState; + } + + public int getAction() { + return action; + } + + public int getKeycode() { + return keycode; + } + + public int getButtons() { + return buttons; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getHScroll() { + return hScroll; + } + + public int getVScroll() { + return vScroll; + } +} diff --git a/server/src/com/genymobile/scrcpy/ControlEventReader.java b/server/src/com/genymobile/scrcpy/ControlEventReader.java new file mode 100644 index 00000000..722226e8 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ControlEventReader.java @@ -0,0 +1,99 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class ControlEventReader { + + private static final int KEYCODE_PAYLOAD_LENGTH = 9; + private static final int MOUSE_PAYLOAD_LENGTH = 13; + private static final int SCROLL_PAYLOAD_LENGTH = 16; + + private final byte[] rawBuffer = new byte[128]; + private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final byte[] textBuffer = new byte[32]; + + public ControlEventReader() { + // invariant: the buffer is always in "get" mode + buffer.limit(0); + } + + public boolean isFull() { + return buffer.remaining() == rawBuffer.length; + } + + public boolean readFrom(InputStream input) throws IOException { + if (isFull()) { + throw new IllegalStateException("Buffer full, call next() to consume"); + } + buffer.compact(); + int head = buffer.position(); + int r = input.read(rawBuffer, head, rawBuffer.length - head); + if (r == -1) { + return false; + } + buffer.position(head + r); + buffer.flip(); + return true; + } + + public ControlEvent next() { + if (!buffer.hasRemaining()) { + return null; + } + int savedPosition = buffer.position(); + + int type = buffer.get(); + switch (type) { + case ControlEvent.TYPE_KEYCODE: { + if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) { + break; + } + int action = buffer.get() & 0xff; // unsigned + int keycode = buffer.getInt(); + int metaState = buffer.getInt(); + return ControlEvent.createKeycodeControlEvent(action, keycode, metaState); + } + case ControlEvent.TYPE_TEXT: { + if (buffer.remaining() < 1) { + break; + } + int len = buffer.get() & 0xff; // unsigned + if (buffer.remaining() < len) { + break; + } + buffer.get(textBuffer, 0, len); + String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8); + return ControlEvent.createTextControlEvent(text); + } + case ControlEvent.TYPE_MOUSE: { + if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) { + break; + } + int action = buffer.get() & 0xff; // unsigned + int buttons = buffer.getInt(); + int x = buffer.getInt(); + int y = buffer.getInt(); + return ControlEvent.createMotionControlEvent(action, buttons, x, y); + } + case ControlEvent.TYPE_SCROLL: { + if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) { + break; + } + int x = buffer.getInt(); + int y = buffer.getInt(); + int hscroll = buffer.getInt(); + int vscroll = buffer.getInt(); + return ControlEvent.createScrollControlEvent(x, y, hscroll, vscroll); + } + default: + Ln.w("Unknown event type: " + type); + } + + // failure, reset savedPosition + buffer.position(savedPosition); + return null; + } +} diff --git a/server/src/com/genymobile/scrcpy/DesktopConnection.java b/server/src/com/genymobile/scrcpy/DesktopConnection.java index f0f557a8..f1a85beb 100644 --- a/server/src/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/com/genymobile/scrcpy/DesktopConnection.java @@ -5,6 +5,8 @@ import android.net.LocalSocketAddress; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; public class DesktopConnection implements Closeable { @@ -14,9 +16,15 @@ public class DesktopConnection implements Closeable { private static final String SOCKET_NAME = "scrcpy"; private final LocalSocket socket; + private final InputStream inputStream; + private final OutputStream outputStream; + + private final ControlEventReader reader = new ControlEventReader(); private DesktopConnection(LocalSocket socket) throws IOException { this.socket = socket; + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); } private static LocalSocket connect(String abstractName) throws IOException { @@ -27,8 +35,9 @@ public class DesktopConnection implements Closeable { public static DesktopConnection open(String deviceName, int width, int height) throws IOException { LocalSocket socket = connect(SOCKET_NAME); - send(socket, deviceName, width, height); - return new DesktopConnection(socket); + DesktopConnection connection = new DesktopConnection(socket); + connection.send(deviceName, width, height); + return connection; } public void close() throws IOException { @@ -37,7 +46,7 @@ public class DesktopConnection implements Closeable { socket.close(); } - private static void send(LocalSocket socket, String deviceName, int width, int height) throws IOException { + private void send(String deviceName, int width, int height) throws IOException { assert width < 0x10000 : "width may not be stored on 16 bits"; assert height < 0x10000 : "height may not be stored on 16 bits"; byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; @@ -51,11 +60,20 @@ public class DesktopConnection implements Closeable { buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; - socket.getOutputStream().write(buffer, 0, buffer.length); + outputStream.write(buffer, 0, buffer.length); } public void sendVideoStream(byte[] videoStreamBuffer, int len) throws IOException { - socket.getOutputStream().write(videoStreamBuffer, 0, len); + outputStream.write(videoStreamBuffer, 0, len); + } + + public ControlEvent receiveControlEvent() throws IOException { + ControlEvent event = reader.next(); + while (event == null) { + reader.readFrom(inputStream); + event = reader.next(); + } + return event; } } diff --git a/server/src/com/genymobile/scrcpy/DeviceUtil.java b/server/src/com/genymobile/scrcpy/DeviceUtil.java index f4ee98be..1c8f78ec 100644 --- a/server/src/com/genymobile/scrcpy/DeviceUtil.java +++ b/server/src/com/genymobile/scrcpy/DeviceUtil.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import android.os.Build; import android.view.IRotationWatcher; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; public class DeviceUtil { @@ -20,4 +21,8 @@ public class DeviceUtil { public static String getDeviceName() { return Build.MODEL; } + + public static InputManager getInputManager() { + return serviceManager.getInputManager(); + } } diff --git a/server/src/com/genymobile/scrcpy/EventController.java b/server/src/com/genymobile/scrcpy/EventController.java new file mode 100644 index 00000000..af65821c --- /dev/null +++ b/server/src/com/genymobile/scrcpy/EventController.java @@ -0,0 +1,127 @@ +package com.genymobile.scrcpy; + +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import com.genymobile.scrcpy.wrappers.InputManager; + +import java.io.IOException; + +public class EventController { + + private final InputManager inputManager; + private final DesktopConnection connection; + + private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + + private long lastMouseDown; + private final MotionEvent.PointerProperties[] pointerProperties = { new MotionEvent.PointerProperties() }; + private final MotionEvent.PointerCoords[] pointerCoords = { new MotionEvent.PointerCoords() }; + + public EventController(DesktopConnection connection) { + this.connection = connection; + inputManager = DeviceUtil.getInputManager(); + initPointer(); + } + + private void initPointer() { + MotionEvent.PointerProperties props = pointerProperties[0]; + props.id = 0; + props.toolType = MotionEvent.TOOL_TYPE_MOUSE; + + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.orientation = 0; + coords.pressure = 1; + coords.size = 1; + coords.toolMajor = 1; + coords.toolMinor = 1; + coords.touchMajor = 1; + coords.touchMinor = 1; + } + + private void setPointerCoords(int x, int y) { + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.x = x; + coords.y = y; + } + + private void setScroll(int hScroll, int vScroll) { + MotionEvent.PointerCoords coords = pointerCoords[0]; + coords.setAxisValue(MotionEvent.AXIS_SCROLL, hScroll); + coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + } + + public void control() throws IOException { + while (handleEvent()); + } + + private boolean handleEvent() throws IOException { + ControlEvent controlEvent = connection.receiveControlEvent(); + if (controlEvent == null) { + return false; + } + switch (controlEvent.getType()) { + case ControlEvent.TYPE_KEYCODE: + injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState()); + break; + case ControlEvent.TYPE_TEXT: + injectText(controlEvent.getText()); + break; + case ControlEvent.TYPE_MOUSE: + injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getX(), controlEvent.getY()); + break; + case ControlEvent.TYPE_SCROLL: + injectScroll(controlEvent.getButtons(), controlEvent.getX(), controlEvent.getY(), controlEvent.getHScroll(), controlEvent.getVScroll()); + } + return true; + } + + private boolean injectKeycode(int action, int keycode, int metaState) { + return injectKeyEvent(action, keycode, 0, metaState); + } + + private boolean injectText(String text) { + KeyEvent[] events = charMap.getEvents(text.toCharArray()); + if (events == null) { + return false; + } + for (KeyEvent event : events) { + if (!injectEvent(event)) { + return false; + } + } + return true; + } + + private boolean injectMouse(int action, int buttons, int x, int y) { + long now = SystemClock.uptimeMillis(); + if (action == MotionEvent.ACTION_DOWN) { + lastMouseDown = now; + } + setPointerCoords(x, y); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + return injectEvent(event); + } + + private boolean injectScroll(int buttons, int x, int y, int hScroll, int vScroll) { + long now = SystemClock.uptimeMillis(); + setPointerCoords(x, y); + setScroll(hScroll, vScroll); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + return injectEvent(event); + } + + private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD); + return injectEvent(event); + } + + private boolean injectEvent(InputEvent event) { + return inputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } +} diff --git a/server/src/com/genymobile/scrcpy/ScrCpyServer.java b/server/src/com/genymobile/scrcpy/ScrCpyServer.java index 045c1cd9..5991495c 100644 --- a/server/src/com/genymobile/scrcpy/ScrCpyServer.java +++ b/server/src/com/genymobile/scrcpy/ScrCpyServer.java @@ -6,13 +6,16 @@ public class ScrCpyServer { private static final String TAG = "scrcpy"; - public static void scrcpy() throws IOException { + private static void scrcpy() throws IOException { String deviceName = DeviceUtil.getDeviceName(); ScreenInfo initialScreenInfo = DeviceUtil.getScreenInfo(); int width = initialScreenInfo.getLogicalWidth(); int height = initialScreenInfo.getLogicalHeight(); try (DesktopConnection connection = DesktopConnection.open(deviceName, width, height)) { try { + // asynchronous + startEventController(connection); + // synchronous new ScreenStreamer(connection).streamScreen(); } catch (IOException e) { Ln.e("Screen streaming interrupted", e); @@ -20,6 +23,19 @@ public class ScrCpyServer { } } + private static void startEventController(final DesktopConnection connection) { + new Thread(new Runnable() { + @Override + public void run() { + try { + new EventController(connection).control(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + public static void main(String... args) throws Exception { try { scrcpy(); diff --git a/server/src/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/com/genymobile/scrcpy/wrappers/InputManager.java new file mode 100644 index 00000000..d34f6f59 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/wrappers/InputManager.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy.wrappers; + +import android.os.IInterface; +import android.view.InputEvent; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class InputManager { + + public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; + public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; + public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; + + private final IInterface manager; + private final Method injectInputEventMethod; + + public InputManager(IInterface manager) { + this.manager = manager; + try { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + public boolean injectInputEvent(InputEvent inputEvent, int mode) { + try { + return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } + } +} diff --git a/server/src/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/com/genymobile/scrcpy/wrappers/ServiceManager.java index e034a0d8..c76a85f0 100644 --- a/server/src/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -33,4 +33,8 @@ public class ServiceManager { public DisplayManager getDisplayManager() { return new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); } + + public InputManager getInputManager() { + return new InputManager(getService("input", "android.hardware.input.IInputManager")); + } } diff --git a/server/tests/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/tests/com/genymobile/scrcpy/ControlEventReaderTest.java new file mode 100644 index 00000000..4d9952d0 --- /dev/null +++ b/server/tests/com/genymobile/scrcpy/ControlEventReaderTest.java @@ -0,0 +1,151 @@ +package com.genymobile.scrcpy; + +import android.view.KeyEvent; +import android.view.MotionEvent; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class ControlEventReaderTest { + + @Test + public void testParseKeycodeEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testParseTextEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_TEXT); + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeByte(text.length); + dos.write("testé".getBytes(StandardCharsets.UTF_8)); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType()); + Assert.assertEquals("testé", event.getText()); + } + + @Test + public void testParseMouseEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testMultiEvents() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + + byte[] packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + ControlEvent event = reader.next(); + Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + event = reader.next(); + Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } + + @Test + public void testPartialEvents() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(KeyEvent.ACTION_UP); + dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(KeyEvent.META_CTRL_ON); + + dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(MotionEvent.ACTION_DOWN); + + byte[] packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + ControlEvent event = reader.next(); + Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + event = reader.next(); + Assert.assertNull(event); // the event is not complete + + bos.reset(); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(KeyEvent.META_CTRL_ON); + packet = bos.toByteArray(); + reader.readFrom(new ByteArrayInputStream(packet)); + + // the event is now complete + event = reader.next(); + Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + } +}