From b8b18a43cc7a37f24eeef3903afbc261ad99fb3e Mon Sep 17 00:00:00 2001 From: Keropon Date: Sun, 21 Jun 2026 04:26:23 -0400 Subject: [PATCH] android: use the native system keyboard for menu text entry --- config.def.h | 6 + configuration.c | 3 +- configuration.h | 1 + frontend/drivers/platform_unix.c | 4 + frontend/drivers/platform_unix.h | 3 + input/drivers/android_input.c | 225 ++++++++++++++++++ input/input_driver.h | 24 ++ intl/msg_hash_lbl.h | 6 + intl/msg_hash_us.h | 10 + menu/cbs/menu_cbs_sublabel.c | 4 + menu/menu_displaylist.c | 4 + menu/menu_driver.c | 32 +++ menu/menu_setting.c | 55 +++-- msg_hash.h | 1 + msg_hash_lbl_str.h | 1 + .../retroactivity/RetroActivityCommon.java | 126 ++++++++++ 16 files changed, 484 insertions(+), 21 deletions(-) diff --git a/config.def.h b/config.def.h index dec7b7c19b87..33c371b7ddc8 100644 --- a/config.def.h +++ b/config.def.h @@ -1717,6 +1717,12 @@ * sensor input, if supported */ #define DEFAULT_INPUT_SENSORS_ENABLE true +/* Use the Android system (IME) keyboard for menu text entry instead of + * the built-in on-screen keyboard. Off by default so gamepad-only + * and no-touch devices keep the navigable on-screen keyboard at + * all times. */ +#define DEFAULT_INPUT_ANDROID_SYSTEM_KEYBOARD false + /* Automatically enable game focus when running or * resuming content */ #define DEFAULT_INPUT_AUTO_GAME_FOCUS AUTO_GAME_FOCUS_OFF diff --git a/configuration.c b/configuration.c index 5d608f560ec8..267a4072dbf2 100644 --- a/configuration.c +++ b/configuration.c @@ -2549,6 +2549,7 @@ static struct config_bool_setting *populate_settings_bool( SETTING_BOOL("menu_swap_ok_cancel_buttons", &settings->bools.input_menu_swap_ok_cancel_buttons, true, DEFAULT_MENU_SWAP_OK_CANCEL_BUTTONS, false); SETTING_BOOL("menu_swap_scroll_buttons", &settings->bools.input_menu_swap_scroll_buttons, true, DEFAULT_MENU_SWAP_SCROLL_BUTTONS, false); #endif + SETTING_BOOL("input_android_system_keyboard", &settings->bools.input_android_system_keyboard, true, DEFAULT_INPUT_ANDROID_SYSTEM_KEYBOARD, false); @@ -9821,7 +9822,7 @@ bool input_remapping_save_file(const char *path) { if (remap_id == RARCH_UNMAPPED) { - if (!runloop_st->system.input_desc_btn[i][j] + if (!runloop_st->system.input_desc_btn[i][j] || !*runloop_st->system.input_desc_btn[i][j]) config_unset(conf, _ident); else diff --git a/configuration.h b/configuration.h index 3ee0720edb1e..5fb6b51b2eb9 100644 --- a/configuration.h +++ b/configuration.h @@ -623,6 +623,7 @@ typedef struct settings bool input_remap_sort_by_controller_enable; bool input_autodetect_enable; bool input_sensors_enable; + bool input_android_system_keyboard; bool input_overlay_enable; bool input_overlay_enable_autopreferred; bool input_overlay_behind_menu; diff --git a/frontend/drivers/platform_unix.c b/frontend/drivers/platform_unix.c index 8a827a25a76a..ff35a81028a3 100644 --- a/frontend/drivers/platform_unix.c +++ b/frontend/drivers/platform_unix.c @@ -2357,6 +2357,10 @@ static void frontend_unix_init(void *data) "isScreenReaderEnabled", "()Z"); GET_METHOD_ID(env, android_app->accessibilitySpeak, class, "accessibilitySpeak", "(Ljava/lang/String;)V"); + GET_METHOD_ID(env, android_app->showKeyboard, class, + "showKeyboard", "(Ljava/lang/String;Ljava/lang/String;)V"); + GET_METHOD_ID(env, android_app->hideKeyboard, class, + "hideKeyboard", "()V"); CALL_BOOLEAN_METHOD(env, android_app->is_play_store_build, android_app->activity->clazz, android_app->isPlayStoreBuild) diff --git a/frontend/drivers/platform_unix.h b/frontend/drivers/platform_unix.h index fd90fcdc2178..34a67e76348d 100644 --- a/frontend/drivers/platform_unix.h +++ b/frontend/drivers/platform_unix.h @@ -190,6 +190,9 @@ struct android_app jmethodID isScreenReaderEnabled; jmethodID accessibilitySpeak; + jmethodID showKeyboard; + jmethodID hideKeyboard; + struct { unsigned width, height; diff --git a/input/drivers/android_input.c b/input/drivers/android_input.c index 3304ece84ab6..8f39459006d7 100644 --- a/input/drivers/android_input.c +++ b/input/drivers/android_input.c @@ -198,6 +198,228 @@ static typeof(AMotionEvent_getButtonState) *p_AMotionEvent_getButtonState; 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; @@ -1961,6 +2183,9 @@ static void android_input_poll(void *data) 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) diff --git a/input/input_driver.h b/input/input_driver.h index ad5d75053190..6acb2faee6bf 100644 --- a/input/input_driver.h +++ b/input/input_driver.h @@ -1068,6 +1068,30 @@ void input_keyboard_line_append( void input_keyboard_line_clear(input_driver_state_t *input_st); void input_keyboard_line_free(input_driver_state_t *input_st); +#ifdef ANDROID +/** + * android_keyboard_start: + * @buffer_ptr : Pointer to the keyboard line buffer. + * @size_ptr : Pointer to the keyboard line size. + * @ptr_ptr : Pointer to the keyboard line cursor. + * @label : Hint shown on the keyboard, or NULL. + * @cb : Line complete callback function. + * @userdata : Userdata passed to the callback. + * + * Raises the native Android (IME) keyboard for menu text entry, made to + * mirror the iOS ios_keyboard_* hooks. Swaps the custom on-screen keyboard + * for the system soft keyboard, enabling paste and password managers. + * Implemented in input/drivers/android_input.c. + * + * Returns: true if the keyboard was shown. + **/ +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); +bool android_keyboard_active(void); +void android_keyboard_end(void); +#endif + /** * input_keyboard_start_line: * @userdata : Userdata. diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index b305ab2a4680..6aa1205c5008 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -1372,6 +1372,12 @@ MSG_HASH( MENU_ENUM_LABEL_INPUT_SELECT_PHYSICAL_KEYBOARD_STR ) #endif +#ifdef ANDROID +MSG_HASH( + MENU_ENUM_LABEL_INPUT_ANDROID_SYSTEM_KEYBOARD, + MENU_ENUM_LABEL_INPUT_ANDROID_SYSTEM_KEYBOARD_STR + ) +#endif MSG_HASH( MENU_ENUM_LABEL_INPUT_SENSOR_ORIENTATION, MENU_ENUM_LABEL_INPUT_SENSOR_ORIENTATION_STR diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 6a8cc47450c8..e2c2eb69bd9f 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -10727,6 +10727,16 @@ MSG_HASH(MENU_ENUM_LABEL_VALUE_##T, us) #undef S_UINT_AT_EX_H #undef S_UINT_AT_EX_NS_H #undef SETTINGS_DEF_STRINGS_PASS +#ifdef ANDROID +MSG_HASH( + MENU_ENUM_LABEL_VALUE_INPUT_ANDROID_SYSTEM_KEYBOARD, + "Use System Keyboard" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_INPUT_ANDROID_SYSTEM_KEYBOARD, + "Use the Android system keyboard for menu text entry instead of the built-in on-screen keyboard. Enables clipboard paste and password managers. Requires a touchscreen or an input method that can be navigated with a gamepad." + ) +#endif MSG_HASH( MENU_ENUM_LABEL_VALUE_INPUT_AUTO_GAME_FOCUS_OFF, "OFF" diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 5f63f8a71e52..550cfc2deb4a 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -514,6 +514,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_nowinkey_enable, MENU_ #ifdef ANDROID DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_select_physical_keyboard, MENU_ENUM_SUBLABEL_INPUT_SELECT_PHYSICAL_KEYBOARD) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_android_input_disconnect_workaround, MENU_ENUM_SUBLABEL_ANDROID_INPUT_DISCONNECT_WORKAROUND) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_input_android_system_keyboard, MENU_ENUM_SUBLABEL_INPUT_ANDROID_SYSTEM_KEYBOARD) #endif #if defined(HAVE_MATERIALUI) || defined(HAVE_XMB) || defined(HAVE_OZONE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_menu_screensaver_animation, MENU_ENUM_SUBLABEL_MENU_SCREENSAVER_ANIMATION) @@ -3054,6 +3055,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_ANDROID_INPUT_DISCONNECT_WORKAROUND: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_android_input_disconnect_workaround); break; + case MENU_ENUM_LABEL_INPUT_ANDROID_SYSTEM_KEYBOARD: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_input_android_system_keyboard); + break; #endif case MENU_ENUM_LABEL_CORE_CHEAT_OPTIONS: #ifdef HAVE_CHEATS diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index d041e0dedf47..421ead2533f4 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -8630,6 +8630,10 @@ unsigned menu_displaylist_build_list( MENU_ENUM_LABEL_INPUT_SELECT_PHYSICAL_KEYBOARD, PARSE_ACTION, true) == 0) count++; + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_INPUT_ANDROID_SYSTEM_KEYBOARD, + PARSE_ONLY_BOOL, true) == 0) + count++; #endif #ifdef HAVE_LIBNX diff --git a/menu/menu_driver.c b/menu/menu_driver.c index 21856f5e017c..676354d9bde6 100644 --- a/menu/menu_driver.c +++ b/menu/menu_driver.c @@ -4529,6 +4529,11 @@ void menu_input_dialog_end(void) if (ios_keyboard_active()) ios_keyboard_end(); #endif +#ifdef ANDROID + /* Dismiss the Android system keyboard if it's currently open */ + if (android_keyboard_active()) + android_keyboard_end(); +#endif } #if defined(_MSC_VER) @@ -8255,6 +8260,17 @@ bool menu_input_dialog_start_search(void) menu_input_search_cb, menu); #endif +#ifdef ANDROID + /* Use the Android system keyboard instead of the custom on-screen one */ + if (config_get_ptr()->bools.input_android_system_keyboard) + android_keyboard_start( + (char **)menu_st->input_dialog_keyboard_buffer, + &input_st->keyboard_line.size, + &input_st->keyboard_line.ptr, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SEARCH), + menu_input_search_cb, + menu); +#endif /* While reading keyboard line input, we have to block all hotkeys. */ input_st->flags |= INP_FLAG_KB_MAPPING_BLOCKED; @@ -8282,6 +8298,11 @@ bool menu_input_dialog_start(menu_input_ctx_line_t *line) if (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY) return false; #endif +#ifdef ANDROID + if ( config_get_ptr()->bools.input_android_system_keyboard + && (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY)) + return false; +#endif #ifdef HAVE_MIST steam_open_osk(); @@ -8328,6 +8349,17 @@ bool menu_input_dialog_start(menu_input_ctx_line_t *line) line->cb, menu); #endif +#ifdef ANDROID + /* Use the Android system keyboard instead of the custom on-screen one */ + if (config_get_ptr()->bools.input_android_system_keyboard) + android_keyboard_start( + (char **)menu_st->input_dialog_keyboard_buffer, + &input_st->keyboard_line.size, + &input_st->keyboard_line.ptr, + line->label, + line->cb, + menu); +#endif /* While reading keyboard line input, we have to block all hotkeys. */ input_st->flags |= INP_FLAG_KB_MAPPING_BLOCKED; diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 85fc90b23b2a..32f58a38dc2a 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -12983,7 +12983,7 @@ static void settings_build_drivers( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + unsigned i, j = 0; struct string_options_entry string_options_entries[14] = {{0}}; @@ -13171,7 +13171,7 @@ static void settings_build_drivers( } GROUP_END(); - + } } @@ -13186,7 +13186,7 @@ static void settings_build_core( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + unsigned i, listing = 0; #ifndef HAVE_DYNAMIC struct bool_entry bool_entries[11]; @@ -13312,7 +13312,7 @@ static void settings_build_core( } GROUP_END(); - + } } @@ -13327,7 +13327,7 @@ static void settings_build_configuration( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + uint8_t i, listing = 0; struct bool_entry bool_entries[10]; START_GROUP(list, list_info, &group_info, @@ -13440,7 +13440,7 @@ static void settings_build_configuration( ADD_DESC(configuration_desc_0); GROUP_END(); - + } } @@ -13455,7 +13455,7 @@ static void settings_build_logging( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + bool *tmp_b = NULL; START_GROUP(list, list_info, &group_info, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_LOGGING_SETTINGS), parent_group); parent_group = MENU_ENUM_LABEL_LOGGING_SETTINGS_STR; @@ -13510,7 +13510,7 @@ ADD_DESC(logging_desc_0); general_write_handler, general_read_handler, SD_FLAG_ADVANCED); - + GROUP_END(); } } @@ -13526,7 +13526,7 @@ static void settings_build_saving( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + uint8_t i, listing = 0; struct bool_entry bool_entries[12]; @@ -13652,7 +13652,7 @@ static void settings_build_saving( ADD_DESC(saving2_desc_0); GROUP_END(); - + } } @@ -14260,7 +14260,7 @@ static void settings_build_video( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + START_GROUP(list, list_info, &group_info, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_SETTINGS), parent_group); @@ -14625,7 +14625,7 @@ static void settings_build_video( ADD_DESC(video_filter_desc); GROUP_END(); - + } } @@ -14818,7 +14818,7 @@ static void settings_build_input( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + START_GROUP(list, list_info, &group_info, MENU_ENUM_LABEL_INPUT_SETTINGS_BEGIN_STR, @@ -14879,6 +14879,21 @@ static void settings_build_input( (*list)[list_info->index - 1].default_value.string = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NONE); } } + + CONFIG_BOOL( + list, list_info, + &settings->bools.input_android_system_keyboard, + MENU_ENUM_LABEL_INPUT_ANDROID_SYSTEM_KEYBOARD, + MENU_ENUM_LABEL_VALUE_INPUT_ANDROID_SYSTEM_KEYBOARD, + DEFAULT_INPUT_ANDROID_SYSTEM_KEYBOARD, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE); #endif ADD_DESC(inp_desc_10); @@ -14955,7 +14970,7 @@ static void settings_build_input( } GROUP_END(); - + } } @@ -15054,7 +15069,7 @@ static void settings_build_input_hotkey( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + unsigned i; START_GROUP(list, list_info, &group_info, MENU_ENUM_LABEL_INPUT_HOTKEY_BINDS_BEGIN_STR, @@ -15088,7 +15103,7 @@ static void settings_build_input_hotkey( } GROUP_END(); - + } } @@ -16438,7 +16453,7 @@ static void settings_build_lakka_services( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + #if defined(HAVE_LAKKA) START_GROUP(list, list_info, &group_info, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_LAKKA_SERVICES), @@ -16554,7 +16569,7 @@ static void settings_build_lakka_services( GROUP_END(); #endif - + } } @@ -16570,7 +16585,7 @@ static void settings_build_lakka_switch_options( subgroup_info.name = NULL; (void)settings; (void)global; (void)group_info; (void)subgroup_info; { - + START_GROUP(list, list_info, &group_info, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_LAKKA_SWITCH_OPTIONS), parent_group); @@ -16629,7 +16644,7 @@ static void settings_build_lakka_switch_options( SD_FLAG_NONE); SETTINGS_ACTION_SET(change, &(*list)[list_info->index - 1], bluetooth_ertm_disable_toggle_change_handler) GROUP_END(); - + } } #endif diff --git a/msg_hash.h b/msg_hash.h index 27fb22867220..48e1b8c26b3b 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -3231,6 +3231,7 @@ enum msg_hash_enums #undef SETTINGS_DEF_ENUM_PASS #ifdef ANDROID MENU_LBL_H(INPUT_SELECT_PHYSICAL_KEYBOARD), + MENU_LABEL(INPUT_ANDROID_SYSTEM_KEYBOARD), #endif /* GENERATED REGION: auto mouse grab setting enum rows (see settings/settings_def_input_auto_mouse_grab.h). */ diff --git a/msg_hash_lbl_str.h b/msg_hash_lbl_str.h index 057e9b8baacc..250db5eeb8f2 100644 --- a/msg_hash_lbl_str.h +++ b/msg_hash_lbl_str.h @@ -348,6 +348,7 @@ #define MENU_ENUM_LABEL_INFORMATION_STR "information" #define MENU_ENUM_LABEL_INFO_SCREEN_STR "info_screen" #define MENU_ENUM_LABEL_INPUT_SELECT_PHYSICAL_KEYBOARD_STR "input_android_physical_keyboard" +#define MENU_ENUM_LABEL_INPUT_ANDROID_SYSTEM_KEYBOARD_STR "input_android_system_keyboard" #define MENU_ENUM_LABEL_INPUT_SENSOR_ORIENTATION_STR "input_sensor_orientation" #define MENU_ENUM_LABEL_INPUT_DESCRIPTOR_HIDE_UNBOUND_STR "input_descriptor_hide_unbound" #define MENU_ENUM_LABEL_INPUT_DESCRIPTOR_LABEL_SHOW_STR "input_descriptor_label_show" diff --git a/pkg/android/phoenix-common/src/com/retroarch/browser/retroactivity/RetroActivityCommon.java b/pkg/android/phoenix-common/src/com/retroarch/browser/retroactivity/RetroActivityCommon.java index ba592d12c409..4d820e2c9b13 100644 --- a/pkg/android/phoenix-common/src/com/retroarch/browser/retroactivity/RetroActivityCommon.java +++ b/pkg/android/phoenix-common/src/com/retroarch/browser/retroactivity/RetroActivityCommon.java @@ -43,6 +43,11 @@ import android.view.InputDevice; import android.view.Surface; import android.view.WindowManager; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import android.app.UiModeManager; import android.os.BatteryManager; import android.os.Build; @@ -53,6 +58,10 @@ import android.os.VibrationEffect; import android.os.VibratorManager; import android.util.Log; +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.EditText; +import android.widget.TextView; import java.io.File; @@ -1014,7 +1023,116 @@ public void deleteCore(String coreName) { PlayCoreManager.getInstance().deleteCore(coreName); } + /////////////// System (IME) keyboard /////////////// + /* A near-invisible EditText that proxies the system soft keyboard for the + * menu's text entry, so users get clipboard paste and password managers + * (which the built-in on-screen keyboard cannot offer). Committed/pasted + * text is forwarded to native via onSystemKeyboardInput(). */ + private EditText keyboardEditText; + private boolean keyboardActive; + private boolean keyboardSuppressWatcher; + + /** + * Raises the native system keyboard for menu text entry. + * + * Called from native code (the menu on-screen keyboard path). Runs the + * UI work on the UI thread. + * + * @param label Hint shown in the keyboard field, or null. + * @param initialText Text to pre-fill the field with, or null. + */ + public void showKeyboard(final String label, final String initialText) { + runOnUiThread(new Runnable() { + @Override public void run() { + if (keyboardEditText == null) { + keyboardEditText = new EditText(RetroActivityCommon.this) { + @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { + /* BACK while the keyboard is up = dismiss without confirming. */ + if (keyCode == KeyEvent.KEYCODE_BACK + && event.getAction() == KeyEvent.ACTION_UP + && keyboardActive) { + hideKeyboard(); + onSystemKeyboardInput(null, true); + return true; + } + return super.onKeyPreIme(keyCode, event); + } + }; + keyboardEditText.setSingleLine(true); + keyboardEditText.setImeOptions(EditorInfo.IME_ACTION_DONE + | EditorInfo.IME_FLAG_NO_EXTRACT_UI + | EditorInfo.IME_FLAG_NO_FULLSCREEN); + keyboardEditText.setAlpha(0.0f); + + keyboardEditText.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int a, int b, int c) {} + @Override public void onTextChanged(CharSequence s, int a, int b, int c) {} + @Override public void afterTextChanged(Editable s) { + if (keyboardActive && !keyboardSuppressWatcher) + onSystemKeyboardInput(s.toString(), false); + } + }); + + keyboardEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_GO + || actionId == EditorInfo.IME_ACTION_SEARCH + || actionId == EditorInfo.IME_ACTION_NEXT + || (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) { + String text = v.getText().toString(); + hideKeyboard(); + onSystemKeyboardInput(text, true); + return true; + } + return false; + } + }); + + addContentView(keyboardEditText, new ViewGroup.LayoutParams(1, 1)); + } + + keyboardActive = true; + keyboardSuppressWatcher = true; + keyboardEditText.setText(initialText != null ? initialText : ""); + keyboardEditText.setSelection(keyboardEditText.getText().length()); + keyboardSuppressWatcher = false; + if (label != null) + keyboardEditText.setHint(label); + keyboardEditText.setVisibility(View.VISIBLE); + keyboardEditText.setFocusable(true); + keyboardEditText.setFocusableInTouchMode(true); + keyboardEditText.requestFocus(); + + InputMethodManager imm = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) + imm.showSoftInput(keyboardEditText, InputMethodManager.SHOW_FORCED); + } + }); + } + + /** + * Dismisses the system keyboard. + * + * Called from native code and after the user confirms or cancels. + */ + public void hideKeyboard() { + runOnUiThread(new Runnable() { + @Override public void run() { + if (keyboardEditText == null) + return; + keyboardActive = false; + InputMethodManager imm = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) + imm.hideSoftInputFromWindow(keyboardEditText.getWindowToken(), 0); + keyboardEditText.clearFocus(); + keyboardEditText.setVisibility(View.GONE); + } + }); + } /////////////// JNI methods /////////////// @@ -1044,6 +1162,14 @@ public void deleteCore(String coreName) { */ public native void safTreeAdded(String tree); + /** + * Forwards system-keyboard text to native menu input. + * + * @param text Current text, or null to cancel (keyboard dismissed). + * @param finished true when the user confirmed (Done/Enter) or cancelled. + */ + public native void onSystemKeyboardInput(String text, boolean finished); + /////////////// Private methods ///////////////