From d5ebfc8e211c0c72a130079621f3e98532ef7f68 Mon Sep 17 00:00:00 2001 From: bunnei Date: Sat, 25 Mar 2023 00:28:45 -0700 Subject: [PATCH] android: Implement basic software keyboard applet. --- .../java/org/yuzu/yuzu_emu/NativeLibrary.java | 12 + .../yuzu_emu/activities/EmulationActivity.kt | 25 ++ .../yuzu_emu/applets/SoftwareKeyboard.java | 252 ++++++++-------- src/android/app/src/main/jni/CMakeLists.txt | 4 + .../jni/android_common/android_common.cpp | 35 +++ .../main/jni/android_common/android_common.h | 12 + .../main/jni/applets/software_keyboard.cpp | 277 ++++++++++++++++++ .../src/main/jni/applets/software_keyboard.h | 78 +++++ src/android/app/src/main/jni/id_cache.cpp | 7 + src/android/app/src/main/jni/native.cpp | 62 ++-- src/android/app/src/main/jni/native.h | 6 + .../app/src/main/res/values/strings.xml | 5 - 12 files changed, 624 insertions(+), 151 deletions(-) create mode 100644 src/android/app/src/main/jni/android_common/android_common.cpp create mode 100644 src/android/app/src/main/jni/android_common/android_common.h create mode 100644 src/android/app/src/main/jni/applets/software_keyboard.cpp create mode 100644 src/android/app/src/main/jni/applets/software_keyboard.h diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java index c7c616a50..c056b7d6d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java @@ -632,6 +632,18 @@ public final class NativeLibrary { */ public static native void LogDeviceInfo(); + /** + * Submits inline keyboard text. Called on input for buttons that result text. + * @param text Text to submit to the inline software keyboard implementation. + */ + public static native void SubmitInlineKeyboardText(String text); + + /** + * Submits inline keyboard input. Used to indicate keys pressed that are not text. + * @param key_code Android Key Code associated with the keyboard input. + */ + public static native void SubmitInlineKeyboardInput(int key_code); + /** * Button type for use in onTouchEvent */ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 2fd0d38fa..8304c2aa5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -8,8 +8,10 @@ import android.content.DialogInterface import android.content.Intent import android.graphics.Rect import android.os.Bundle +import android.view.KeyEvent import android.view.View import android.view.WindowManager +import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager @@ -80,6 +82,29 @@ open class EmulationActivity : AppCompatActivity() { //startForegroundService(foregroundService); } + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (event.action == android.view.KeyEvent.ACTION_DOWN) { + if (keyCode == android.view.KeyEvent.KEYCODE_ENTER) { + // Special case, we do not support multiline input, dismiss the keyboard. + val overlayView: View = + this.findViewById(R.id.surface_input_overlay) + val im = + overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + im.hideSoftInputFromWindow(overlayView.windowToken, 0); + } else { + val textChar = event.getUnicodeChar(); + if (textChar == 0) { + // No text, button input. + NativeLibrary.SubmitInlineKeyboardInput(keyCode); + } else { + // Text submitted. + NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString()); + } + } + } + return super.onKeyDown(keyCode, event) + } + override fun onSaveInstanceState(outState: Bundle) { outState.putParcelable(EXTRA_SELECTED_GAME, game) super.onSaveInstanceState(outState) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java index 894da8801..8ad4b1e22 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java @@ -1,22 +1,28 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later package org.yuzu.yuzu_emu.applets; import android.app.Activity; import android.app.Dialog; +import android.content.Context; import android.content.DialogInterface; +import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; import android.text.InputFilter; -import android.text.Spanned; +import android.text.InputType; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.view.ViewCompat; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -25,72 +31,66 @@ import org.yuzu.yuzu_emu.YuzuApplication; import org.yuzu.yuzu_emu.NativeLibrary; import org.yuzu.yuzu_emu.R; import org.yuzu.yuzu_emu.activities.EmulationActivity; -import org.yuzu.yuzu_emu.utils.Log; import java.util.Objects; public final class SoftwareKeyboard { - /// Corresponds to Frontend::ButtonConfig - private interface ButtonConfig { - int Single = 0; /// Ok button - int Dual = 1; /// Cancel | Ok buttons - int Triple = 2; /// Cancel | I Forgot | Ok buttons - int None = 3; /// No button (returned by swkbdInputText in special cases) - } + /// Corresponds to Service::AM::Applets::SwkbdType + private interface SwkbdType { + int Normal = 0; + int NumberPad = 1; + int Qwerty = 2; + int Unknown3 = 3; + int Latin = 4; + int SimplifiedChinese = 5; + int TraditionalChinese = 6; + int Korean = 7; + }; - /// Corresponds to Frontend::ValidationError - public enum ValidationError { - None, - // Button Selection - ButtonOutOfRange, - // Configured Filters - MaxDigitsExceeded, - AtSignNotAllowed, - PercentNotAllowed, - BackslashNotAllowed, - ProfanityNotAllowed, - CallbackFailed, - // Allowed Input Type - FixedLengthRequired, - MaxLengthExceeded, - BlankInputNotAllowed, - EmptyInputNotAllowed, - } + /// Corresponds to Service::AM::Applets::SwkbdPasswordMode + private interface SwkbdPasswordMode { + int Disabled = 0; + int Enabled = 1; + }; + + /// Corresponds to Service::AM::Applets::SwkbdResult + private interface SwkbdResult { + int Ok = 0; + int Cancel = 1; + }; public static class KeyboardConfig implements java.io.Serializable { - public int button_config; + public String ok_text; + public String header_text; + public String sub_text; + public String guide_text; + public String initial_text; + public short left_optional_symbol_key; + public short right_optional_symbol_key; public int max_text_length; - public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input - public String hint_text; /// Displayed in the field as a hint before - @Nullable - public String[] button_text; /// Contains the button text that the caller provides + public int min_text_length; + public int initial_cursor_position; + public int type; + public int password_mode; + public int text_draw_type; + public int key_disable_flags; + public boolean use_blur_background; + public boolean enable_backspace_button; + public boolean enable_return_button; + public boolean disable_cancel_button; } /// Corresponds to Frontend::KeyboardData public static class KeyboardData { - public int button; + public int result; public String text; - private KeyboardData(int button, String text) { - this.button = button; + private KeyboardData(int result, String text) { + this.result = result; this.text = text; } } - private static class Filter implements InputFilter { - @Override - public CharSequence filter(CharSequence source, int start, int end, Spanned dest, - int dstart, int dend) { - String text = new StringBuilder(dest) - .replace(dstart, dend, source.subSequence(start, end).toString()) - .toString(); - if (ValidateFilters(text) == ValidationError.None) { - return null; // Accept replacement - } - return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged - } - } - public static class KeyboardDialogFragment extends DialogFragment { static KeyboardDialogFragment newInstance(KeyboardConfig config) { KeyboardDialogFragment frag = new KeyboardDialogFragment(); @@ -113,60 +113,65 @@ public final class SoftwareKeyboard { R.dimen.dialog_margin); KeyboardConfig config = Objects.requireNonNull( - (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); + (KeyboardConfig) requireArguments().getSerializable("config")); // Set up the input EditText editText = new EditText(YuzuApplication.getAppContext()); - editText.setHint(config.hint_text); - editText.setSingleLine(!config.multiline_mode); + editText.setHint(config.initial_text); + editText.setSingleLine(!config.enable_return_button); editText.setLayoutParams(params); - editText.setFilters(new InputFilter[]{ - new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); + editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(config.max_text_length)}); + + // Handle input type + int input_type = 0; + switch (config.type) + { + case SwkbdType.Normal: + case SwkbdType.Qwerty: + case SwkbdType.Unknown3: + case SwkbdType.Latin: + case SwkbdType.SimplifiedChinese: + case SwkbdType.TraditionalChinese: + case SwkbdType.Korean: + default: + input_type = InputType.TYPE_CLASS_TEXT; + if (config.password_mode == SwkbdPasswordMode.Enabled) + { + input_type |= InputType.TYPE_TEXT_VARIATION_PASSWORD; + } + break; + case SwkbdType.NumberPad: + input_type = InputType.TYPE_CLASS_NUMBER; + if (config.password_mode == SwkbdPasswordMode.Enabled) + { + input_type |= InputType.TYPE_NUMBER_VARIATION_PASSWORD; + } + break; + } + + // Apply input type + editText.setInputType(input_type); FrameLayout container = new FrameLayout(emulationActivity); container.addView(editText); + String headerText = config.header_text.isEmpty() ? emulationActivity.getString(R.string.software_keyboard) : config.header_text; + String okText = config.header_text.isEmpty() ? emulationActivity.getString(android.R.string.ok) : config.ok_text; + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity) - .setTitle(R.string.software_keyboard) + .setTitle(headerText) .setView(container); setCancelable(false); - switch (config.button_config) { - case ButtonConfig.Triple: { - final String text = config.button_text[1].isEmpty() - ? emulationActivity.getString(R.string.i_forgot) - : config.button_text[1]; - builder.setNeutralButton(text, null); - } - // fallthrough - case ButtonConfig.Dual: { - final String text = config.button_text[0].isEmpty() - ? emulationActivity.getString(android.R.string.cancel) - : config.button_text[0]; - builder.setNegativeButton(text, null); - } - // fallthrough - case ButtonConfig.Single: { - final String text = config.button_text[2].isEmpty() - ? emulationActivity.getString(android.R.string.ok) - : config.button_text[2]; - builder.setPositiveButton(text, null); - break; - } - } + builder.setPositiveButton(okText, null); + builder.setNegativeButton(emulationActivity.getString(android.R.string.cancel), null); final AlertDialog dialog = builder.create(); dialog.create(); if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { - data.button = config.button_config; + data.result = SwkbdResult.Ok; data.text = editText.getText().toString(); - final ValidationError error = ValidateInput(data.text); - if (error != ValidationError.None) { - HandleValidationError(config, error); - return; - } - dialog.dismiss(); synchronized (finishLock) { @@ -176,7 +181,7 @@ public final class SoftwareKeyboard { } if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { - data.button = 1; + data.result = SwkbdResult.Ok; dialog.dismiss(); synchronized (finishLock) { finishLock.notifyAll(); @@ -185,7 +190,7 @@ public final class SoftwareKeyboard { } if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { - data.button = 0; + data.result = SwkbdResult.Cancel; dialog.dismiss(); synchronized (finishLock) { finishLock.notifyAll(); @@ -200,49 +205,42 @@ public final class SoftwareKeyboard { private static KeyboardData data; private static final Object finishLock = new Object(); - private static void ExecuteImpl(KeyboardConfig config) { + private static void ExecuteNormalImpl(KeyboardConfig config) { final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - data = new KeyboardData(0, ""); + data = new KeyboardData(SwkbdResult.Cancel, ""); KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); } - private static void HandleValidationError(KeyboardConfig config, ValidationError error) { + private static void ExecuteInlineImpl(KeyboardConfig config) { final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - String message = ""; - switch (error) { - case FixedLengthRequired: - message = - emulationActivity.getString(R.string.fixed_length_required, config.max_text_length); - break; - case MaxLengthExceeded: - message = - emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length); - break; - case BlankInputNotAllowed: - message = emulationActivity.getString(R.string.blank_input_not_allowed); - break; - case EmptyInputNotAllowed: - message = emulationActivity.getString(R.string.empty_input_not_allowed); - break; - } - new MaterialAlertDialogBuilder(emulationActivity) - .setTitle(R.string.software_keyboard) - .setMessage(message) - .setPositiveButton(android.R.string.ok, null) - .show(); + var overlayView = emulationActivity.findViewById(R.id.surface_input_overlay); + InputMethodManager im = (InputMethodManager)overlayView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED); + + // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result. + final Handler handler = new Handler(); + final int delayMs = 500; + handler.postDelayed(new Runnable() { + public void run() { + var insets = ViewCompat.getRootWindowInsets(overlayView); + var isKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); + if (isKeyboardVisible) { + handler.postDelayed(this, delayMs); + return; + } + + // No longer visible, submit the result. + NativeLibrary.SubmitInlineKeyboardInput(android.view.KeyEvent.KEYCODE_ENTER); + } + }, delayMs); } - public static KeyboardData Execute(KeyboardConfig config) { - if (config.button_config == ButtonConfig.None) { - Log.error("Unexpected button config None"); - return new KeyboardData(0, ""); - } - - NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); + public static KeyboardData ExecuteNormal(KeyboardConfig config) { + NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteNormalImpl(config)); synchronized (finishLock) { try { @@ -254,13 +252,13 @@ public final class SoftwareKeyboard { return data; } + public static void ExecuteInline(KeyboardConfig config) { + NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteInlineImpl(config)); + } + public static void ShowError(String error) { NativeLibrary.displayAlertMsg( YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard), error, false); } - - private static native ValidationError ValidateFilters(String text); - - private static native ValidationError ValidateInput(String text); } diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 21c27d4ee..3cf36b7d1 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -1,4 +1,8 @@ add_library(yuzu-android SHARED + android_common/android_common.cpp + android_common/android_common.h + applets/software_keyboard.cpp + applets/software_keyboard.h config.cpp config.h default_ini.h diff --git a/src/android/app/src/main/jni/android_common/android_common.cpp b/src/android/app/src/main/jni/android_common/android_common.cpp new file mode 100644 index 000000000..52d8ecfeb --- /dev/null +++ b/src/android/app/src/main/jni/android_common/android_common.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "jni/android_common/android_common.h" + +#include +#include + +#include + +#include "common/string_util.h" + +std::string GetJString(JNIEnv* env, jstring jstr) { + if (!jstr) { + return {}; + } + + const jchar* jchars = env->GetStringChars(jstr, nullptr); + const jsize length = env->GetStringLength(jstr); + const std::u16string_view string_view(reinterpret_cast(jchars), length); + const std::string converted_string = Common::UTF16ToUTF8(string_view); + env->ReleaseStringChars(jstr, jchars); + + return converted_string; +} + +jstring ToJString(JNIEnv* env, std::string_view str) { + const std::u16string converted_string = Common::UTF8ToUTF16(str); + return env->NewString(reinterpret_cast(converted_string.data()), + static_cast(converted_string.size())); +} + +jstring ToJString(JNIEnv* env, std::u16string_view str) { + return ToJString(env, Common::UTF16ToUTF8(str)); +} diff --git a/src/android/app/src/main/jni/android_common/android_common.h b/src/android/app/src/main/jni/android_common/android_common.h new file mode 100644 index 000000000..ccb0c06f7 --- /dev/null +++ b/src/android/app/src/main/jni/android_common/android_common.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +std::string GetJString(JNIEnv* env, jstring jstr); +jstring ToJString(JNIEnv* env, std::string_view str); +jstring ToJString(JNIEnv* env, std::u16string_view str); diff --git a/src/android/app/src/main/jni/applets/software_keyboard.cpp b/src/android/app/src/main/jni/applets/software_keyboard.cpp new file mode 100644 index 000000000..278137b4c --- /dev/null +++ b/src/android/app/src/main/jni/applets/software_keyboard.cpp @@ -0,0 +1,277 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include + +#include "common/logging/log.h" +#include "common/string_util.h" +#include "core/core.h" +#include "jni/android_common/android_common.h" +#include "jni/applets/software_keyboard.h" +#include "jni/id_cache.h" + +static jclass s_software_keyboard_class; +static jclass s_keyboard_config_class; +static jclass s_keyboard_data_class; +static jmethodID s_swkbd_execute_normal; +static jmethodID s_swkbd_execute_inline; + +namespace SoftwareKeyboard { + +static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) { + JNIEnv* env = IDCache::GetEnvForThread(); + jobject object = env->AllocObject(s_keyboard_config_class); + + env->SetObjectField(object, + env->GetFieldID(s_keyboard_config_class, "ok_text", "Ljava/lang/String;"), + ToJString(env, config.ok_text)); + env->SetObjectField( + object, env->GetFieldID(s_keyboard_config_class, "header_text", "Ljava/lang/String;"), + ToJString(env, config.header_text)); + env->SetObjectField(object, + env->GetFieldID(s_keyboard_config_class, "sub_text", "Ljava/lang/String;"), + ToJString(env, config.sub_text)); + env->SetObjectField( + object, env->GetFieldID(s_keyboard_config_class, "guide_text", "Ljava/lang/String;"), + ToJString(env, config.guide_text)); + env->SetObjectField( + object, env->GetFieldID(s_keyboard_config_class, "initial_text", "Ljava/lang/String;"), + ToJString(env, config.initial_text)); + env->SetShortField(object, + env->GetFieldID(s_keyboard_config_class, "left_optional_symbol_key", "S"), + static_cast(config.left_optional_symbol_key)); + env->SetShortField(object, + env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"), + static_cast(config.right_optional_symbol_key)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"), + static_cast(config.max_text_length)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"), + static_cast(config.min_text_length)); + env->SetIntField(object, + env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"), + static_cast(config.initial_cursor_position)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"), + static_cast(config.type)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"), + static_cast(config.password_mode)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"), + static_cast(config.text_draw_type)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"), + static_cast(config.key_disable_flags.raw)); + env->SetBooleanField(object, + env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"), + static_cast(config.use_blur_background)); + env->SetBooleanField(object, + env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"), + static_cast(config.enable_backspace_button)); + env->SetBooleanField(object, + env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"), + static_cast(config.enable_return_button)); + env->SetBooleanField(object, + env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"), + static_cast(config.disable_cancel_button)); + + return object; +} + +AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) { + JNIEnv* env = IDCache::GetEnvForThread(); + const jstring string = reinterpret_cast(env->GetObjectField( + object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;"))); + return ResultData{GetJString(env, string), + static_cast(env->GetIntField( + object, env->GetFieldID(s_keyboard_data_class, "result", "I")))}; +} + +AndroidKeyboard::~AndroidKeyboard() = default; + +void AndroidKeyboard::InitializeKeyboard( + bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters, + SubmitNormalCallback submit_normal_callback_, SubmitInlineCallback submit_inline_callback_) { + if (is_inline) { + LOG_WARNING( + Frontend, + "(STUBBED) called, backend requested to initialize the inline software keyboard."); + + submit_inline_callback = std::move(submit_inline_callback_); + } else { + LOG_WARNING( + Frontend, + "(STUBBED) called, backend requested to initialize the normal software keyboard."); + + submit_normal_callback = std::move(submit_normal_callback_); + } + + parameters = std::move(initialize_parameters); + + LOG_INFO(Frontend, + "\nKeyboardInitializeParameters:" + "\nok_text={}" + "\nheader_text={}" + "\nsub_text={}" + "\nguide_text={}" + "\ninitial_text={}" + "\nmax_text_length={}" + "\nmin_text_length={}" + "\ninitial_cursor_position={}" + "\ntype={}" + "\npassword_mode={}" + "\ntext_draw_type={}" + "\nkey_disable_flags={}" + "\nuse_blur_background={}" + "\nenable_backspace_button={}" + "\nenable_return_button={}" + "\ndisable_cancel_button={}", + Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text), + Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text), + Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length, + parameters.min_text_length, parameters.initial_cursor_position, parameters.type, + parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw, + parameters.use_blur_background, parameters.enable_backspace_button, + parameters.enable_return_button, parameters.disable_cancel_button); +} + +void AndroidKeyboard::ShowNormalKeyboard() const { + LOG_DEBUG(Frontend, "called, backend requested to show the normal software keyboard."); + + ResultData data{}; + + // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber. + std::thread([&] { + data = ResultData::CreateFromFrontend(IDCache::GetEnvForThread()->CallStaticObjectMethod( + s_software_keyboard_class, s_swkbd_execute_normal, ToJKeyboardParams(parameters))); + }).join(); + + SubmitNormalText(data); +} + +void AndroidKeyboard::ShowTextCheckDialog( + Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const { + LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog."); +} + +void AndroidKeyboard::ShowInlineKeyboard( + Core::Frontend::InlineAppearParameters appear_parameters) const { + LOG_WARNING(Frontend, + "(STUBBED) called, backend requested to show the inline software keyboard."); + + LOG_INFO(Frontend, + "\nInlineAppearParameters:" + "\nmax_text_length={}" + "\nmin_text_length={}" + "\nkey_top_scale_x={}" + "\nkey_top_scale_y={}" + "\nkey_top_translate_x={}" + "\nkey_top_translate_y={}" + "\ntype={}" + "\nkey_disable_flags={}" + "\nkey_top_as_floating={}" + "\nenable_backspace_button={}" + "\nenable_return_button={}" + "\ndisable_cancel_button={}", + appear_parameters.max_text_length, appear_parameters.min_text_length, + appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y, + appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y, + appear_parameters.type, appear_parameters.key_disable_flags.raw, + appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button, + appear_parameters.enable_return_button, appear_parameters.disable_cancel_button); + + // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber. + m_is_inline_active = true; + std::thread([&] { + IDCache::GetEnvForThread()->CallStaticVoidMethod( + s_software_keyboard_class, s_swkbd_execute_inline, ToJKeyboardParams(parameters)); + }).join(); +} + +void AndroidKeyboard::HideInlineKeyboard() const { + LOG_WARNING(Frontend, + "(STUBBED) called, backend requested to hide the inline software keyboard."); +} + +void AndroidKeyboard::InlineTextChanged( + Core::Frontend::InlineTextParameters text_parameters) const { + LOG_WARNING(Frontend, + "(STUBBED) called, backend requested to change the inline keyboard text."); + + LOG_INFO(Frontend, + "\nInlineTextParameters:" + "\ninput_text={}" + "\ncursor_position={}", + Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position); + + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, + text_parameters.input_text, text_parameters.cursor_position); +} + +void AndroidKeyboard::ExitKeyboard() const { + LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard."); +} + +void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) { + if (!m_is_inline_active) { + return; + } + + m_current_text += submitted_text; + + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text, + m_current_text.size()); +} + +void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) { + static constexpr int KEYCODE_BACK = 4; + static constexpr int KEYCODE_ENTER = 66; + static constexpr int KEYCODE_DEL = 67; + + if (!m_is_inline_active) { + return; + } + + switch (key_code) { + case KEYCODE_BACK: + case KEYCODE_ENTER: + m_is_inline_active = false; + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text, + static_cast(m_current_text.size())); + break; + case KEYCODE_DEL: + m_current_text.pop_back(); + submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text, + m_current_text.size()); + break; + } +} + +void AndroidKeyboard::SubmitNormalText(const ResultData& data) const { + submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true); +} + +void InitJNI(JNIEnv* env) { + s_software_keyboard_class = reinterpret_cast( + env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard"))); + s_keyboard_config_class = reinterpret_cast(env->NewGlobalRef( + env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig"))); + s_keyboard_data_class = reinterpret_cast(env->NewGlobalRef( + env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardData"))); + + s_swkbd_execute_normal = env->GetStaticMethodID( + s_software_keyboard_class, "ExecuteNormal", + "(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/" + "applets/SoftwareKeyboard$KeyboardData;"); + s_swkbd_execute_inline = + env->GetStaticMethodID(s_software_keyboard_class, "ExecuteInline", + "(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)V"); +} + +void CleanupJNI(JNIEnv* env) { + env->DeleteGlobalRef(s_software_keyboard_class); + env->DeleteGlobalRef(s_keyboard_config_class); + env->DeleteGlobalRef(s_keyboard_data_class); +} + +} // namespace SoftwareKeyboard diff --git a/src/android/app/src/main/jni/applets/software_keyboard.h b/src/android/app/src/main/jni/applets/software_keyboard.h new file mode 100644 index 000000000..b2fb59b68 --- /dev/null +++ b/src/android/app/src/main/jni/applets/software_keyboard.h @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "core/frontend/applets/software_keyboard.h" + +namespace SoftwareKeyboard { + +class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet { +public: + ~AndroidKeyboard() override; + + void Close() const override { + ExitKeyboard(); + } + + void InitializeKeyboard(bool is_inline, + Core::Frontend::KeyboardInitializeParameters initialize_parameters, + SubmitNormalCallback submit_normal_callback_, + SubmitInlineCallback submit_inline_callback_) override; + + void ShowNormalKeyboard() const override; + + void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, + std::u16string text_check_message) const override; + + void ShowInlineKeyboard( + Core::Frontend::InlineAppearParameters appear_parameters) const override; + + void HideInlineKeyboard() const override; + + void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override; + + void ExitKeyboard() const override; + + void SubmitInlineKeyboardText(std::u16string submitted_text); + + void SubmitInlineKeyboardInput(int key_code); + +private: + struct ResultData { + static ResultData CreateFromFrontend(jobject object); + + std::string text; + Service::AM::Applets::SwkbdResult result{}; + }; + + void SubmitNormalText(const ResultData& result) const; + + Core::Frontend::KeyboardInitializeParameters parameters{}; + + mutable SubmitNormalCallback submit_normal_callback; + mutable SubmitInlineCallback submit_inline_callback; + +private: + mutable bool m_is_inline_active{}; + std::u16string m_current_text; +}; + +// Should be called in JNI_Load +void InitJNI(JNIEnv* env); + +// Should be called in JNI_Unload +void CleanupJNI(JNIEnv* env); + +} // namespace SoftwareKeyboard + +// Native function calls +extern "C" { +JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters( + JNIEnv* env, jclass clazz, jstring text); + +JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput( + JNIEnv* env, jclass clazz, jstring text); +} diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 8f085798d..6291c8652 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -4,6 +4,7 @@ #include #include "common/fs/fs_android.h" +#include "jni/applets/software_keyboard.h" #include "jni/id_cache.h" static JavaVM* s_java_vm; @@ -63,6 +64,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { // Initialize Android Storage Common::FS::Android::RegisterCallbacks(env, s_native_library_class); + // Initialize applets + SoftwareKeyboard::InitJNI(env); + return JNI_VERSION; } @@ -75,6 +79,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { // UnInitialize Android Storage Common::FS::Android::UnRegisterCallbacks(); env->DeleteGlobalRef(s_native_library_class); + + // UnInitialze applets + SoftwareKeyboard::CleanupJNI(env); } #ifdef __cplusplus diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 6e670e899..10603c8fa 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -23,15 +23,29 @@ #include "common/scm_rev.h" #include "common/scope_exit.h" #include "common/settings.h" +#include "common/string_util.h" #include "core/core.h" #include "core/cpu_manager.h" #include "core/crypto/key_manager.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/vfs_real.h" +#include "core/frontend/applets/cabinet.h" +#include "core/frontend/applets/controller.h" +#include "core/frontend/applets/error.h" +#include "core/frontend/applets/general_frontend.h" +#include "core/frontend/applets/mii_edit.h" +#include "core/frontend/applets/profile_select.h" +#include "core/frontend/applets/software_keyboard.h" +#include "core/frontend/applets/web_browser.h" #include "core/hid/hid_core.h" +#include "core/hle/service/am/applet_ae.h" +#include "core/hle/service/am/applet_oe.h" +#include "core/hle/service/am/applets/applets.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/loader.h" #include "core/perf_stats.h" +#include "jni/android_common/android_common.h" +#include "jni/applets/software_keyboard.h" #include "jni/config.h" #include "jni/emu_window/emu_window.h" #include "jni/id_cache.h" @@ -135,11 +149,24 @@ public: m_vulkan_library); // Initialize system. + auto android_keyboard = std::make_unique(); + m_software_keyboard = android_keyboard.get(); m_system.SetShuttingDown(false); m_system.ApplySettings(); m_system.HIDCore().ReloadInputDevices(); m_system.SetContentProvider(std::make_unique()); m_system.SetFilesystem(std::make_shared()); + m_system.SetAppletFrontendSet({ + nullptr, // Amiibo Settings + nullptr, // Controller Selector + nullptr, // Error Display + nullptr, // Mii Editor + nullptr, // Parental Controls + nullptr, // Photo Viewer + nullptr, // Profile Selector + std::move(android_keyboard), // Software Keyboard + nullptr, // Web Browser + }); m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem()); // Load the ROM. @@ -233,6 +260,10 @@ public: m_rom_metadata_cache.clear(); } + SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard() { + return m_software_keyboard; + } + private: struct RomMetadata { std::string title; @@ -278,6 +309,7 @@ private: std::shared_ptr m_vfs; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; bool m_is_running{}; + SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; // GPU driver parameters std::shared_ptr m_vulkan_library; @@ -290,25 +322,6 @@ private: /*static*/ EmulationSession EmulationSession::s_instance; -std::string UTF16ToUTF8(std::u16string_view input) { - std::wstring_convert, char16_t> convert; - return convert.to_bytes(input.data(), input.data() + input.size()); -} - -std::string GetJString(JNIEnv* env, jstring jstr) { - if (!jstr) { - return {}; - } - - const jchar* jchars = env->GetStringChars(jstr, nullptr); - const jsize length = env->GetStringLength(jstr); - const std::u16string_view string_view(reinterpret_cast(jchars), length); - const std::string converted_string = UTF16ToUTF8(string_view); - env->ReleaseStringChars(jstr, jchars); - - return converted_string; -} - } // Anonymous namespace static Core::SystemResultStatus RunEmulation(const std::string& filepath) { @@ -605,4 +618,15 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo([[maybe_unused]] JNIEnv LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(JNIEnv* env, jclass clazz, + jstring j_text) { + const std::u16string input = Common::UTF8ToUTF16(GetJString(env, j_text)); + EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(JNIEnv* env, jclass clazz, + jint j_key_code) { + EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); +} + } // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 192c9261d..d30351c16 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -133,6 +133,12 @@ JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStat JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, jclass clazz); +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText( + JNIEnv* env, jclass clazz, jstring j_text); + +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput( + JNIEnv* env, jclass clazz, jint j_key_code); + #ifdef __cplusplus } #endif diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 0014b2146..5c31fb322 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -101,11 +101,6 @@ Software Keyboard - I Forgot - Text length is not correct (should be %d characters) - Text is too long (should be no more than %d characters) - Blank input is not allowed - Empty input is not allowed Abort