Просмотр исходного кода

android: Implement basic software keyboard applet.

bunnei 3 лет назад
Родитель
Сommit
d5ebfc8e21

+ 12 - 0
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
      */

+ 25 - 0
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)

+ 126 - 128
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);
 }

+ 4 - 0
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

+ 35 - 0
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));
+}

+ 12 - 0
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);

+ 277 - 0
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

+ 78 - 0
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);
+}

+ 7 - 0
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

+ 43 - 19
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"

+ 6 - 0
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

+ 0 - 5
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>