Files
RetroArch/input/drivers/android_input.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

3051 lines
104 KiB
C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2012-2015 - Michael Lelli
* Copyright (C) 2013-2014 - Steven Crowe
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <errno.h>
#include <unistd.h>
#include <dlfcn.h>
#include <android/keycodes.h>
#include <dynamic/dylib.h>
#include <retro_inline.h>
#include <string/stdstring.h>
#include <retro_miscellaneous.h>
#ifdef HAVE_CONFIG_H
#include "../../config.h"
#endif
#include "../../config.def.h"
#ifdef HAVE_MENU
#include "../../menu/menu_driver.h"
#endif
#include "../../command.h"
#include "../../frontend/drivers/platform_unix.h"
#include "../drivers_keyboard/keyboard_event_android.h"
#include "../../tasks/tasks_internal.h"
#include "../../performance_counters.h"
#include "../../configuration.h"
#include "../../retroarch.h"
#include "../../runloop.h"
#include "../input_driver.h"
#ifdef HAVE_THREADS
#include "../../gfx/video_thread_wrapper.h"
#endif
#define MAX_TOUCH 16
#define MAX_NUM_KEYBOARDS 3
#define DEFAULT_ASENSOR_EVENT_RATE 60
/* If using an SDK lower than 14 then add missing mouse button codes */
#if __ANDROID_API__ < 14
enum {
AMOTION_EVENT_BUTTON_PRIMARY = 1 << 0,
AMOTION_EVENT_BUTTON_SECONDARY = 1 << 1,
AMOTION_EVENT_BUTTON_TERTIARY = 1 << 2,
AMOTION_EVENT_BUTTON_BACK = 1 << 3,
AMOTION_EVENT_BUTTON_FORWARD = 1 << 4,
AMOTION_EVENT_AXIS_VSCROLL = 9,
AMOTION_EVENT_ACTION_HOVER_MOVE = 7,
AINPUT_SOURCE_STYLUS = 0x00004000
};
#endif
/* If using an NDK lower than 16b then add missing definition */
#ifndef __ANDROID_API_O_MR1__
enum {
AINPUT_SOURCE_MOUSE_RELATIVE = 0x00020000 | AINPUT_SOURCE_CLASS_NAVIGATION
};
#endif
/* If using an SDK lower than 24 then add missing relative axis codes */
#ifndef AMOTION_EVENT_AXIS_RELATIVE_X
#define AMOTION_EVENT_AXIS_RELATIVE_X 27
#endif
#ifndef AMOTION_EVENT_AXIS_RELATIVE_Y
#define AMOTION_EVENT_AXIS_RELATIVE_Y 28
#endif
/* Use this to enable/disable using the touch screen as mouse */
#define ENABLE_TOUCH_SCREEN_MOUSE 0
#define AKEYCODE_ASSIST 219
#define LAST_KEYCODE AKEYCODE_ASSIST
#define MAX_KEYS ((LAST_KEYCODE + 7) / 8)
/* First ports are used to keep track of gamepad states.
* Last port is used for keyboard state.
* Every bit index used against a row must be < LAST_KEYCODE;
* writers bound incoming keycodes, readers bound bind keysyms. */
static uint8_t android_key_state[DEFAULT_MAX_PADS + 1][MAX_KEYS];
#define ANDROID_KEYBOARD_PORT_INPUT_PRESSED(binds, id) (BIT_GET(android_key_state[ANDROID_KEYBOARD_PORT], rarch_keysym_lut[(binds)[(id)].key]))
#define ANDROID_KEYBOARD_INPUT_PRESSED(key) (BIT_GET(android_key_state[0], (key)))
uint8_t *android_keyboard_state_get(unsigned port)
{
return android_key_state[port];
}
typedef struct
{
float x;
float y;
float z;
} sensor_t;
struct input_pointer
{
int16_t x, y;
int16_t confined_x, confined_y;
int16_t full_x, full_y;
};
static int pad_id1 = -1;
static int pad_id2 = -1;
static int kbd_id[MAX_NUM_KEYBOARDS];
static int kbd_num = 0;
enum
{
AXIS_X = 0,
AXIS_Y = 1,
AXIS_Z = 11,
AXIS_RZ = 14,
AXIS_HAT_X = 15,
AXIS_HAT_Y = 16,
AXIS_LTRIGGER = 17,
AXIS_RTRIGGER = 18,
AXIS_GAS = 22,
AXIS_BRAKE = 23
};
typedef struct state_device
{
int id;
int port;
char name[256];
} state_device_t;
typedef struct android_input
{
int64_t quick_tap_time;
state_device_t pad_states[MAX_USERS]; /* int alignment */
int mouse_x, mouse_y;
int16_t mouse_x_viewport_screen, mouse_y_viewport_screen;
int16_t mouse_x_viewport, mouse_y_viewport;
int mouse_x_delta, mouse_y_delta;
int mouse_l, mouse_r, mouse_m, mouse_wu, mouse_wd;
bool mouse_activated;
unsigned pads_connected;
unsigned pointer_count;
sensor_t accelerometer_state; /* float alignment */
sensor_t gyroscope_state; /* float alignment */
float mouse_x_prev, mouse_y_prev;
struct input_pointer pointer[MAX_TOUCH]; /* int16_t alignment */
char device_model[256];
} android_input_t;
bool (*engine_lookup_name)(char *buf,
int *vendorId, int *productId, size_t len, int id);
void (*engine_handle_dpad)(struct android_app *, AInputEvent*, int, int);
static void android_input_poll_input_gingerbread(android_input_t *android);
static void android_input_poll_input_default(android_input_t *android);
static void (*android_input_poll_input)(android_input_t *android);
static bool android_input_set_sensor_state(void *data, unsigned port,
enum retro_sensor_action action, unsigned event_rate);
static void android_keycode_map_free(JNIEnv *env);
static void android_input_enable_sensor_manager(struct android_app *android_app);
static bool android_enable_sensor(struct android_app *android_app,
const ASensor *sensor, unsigned rate,
uint64_t enable_bit, uint64_t disable_bit);
extern float AMotionEvent_getAxisValue(const AInputEvent* motion_event,
int32_t axis, size_t pointer_idx);
static typeof(AMotionEvent_getAxisValue) *p_AMotionEvent_getAxisValue;
#define AMotionEvent_getAxisValue (*p_AMotionEvent_getAxisValue)
extern int32_t AMotionEvent_getButtonState(const AInputEvent* motion_event);
static typeof(AMotionEvent_getButtonState) *p_AMotionEvent_getButtonState;
#define AMotionEvent_getButtonState (*p_AMotionEvent_getButtonState)
#ifdef HAVE_DYLIB
static void *libandroid_handle;
#endif
/* Android system (IME) keyboard support for the menu OSK.
*
* Mirrors the iOS ios_keyboard_* hooks (ui/drivers/ui_cocoatouch.m):
* when the menu wants text input, the keyboard line buffer is repointed
* to an owned 512-byte buffer and the Java side raises the native soft
* keyboard (which gives copy/paste and password managers that the
* custom on-screen keyboard cannot).
*
* The Android soft keyboard runs on the UI thread, so committed/pasted
* text arrives via the onSystemKeyboardInput JNI callback on that
* thread; it is staged there (under a lock) and applied on the
* RetroArch input thread in android_keyboard_poll(). */
#define ANDROID_KBD_BUFFER_SIZE 512
static slock_t *android_kbd_lock = NULL;
static char *android_kbd_buffer = NULL; /* == keyboard_line.buffer */
static size_t *android_kbd_size_ptr = NULL;
static size_t *android_kbd_ptr_ptr = NULL;
static input_keyboard_line_complete_t android_kbd_cb = NULL;
static void *android_kbd_userdata = NULL;
static bool android_kbd_open = false;
/* Staging: written by the JNI/UI thread, drained by the input thread. */
static char android_kbd_staging[ANDROID_KBD_BUFFER_SIZE];
static bool android_kbd_dirty = false;
static bool android_kbd_finished = false;
static bool android_kbd_cancel = false;
/* Called by Java (UI thread) on every text change, and once more with
* finished = true on Done/Enter. A null text means the keyboard was
* dismissed without confirming, i.e. cancel. */
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCommon_onSystemKeyboardInput(
JNIEnv *env, jobject this_obj, jstring text_obj, jboolean finished)
{
if (!android_kbd_lock)
return;
slock_lock(android_kbd_lock);
if (text_obj)
{
const char *text = (*env)->GetStringUTFChars(env, text_obj, NULL);
if (text)
{
strlcpy(android_kbd_staging, text, sizeof(android_kbd_staging));
(*env)->ReleaseStringUTFChars(env, text_obj, text);
}
android_kbd_cancel = false;
}
else
{
android_kbd_staging[0] = '\0';
android_kbd_cancel = true;
}
android_kbd_dirty = true;
if (finished)
android_kbd_finished = true;
slock_unlock(android_kbd_lock);
}
bool android_keyboard_start(char **buffer_ptr, size_t *size_ptr,
size_t *ptr_ptr, const char *label,
input_keyboard_line_complete_t cb, void *userdata)
{
JNIEnv *env;
char *allocated;
size_t len;
struct android_app *android_app = (struct android_app*)g_android;
if (!android_app || !android_app->showKeyboard || !buffer_ptr || !size_ptr)
return false;
if (!android_kbd_lock && !(android_kbd_lock = slock_new()))
return false;
if (!(allocated = (char*)malloc(ANDROID_KBD_BUFFER_SIZE)))
return false;
/* Seed with any existing content (e.g. when editing a value). */
if (*buffer_ptr && **buffer_ptr)
strlcpy(allocated, *buffer_ptr, ANDROID_KBD_BUFFER_SIZE);
else
allocated[0] = '\0';
/* Repoint the keyboard line at our buffer; it is freed later by
* input_keyboard_line_free(), mirroring the iOS path. */
*buffer_ptr = allocated;
len = strlen(allocated);
*size_ptr = len;
if (ptr_ptr)
*ptr_ptr = len;
slock_lock(android_kbd_lock);
android_kbd_buffer = allocated;
android_kbd_size_ptr = size_ptr;
android_kbd_ptr_ptr = ptr_ptr;
android_kbd_cb = cb;
android_kbd_userdata = userdata;
android_kbd_staging[0] = '\0';
android_kbd_dirty = false;
android_kbd_finished = false;
android_kbd_cancel = false;
android_kbd_open = true;
slock_unlock(android_kbd_lock);
if ((env = jni_thread_getenv()))
{
jstring jlabel = label ? (*env)->NewStringUTF(env, label) : NULL;
jstring jinit = (*env)->NewStringUTF(env, allocated);
CALL_VOID_METHOD_PARAM(env, android_app->activity->clazz,
android_app->showKeyboard, jlabel, jinit);
if (jlabel)
(*env)->DeleteLocalRef(env, jlabel);
if (jinit)
(*env)->DeleteLocalRef(env, jinit);
}
return true;
}
bool android_keyboard_active(void)
{
return android_kbd_open;
}
void android_keyboard_end(void)
{
JNIEnv *env = NULL;
struct android_app *android_app = (struct android_app*)g_android;
if (!android_kbd_open || !android_kbd_lock)
return;
slock_lock(android_kbd_lock);
android_kbd_open = false;
android_kbd_buffer = NULL;
android_kbd_size_ptr = NULL;
android_kbd_ptr_ptr = NULL;
android_kbd_cb = NULL;
android_kbd_userdata = NULL;
android_kbd_dirty = false;
android_kbd_finished = false;
android_kbd_cancel = false;
slock_unlock(android_kbd_lock);
if (android_app && android_app->hideKeyboard && (env = jni_thread_getenv()))
CALL_VOID_METHOD(env, android_app->activity->clazz,
android_app->hideKeyboard);
}
/* Drain staged IME text on the RetroArch input thread. */
void android_keyboard_poll(void)
{
bool finished;
bool cancel;
char *buffer;
void *userdata;
input_keyboard_line_complete_t cb;
if (!android_kbd_open || !android_kbd_lock)
return;
slock_lock(android_kbd_lock);
if (!android_kbd_dirty)
{
slock_unlock(android_kbd_lock);
return;
}
/* Sync staged text into the live keyboard line buffer so the menu
* displays it (same role as the iOS UITextField delegate). */
if (!android_kbd_cancel && android_kbd_buffer)
{
size_t len;
strlcpy(android_kbd_buffer, android_kbd_staging, ANDROID_KBD_BUFFER_SIZE);
len = strlen(android_kbd_buffer);
if (android_kbd_size_ptr)
*android_kbd_size_ptr = len;
if (android_kbd_ptr_ptr)
*android_kbd_ptr_ptr = len;
}
finished = android_kbd_finished;
cancel = android_kbd_cancel;
cb = android_kbd_cb;
userdata = android_kbd_userdata;
buffer = android_kbd_buffer;
android_kbd_dirty = false;
android_kbd_finished = false;
slock_unlock(android_kbd_lock);
if (finished)
{
input_driver_state_t *input_st = input_state_get_ptr();
/* Mirror the iOS completion block: fire the callback (NULL line
* on cancel), then release the keyboard line and unblock hotkeys.
* The callback closes the dialog, which hides the soft keyboard
* via menu_input_dialog_end() -> android_keyboard_end(). */
if (cb)
cb(userdata, cancel ? NULL : buffer);
if (input_st)
{
input_keyboard_line_free(input_st);
input_st->flags &= ~INP_FLAG_KB_MAPPING_BLOCKED;
}
/* The callback normally closes the dialog (-> android_keyboard_end),
* which clears our state. If it didn't, drop the now-freed buffer
* pointer so a late JNI callback can't use it after free. */
slock_lock(android_kbd_lock);
if (android_kbd_buffer == buffer)
{
android_kbd_buffer = NULL;
android_kbd_open = false;
android_kbd_dirty = false;
android_kbd_finished = false;
}
slock_unlock(android_kbd_lock);
}
}
static void android_keyboard_free(void)
{
unsigned i, j;
for (i = 0; i < DEFAULT_MAX_PADS; i++)
for (j = 0; j < MAX_KEYS; j++)
android_key_state[i][j] = 0;
for (i = 0; i < (unsigned) kbd_num; i++)
kbd_id[i] = -1;
kbd_num = 0;
}
static bool android_input_lookup_name_prekitkat(char *s,
int *vendorId, int *productId, size_t len, int id)
{
jobject name = NULL;
jmethodID getName = NULL;
jobject device = NULL;
jmethodID method = NULL;
jclass class = 0;
const char *str = NULL;
JNIEnv *env = (JNIEnv*)jni_thread_getenv();
if (!env)
return false;
FIND_CLASS(env, class, "android/view/InputDevice");
if (!class)
return false;
GET_STATIC_METHOD_ID(env, method, class, "getDevice",
"(I)Landroid/view/InputDevice;");
if (!method)
return false;
CALL_OBJ_STATIC_METHOD_PARAM(env, device, class, method, (jint)id);
if (!device)
return false;
GET_METHOD_ID(env, getName, class, "getName", "()Ljava/lang/String;");
if (!getName)
return false;
CALL_OBJ_METHOD(env, name, device, getName);
if (!name)
return false;
s[0] = '\0';
str = (*env)->GetStringUTFChars(env, name, 0);
if (str)
strlcpy(s, str, len);
(*env)->ReleaseStringUTFChars(env, name, str);
return true;
}
static bool android_input_lookup_name(char *s,
int *vendorId, int *productId, size_t len, int id)
{
jmethodID getVendorId = NULL;
jmethodID getProductId = NULL;
jmethodID getName = NULL;
jobject device = NULL;
jobject name = NULL;
jmethodID method = NULL;
jclass class = NULL;
const char *str = NULL;
JNIEnv *env = (JNIEnv*)jni_thread_getenv();
if (!env)
return false;
FIND_CLASS(env, class, "android/view/InputDevice");
if (!class)
return false;
GET_STATIC_METHOD_ID(env, method, class, "getDevice",
"(I)Landroid/view/InputDevice;");
if (!method)
return false;
CALL_OBJ_STATIC_METHOD_PARAM(env, device, class, method, (jint)id);
if (!device)
return false;
GET_METHOD_ID(env, getName, class, "getName", "()Ljava/lang/String;");
if (!getName)
return false;
CALL_OBJ_METHOD(env, name, device, getName);
if (!name)
return false;
s[0] = '\0';
str = (*env)->GetStringUTFChars(env, name, 0);
if (str)
strlcpy(s, str, len);
(*env)->ReleaseStringUTFChars(env, name, str);
GET_METHOD_ID(env, getVendorId, class, "getVendorId", "()I");
if (!getVendorId)
return false;
CALL_INT_METHOD(env, *vendorId, device, getVendorId);
GET_METHOD_ID(env, getProductId, class, "getProductId", "()I");
if (!getProductId)
return false;
*productId = 0;
CALL_INT_METHOD(env, *productId, device, getProductId);
return true;
}
static bool android_input_can_be_keyboard_jni(int id)
{
jmethodID getKeyboardType = NULL;
jobject device = NULL;
jint keyboard_type = -1;
jmethodID method = NULL;
jclass class = NULL;
const char *str = NULL;
JNIEnv *env = (JNIEnv*)jni_thread_getenv();
if (!env)
return false;
FIND_CLASS(env, class, "android/view/InputDevice");
if (!class)
return false;
GET_STATIC_METHOD_ID(env, method, class, "getDevice",
"(I)Landroid/view/InputDevice;");
if (!method)
return false;
CALL_OBJ_STATIC_METHOD_PARAM(env, device, class, method, (jint)id);
if (!device)
return false;
GET_METHOD_ID(env, getKeyboardType, class, "getKeyboardType", "()I");
if (!getKeyboardType)
return false;
CALL_INT_METHOD(env, keyboard_type, device, getKeyboardType);
if (keyboard_type < 0)
return false;
return keyboard_type == AINPUT_KEYBOARD_TYPE_ALPHABETIC;
}
bool android_input_can_be_keyboard(void *data, int port)
{
android_input_t *android = (android_input_t *) data;
if (!android)
return false;
if (port < 0 || port >= android->pads_connected)
return false;
state_device_t *device = &android->pad_states[port];
if (!device->id && !*device->name)
return false;
return android_input_can_be_keyboard_jni(device->id);
}
#ifdef HAVE_THREADS
/* EGL bindings are per-thread. Under threaded video the context is made
* current on the video thread, so an eglMakeCurrent() issued from here
* releases this thread's (empty) binding and leaves the surface current
* on the worker - and a later create_surface() would bind the same
* context a second time, on a second thread. Both entry points therefore
* run where the context lives. */
static uintptr_t android_ctx_create_surface_cb(void *data)
{
video_driver_state_t *state = video_state_get_ptr();
if (!state->current_video_context.create_surface)
return 0;
return state->current_video_context.create_surface(data) ? 1 : 0;
}
static uintptr_t android_ctx_destroy_surface_cb(void *data)
{
video_driver_state_t *state = video_state_get_ptr();
if (state->current_video_context.destroy_surface)
state->current_video_context.destroy_surface(data);
return 0;
}
#endif
static void android_input_destroy_surface(video_driver_state_t *state)
{
if (!state || !state->current_video_context.destroy_surface)
return;
#ifdef HAVE_THREADS
/* The video worker may still be recording a frame that references the
* surface. Drain it before the context driver frees the surface. */
video_thread_wait_idle();
/* Dispatches to the video thread, or calls straight through when the
* wrapper is inactive or this already is the video thread. */
video_thread_texture_handle(state->context_data,
android_ctx_destroy_surface_cb);
#else
state->current_video_context.destroy_surface(state->context_data);
#endif
}
/* Set once the pause-time flush has been performed, cleared again on
* resume. A pause -> resume -> pause cycle therefore flushes twice, but a
* duplicate APP_CMD_PAUSE does not rewrite the config a second time. */
static bool android_state_flushed = false;
/* Set by the APP_CMD_PAUSE handler, consumed by
* android_input_flush_pending_state() at the top of the next runloop
* iteration. */
static bool android_state_flush_pending = false;
/* Android may reclaim the process at any point after onPause() has
* returned. onDestroy() is not guaranteed to run at all - in particular,
* swiping the task away from Recents never delivers it - so onPause() is
* the last callback that can be relied upon.
*
* Everything that would otherwise only be written by retroarch_main_quit()
* is therefore flushed here instead, so that settings survive the process
* being killed without the user having to invoke 'Quit RetroArch'.
*
* Called from the runloop rather than from the command handler: the
* command pipe is drained by android_input_poll(), which a core reaches
* through the input poll callback, so a flush performed there would read
* core memory and rewrite the config from inside retro_run(). One frame
* of latency is well inside the window Android allows after onPause(). */
void android_input_flush_pending_state(void)
{
settings_t *settings = config_get_ptr();
runloop_state_t *runloop_st = runloop_state_get_ptr();
if (!android_state_flush_pending)
return;
android_state_flush_pending = false;
if (android_state_flushed)
return;
android_state_flushed = true;
/* Config subsystem is not up yet - nothing to persist. */
if (!settings)
return;
/* SRAM first: it is the more expensive of the two, and the more
* painful to lose. Non-SRAM cores make this a no-op.
*
* Only flush while content is actually loaded. APP_CMD_PAUSE can be
* delivered while frontend_unix_init() is still pumping events
* waiting for the window (activity created, then immediately
* backgrounded / screen locked), long before any core exists - and,
* worse, while a previous activity instance in the same process may
* still be mid-teardown, leaving runloop_state.current_core in a
* transient state. There is nothing to save in either case:
* CMD_EVENT_SAVE_FILES exists to persist SRAM and game-specific
* cheats, both of which require loaded content. */
if (runloop_st->current_core.flags & RETRO_CORE_FLAG_GAME_LOADED)
command_event(CMD_EVENT_SAVE_FILES, NULL);
if (settings->bools.config_save_on_exit)
command_event(CMD_EVENT_MENU_SAVE_CURRENT_CONFIG, NULL);
}
static void android_input_poll_main_cmd(void)
{
int8_t cmd;
ssize_t ret;
struct android_app *android_app = (struct android_app*)g_android;
/* A command dropped here is never acknowledged, and a lifecycle
* callback waiting on it would block the Java UI thread until
* ActivityManager gives up. Retry rather than lose the byte. */
do
{
ret = read(android_app->msgread, &cmd, sizeof(cmd));
} while (ret < 0 && errno == EINTR);
if (ret != (ssize_t)sizeof(cmd))
cmd = -1;
switch (cmd)
{
case APP_CMD_REINIT_DONE:
slock_lock(android_app->mutex);
android_app->reinitRequested = 0;
scond_broadcast(android_app->cond);
slock_unlock(android_app->mutex);
break;
case APP_CMD_INPUT_CHANGED:
slock_lock(android_app->mutex);
if (android_app->inputQueue)
AInputQueue_detachLooper(android_app->inputQueue);
android_app->inputQueue = android_app->pendingInputQueue;
if (android_app->inputQueue)
AInputQueue_attachLooper(android_app->inputQueue,
android_app->looper, LOOPER_ID_INPUT, NULL,
NULL);
android_app->done_seq++;
scond_broadcast(android_app->cond);
slock_unlock(android_app->mutex);
/* The set of attached input devices has changed, so a cached
* KeyCharacterMap may now belong to a device that is gone or
* has been replaced under the same id. Drop the cache and let
* it re-resolve on the next key event. */
android_keycode_map_free((JNIEnv*)jni_thread_getenv());
break;
case APP_CMD_INIT_WINDOW:
slock_lock(android_app->mutex);
android_app->window = android_app->pendingWindow;
android_app->reinitRequested = 1;
android_app->done_seq++;
scond_broadcast(android_app->cond);
slock_unlock(android_app->mutex);
/* A resume brings a NEW window, and the display mode and
* frame rate chosen for the old one do not come with it.
* Assert them again, or a mode the user picked reverts
* every time they come back from the Android UI. */
android_display_server_reapply_mode();
break;
case APP_CMD_RESUME:
case APP_CMD_START:
case APP_CMD_PAUSE:
{
video_driver_state_t *state = video_state_get_ptr();
slock_lock(android_app->mutex);
android_app->activityState = cmd;
/* RESUME/START can arrive before INIT_WINDOW. In that case,
* wait for INIT_WINDOW rather than falling back to a full
* video-driver reinitialization without a native window. */
if ( (cmd == APP_CMD_RESUME || cmd == APP_CMD_START)
&& state->current_video_context.ident
&& string_is_equal(state->current_video_context.ident,
"vk_android")
&& android_app->window
&& state->current_video_context.create_surface)
android_app->reinitRequested = 1;
scond_broadcast(android_app->cond);
slock_unlock(android_app->mutex);
if (cmd == APP_CMD_PAUSE)
android_state_flush_pending = true;
else
{
android_state_flush_pending = false;
android_state_flushed = false;
}
#ifdef HAVE_ANDROID_LIFECYCLE_HOOKS
/* After the acknowledgement above, so a slow hook delays this
* thread rather than the UI thread waiting in onStart(). */
if (cmd == APP_CMD_START)
android_run_lifecycle_hook(android_app, "switch");
#endif
break;
}
case APP_CMD_STOP:
{
video_driver_state_t *state = video_state_get_ptr();
slock_lock(android_app->mutex);
android_app->activityState = cmd;
scond_broadcast(android_app->cond);
slock_unlock(android_app->mutex);
/* Android may retain the same ANativeWindow while the app is
* backgrounded. Release Vulkan's acquired buffers anyway so BLAST
* cannot wedge before APP_CMD_TERM_WINDOW is delivered. */
if ( state->current_video_context.ident
&& string_is_equal(state->current_video_context.ident,
"vk_android"))
android_input_destroy_surface(state);
break;
}
case APP_CMD_CONFIG_CHANGED:
AConfiguration_fromAssetManager(android_app->config,
android_app->activity->assetManager);
break;
case APP_CMD_TERM_WINDOW:
{
video_driver_state_t *state = video_state_get_ptr();
android_input_destroy_surface(state);
slock_lock(android_app->mutex);
/* The window is being hidden or closed, clean it up. */
/* terminate display/EGL context here */
android_app->window = NULL;
android_app->done_seq++;
scond_broadcast(android_app->cond);
slock_unlock(android_app->mutex);
break;
}
case APP_CMD_GAINED_FOCUS:
{
runloop_state_t *runloop_st = runloop_state_get_ptr();
settings_t *settings = config_get_ptr();
bool sensors_allowed = !settings || settings->bools.input_sensors_enable;
/* Re-enable sensors that were disabled on focus loss */
bool enable_accelerometer = (android_app->sensor_state_mask &
(UINT64_C(1) << RETRO_SENSOR_ACCELEROMETER_DISABLE));
bool enable_gyroscope = (android_app->sensor_state_mask &
(UINT64_C(1) << RETRO_SENSOR_GYROSCOPE_DISABLE));
/* On first focus (no sensor state yet), enable if setting is on.
* This handles shader sensor access without going through cores.
* Default to enabling if settings aren't loaded yet (first launch). */
if (!enable_accelerometer &&
!(android_app->sensor_state_mask & (UINT64_C(1) << RETRO_SENSOR_ACCELEROMETER_ENABLE)))
{
if (sensors_allowed)
enable_accelerometer = true;
}
if (!enable_gyroscope &&
!(android_app->sensor_state_mask & (UINT64_C(1) << RETRO_SENSOR_GYROSCOPE_ENABLE)))
{
if (sensors_allowed)
enable_gyroscope = true;
}
if (!sensors_allowed)
{
enable_accelerometer = false;
enable_gyroscope = false;
}
runloop_st->flags &= ~(RUNLOOP_FLAG_PAUSED
| RUNLOOP_FLAG_IDLE);
video_driver_unset_stub_frame();
/* Try to enable sensors via input driver. If that fails before the
* input driver has initialized, enable directly via sensor API. */
if (enable_accelerometer)
{
if (!input_set_sensor_state(0,
RETRO_SENSOR_ACCELEROMETER_ENABLE,
android_app->accelerometer_event_rate) &&
!android_app->input_alive)
{
/* Input driver not ready — enable sensor directly */
unsigned rate = android_app->accelerometer_event_rate;
if (rate == 0)
rate = DEFAULT_ASENSOR_EVENT_RATE;
android_input_enable_sensor_manager(android_app);
if (android_enable_sensor(android_app,
android_app->accelerometerSensor, rate,
RETRO_SENSOR_ACCELEROMETER_ENABLE,
RETRO_SENSOR_ACCELEROMETER_DISABLE))
android_app->accelerometer_event_rate = rate;
}
}
if (enable_gyroscope)
{
if (!input_set_sensor_state(0,
RETRO_SENSOR_GYROSCOPE_ENABLE,
android_app->gyroscope_event_rate) &&
!android_app->input_alive)
{
/* Input driver not ready — enable sensor directly */
unsigned rate = android_app->gyroscope_event_rate;
if (rate == 0)
rate = DEFAULT_ASENSOR_EVENT_RATE;
android_input_enable_sensor_manager(android_app);
if (android_enable_sensor(android_app,
android_app->gyroscopeSensor, rate,
RETRO_SENSOR_GYROSCOPE_ENABLE,
RETRO_SENSOR_GYROSCOPE_DISABLE))
android_app->gyroscope_event_rate = rate;
}
}
/* Start gravity-based orientation detection (once per app launch only).
* Samples accelerometer data over ~30 frames to determine which axis
* gravity is on, detecting both display rotation and sensor IC
* misalignment automatically. */
if (!android_app->gravity_calibrated)
{
android_app->gravity_accum_x = 0.0f;
android_app->gravity_accum_y = 0.0f;
android_app->gravity_sample_count = 0;
}
else
{
/* Gravity already calibrated (not first launch),
* start rest position capture immediately */
input_sensor_start_rest_capture();
}
}
/* No waiter: onWindowFocusChanged() posts the command and
* returns without blocking, so the lock and broadcast only
* guarded this one scalar. The field is atomic now. */
retro_atomic_store_release_int(&android_app->unfocused, 0);
break;
case APP_CMD_LOST_FOCUS:
{
runloop_state_t *runloop_st = runloop_state_get_ptr();
bool disable_accelerometer = (android_app->sensor_state_mask &
(UINT64_C(1) << RETRO_SENSOR_ACCELEROMETER_ENABLE)) &&
android_app->accelerometerSensor;
bool disable_gyroscope = (android_app->sensor_state_mask &
(UINT64_C(1) << RETRO_SENSOR_GYROSCOPE_ENABLE)) &&
android_app->gyroscopeSensor;
runloop_st->flags |= (RUNLOOP_FLAG_PAUSED
| RUNLOOP_FLAG_IDLE);
video_driver_set_stub_frame();
/* Avoid draining battery while app is not being used. */
if (disable_accelerometer)
input_set_sensor_state(0,
RETRO_SENSOR_ACCELEROMETER_DISABLE,
android_app->accelerometer_event_rate);
if (disable_gyroscope)
input_set_sensor_state(0,
RETRO_SENSOR_GYROSCOPE_DISABLE,
android_app->gyroscope_event_rate);
}
/* No waiter: onWindowFocusChanged() posts the command and
* returns without blocking, so the lock and broadcast only
* guarded this one scalar. The field is atomic now. */
retro_atomic_store_release_int(&android_app->unfocused, 1);
break;
case APP_CMD_DESTROY:
android_app->destroyRequested = 1;
break;
}
}
static void engine_handle_dpad_default(struct android_app *android,
AInputEvent *event, int port, int source)
{
size_t motion_ptr = AMotionEvent_getAction(event) >>
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
float x = AMotionEvent_getX(event, motion_ptr);
float y = AMotionEvent_getY(event, motion_ptr);
if (port < 0 || port >= DEFAULT_MAX_PADS)
return;
android->analog_state[port][0] = (int16_t)(x * 32767.0f);
android->analog_state[port][1] = (int16_t)(y * 32767.0f);
}
#ifdef HAVE_DYLIB
static void engine_handle_dpad_getaxisvalue(struct android_app *android,
AInputEvent *event, int port, int source)
{
size_t motion_ptr = AMotionEvent_getAction(event) >>
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
float x = AMotionEvent_getAxisValue(event, AXIS_X, motion_ptr);
float y = AMotionEvent_getAxisValue(event, AXIS_Y, motion_ptr);
float z = AMotionEvent_getAxisValue(event, AXIS_Z, motion_ptr);
float rz = AMotionEvent_getAxisValue(event, AXIS_RZ, motion_ptr);
float hatx = AMotionEvent_getAxisValue(event, AXIS_HAT_X, motion_ptr);
float haty = AMotionEvent_getAxisValue(event, AXIS_HAT_Y, motion_ptr);
float ltrig = AMotionEvent_getAxisValue(event, AXIS_LTRIGGER, motion_ptr);
float rtrig = AMotionEvent_getAxisValue(event, AXIS_RTRIGGER, motion_ptr);
float brake = AMotionEvent_getAxisValue(event, AXIS_BRAKE, motion_ptr);
float gas = AMotionEvent_getAxisValue(event, AXIS_GAS, motion_ptr);
if (port < 0 || port >= DEFAULT_MAX_PADS)
return;
android->hat_state[port][0] = (int)hatx;
android->hat_state[port][1] = (int)haty;
/* XXX: this could be a loop instead, but do we really want to
* loop through every axis? */
android->analog_state[port][0] = (int16_t)(x * 32767.0f);
android->analog_state[port][1] = (int16_t)(y * 32767.0f);
android->analog_state[port][2] = (int16_t)(z * 32767.0f);
android->analog_state[port][3] = (int16_t)(rz * 32767.0f);
android->analog_state[port][6] = (int16_t)(ltrig * 32767.0f);
android->analog_state[port][7] = (int16_t)(rtrig * 32767.0f);
android->analog_state[port][8] = (int16_t)(brake * 32767.0f);
android->analog_state[port][9] = (int16_t)(gas * 32767.0f);
}
#endif
static bool android_input_init_handle(void)
{
#ifdef HAVE_DYLIB
if (libandroid_handle != NULL) /* already initialized */
return true;
#if defined (ANDROID_AARCH64) || defined(ANDROID_X64)
if ((libandroid_handle = dlopen("/system/lib64/libandroid.so",
RTLD_LOCAL | RTLD_LAZY)) == 0)
return false;
#else
if ((libandroid_handle = dlopen("/system/lib/libandroid.so",
RTLD_LOCAL | RTLD_LAZY)) == 0)
return false;
#endif
if ((p_AMotionEvent_getAxisValue = dlsym(RTLD_DEFAULT,
"AMotionEvent_getAxisValue")))
engine_handle_dpad = engine_handle_dpad_getaxisvalue;
p_AMotionEvent_getButtonState = dlsym(RTLD_DEFAULT,
"AMotionEvent_getButtonState");
#endif
pad_id1 = -1;
pad_id2 = -1;
return true;
}
static void *android_input_init(const char *joypad_driver)
{
int32_t sdk;
struct android_app *android_app = (struct android_app*)g_android;
android_input_t *android = (android_input_t*)
calloc(1, sizeof(*android));
if (!android)
return NULL;
android->mouse_activated = false;
android->pads_connected = 0;
android->quick_tap_time = 0;
input_keymaps_init_keyboard_lut(rarch_key_map_android);
frontend_android_get_version_sdk(&sdk);
if (sdk >= 19)
engine_lookup_name = android_input_lookup_name;
else
engine_lookup_name = android_input_lookup_name_prekitkat;
engine_handle_dpad = engine_handle_dpad_default;
if (sdk > 10)
android_input_poll_input = android_input_poll_input_default;
else
android_input_poll_input = android_input_poll_input_gingerbread;
if (!android_input_init_handle())
{
RARCH_WARN("[Android] Unable to open libandroid.so\n");
}
frontend_android_get_name(android->device_model,
sizeof(android->device_model));
android_app->input_alive = true;
/* Enable sensors on init if setting is on (or not yet loaded)
* and they haven't already been enabled by GAINED_FOCUS.
* Check sensor_state_mask to avoid double-enabling, since
* ASensorEventQueue_enableSensor is reference-counted. */
{
settings_t *settings = config_get_ptr();
bool enable_sensors = !settings || settings->bools.input_sensors_enable;
if (enable_sensors && !android_app->sensor_state_mask)
{
unsigned rate = android_app->accelerometer_event_rate;
if (rate == 0)
rate = DEFAULT_ASENSOR_EVENT_RATE;
android_input_enable_sensor_manager(android_app);
if (android_enable_sensor(android_app,
android_app->accelerometerSensor, rate,
RETRO_SENSOR_ACCELEROMETER_ENABLE,
RETRO_SENSOR_ACCELEROMETER_DISABLE))
android_app->accelerometer_event_rate = rate;
rate = android_app->gyroscope_event_rate;
if (rate == 0)
rate = DEFAULT_ASENSOR_EVENT_RATE;
if (android_enable_sensor(android_app,
android_app->gyroscopeSensor, rate,
RETRO_SENSOR_GYROSCOPE_ENABLE,
RETRO_SENSOR_GYROSCOPE_DISABLE))
android_app->gyroscope_event_rate = rate;
}
}
return android;
}
static int android_check_quick_tap(android_input_t *android)
{
/* Check if the touch screen has been been quick tapped
* and then not touched again for 200ms
* If so then return true and deactivate quick tap timer */
retro_time_t now = cpu_features_get_time_usec();
if (android->quick_tap_time &&
(now / 1000 - android->quick_tap_time / 1000000) >= 200)
{
android->quick_tap_time = 0;
return 1;
}
return 0;
}
static INLINE void android_mouse_calculate_deltas(android_input_t *android,
AInputEvent *event,size_t motion_ptr,int source)
{
unsigned video_width, video_height;
video_driver_get_output_size(&video_width, &video_height);
float x = 0;
float x_delta = 0;
float x_min = 0;
float x_max = (float)video_width;
float y = 0;
float y_delta = 0;
float y_min = 0;
float y_max = (float)video_height;
struct video_viewport vp = {0};
int16_t res_x = 0;
int16_t res_y = 0;
int16_t res_screen_x = 0;
int16_t res_screen_y = 0;
/* AINPUT_SOURCE_MOUSE_RELATIVE is available on Oreo (SDK 26) and newer,
* it passes the relative coordinates in the regular X and Y parts.
* NOTE: AINPUT_SOURCE_* defines have multiple bits set so do full check */
if ((source & AINPUT_SOURCE_MOUSE_RELATIVE) == AINPUT_SOURCE_MOUSE_RELATIVE)
{
x_delta = AMotionEvent_getX(event, motion_ptr);
y_delta = AMotionEvent_getY(event, motion_ptr);
}
else
{
/* This axis is only available on Android Nougat or on
* Android devices with NVIDIA extensions */
if (p_AMotionEvent_getAxisValue)
{
x_delta = AMotionEvent_getAxisValue(event,AMOTION_EVENT_AXIS_RELATIVE_X,
motion_ptr);
y_delta = AMotionEvent_getAxisValue(event,AMOTION_EVENT_AXIS_RELATIVE_Y,
motion_ptr);
}
/* If AXIS_RELATIVE had 0 values it might be because we're not
* running Android Nougat or on a device
* with NVIDIA extension, so re-calculate deltas based on
* AXIS_X and AXIS_Y. This has limitations
* compared to AXIS_RELATIVE because once the Android mouse cursor
* hits the edge of the screen it is
* not possible to move the in-game mouse any further in that direction.
*/
if (!x_delta && !y_delta)
{
x = AMotionEvent_getX(event, motion_ptr);
y = AMotionEvent_getY(event, motion_ptr);
x_delta = (x_delta - android->mouse_x_prev);
y_delta = (y_delta - android->mouse_y_prev);
android->mouse_x_prev = x;
android->mouse_y_prev = y;
}
}
android->mouse_x_delta = x_delta;
android->mouse_y_delta = y_delta;
if (!x) x = android->mouse_x + android->mouse_x_delta;
if (!y) y = android->mouse_y + android->mouse_y_delta;
video_driver_translate_coord_viewport_confined_wrap(&vp,
(int) x, (int) y,
&android->mouse_x_viewport, &android->mouse_y_viewport,
&android->mouse_x_viewport_screen, &android->mouse_y_viewport_screen);
/* x and y are used for the screen mouse, so we want
* to avoid values outside of the viewport resolution */
if (x < x_min) x = x_min;
else if (x > x_max) x = x_max;
if (y < y_min) y = y_min;
else if (y > y_max) y = y_max;
android->mouse_x = x;
android->mouse_y = y;
}
static INLINE void android_input_poll_event_type_motion(
android_input_t *android, AInputEvent *event,
int port, int source)
{
int getaction = AMotionEvent_getAction(event);
int action = getaction & AMOTION_EVENT_ACTION_MASK;
size_t motion_ptr = getaction >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
bool keyup = (
action == AMOTION_EVENT_ACTION_UP
|| action == AMOTION_EVENT_ACTION_CANCEL
|| action == AMOTION_EVENT_ACTION_POINTER_UP);
/* If source is mouse then calculate button state
* and mouse deltas and don't process as touchscreen event.
* NOTE: AINPUT_SOURCE_* defines have multiple bits set so do full check */
if ( (source & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE
|| (source & AINPUT_SOURCE_MOUSE_RELATIVE) == AINPUT_SOURCE_MOUSE_RELATIVE)
{
if (!android->mouse_activated)
{
RARCH_LOG("[Android Input] Mouse activated.\n");
android->mouse_activated = true;
}
/* getButtonState requires API level 14 */
if (p_AMotionEvent_getButtonState)
{
int btn = (int)AMotionEvent_getButtonState(event);
android->mouse_l = (btn & AMOTION_EVENT_BUTTON_PRIMARY);
android->mouse_r = (btn & AMOTION_EVENT_BUTTON_SECONDARY);
android->mouse_m = (btn & AMOTION_EVENT_BUTTON_TERTIARY);
btn = (int)AMotionEvent_getAxisValue(event,
AMOTION_EVENT_AXIS_VSCROLL, motion_ptr);
if (btn > 0)
android->mouse_wu = btn;
else if (btn < 0)
android->mouse_wd = btn;
}
else
{
/* If getButtonState is not available
* then treat all MotionEvent.ACTION_DOWN as left button presses */
if (action == AMOTION_EVENT_ACTION_DOWN)
android->mouse_l = 1;
if (action == AMOTION_EVENT_ACTION_UP)
android->mouse_l = 0;
}
android_mouse_calculate_deltas(android,event,motion_ptr,source);
return;
}
if (keyup && motion_ptr < MAX_TOUCH)
{
if (action == AMOTION_EVENT_ACTION_UP && ENABLE_TOUCH_SCREEN_MOUSE)
{
/* If touchscreen was pressed for less than 200ms
* then register time stamp of a quick tap */
if ((AMotionEvent_getEventTime(event)-AMotionEvent_getDownTime(event))/1000000 < 200)
{
/* Prevent the quick tap if a button on the overlay is down */
input_driver_state_t *input_st = input_state_get_ptr();
if (!(input_st->flags & INP_FLAG_BLOCK_POINTER_INPUT))
android->quick_tap_time = AMotionEvent_getEventTime(event);
}
android->mouse_l = 0;
}
memmove(android->pointer + motion_ptr,
android->pointer + motion_ptr + 1,
(MAX_TOUCH - motion_ptr - 1) * sizeof(struct input_pointer));
if (android->pointer_count > 0)
android->pointer_count--;
}
else
{
int pointer_max = MIN(
AMotionEvent_getPointerCount(event), MAX_TOUCH);
if (action == AMOTION_EVENT_ACTION_DOWN && ENABLE_TOUCH_SCREEN_MOUSE)
{
/* When touch screen is pressed, set mouse
* previous position to current position
* before starting to calculate mouse movement deltas. */
android->mouse_x_prev = AMotionEvent_getX(event, motion_ptr);
android->mouse_y_prev = AMotionEvent_getY(event, motion_ptr);
/* If another touch happened within 200ms after a quick tap
* then cancel the quick tap and register left mouse button
* as being held down */
if ((AMotionEvent_getEventTime(event) - android->quick_tap_time)/1000000 < 200)
{
android->quick_tap_time = 0;
android->mouse_l = 1;
}
}
if (( action == AMOTION_EVENT_ACTION_MOVE
|| action == AMOTION_EVENT_ACTION_HOVER_MOVE)
&& ENABLE_TOUCH_SCREEN_MOUSE)
android_mouse_calculate_deltas(android,event,motion_ptr,source);
for (motion_ptr = 0; motion_ptr < pointer_max; motion_ptr++)
{
struct video_viewport vp = {0};
float x = AMotionEvent_getX(event, motion_ptr);
float y = AMotionEvent_getY(event, motion_ptr);
/* On other platforms, pointer query uses the confined wrap function, *
* but some extra functionality is added to Android which needs the *
* true offscreen value -0x8000, so both variants are called. */
video_driver_translate_coord_viewport_confined_wrap(
&vp,
x, y,
&android->pointer[motion_ptr].confined_x,
&android->pointer[motion_ptr].confined_y,
&android->pointer[motion_ptr].full_x,
&android->pointer[motion_ptr].full_y);
video_driver_translate_coord_viewport_wrap(
&vp,
x, y,
&android->pointer[motion_ptr].x,
&android->pointer[motion_ptr].y,
&android->pointer[motion_ptr].full_x,
&android->pointer[motion_ptr].full_y);
android->pointer_count = MAX(
android->pointer_count,
motion_ptr + 1);
}
}
/* If more than one pointer detected
* then count it as a mouse right click */
if (ENABLE_TOUCH_SCREEN_MOUSE)
android->mouse_r = (android->pointer_count == 2);
}
static bool android_is_keyboard_id(int id)
{
unsigned i;
for (i = 0; i < (unsigned)kbd_num; i++)
if (id == kbd_id[i])
return true;
return false;
}
/* Resolves the printable Unicode codepoint produced by a hardware-key
* press, honouring the active keyboard layout and modifier (shift, caps
* lock, ...) state via the device's KeyCharacterMap - this mirrors what
* android.view.KeyEvent.getUnicodeChar() does.
*
* Returns 0 for keys that do not produce a character (modifiers, lock
* keys, navigation/function keys, ...). That is exactly what the
* keyboard line-editor expects: such keys must not emit text, and were
* previously leaking into menu text fields as '?'.
*
* Also returns 0 (rather than failing) whenever JNI is unavailable, so
* the caller can fall back to its previous keysym-based behaviour. */
/* Resolved once and held for the lifetime of the input driver.
* @kcm_class is a global reference: the method IDs below stay valid only
* while the class is reachable, and a local reference would not survive
* the call that created it. */
#define ANDROID_KCM_CACHE_SIZE 4
static jclass kcm_class = NULL;
static jmethodID kcm_load = NULL;
static jmethodID kcm_get = NULL;
static jobject kcm_obj[ANDROID_KCM_CACHE_SIZE];
static int kcm_obj_device[ANDROID_KCM_CACHE_SIZE];
static int kcm_obj_next = 0;
static bool kcm_resolve_failed = false;
static void android_keycode_map_free(JNIEnv *env)
{
int i;
if (!env)
return;
for (i = 0; i < ANDROID_KCM_CACHE_SIZE; i++)
{
if (kcm_obj[i])
(*env)->DeleteGlobalRef(env, kcm_obj[i]);
kcm_obj[i] = NULL;
kcm_obj_device[i] = 0;
}
if (kcm_class)
(*env)->DeleteGlobalRef(env, kcm_class);
kcm_class = NULL;
kcm_load = NULL;
kcm_get = NULL;
kcm_obj_next = 0;
kcm_resolve_failed = false;
}
/* Return the KeyCharacterMap for @device_id, resolving and caching the
* class, its method IDs and the per-device map object on first use.
*
* Devices are few and long-lived, so a small round-robin cache covers
* the realistic case (one or two attached keyboards) without needing
* invalidation on hotplug: a stale entry is simply evicted in turn, and
* a reconnected device re-resolves. */
static jobject android_keycode_map_get(JNIEnv *env, int device_id)
{
int i;
jobject local = NULL;
if (kcm_resolve_failed)
return NULL;
if (!kcm_class)
{
jclass found = NULL;
FIND_CLASS(env, found, "android/view/KeyCharacterMap");
if (!found)
{
kcm_resolve_failed = true;
return NULL;
}
kcm_class = (jclass)(*env)->NewGlobalRef(env, found);
(*env)->DeleteLocalRef(env, found);
if (!kcm_class)
{
kcm_resolve_failed = true;
return NULL;
}
GET_STATIC_METHOD_ID(env, kcm_load, kcm_class, "load",
"(I)Landroid/view/KeyCharacterMap;");
GET_METHOD_ID(env, kcm_get, kcm_class, "get", "(II)I");
if (!kcm_load || !kcm_get)
{
android_keycode_map_free(env);
kcm_resolve_failed = true;
return NULL;
}
}
for (i = 0; i < ANDROID_KCM_CACHE_SIZE; i++)
if (kcm_obj[i] && kcm_obj_device[i] == device_id)
return kcm_obj[i];
CALL_OBJ_STATIC_METHOD_PARAM(env, local, kcm_class, kcm_load,
(jint)device_id);
if (!local)
return NULL;
if (kcm_obj[kcm_obj_next])
(*env)->DeleteGlobalRef(env, kcm_obj[kcm_obj_next]);
kcm_obj[kcm_obj_next] = (*env)->NewGlobalRef(env, local);
kcm_obj_device[kcm_obj_next] = device_id;
(*env)->DeleteLocalRef(env, local);
local = kcm_obj[kcm_obj_next];
kcm_obj_next = (kcm_obj_next + 1) % ANDROID_KCM_CACHE_SIZE;
return local;
}
static unsigned android_keycode_to_unicode(int device_id,
int keycode, int meta_state)
{
jint unicode = 0;
jobject kcm = NULL;
JNIEnv *env = (JNIEnv*)jni_thread_getenv();
if (!env)
return 0;
/* No local frame: the cached path creates no local references at all,
* and the resolve path deletes the two it makes explicitly. */
if ((kcm = android_keycode_map_get(env, device_id)))
CALL_INT_METHOD_PARAM(env, unicode, kcm, kcm_get,
(jint)keycode, (jint)meta_state);
/* KeyCharacterMap.get() sets the COMBINING_ACCENT (0x80000000) flag
* for dead keys; the menu line-editor cannot compose those, so strip
* the flag and keep the base accent character. */
return ((unsigned)unicode) & 0x7fffffff;
}
static INLINE void android_input_poll_event_type_keyboard(
AInputEvent *event, int keycode, int *handled)
{
int keydown = (AKeyEvent_getAction(event)
== AKEY_EVENT_ACTION_DOWN);
unsigned keyboardcode = input_keymaps_translate_keysym_to_rk(keycode);
/* Set keyboard modifier based on shift,ctrl and alt state */
uint16_t mod = 0;
int meta = AKeyEvent_getMetaState(event);
uint32_t character = 0;
if (meta & AMETA_ALT_ON)
mod |= RETROKMOD_ALT;
if (meta & AMETA_CTRL_ON)
mod |= RETROKMOD_CTRL;
if (meta & AMETA_SHIFT_ON)
mod |= RETROKMOD_SHIFT;
if (meta & AMETA_CAPS_LOCK_ON)
mod |= RETROKMOD_CAPSLOCK;
if (meta & AMETA_NUM_LOCK_ON)
mod |= RETROKMOD_NUMLOCK;
if (meta & AMETA_SCROLL_LOCK_ON)
mod |= RETROKMOD_SCROLLOCK;
if (meta & AMETA_META_ON)
mod |= RETROKMOD_META;
/* Resolve the actual printable character for this key in the current
* layout + modifier state. This produces capitals and shifted
* symbols, and yields 0 for modifier/lock keys so they no longer leak
* into menu text fields as '?'. */
character = android_keycode_to_unicode(
AInputEvent_getDeviceId(event), keycode, meta);
/* Fall back to the raw keysym for the plain ASCII range when the
* platform could not resolve a character (e.g. JNI unavailable).
* This preserves the previous behaviour for lowercase input while
* still suppressing modifier/lock keys, whose keysyms are >= 0x80,
* from being emitted as text. */
if (character == 0 && keyboardcode < 0x80)
character = keyboardcode;
input_keyboard_event(keydown, keyboardcode,
character, mod, RETRO_DEVICE_KEYBOARD);
if ((keycode == AKEYCODE_VOLUME_UP || keycode == AKEYCODE_VOLUME_DOWN))
*handled = 0;
}
static INLINE void android_input_poll_event_type_key(
struct android_app *android_app,
AInputEvent *event, int port, int keycode, int source,
int type_event, int *handled)
{
uint8_t *buf;
int action = AKeyEvent_getAction(event);
int keysym = keycode;
bool alias_back_as_x =
(source & AINPUT_SOURCE_GAMEPAD) != AINPUT_SOURCE_GAMEPAD
&& (source & AINPUT_SOURCE_JOYSTICK) != AINPUT_SOURCE_JOYSTICK;
/* android_key_state[] has one row per pad slot plus the
* dedicated keyboard row at ANDROID_KEYBOARD_PORT. */
if (port < 0 || port > ANDROID_KEYBOARD_PORT)
return;
buf = android_key_state[port];
/* Handle 'duplicate' inputs that correspond
* to the same RETROK_* key */
switch (keycode)
{
case AKEYCODE_DPAD_CENTER:
keysym = AKEYCODE_ENTER;
default:
break;
}
/* Rows are MAX_KEYS bytes wide and readers bound their lookups at
* LAST_KEYCODE (android_joypad_button_state). Keycodes arrive
* straight from the platform and are not confined to that range:
* public codes run well past AKEYCODE_ASSIST (AKEYCODE_WAKEUP is
* 224, AKEYCODE_PROFILE_SWITCH 288) and vendor codes are
* unbounded, so an unguarded BIT_SET writes past the row - and
* past the array itself for ANDROID_KEYBOARD_PORT, which is the
* last one - corrupting whatever the linker placed next to it.
* Nothing above LAST_KEYCODE is bindable, so drop it. */
if (keysym >= 0 && keysym < LAST_KEYCODE)
{
/* some controllers send both the up and down events at once
* when the button is released for "special" buttons, like menu buttons
* work around that by only using down events for meta keys (which get
* cleared every poll anyway)
*/
switch (action)
{
case AKEY_EVENT_ACTION_UP:
BIT_CLEAR(buf, keysym);
if (keysym == AKEYCODE_BACK && alias_back_as_x)
{
BIT_CLEAR(buf, AKEYCODE_X); /* alias BACK on remote */
BIT_CLEAR(android_key_state[ANDROID_KEYBOARD_PORT], AKEYCODE_X);
}
break;
case AKEY_EVENT_ACTION_DOWN:
BIT_SET(buf, keysym);
if (keysym == AKEYCODE_BACK && alias_back_as_x)
{
BIT_SET(buf, AKEYCODE_X);
BIT_SET(android_key_state[ANDROID_KEYBOARD_PORT], AKEYCODE_X);
}
break;
}
}
if ((keycode == AKEYCODE_VOLUME_UP || keycode == AKEYCODE_VOLUME_DOWN))
*handled = 0;
}
static int android_input_get_id_port(android_input_t *android, int id,
int source)
{
unsigned i;
int ret = -1;
if (source & (AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_MOUSE |
AINPUT_SOURCE_MOUSE_RELATIVE | AINPUT_SOURCE_TOUCHPAD))
ret = 0; /* touch overlay is always user 1 */
for (i = 0; i < android->pads_connected; i++)
{
if (android->pad_states[i].id == id)
{
ret = i;
break;
}
}
return ret;
}
/* Returns the index inside android->pad_state */
static int android_input_get_id_index_from_name(android_input_t *android,
const char *name)
{
int i;
for (i = 0; i < android->pads_connected; i++)
{
if (string_is_equal(name, android->pad_states[i].name))
return i;
}
return -1;
}
static int android_input_recover_port(android_input_t *android, int id)
{
char device_name[256] = { 0 };
int vendorId = 0;
int productId = 0;
int ret = -1;
settings_t *settings = config_get_ptr();
if (!engine_lookup_name(device_name, &vendorId,
&productId, sizeof(device_name), id))
return -1;
ret = android_input_get_id_index_from_name(android, device_name);
if (ret < 0)
return -1;
if (!settings->bools.android_input_disconnect_workaround)
{
char stale_name[256];
stale_name[0] = '\0';
/* Even without the user-facing disconnect workaround enabled,
* rebind the device to its old port when the previously mapped
* id has verifiably vanished (InputDevice.getDevice() returns
* NULL for it). Android re-enumerates input devices with fresh
* ids across suspend/resume, which would otherwise burn one pad
* slot per wake-up. If the old id still resolves, this is a
* second identical controller and must get its own port. */
if (engine_lookup_name(stale_name, &vendorId, &productId,
sizeof(stale_name), android->pad_states[ret].id))
return -1;
}
android->pad_states[ret].id = id;
/* Keep the frontend id table in sync so rumble keeps
* targeting the right device after the rebind. */
g_android->id[ret] = id;
return ret;
}
static bool is_configured_as_physical_keyboard(int vendor_id, int product_id, const char *device_name)
{
bool is_keyboard;
bool compare_by_id;
int keyboard_vendor_id;
int keyboard_product_id;
char keyboard_name[256];
settings_t *settings = config_get_ptr();
{
const char *str = settings->arrays.input_android_physical_keyboard;
char *end = NULL;
long vid, pid;
vid = strtol(str, &end, 16);
if (end && end != str && *end == ':')
{
const char *pid_start = end + 1;
pid = strtol(pid_start, &end, 16);
if (end && end != pid_start && (*end == ' ' || *end == '\0'))
{
keyboard_vendor_id = (int)vid;
keyboard_product_id = (int)pid;
is_keyboard = (vendor_id == keyboard_vendor_id && product_id == keyboard_product_id);
compare_by_id = true;
}
else
{
strlcpy(keyboard_name, str, sizeof(keyboard_name));
is_keyboard = string_is_equal(device_name, keyboard_name);
compare_by_id = false;
}
}
else
{
strlcpy(keyboard_name, str, sizeof(keyboard_name));
is_keyboard = string_is_equal(device_name, keyboard_name);
compare_by_id = false;
}
}
if (is_keyboard)
{
int i;
/*
* Check that there is not already a similar physical keyboard attached
* attached to the system
*/
for (i = 0; i < kbd_num; i++)
{
char kbd_device_name[256] = { 0 };
int kbd_vendor_id = 0;
int kbd_product_id = 0;
if (!engine_lookup_name(kbd_device_name, &kbd_vendor_id,
&kbd_product_id, sizeof(kbd_device_name), kbd_id[i]))
return false;
if (compare_by_id && vendor_id == kbd_vendor_id && product_id == kbd_product_id)
return false;
if (!compare_by_id && string_is_equal(device_name, kbd_device_name))
return false;
}
return true;
}
return false;
}
static void handle_hotplug(android_input_t *android,
struct android_app *android_app, int *port, int id,
int source)
{
char device_name[256];
char name_buf[256];
int vendorId = 0;
int productId = 0;
const char *device_model = android->device_model;
device_name[0] = name_buf[0] = '\0';
if (!engine_lookup_name(device_name, &vendorId,
&productId, sizeof(device_name), id))
return;
/* FIXME - per-device hacks for NVidia Shield, Xperia Play and
* similar devices
*
* These hacks depend on autoconf, but can work with user
* created autoconfs properly
*/
/* NVIDIA Shield Console
* This is the most complicated example, the built-in controller
* has an extra button that can't be used and a remote.
*
* We map the remote for navigation and overwrite whenever a
* real controller is connected.
* Also group the NVIDIA button on the controller with the
* main controller inputs so it's usable. It's mapped to
* menu by default
*
* The NVIDIA button is identified as "Virtual" device when first
* pressed. CEC remote input is also identified as "Virtual" device.
* If a virtual device is detected before a controller then it will
* be assigned to port 0 as "SHIELD Virtual Controller". When a real
* controller is detected it will overwrite the virtual controller
* and be grouped with the NVIDIA button of the virtual device.
*
*/
if (strstr(device_model, "SHIELD Android TV") && (
strstr(device_name, "Virtual") ||
strstr(device_name, "NVIDIA Corporation NVIDIA Controller v01.0")))
{
/* only use the hack if the device is one of the built-in devices */
RARCH_LOG("[Android] Special device detected: %s.\n", device_model);
{
/* Remove the remote or virtual controller device if it is mapped */
if ( strstr(android->pad_states[0].name, "SHIELD Remote")
|| strstr(android->pad_states[0].name, "SHIELD Virtual Controller"))
{
pad_id1 = -1;
pad_id2 = -1;
android->pads_connected = 0;
*port = 0;
strlcpy(name_buf, device_name, sizeof(name_buf));
}
/* if the actual controller has not been mapped yet,
* then configure Virtual device for now */
if (strstr(device_name, "Virtual") && android->pads_connected==0)
strlcpy_lit(name_buf, "SHIELD Virtual Controller", sizeof(name_buf));
else
strlcpy_lit(name_buf, "NVIDIA SHIELD Controller", sizeof(name_buf));
/* apply the hack only for the first controller
* store the id for later use
*/
if (strstr(device_name, "NVIDIA Corporation NVIDIA Controller v01.0")
&& android->pads_connected==0)
pad_id1 = id;
else if (strstr(device_name, "Virtual") && pad_id1 != -1)
{
id = pad_id1;
return;
}
}
}
else if (strstr(device_model, "SHIELD") && (
strstr(device_name, "Virtual") || strstr(device_name, "gpio") ||
strstr(device_name, "NVIDIA Corporation NVIDIA Controller v01.01") ||
strstr(device_name, "NVIDIA Corporation NVIDIA Controller v01.02")))
{
/* only use the hack if the device is one of the built-in devices */
RARCH_LOG("[Android] Special device detected: %s.\n", device_model);
{
if ( pad_id1 < 0 )
pad_id1 = id;
else
pad_id2 = id;
if ( pad_id2 > 0)
return;
strlcpy_lit(name_buf, "NVIDIA SHIELD Portable", sizeof(name_buf));
}
}
else if (strstr(device_model, "SHIELD") && (
strstr(device_name, "Virtual") || strstr(device_name, "gpio") ||
strstr(device_name, "NVIDIA Corporation NVIDIA Controller v01.03")))
{
/* only use the hack if the device is one of the built-in devices */
RARCH_LOG("[Android] Special device detected: %s.\n", device_model);
{
if (strstr(device_name, "NVIDIA Corporation NVIDIA Controller v01.03")
&& android->pads_connected==0)
pad_id1 = id;
else if (strstr(device_name, "Virtual") || strstr(device_name, "gpio"))
{
id = pad_id1;
return;
}
strlcpy_lit(name_buf, "NVIDIA SHIELD Gamepad", sizeof(name_buf));
}
}
/* Other ATV Devices
* Add other common ATV devices that will follow the Android
* Gaempad convention as "Android Gamepad"
*/
/* to-do: add DS4 on Bravia ATV */
else if (strstr(device_name, "NVIDIA"))
strlcpy_lit(name_buf, "Android Gamepad", sizeof(name_buf));
/* GPD XD
* This is a simple hack, basically groups the "back"
* button with the rest of the gamepad
*/
else if (strstr(device_model, "XD") && (
strstr(device_name, "Virtual") || strstr(device_name, "rk29-keypad") ||
strstr(device_name,"Playstation3") || strstr(device_name,"XBOX")))
{
/* only use the hack if the device is one of the built-in devices */
RARCH_LOG("[Android] Special device detected: %s.\n", device_model);
{
if ( pad_id1 < 0 )
pad_id1 = id;
else
pad_id2 = id;
if ( pad_id2 > 0)
return;
strlcpy_lit(name_buf, "GPD XD", sizeof(name_buf));
*port = 0;
}
}
/* XPERIA Play
* This device is composed of two hid devices
* We make it look like one device
*/
else if (
(
string_starts_with_size(device_model, "R800", STRLEN_CONST("R800")) ||
strstr(device_model, "Xperia Play") ||
strstr(device_model, "Play") ||
strstr(device_model, "SO-01D")
) || (
strstr(device_name, "keypad-game-zeus") ||
strstr(device_name, "keypad-zeus") ||
strstr(device_name, "Android Gamepad")
)
)
{
/* only use the hack if the device is one of the built-in devices */
RARCH_LOG("[Android] Special device detected: %s.\n", device_model);
{
if ( pad_id1 < 0 )
pad_id1 = id;
else
pad_id2 = id;
if ( pad_id2 > 0)
return;
strlcpy_lit(name_buf, "XPERIA Play", sizeof(name_buf));
*port = 0;
}
}
/* ARCHOS Gamepad
* This device is composed of two hid devices
* We make it look like one device
*/
else if (strstr(device_model, "ARCHOS GAMEPAD") && (
strstr(device_name, "joy_key") || strstr(device_name, "joystick")))
{
/* only use the hack if the device is one of the built-in devices */
RARCH_LOG("[Android] ARCHOS GAMEPAD Detected: %s.\n", device_model);
{
if ( pad_id1 < 0 )
pad_id1 = id;
else
pad_id2 = id;
if ( pad_id2 > 0)
return;
strlcpy_lit(name_buf, "ARCHOS GamePad", sizeof(name_buf));
*port = 0;
}
}
/* Amazon Fire TV & Fire stick */
else if (
string_starts_with_size(device_model, "AFT", STRLEN_CONST("AFT")) &&
(
strstr(device_model, "AFTB") ||
strstr(device_model, "AFTT") ||
strstr(device_model, "AFTS") ||
strstr(device_model, "AFTM") ||
strstr(device_model, "AFTRS")
)
)
{
RARCH_LOG("[Android] Special device detected: %s\n", device_model);
{
/* always map remote to port #0 */
if (strstr(device_name, "Amazon Fire TV Remote"))
{
android->pads_connected = 0;
*port = 0;
strlcpy(name_buf, device_name, sizeof(name_buf));
}
/* remove the remote when a gamepad enters */
else if (strstr(android->pad_states[0].name,"Amazon Fire TV Remote"))
{
android->pads_connected = 0;
*port = 0;
strlcpy(name_buf, device_name, sizeof(name_buf));
}
else
strlcpy(name_buf, device_name, sizeof(name_buf));
}
}
/* Other uncommon devices
* These are mostly remote control type devices, bind them always to port 0
* And overwrite the binding whenever a controller button is pressed
*/
else if (strstr(device_name, "Amazon Fire TV Remote")
|| strstr(device_name, "Nexus Remote")
|| strstr(device_name, "SHIELD Remote"))
{
android->pads_connected = 0;
*port = 0;
strlcpy(name_buf, device_name, sizeof(name_buf));
}
else if (strstr(device_name, "iControlPad-"))
strlcpy_lit(name_buf, "iControlPad HID Joystick profile", sizeof(name_buf));
else if (strstr(device_name, "TTT THT Arcade console 2P USB Play"))
{
if (*port == 0)
strlcpy_lit(name_buf, "TTT THT Arcade (User 1)", sizeof(name_buf));
else if (*port == 1)
strlcpy_lit(name_buf, "TTT THT Arcade (User 2)", sizeof(name_buf));
}
else if (strstr(device_name, "MOGA"))
strlcpy_lit(name_buf, "Moga IME", sizeof(name_buf));
/* If device is keyboard only and didn't match any of the devices above
* then assume it is a keyboard, register the id, and return unless the
* maximum number of keyboards are already registered. */
else if ((source == AINPUT_SOURCE_KEYBOARD ||
source == (AINPUT_SOURCE_KEYBOARD | AINPUT_SOURCE_DPAD))
&& kbd_num < MAX_NUM_KEYBOARDS)
{
kbd_id[kbd_num] = id;
kbd_num++;
return;
}
/* If the device is a keyboard, didn't match any of the devices above
* and is designated as the physical keyboard, then assume it is a keyboard,
* register the id, and return unless the
* maximum number of keyboards are already registered. */
else if ((source & AINPUT_SOURCE_KEYBOARD) && kbd_num < MAX_NUM_KEYBOARDS &&
is_configured_as_physical_keyboard(vendorId, productId, device_name))
{
kbd_id[kbd_num] = id;
kbd_num++;
return;
}
/* if device was not keyboard only, yet did not match any of the devices
* then try to autoconfigure as gamepad based on device_name. */
else if (*device_name)
strlcpy(name_buf, device_name, sizeof(name_buf));
if (strstr(android_app->current_ime, "net.obsidianx.android.mogaime"))
strlcpy(name_buf, android_app->current_ime, sizeof(name_buf));
else if (strstr(android_app->current_ime, "com.ccpcreations.android.WiiUseAndroid"))
strlcpy(name_buf, android_app->current_ime, sizeof(name_buf));
else if (strstr(android_app->current_ime, "com.hexad.bluezime"))
strlcpy(name_buf, android_app->current_ime, sizeof(name_buf));
/* All pad slots exhausted: refuse to register the device instead
* of writing out of bounds. Although pad_states[] holds MAX_USERS
* entries, every consumer of the port index on the event path
* (analog_state[], hat_state[], android_key_state[]) is sized
* DEFAULT_MAX_PADS, so any port at or beyond that limit corrupts
* adjacent memory. Slots can realistically run out because Android
* re-enumerates input devices with fresh ids on every
* suspend/resume cycle. */
if (android->pads_connected >= DEFAULT_MAX_PADS)
{
RARCH_ERR("[Android] Input device \"%s\" ignored: all %d pad slots in use.\n",
device_name, DEFAULT_MAX_PADS);
*port = -1;
return;
}
if (*port < 0)
*port = android->pads_connected;
input_autoconfigure_connect(
name_buf,
NULL, NULL,
android_joypad.ident,
*port,
vendorId,
productId);
android->pad_states[android->pads_connected].id =
g_android->id[android->pads_connected] = id;
android->pad_states[android->pads_connected].port = *port;
strlcpy(android->pad_states[*port].name, name_buf,
sizeof(android->pad_states[*port].name));
android->pads_connected++;
}
static int android_input_get_id(AInputEvent *event)
{
int id = AInputEvent_getDeviceId(event);
if (id == pad_id2)
return pad_id1;
return id;
}
struct TOUCHSTATE
{
int down;
int x;
int y;
};
static void engine_handle_touchpad(
struct android_app *android, AInputEvent *event, int port)
{
size_t n;
static struct TOUCHSTATE touchstate[64];
int raw_action = AMotionEvent_getAction(event);
int action = AMOTION_EVENT_ACTION_MASK & raw_action;
size_t ptr_count = AMotionEvent_getPointerCount(event);
int action_id = -1;
/* Every other handler on this path bounds the port it is given.
* This one is in range only because android_input_get_id_port maps
* a touchpad source to port 0 or to a pad slot below
* DEFAULT_MAX_PADS, which is an invariant of a different function. */
if (port < 0 || port >= DEFAULT_MAX_PADS)
return;
/* The index of the pointer that went down or up rides in the action
* word, and AMotionEvent_getPointerId indexes the event's pointer
* array with it without checking it against the count - an index
* past the end returns whatever is behind the array, which then
* indexes touchstate. Nothing in an event that names a pointer it
* does not carry is worth routing. */
if ( action == AMOTION_EVENT_ACTION_POINTER_DOWN
|| action == AMOTION_EVENT_ACTION_POINTER_UP)
{
size_t action_idx = (size_t)
((raw_action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
>> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
if (action_idx >= ptr_count)
return;
action_id = AMotionEvent_getPointerId(event, action_idx);
}
for (n = 0; n < ptr_count; ++n)
{
int pointer_id = (action_id >= 0)
? action_id
: AMotionEvent_getPointerId(event, n);
/* Pointer ids run to 31 on every Android release, so this only
* fires on an id the framework never produces. */
if (pointer_id < 0 || pointer_id >= (int)ARRAY_SIZE(touchstate))
continue;
if ( action == AMOTION_EVENT_ACTION_DOWN
|| action == AMOTION_EVENT_ACTION_POINTER_DOWN )
touchstate[pointer_id].down = 1;
else if (action == AMOTION_EVENT_ACTION_UP
|| action == AMOTION_EVENT_ACTION_POINTER_UP
|| action == AMOTION_EVENT_ACTION_CANCEL )
touchstate[pointer_id].down = 0;
if (touchstate[pointer_id].down)
{
int x = touchstate[pointer_id].x = AMotionEvent_getX(event, n);
int y = touchstate[pointer_id].y = AMotionEvent_getY(event, n);
if (x < 360)
{
android->analog_state[port][0] =
(int16_t)((x / 180.0 - 1.0f) * 32767.0f);
android->analog_state[port][1] =
(int16_t)((y / 180.0 - 1.0f) * 32767.0f);
}
else if (x >= 606)
{
x -= 606;
android->analog_state[port][2] =
(int16_t)((x / 180.0 - 1.0f) * 32767.0f);
android->analog_state[port][3] =
(int16_t)((y / 180.0 - 1.0f) * 32767.0f);
}
}
else
{
if (touchstate[pointer_id].x < 360)
{
android->analog_state[port][0] = 0.0f;
android->analog_state[port][1] = 0.0f;
}
else if (touchstate[pointer_id].x >= 606)
{
android->analog_state[port][2] = 0.0f;
android->analog_state[port][3] = 0.0f;
}
}
}
}
static void android_input_poll_input_gingerbread(
android_input_t *android)
{
AInputEvent *event = NULL;
struct android_app *android_app = (struct android_app*)g_android;
/* The queue is swapped out from under this poll: a destroyed input
* queue arrives as a NULL through APP_CMD_INPUT_CHANGED, and a
* LOOPER_ID_INPUT raised before that command was serviced is still
* delivered afterwards in the same pollOnce batch.
* android_input_discard_events guards the same pointer. */
if (!android_app || !android_app->inputQueue)
return;
/* Read all pending events. */
if (AInputQueue_getEvent(android_app->inputQueue, &event) >= 0)
{
int source, type_event, id, port;
int32_t handled = 0;
if (AInputQueue_preDispatchEvent(android_app->inputQueue, event))
return;
source = AInputEvent_getSource(event);
type_event = AInputEvent_getType(event);
id = android_input_get_id(event);
port = android_input_get_id_port(android, id, source);
if (port < 0 && !android_is_keyboard_id(id))
port = android_input_recover_port(android, id);
if (port < 0 && !android_is_keyboard_id(id))
handle_hotplug(android, android_app,
&port, id, source);
switch (type_event)
{
case AINPUT_EVENT_TYPE_MOTION:
if ((source & AINPUT_SOURCE_TOUCHPAD))
engine_handle_touchpad(android_app, event, port);
/* Only handle events from a touchscreen or mouse */
else if ((source & (AINPUT_SOURCE_TOUCHSCREEN
| AINPUT_SOURCE_STYLUS | AINPUT_SOURCE_MOUSE)))
android_input_poll_event_type_motion(android, event,
port, source);
else
engine_handle_dpad(android_app, event, port, source);
handled = 1;
break;
case AINPUT_EVENT_TYPE_KEY:
{
int keycode = AKeyEvent_getKeyCode(event);
if (!keycode)
break;
if (android_is_keyboard_id(id))
{
android_input_poll_event_type_keyboard(
event, keycode, &handled);
android_input_poll_event_type_key(
android_app, event, ANDROID_KEYBOARD_PORT,
keycode, source, type_event, &handled);
}
else
android_input_poll_event_type_key(android_app,
event, port, keycode, source, type_event, &handled);
}
break;
}
AInputQueue_finishEvent(android_app->inputQueue, event, handled);
}
}
static void android_input_poll_input_default(android_input_t *android)
{
AInputEvent *event = NULL;
struct android_app *android_app = (struct android_app*)g_android;
/* The queue is swapped out from under this poll: a destroyed input
* queue arrives as a NULL through APP_CMD_INPUT_CHANGED, and a
* LOOPER_ID_INPUT raised before that command was serviced is still
* delivered afterwards in the same pollOnce batch.
* android_input_discard_events guards the same pointer. */
if (!android_app || !android_app->inputQueue)
return;
/* Read all pending events. */
while (AInputQueue_hasEvents(android_app->inputQueue))
{
while (AInputQueue_getEvent(android_app->inputQueue, &event) >= 0)
{
int32_t handled;
int source, type_event, id, port;
/* A pre-dispatched event belongs to the IME from here on and
* reappears in the queue if it goes unconsumed, so it can be
* neither read from, routed nor finished once this returns
* non-zero. */
if (AInputQueue_preDispatchEvent(android_app->inputQueue, event))
continue;
handled = 1;
source = AInputEvent_getSource(event);
type_event = AInputEvent_getType(event);
id = android_input_get_id(event);
port = android_input_get_id_port(android, id, source);
if (port < 0 && !android_is_keyboard_id(id))
port = android_input_recover_port(android, id);
if (port < 0 && !android_is_keyboard_id(id))
handle_hotplug(android, android_app,
&port, id, source);
switch (type_event)
{
case AINPUT_EVENT_TYPE_MOTION:
if ((source & AINPUT_SOURCE_TOUCHPAD))
engine_handle_touchpad(android_app, event, port);
/* Only handle events from a touchscreen or mouse */
else if ((source & (AINPUT_SOURCE_TOUCHSCREEN
| AINPUT_SOURCE_MOUSE_RELATIVE
| AINPUT_SOURCE_STYLUS | AINPUT_SOURCE_MOUSE)))
android_input_poll_event_type_motion(android, event,
port, source);
else
engine_handle_dpad(android_app, event, port, source);
break;
case AINPUT_EVENT_TYPE_KEY:
{
int keycode = AKeyEvent_getKeyCode(event);
if (!keycode)
break;
if (android_is_keyboard_id(id))
{
android_input_poll_event_type_keyboard(
event, keycode, &handled);
android_input_poll_event_type_key(
android_app, event, ANDROID_KEYBOARD_PORT,
keycode, source, type_event, &handled);
}
else
android_input_poll_event_type_key(android_app,
event, port, keycode, source, type_event, &handled);
}
break;
}
AInputQueue_finishEvent(android_app->inputQueue, event, handled);
}
}
}
static void android_input_poll_user(android_input_t *android)
{
struct android_app *android_app = (struct android_app*)g_android;
bool poll_accelerometer = false;
bool poll_gyroscope = false;
if (!android_app->sensorEventQueue)
return;
poll_accelerometer = (android_app->sensor_state_mask &
(UINT64_C(1) << RETRO_SENSOR_ACCELEROMETER_ENABLE)) &&
android_app->accelerometerSensor;
poll_gyroscope = (android_app->sensor_state_mask &
(UINT64_C(1) << RETRO_SENSOR_GYROSCOPE_ENABLE)) &&
android_app->gyroscopeSensor;
if (poll_accelerometer || poll_gyroscope)
{
ASensorEvent event;
while (ASensorEventQueue_getEvents(
android_app->sensorEventQueue, &event, 1) > 0)
{
switch (event.type)
{
case ASENSOR_TYPE_ACCELEROMETER:
android->accelerometer_state.x = event.acceleration.x;
android->accelerometer_state.y = event.acceleration.y;
android->accelerometer_state.z = event.acceleration.z;
/* Gravity-based orientation auto-detection: accumulate
* raw accelerometer X/Y over 30 samples, then determine
* which axis gravity is dominant on to detect sensor IC
* mounting orientation. */
if (!android_app->gravity_calibrated)
{
android_app->gravity_accum_x += event.acceleration.x;
android_app->gravity_accum_y += event.acceleration.y;
android_app->gravity_sample_count++;
if (android_app->gravity_sample_count >= 30)
{
float avg_x = android_app->gravity_accum_x / 30.0f;
float avg_y = android_app->gravity_accum_y / 30.0f;
float abs_x = (avg_x < 0.0f) ? -avg_x : avg_x;
float abs_y = (avg_y < 0.0f) ? -avg_y : avg_y;
/* Only commit if device is tilted enough from flat.
* 5.0 m/s² threshold = ~30° from horizontal. */
if (abs_x > 5.0f || abs_y > 5.0f)
{
if (abs_y >= abs_x)
android_app->detected_screen_rotation =
(avg_y > 0.0f) ? 0 : 2;
else
android_app->detected_screen_rotation =
(avg_x < 0.0f) ? 1 : 3;
android_app->gravity_calibrated = true;
/* Now that orientation is known, start rest
* position capture with correct rotation */
input_sensor_start_rest_capture();
}
else
{
/* Device is flat — reset and retry */
android_app->gravity_accum_x = 0.0f;
android_app->gravity_accum_y = 0.0f;
android_app->gravity_sample_count = 0;
}
}
}
break;
case ASENSOR_TYPE_GYROSCOPE:
/* ASensorEvent struct is mysterious - have to
* read the raw 'data' field to get rate of
* rotation... */
android->gyroscope_state.x = event.data[0];
android->gyroscope_state.y = event.data[1];
android->gyroscope_state.z = event.data[2];
break;
default:
break;
}
}
}
}
static void android_input_reinit(void)
{
struct android_app *android_app = (struct android_app*)g_android;
uint32_t runloop_flags = runloop_get_flags();
if (runloop_flags & RUNLOOP_FLAG_PAUSED)
{
/* When the app is paused (e.g. by opening the app switcher), Android may
* destroy the presentation surface while retaining the OpenGL context or
* Vulkan device. Recreate only the surface when supported before falling
* back to a full video driver reinitialization. */
video_driver_state_t *state = video_state_get_ptr();
bool recreated = false;
#ifdef HAVE_THREADS
/* The callback null-checks the hook and reports failure, and the
* dispatch reports failure when the worker is gone, so either one
* falls through to the full reinitialization below. */
recreated = (video_thread_texture_handle(state->context_data,
android_ctx_create_surface_cb) != 0);
#else
if (state->current_video_context.create_surface)
recreated = state->current_video_context.create_surface(
state->context_data);
#endif
if (!recreated)
command_event(CMD_EVENT_REINIT, NULL);
}
android_app_write_cmd(android_app, APP_CMD_REINIT_DONE);
}
/* Handle all events.
*/
static void android_input_poll(void *data)
{
int ident;
struct android_app *android_app = (struct android_app*)g_android;
android_input_t *android = (android_input_t*)data;
settings_t *settings = config_get_ptr();
/* Apply any text staged by the native (IME) keyboard. */
android_keyboard_poll();
while ((ident =
ALooper_pollOnce(settings->uints.input_block_timeout,
NULL, NULL, NULL)) >= 0)
{
switch (ident)
{
case LOOPER_ID_INPUT:
android_input_poll_input(android);
break;
case LOOPER_ID_USER:
android_input_poll_user(android);
break;
case LOOPER_ID_MAIN:
android_input_poll_main_cmd();
break;
}
if (android_app->destroyRequested != 0)
{
retroarch_ctl(RARCH_CTL_SET_SHUTDOWN, NULL);
return;
}
if (android_app->reinitRequested != 0)
{
android_input_reinit();
return;
}
}
}
/* Dismiss queued input from before the input driver exists. ALooper
* reports the queue readable until every event has been finished, so
* leaving them in place spins the caller and holds the window
* unresponsive until the dispatcher raises an ANR. Events are finished
* unhandled: there is nowhere to route them yet, and the framework's
* fallback handling is preferable to dropping them silently. */
static void android_input_discard_events(struct android_app *android_app)
{
AInputEvent *event = NULL;
if (!android_app->inputQueue)
return;
while (AInputQueue_getEvent(android_app->inputQueue, &event) >= 0)
{
if (AInputQueue_preDispatchEvent(android_app->inputQueue, event))
continue;
AInputQueue_finishEvent(android_app->inputQueue, event, 0);
}
}
bool android_run_events(void *data)
{
struct android_app *android_app = (struct android_app*)g_android;
/* LOOPER_ID_USER needs no arm here: the sensor queue is attached by
* the input driver, which does not exist while this pump runs. */
switch (ALooper_pollOnce(-1, NULL, NULL, NULL))
{
case LOOPER_ID_MAIN:
android_input_poll_main_cmd();
break;
case LOOPER_ID_INPUT:
android_input_discard_events(android_app);
break;
default:
break;
}
/* Check if we are exiting. */
if (android_app->destroyRequested != 0)
{
retroarch_ctl(RARCH_CTL_SET_SHUTDOWN, NULL);
return false;
}
if (android_app->reinitRequested != 0)
android_input_reinit();
return true;
}
static int16_t android_input_state(
void *data,
const input_device_driver_t *joypad,
const input_device_driver_t *sec_joypad,
rarch_joypad_info_t *joypad_info,
const retro_keybind_set *binds,
bool keyboard_mapping_blocked,
unsigned port,
unsigned device,
unsigned idx,
unsigned id)
{
android_input_t *android = (android_input_t*)data;
/* The MOUSE/LIGHTGUN/POINTER paths below dereference the driver
* data unconditionally. current_data can be NULL in the window
* around driver teardown/reinit (surface loss on sleep/wake),
* so fail closed rather than fault. */
if (!android)
return 0;
switch (device)
{
case RETRO_DEVICE_JOYPAD:
if (id == RETRO_DEVICE_ID_JOYPAD_MASK)
{
unsigned i;
int16_t ret = 0;
if (!keyboard_mapping_blocked)
{
for (i = 0; i < RARCH_FIRST_CUSTOM_BIND; i++)
{
if (binds[port][i].valid)
{
if ( (binds[port][i].key && binds[port][i].key < RETROK_LAST)
&& ANDROID_KEYBOARD_PORT_INPUT_PRESSED(binds[port], i))
ret |= (1 << i);
}
}
}
return ret;
}
if (id < RARCH_BIND_LIST_END)
{
if (binds[port][id].valid)
{
if ( (binds[port][id].key && binds[port][id].key < RETROK_LAST)
&& ANDROID_KEYBOARD_PORT_INPUT_PRESSED(binds[port], id)
&& (id == RARCH_GAME_FOCUS_TOGGLE || !keyboard_mapping_blocked)
)
return 1;
}
}
break;
case RETRO_DEVICE_ANALOG:
break;
case RETRO_DEVICE_KEYBOARD:
return (id && id < RETROK_LAST) && BIT_GET(android_key_state[ANDROID_KEYBOARD_PORT], rarch_keysym_lut[id]);
case RETRO_DEVICE_MOUSE:
case RARCH_DEVICE_MOUSE_SCREEN:
{
/* Same mouse state is reported for all ports. */
int val = 0;
switch (id)
{
case RETRO_DEVICE_ID_MOUSE_LEFT:
return android->mouse_l || android_check_quick_tap(android);
case RETRO_DEVICE_ID_MOUSE_RIGHT:
return android->mouse_r;
case RETRO_DEVICE_ID_MOUSE_MIDDLE:
return android->mouse_m;
case RETRO_DEVICE_ID_MOUSE_X:
if (device == RARCH_DEVICE_MOUSE_SCREEN)
return android->mouse_x_viewport_screen;
val = android->mouse_x_delta;
android->mouse_x_delta = 0;
/* flush delta after it has been read */
return val;
case RETRO_DEVICE_ID_MOUSE_Y:
if (device == RARCH_DEVICE_MOUSE_SCREEN)
return android->mouse_y_viewport_screen;
val = android->mouse_y_delta;
android->mouse_y_delta = 0;
/* flush delta after it has been read */
return val;
case RETRO_DEVICE_ID_MOUSE_WHEELUP:
val = android->mouse_wu;
android->mouse_wu = 0;
return val;
case RETRO_DEVICE_ID_MOUSE_WHEELDOWN:
val = android->mouse_wd;
android->mouse_wd = 0;
return val;
}
}
break;
case RETRO_DEVICE_LIGHTGUN:
{
/* Same lightgun state is reported for all ports. */
int val = 0;
switch (id)
{
/* Favor mouse for lightgun control. */
case RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X:
if (android->mouse_activated)
return android->mouse_x_viewport_screen;
else if (idx < MAX_TOUCH)
return android->pointer[idx].x;
return 0;
case RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y:
if (android->mouse_activated)
return android->mouse_y_viewport_screen;
else if (idx < MAX_TOUCH)
return android->pointer[idx].y;
return 0;
/* Deprecated relative lightgun. */
case RETRO_DEVICE_ID_LIGHTGUN_X:
val = android->mouse_x_delta;
android->mouse_x_delta = 0;
/* flush delta after it has been read */
return val;
case RETRO_DEVICE_ID_LIGHTGUN_Y:
val = android->mouse_y_delta;
android->mouse_y_delta = 0;
/* flush delta after it has been read */
return val;
case RETRO_DEVICE_ID_LIGHTGUN_CURSOR:
case RETRO_DEVICE_ID_LIGHTGUN_RELOAD:
return android->mouse_m || android->pointer_count == 3;
case RETRO_DEVICE_ID_LIGHTGUN_SELECT:
return android->mouse_r && android->mouse_l;
case RETRO_DEVICE_ID_LIGHTGUN_START:
case RETRO_DEVICE_ID_LIGHTGUN_TURBO:
return android->mouse_r || android->pointer_count == 2;
case RETRO_DEVICE_ID_LIGHTGUN_TRIGGER:
return android->mouse_l || android_check_quick_tap(android) || android->pointer_count == 1;
case RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN:
if (idx >= MAX_TOUCH)
return 0;
return input_driver_pointer_is_offscreen(android->pointer[idx].x, android->pointer[idx].y);
}
}
break;
case RETRO_DEVICE_POINTER:
case RARCH_DEVICE_POINTER_SCREEN:
/* Same pointer state is reported for all ports.
* idx is core/frontend-controlled: bounds-check it before
* any pointer[] access. PRESSED already range-checks via
* pointer_count (<= MAX_TOUCH). */
switch (id)
{
case RETRO_DEVICE_ID_POINTER_X:
if (idx >= MAX_TOUCH)
return 0;
if (device == RARCH_DEVICE_POINTER_SCREEN)
return android->pointer[idx].full_x;
return android->pointer[idx].confined_x;
case RETRO_DEVICE_ID_POINTER_Y:
if (idx >= MAX_TOUCH)
return 0;
if (device == RARCH_DEVICE_POINTER_SCREEN)
return android->pointer[idx].full_y;
return android->pointer[idx].confined_y;
case RETRO_DEVICE_ID_POINTER_PRESSED:
/* On mobile platforms, touches outside screen / core viewport are not reported. */
if (device == RARCH_DEVICE_POINTER_SCREEN)
return (idx < android->pointer_count) &&
(android->pointer[idx].full_x != -0x8000) &&
(android->pointer[idx].full_y != -0x8000);
return (idx < android->pointer_count) &&
(android->pointer[idx].x != -0x8000) &&
(android->pointer[idx].y != -0x8000);
case RETRO_DEVICE_ID_POINTER_IS_OFFSCREEN:
if (idx >= MAX_TOUCH)
return 0;
return input_driver_pointer_is_offscreen(android->pointer[idx].x, android->pointer[idx].y);
case RETRO_DEVICE_ID_POINTER_COUNT:
return android->pointer_count;
case RARCH_DEVICE_ID_POINTER_BACK:
{
const struct retro_keybind *keyptr =
&input_autoconf_binds[0][RARCH_MENU_TOGGLE];
if (keyptr->joykey == 0)
return ANDROID_KEYBOARD_INPUT_PRESSED(AKEYCODE_BACK);
}
}
break;
}
return 0;
}
static void android_input_free_input(void *data)
{
android_input_t *android = (android_input_t*)data;
struct android_app *android_app = (struct android_app*)g_android;
if (!android)
return;
if (android_app->sensorManager &&
android_app->sensorEventQueue)
ASensorManager_destroyEventQueue(android_app->sensorManager,
android_app->sensorEventQueue);
android_app->sensorEventQueue = NULL;
android_app->accelerometerSensor = NULL;
android_app->gyroscopeSensor = NULL;
android_app->sensorManager = NULL;
android_app->sensor_state_mask = 0;
android_app->input_alive = false;
#ifdef HAVE_DYLIB
dylib_close((dylib_t)libandroid_handle);
libandroid_handle = NULL;
#endif
android_keycode_map_free((JNIEnv*)jni_thread_getenv());
android_keyboard_free();
free(data);
}
static uint64_t android_input_get_capabilities(void *data)
{
return
(1 << RETRO_DEVICE_JOYPAD)
| (1 << RETRO_DEVICE_POINTER)
| (1 << RETRO_DEVICE_MOUSE)
| (1 << RETRO_DEVICE_KEYBOARD)
| (1 << RETRO_DEVICE_LIGHTGUN)
| (1 << RETRO_DEVICE_ANALOG);
}
static void android_input_enable_sensor_manager(struct android_app *android_app)
{
if (!android_app->sensorManager)
android_app->sensorManager = ASensorManager_getInstance();
if (android_app->sensorManager)
{
if (!android_app->accelerometerSensor)
android_app->accelerometerSensor =
ASensorManager_getDefaultSensor(android_app->sensorManager,
ASENSOR_TYPE_ACCELEROMETER);
if (!android_app->gyroscopeSensor)
android_app->gyroscopeSensor =
ASensorManager_getDefaultSensor(android_app->sensorManager,
ASENSOR_TYPE_GYROSCOPE);
if (!android_app->sensorEventQueue)
android_app->sensorEventQueue =
ASensorManager_createEventQueue(android_app->sensorManager,
android_app->looper, LOOPER_ID_USER, NULL, NULL);
}
}
/**
* Enable a single sensor, set its event rate, and update the bitmask.
* Returns true on success.
*/
static bool android_enable_sensor(struct android_app *android_app,
const ASensor *sensor, unsigned rate,
uint64_t enable_bit, uint64_t disable_bit)
{
if (!android_app->sensorEventQueue || !sensor)
return false;
if (ASensorEventQueue_enableSensor(
android_app->sensorEventQueue, sensor) >= 0)
{
ASensorEventQueue_setEventRate(android_app->sensorEventQueue,
sensor, (1000L / rate) * 1000);
BIT64_CLEAR(android_app->sensor_state_mask, disable_bit);
BIT64_SET(android_app->sensor_state_mask, enable_bit);
return true;
}
return false;
}
static bool android_input_set_sensor_state(void *data, unsigned port,
enum retro_sensor_action action, unsigned event_rate)
{
if (port == 0)
{
struct android_app *android_app = (struct android_app*)g_android;
android_input_t *android = (android_input_t*)data;
if (!android)
return false;
if (event_rate == 0)
event_rate = DEFAULT_ASENSOR_EVENT_RATE;
switch (action)
{
case RETRO_SENSOR_ACCELEROMETER_ENABLE:
if (!android_app->accelerometerSensor)
android_input_enable_sensor_manager(android_app);
if (android_enable_sensor(android_app,
android_app->accelerometerSensor, event_rate,
RETRO_SENSOR_ACCELEROMETER_ENABLE,
RETRO_SENSOR_ACCELEROMETER_DISABLE))
{
android_app->accelerometer_event_rate = event_rate;
return true;
}
return false;
case RETRO_SENSOR_ACCELEROMETER_DISABLE:
if (android_app->sensorEventQueue &&
android_app->accelerometerSensor)
{
/* Note: Always update state even if disable fails, as we want
* to stop attempting to read from a potentially broken sensor */
ASensorEventQueue_disableSensor(android_app->sensorEventQueue,
android_app->accelerometerSensor);
}
android->accelerometer_state.x = 0.0f;
android->accelerometer_state.y = 0.0f;
android->accelerometer_state.z = 0.0f;
BIT64_CLEAR(android_app->sensor_state_mask, RETRO_SENSOR_ACCELEROMETER_ENABLE);
BIT64_SET(android_app->sensor_state_mask, RETRO_SENSOR_ACCELEROMETER_DISABLE);
return true;
case RETRO_SENSOR_GYROSCOPE_ENABLE:
if (!android_app->gyroscopeSensor)
android_input_enable_sensor_manager(android_app);
if (android_enable_sensor(android_app,
android_app->gyroscopeSensor, event_rate,
RETRO_SENSOR_GYROSCOPE_ENABLE,
RETRO_SENSOR_GYROSCOPE_DISABLE))
{
android_app->gyroscope_event_rate = event_rate;
return true;
}
return false;
case RETRO_SENSOR_GYROSCOPE_DISABLE:
if (android_app->sensorEventQueue &&
android_app->gyroscopeSensor)
{
/* Note: Always update state even if disable fails, as we want
* to stop attempting to read from a potentially broken sensor */
ASensorEventQueue_disableSensor(android_app->sensorEventQueue,
android_app->gyroscopeSensor);
}
android->gyroscope_state.x = 0.0f;
android->gyroscope_state.y = 0.0f;
android->gyroscope_state.z = 0.0f;
BIT64_CLEAR(android_app->sensor_state_mask, RETRO_SENSOR_GYROSCOPE_ENABLE);
BIT64_SET(android_app->sensor_state_mask, RETRO_SENSOR_GYROSCOPE_DISABLE);
return true;
default:
break;
}
}
return false;
}
static float android_input_get_sensor_input(void *data,
unsigned port, unsigned id)
{
if (port == 0)
{
android_input_t *android = (android_input_t*)data;
switch (id)
{
case RETRO_SENSOR_ACCELEROMETER_X:
return android->accelerometer_state.x / 9.80665f;
case RETRO_SENSOR_ACCELEROMETER_Y:
return android->accelerometer_state.y / 9.80665f;
case RETRO_SENSOR_ACCELEROMETER_Z:
return android->accelerometer_state.z / 9.80665f;
case RETRO_SENSOR_GYROSCOPE_X:
return android->gyroscope_state.x;
case RETRO_SENSOR_GYROSCOPE_Y:
return android->gyroscope_state.y;
case RETRO_SENSOR_GYROSCOPE_Z:
return android->gyroscope_state.z;
}
}
return 0.0f;
}
static void android_input_grab_mouse(void *data, bool state)
{
JNIEnv *env = jni_thread_getenv();
if (!env || !g_android)
return;
if (g_android->inputGrabMouse)
CALL_VOID_METHOD_PARAM(env, g_android->activity->clazz,
g_android->inputGrabMouse, state);
}
static void android_input_keypress_vibrate()
{
static const int keyboard_press = 3;
JNIEnv *env = (JNIEnv*)jni_thread_getenv();
if (!env)
return;
CALL_VOID_METHOD_PARAM(env, g_android->activity->clazz,
g_android->doHapticFeedback, (jint)keyboard_press);
}
input_driver_t input_android = {
android_input_init,
android_input_poll,
android_input_state,
android_input_free_input,
android_input_set_sensor_state,
android_input_get_sensor_input,
android_input_get_capabilities,
"android",
android_input_grab_mouse,
NULL,
android_input_keypress_vibrate
};