diff options
| author | bunnei <bunneidev@gmail.com> | 2023-03-25 00:28:45 -0700 | 
|---|---|---|
| committer | bunnei <bunneidev@gmail.com> | 2023-06-03 00:05:47 -0700 | 
| commit | d5ebfc8e211c0c72a130079621f3e98532ef7f68 (patch) | |
| tree | 659c26500993c9cb7c44eded406ef67997b5a966 | |
| parent | 58ede89c60c4cf3c603cbf2186bdb143280f2d60 (diff) | |
android: Implement basic software keyboard applet.
12 files changed, 625 insertions, 152 deletions
| 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 @@ -633,6 +633,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       */      public static final class ButtonType { 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<View>(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 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::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 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; +                } -    public static KeyboardData Execute(KeyboardConfig config) { -        if (config.button_config == ButtonConfig.None) { -            Log.error("Unexpected button config None"); -            return new KeyboardData(0, ""); -        } +                // No longer visible, submit the result. +                NativeLibrary.SubmitInlineKeyboardInput(android.view.KeyEvent.KEYCODE_ENTER); +            } +        }, delayMs); +    } -        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 <string> +#include <string_view> + +#include <jni.h> + +#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<const char16_t*>(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<const jchar*>(converted_string.data()), +                          static_cast<jint>(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 <string> + +#include <jni.h> + +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 <map> +#include <thread> + +#include <jni.h> + +#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<jshort>(config.left_optional_symbol_key)); +    env->SetShortField(object, +                       env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"), +                       static_cast<jshort>(config.right_optional_symbol_key)); +    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"), +                     static_cast<jint>(config.max_text_length)); +    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"), +                     static_cast<jint>(config.min_text_length)); +    env->SetIntField(object, +                     env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"), +                     static_cast<jint>(config.initial_cursor_position)); +    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"), +                     static_cast<jint>(config.type)); +    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"), +                     static_cast<jint>(config.password_mode)); +    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"), +                     static_cast<jint>(config.text_draw_type)); +    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"), +                     static_cast<jint>(config.key_disable_flags.raw)); +    env->SetBooleanField(object, +                         env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"), +                         static_cast<jboolean>(config.use_blur_background)); +    env->SetBooleanField(object, +                         env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"), +                         static_cast<jboolean>(config.enable_backspace_button)); +    env->SetBooleanField(object, +                         env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"), +                         static_cast<jboolean>(config.enable_return_button)); +    env->SetBooleanField(object, +                         env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"), +                         static_cast<jboolean>(config.disable_cancel_button)); + +    return object; +} + +AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) { +    JNIEnv* env = IDCache::GetEnvForThread(); +    const jstring string = reinterpret_cast<jstring>(env->GetObjectField( +        object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;"))); +    return ResultData{GetJString(env, string), +                      static_cast<Service::AM::Applets::SwkbdResult>(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<s32>(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<jclass>( +        env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard"))); +    s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef( +        env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig"))); +    s_keyboard_data_class = reinterpret_cast<jclass>(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 <jni.h> + +#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 <jni.h>  #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<SoftwareKeyboard::AndroidKeyboard>(); +        m_software_keyboard = android_keyboard.get();          m_system.SetShuttingDown(false);          m_system.ApplySettings();          m_system.HIDCore().ReloadInputDevices();          m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());          m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); +        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<FileSys::RealVfsFilesystem> 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<Common::DynamicLibrary> m_vulkan_library; @@ -290,25 +322,6 @@ private:  /*static*/ EmulationSession EmulationSession::s_instance; -std::string UTF16ToUTF8(std::u16string_view input) { -    std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, 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<const char16_t*>(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 -->      <string name="software_keyboard">Software Keyboard</string> -    <string name="i_forgot">I Forgot</string> -    <string name="fixed_length_required">Text length is not correct (should be %d characters)</string> -    <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string> -    <string name="blank_input_not_allowed">Blank input is not allowed</string> -    <string name="empty_input_not_allowed">Empty input is not allowed</string>      <!-- Errors and warnings -->      <string name="abort_button">Abort</string> | 
