From 5452999b2a95cc230e734c721759b9101921886f Mon Sep 17 00:00:00 2001 From: Patrick Stankard Date: Tue, 19 Mar 2024 08:33:02 -0400 Subject: [PATCH] Fix mouse grab behavior on Android (#16203) * Add grab_mouse interface for Android Makes mouse grabbing and 'Game Focus' work on Android with a real mouse Properly handle relative mouse motion events on Android (SDK 28 and newer) * Enable workflow_dispatch on CI Android * Update android_mouse_calculate_deltas callsites * Add RETRO_DEVICE_MOUSE to android_input_get_capabilities * Use Handler to trigger UI events (toggle mouse, immersive mode) with 300ms delay * Enable input_auto_mouse_grab by default for Android * Handle RARCH_DEVICE_MOUSE_SCREEN in Android input driver * Add android.hardware.type.pc to manifest * Don't attempt to set pointer speed via scaling in android_mouse_calculate_deltas * Keep x/y values within viewport resolution for screen mouse * Use video_driver_get_size to get width/height --------- Co-authored-by: Bernhard Schelling <14200249+schellingb@users.noreply.github.com> --- .github/workflows/Android.yml | 2 + config.def.h | 8 + configuration.c | 2 +- frontend/drivers/platform_unix.c | 2 + frontend/drivers/platform_unix.h | 1 + input/drivers/android_input.c | 161 ++++++++----- menu/menu_setting.c | 2 +- pkg/android/phoenix/AndroidManifest.xml | 1 + .../retroactivity/RetroActivityFuture.java | 221 +++++++++++++----- 9 files changed, 283 insertions(+), 117 deletions(-) diff --git a/.github/workflows/Android.yml b/.github/workflows/Android.yml index c147eb36be..5ef7e910a4 100644 --- a/.github/workflows/Android.yml +++ b/.github/workflows/Android.yml @@ -3,8 +3,10 @@ name: CI Android on: push: pull_request: + workflow_dispatch: repository_dispatch: types: [run_build] + permissions: contents: read diff --git a/config.def.h b/config.def.h index 3f43614efc..b33a181721 100644 --- a/config.def.h +++ b/config.def.h @@ -1552,6 +1552,14 @@ #define DEFAULT_TURBO_DEFAULT_BTN RETRO_DEVICE_ID_JOYPAD_B #define DEFAULT_ALLOW_TURBO_DPAD false +/* Enable automatic mouse grab by default + * only on Android */ +#if defined(ANDROID) +#define DEFAULT_INPUT_AUTO_MOUSE_GRAB true +#else +#define DEFAULT_INPUT_AUTO_MOUSE_GRAB false +#endif + #if TARGET_OS_IPHONE #define DEFAULT_INPUT_KEYBOARD_GAMEPAD_ENABLE false #else diff --git a/configuration.c b/configuration.c index 425740ee25..618772b8ad 100644 --- a/configuration.c +++ b/configuration.c @@ -2086,7 +2086,7 @@ static struct config_bool_setting *populate_settings_bool( SETTING_BOOL("keyboard_gamepad_enable", &settings->bools.input_keyboard_gamepad_enable, true, DEFAULT_INPUT_KEYBOARD_GAMEPAD_ENABLE, false); SETTING_BOOL("input_autodetect_enable", &settings->bools.input_autodetect_enable, true, DEFAULT_INPUT_AUTODETECT_ENABLE, false); SETTING_BOOL("input_allow_turbo_dpad", &settings->bools.input_allow_turbo_dpad, true, DEFAULT_ALLOW_TURBO_DPAD, false); - SETTING_BOOL("input_auto_mouse_grab", &settings->bools.input_auto_mouse_grab, true, false, false); + SETTING_BOOL("input_auto_mouse_grab", &settings->bools.input_auto_mouse_grab, true, DEFAULT_INPUT_AUTO_MOUSE_GRAB, false); SETTING_BOOL("input_remap_binds_enable", &settings->bools.input_remap_binds_enable, true, true, false); SETTING_BOOL("input_hotkey_device_merge", &settings->bools.input_hotkey_device_merge, true, DEFAULT_INPUT_HOTKEY_DEVICE_MERGE, false); SETTING_BOOL("all_users_control_menu", &settings->bools.input_all_users_control_menu, true, DEFAULT_ALL_USERS_CONTROL_MENU, false); diff --git a/frontend/drivers/platform_unix.c b/frontend/drivers/platform_unix.c index 7206bb0cf7..b153565145 100644 --- a/frontend/drivers/platform_unix.c +++ b/frontend/drivers/platform_unix.c @@ -2090,6 +2090,8 @@ static void frontend_unix_init(void *data) "getVolumeCount", "()I"); GET_METHOD_ID(env, android_app->getVolumePath, class, "getVolumePath", "(Ljava/lang/String;)Ljava/lang/String;"); + GET_METHOD_ID(env, android_app->inputGrabMouse, class, + "inputGrabMouse", "(Z)V"); GET_OBJECT_CLASS(env, class, obj); GET_METHOD_ID(env, android_app->getStringExtra, class, diff --git a/frontend/drivers/platform_unix.h b/frontend/drivers/platform_unix.h index 95b228ebcd..6217b30a19 100644 --- a/frontend/drivers/platform_unix.h +++ b/frontend/drivers/platform_unix.h @@ -178,6 +178,7 @@ struct android_app jmethodID getVolumeCount; jmethodID getVolumePath; + jmethodID inputGrabMouse; struct { diff --git a/input/drivers/android_input.c b/input/drivers/android_input.c index 9a54b9bfa9..944cac5922 100644 --- a/input/drivers/android_input.c +++ b/input/drivers/android_input.c @@ -61,11 +61,17 @@ enum { AMOTION_EVENT_BUTTON_FORWARD = 1 << 4, AMOTION_EVENT_AXIS_VSCROLL = 9, AMOTION_EVENT_ACTION_HOVER_MOVE = 7, - AINPUT_SOURCE_STYLUS = 0x00004002, + AINPUT_SOURCE_STYLUS = 0x00004000 | AINPUT_SOURCE_CLASS_POINTER, AMOTION_EVENT_BUTTON_STYLUS_PRIMARY = 1 << 5, AMOTION_EVENT_BUTTON_STYLUS_SECONDARY = 1 << 6 }; #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 @@ -144,6 +150,7 @@ typedef struct android_input { int64_t quick_tap_time; state_device_t pad_states[MAX_USERS]; /* int alignment */ + int mouse_x, mouse_y; int mouse_x_delta, mouse_y_delta; int mouse_l, mouse_r, mouse_m, mouse_wu, mouse_wd; unsigned pads_connected; @@ -638,53 +645,77 @@ static int android_check_quick_tap(android_input_t *android) } static INLINE void android_mouse_calculate_deltas(android_input_t *android, - AInputEvent *event,size_t motion_ptr) + AInputEvent *event,size_t motion_ptr,int source) { - /* Adjust mouse speed based on ratio - * between core resolution and system resolution */ - float x = 0, y = 0; - float x_scale = 1; - float y_scale = 1; - settings_t *settings = config_get_ptr(); - video_driver_state_t *video_st = video_state_get_ptr(); - struct retro_system_av_info *av_info = &video_st->av_info; + unsigned video_width, video_height; + video_driver_get_size(&video_width, &video_height); - if (av_info) + 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; + + /* 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) { - video_viewport_t *custom_vp = &settings->video_viewport_custom; - const struct retro_game_geometry *geom = (const struct retro_game_geometry*)&av_info->geometry; - x_scale = 2 * (float)geom->base_width / (float)custom_vp->width; - y_scale = 2 * (float)geom->base_height / (float)custom_vp->height; + 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; + } } - /* This axis is only available on Android Nougat and on - * Android devices with NVIDIA extensions */ - if (p_AMotionEvent_getAxisValue) - { - x = AMotionEvent_getAxisValue(event,AMOTION_EVENT_AXIS_RELATIVE_X, - motion_ptr); - y = AMotionEvent_getAxisValue(event,AMOTION_EVENT_AXIS_RELATIVE_Y, - motion_ptr); - } + android->mouse_x_delta = x_delta; + android->mouse_y_delta = y_delta; - /* 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 && !y) - { - x = (AMotionEvent_getX(event, motion_ptr) - android->mouse_x_prev); - y = (AMotionEvent_getY(event, motion_ptr) - android->mouse_y_prev); - android->mouse_x_prev = AMotionEvent_getX(event, motion_ptr); - android->mouse_y_prev = AMotionEvent_getY(event, motion_ptr); - } + if (!x) x = android->mouse_x + android->mouse_x_delta; + if (!y) y = android->mouse_y + android->mouse_y_delta; - android->mouse_x_delta = ceil(x) * x_scale; - android->mouse_y_delta = ceil(y) * y_scale; + /* 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( @@ -697,13 +728,13 @@ static INLINE void android_input_poll_event_type_motion( bool keyup = ( action == AMOTION_EVENT_ACTION_UP || action == AMOTION_EVENT_ACTION_CANCEL - || action == AMOTION_EVENT_ACTION_POINTER_UP) - || (source == AINPUT_SOURCE_MOUSE && - action != AMOTION_EVENT_ACTION_DOWN); + || action == AMOTION_EVENT_ACTION_POINTER_UP); /* If source is mouse then calculate button state - * and mouse deltas and don't process as touchscreen event */ - if (source == AINPUT_SOURCE_MOUSE) + * 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) { /* getButtonState requires API level 14 */ if (p_AMotionEvent_getButtonState) @@ -732,7 +763,7 @@ static INLINE void android_input_poll_event_type_motion( android->mouse_l = 0; } - android_mouse_calculate_deltas(android,event,motion_ptr); + android_mouse_calculate_deltas(android,event,motion_ptr,source); return; } @@ -785,7 +816,7 @@ static INLINE void android_input_poll_event_type_motion( if (( action == AMOTION_EVENT_ACTION_MOVE || action == AMOTION_EVENT_ACTION_HOVER_MOVE) && ENABLE_TOUCH_SCREEN_MOUSE) - android_mouse_calculate_deltas(android,event,motion_ptr); + android_mouse_calculate_deltas(android,event,motion_ptr,source); for (motion_ptr = 0; motion_ptr < pointer_max; motion_ptr++) { @@ -850,7 +881,7 @@ static INLINE void android_input_poll_event_type_motion_stylus( android->mouse_l = 0; } - android_mouse_calculate_deltas(android,event,motion_ptr); + android_mouse_calculate_deltas(android,event,motion_ptr,source); } if (action == AMOTION_EVENT_ACTION_MOVE) { @@ -893,7 +924,7 @@ static INLINE void android_input_poll_event_type_motion_stylus( { android->mouse_l = 0; - android_mouse_calculate_deltas(android,event,motion_ptr); + android_mouse_calculate_deltas(android,event,motion_ptr,source); } // pointer was already released during AMOTION_EVENT_ACTION_HOVER_MOVE @@ -967,7 +998,7 @@ static int android_input_get_id_port(android_input_t *android, int id, unsigned i; int ret = -1; if (source & (AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_MOUSE | - AINPUT_SOURCE_TOUCHPAD)) + AINPUT_SOURCE_MOUSE_RELATIVE | AINPUT_SOURCE_TOUCHPAD)) ret = 0; /* touch overlay is always user 1 */ for (i = 0; i < android->pads_connected; i++) @@ -1565,7 +1596,10 @@ static void android_input_poll_input_default(android_input_t *android) else if ((source & AINPUT_SOURCE_STYLUS) == AINPUT_SOURCE_STYLUS) android_input_poll_event_type_motion_stylus(android, event, port, source); - else if ((source & (AINPUT_SOURCE_TOUCHSCREEN | AINPUT_SOURCE_MOUSE))) + /* Only handle events from a touchscreen or mouse */ + else if ((source & (AINPUT_SOURCE_TOUCHSCREEN + | AINPUT_SOURCE_MOUSE + | AINPUT_SOURCE_MOUSE_RELATIVE))) android_input_poll_event_type_motion(android, event, port, source); else @@ -1774,6 +1808,7 @@ static int16_t android_input_state( 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: { int val = 0; if (port > 0) @@ -1788,11 +1823,17 @@ static int16_t android_input_state( 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; + 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; + val = android->mouse_y_delta; android->mouse_y_delta = 0; /* flush delta after it has been read */ @@ -1907,6 +1948,7 @@ 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); @@ -2056,6 +2098,18 @@ static float android_input_get_sensor_input(void *data, 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; @@ -2077,8 +2131,7 @@ input_driver_t input_android = { android_input_get_sensor_input, android_input_get_capabilities, "android", - - NULL, /* grab_mouse */ + android_input_grab_mouse, NULL, android_input_keypress_vibrate }; diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 2915ae4eb6..dd57f20578 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -15272,7 +15272,7 @@ static bool setting_append_list( &settings->bools.input_auto_mouse_grab, MENU_ENUM_LABEL_INPUT_AUTO_MOUSE_GRAB, MENU_ENUM_LABEL_VALUE_INPUT_AUTO_MOUSE_GRAB, - false, + DEFAULT_INPUT_AUTO_MOUSE_GRAB, MENU_ENUM_LABEL_VALUE_OFF, MENU_ENUM_LABEL_VALUE_ON, &group_info, diff --git a/pkg/android/phoenix/AndroidManifest.xml b/pkg/android/phoenix/AndroidManifest.xml index edc9ca591d..60b9dd13e4 100644 --- a/pkg/android/phoenix/AndroidManifest.xml +++ b/pkg/android/phoenix/AndroidManifest.xml @@ -6,6 +6,7 @@ android:versionName="1.17.0" android:installLocation="internalOnly"> + diff --git a/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java b/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java index c719876625..08d8ef1aa9 100644 --- a/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java +++ b/pkg/android/phoenix/src/com/retroarch/browser/retroactivity/RetroActivityFuture.java @@ -1,12 +1,17 @@ package com.retroarch.browser.retroactivity; import android.util.Log; +import android.view.PointerIcon; import android.view.View; import android.view.WindowManager; import android.content.Intent; import android.content.Context; import android.hardware.input.InputManager; import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import com.retroarch.browser.preferences.util.ConfigFile; import com.retroarch.browser.preferences.util.UserPreferences; import java.lang.reflect.InvocationTargetException; @@ -14,58 +19,69 @@ import java.lang.reflect.Method; public final class RetroActivityFuture extends RetroActivityCamera { - // If set to true then Retroarch will completely exit when it loses focus + // If set to true then RetroArch will completely exit when it loses focus private boolean quitfocus = false; + // Top-level window decor view + private View mDecorView; + + // Constants used for Handler messages + private static final int HANDLER_WHAT_TOGGLE_IMMERSIVE = 1; + private static final int HANDLER_WHAT_TOGGLE_POINTER_CAPTURE = 2; + private static final int HANDLER_WHAT_TOGGLE_POINTER_NVIDIA = 3; + private static final int HANDLER_WHAT_TOGGLE_POINTER_ICON = 4; + private static final int HANDLER_ARG_TRUE = 1; + private static final int HANDLER_ARG_FALSE = 0; + private static final int HANDLER_MESSAGE_DELAY_DEFAULT_MS = 300; + + // Handler used for UI events + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + boolean state = (msg.arg1 == HANDLER_ARG_TRUE) ? true : false; + + if (msg.what == HANDLER_WHAT_TOGGLE_IMMERSIVE) { + attemptToggleImmersiveMode(state); + } else if (msg.what == HANDLER_WHAT_TOGGLE_POINTER_CAPTURE) { + attemptTogglePointerCapture(state); + } else if (msg.what == HANDLER_WHAT_TOGGLE_POINTER_NVIDIA) { + attemptToggleNvidiaCursorVisibility(state); + } else if (msg.what == HANDLER_WHAT_TOGGLE_POINTER_ICON) { + attemptTogglePointerIcon(state); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mDecorView = getWindow().getDecorView(); + + // If QUITFOCUS parameter is provided then enable that Retroarch quits when focus is lost + quitfocus = getIntent().hasExtra("QUITFOCUS"); + } + @Override public void onResume() { super.onResume(); setSustainedPerformanceMode(sustainedPerformanceMode); - if (Build.VERSION.SDK_INT >= 19) { - // Immersive mode + // Check for Android UI specific parameters + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String refresh = getIntent().getStringExtra("REFRESH"); - // Constants from API > 14 - final int API_SYSTEM_UI_FLAG_LAYOUT_STABLE = 0x00000100; - final int API_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 0x00000200; - final int API_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400; - final int API_SYSTEM_UI_FLAG_FULLSCREEN = 0x00000004; - final int API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 0x00001000; - - View thisView = getWindow().getDecorView(); - thisView.setSystemUiVisibility(API_SYSTEM_UI_FLAG_LAYOUT_STABLE - | API_SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | API_SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | API_SYSTEM_UI_FLAG_FULLSCREEN - | API_SYSTEM_UI_FLAG_IMMERSIVE_STICKY); - - // Check for Android UI specific parameters - Intent retro = getIntent(); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - String refresh = retro.getStringExtra("REFRESH"); - - // If REFRESH parameter is provided then try to set refreshrate accordingly - if(refresh != null) { - WindowManager.LayoutParams params = getWindow().getAttributes(); - params.preferredRefreshRate = Integer.parseInt(refresh); - getWindow().setAttributes(params); - } + // If REFRESH parameter is provided then try to set refreshrate accordingly + if (refresh != null) { + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.preferredRefreshRate = Integer.parseInt(refresh); + getWindow().setAttributes(params); } - - // If QUITFOCUS parameter is provided then enable that Retroarch quits when focus is lost - quitfocus = retro.hasExtra("QUITFOCUS"); - - // If HIDEMOUSE parameters is provided then hide the mourse cursor - // This requires NVIDIA Android extensions (available on NVIDIA Shield), if they are not - // available then nothing will be done - if (retro.hasExtra("HIDEMOUSE")) hideMouseCursor(); } - //Checks if Android versions is above 9.0 (28) and enable the screen to write over notch if the user desires - if (Build.VERSION.SDK_INT >= 28) { + // Checks if Android versions is above 9.0 (28) and enable the screen to write over notch if the user desires + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { ConfigFile configFile = new ConfigFile(UserPreferences.getDefaultConfigPath(this)); try { if (configFile.getBoolean("video_notch_write_over_enable")) { @@ -77,31 +93,114 @@ public final class RetroActivityFuture extends RetroActivityCamera { } } - public void hideMouseCursor() { - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - // Check for NVIDIA extensions and minimum SDK version - Method mInputManager_setCursorVisibility; - try { mInputManager_setCursorVisibility = - InputManager.class.getMethod("setCursorVisibility", boolean.class); - } - catch (NoSuchMethodException ex) { - return; // Extensions were not available so do nothing - } - - // Hide the mouse cursor - InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); - try { mInputManager_setCursorVisibility.invoke(inputManager, false); } - catch (InvocationTargetException ite) { } - catch (IllegalAccessException iae) { } - } - } - @Override public void onStop() { super.onStop(); - // If QUITFOCUS parameter was set then completely exit Retroarch when focus is lost + // If QUITFOCUS parameter was set then completely exit RetroArch when focus is lost if (quitfocus) System.exit(0); } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + mHandlerSendUiMessage(HANDLER_WHAT_TOGGLE_IMMERSIVE, hasFocus); + + try { + ConfigFile configFile = new ConfigFile(UserPreferences.getDefaultConfigPath(this)); + if (configFile.getBoolean("input_auto_mouse_grab")) { + inputGrabMouse(hasFocus); + } + } catch (Exception e) { + Log.w("[onWindowFocusChanged] exception thrown:", e.getMessage()); + } + } + + private void mHandlerSendUiMessage(int what, boolean state) { + int arg1 = (state ? HANDLER_ARG_TRUE : HANDLER_ARG_FALSE); + int arg2 = -1; + + Message message = mHandler.obtainMessage(what, arg1, arg2); + mHandler.sendMessageDelayed(message, HANDLER_MESSAGE_DELAY_DEFAULT_MS); + } + + public void inputGrabMouse(boolean state) { + mHandlerSendUiMessage(HANDLER_WHAT_TOGGLE_POINTER_CAPTURE, state); + mHandlerSendUiMessage(HANDLER_WHAT_TOGGLE_POINTER_NVIDIA, state); + mHandlerSendUiMessage(HANDLER_WHAT_TOGGLE_POINTER_ICON, state); + } + + private void attemptToggleImmersiveMode(boolean state) { + // Attempt to toggle "Immersive Mode" for Android 4.4 (19) and up + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + try { + if (state) { + mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_LOW_PROFILE + | View.SYSTEM_UI_FLAG_IMMERSIVE); + } else { + mDecorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + } catch (Exception e) { + Log.w("[attemptToggleImmersiveMode] exception thrown:", e.getMessage()); + } + } + } + + private void attemptTogglePointerCapture(boolean state) { + // Attempt requestPointerCapture for Android 8.0 (26) and up + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + if (state) { + mDecorView.requestPointerCapture(); + } else { + mDecorView.releasePointerCapture(); + } + } catch (Exception e) { + Log.w("[attemptTogglePointerCapture] exception thrown:", e.getMessage()); + } + } + } + + private void attemptToggleNvidiaCursorVisibility(boolean state) { + // Attempt setCursorVisibility for Android 4.1 (16) and up + // only works if NVIDIA Android extensions for NVIDIA Shield are available + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + try { + boolean cursorVisibility = !state; + Method mInputManager_setCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class); + InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); + mInputManager_setCursorVisibility.invoke(inputManager, cursorVisibility); + } catch (NoSuchMethodException e) { + // Extensions were not available so do nothing + } catch (Exception e) { + Log.w("[attemptToggleNvidiaCursorVisibility] exception thrown:", e.getMessage()); + } + } + } + + private void attemptTogglePointerIcon(boolean state) { + // Attempt setPointerIcon for Android 7.x (24, 25) only + // For Android 8.0+, requestPointerCapture is used + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + try { + if (state) { + PointerIcon nullPointerIcon = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL); + mDecorView.setPointerIcon(nullPointerIcon); + } else { + // Restore the pointer icon to it's default value + mDecorView.setPointerIcon(null); + } + } catch (Exception e) { + Log.w("[attemptTogglePointerIcon] exception thrown:", e.getMessage()); + } + } + } }