summaryrefslogtreecommitdiffstats
path: root/src/android/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java254
-rw-r--r--src/android/app/src/main/jni/CMakeLists.txt4
-rw-r--r--src/android/app/src/main/jni/android_common/android_common.cpp35
-rw-r--r--src/android/app/src/main/jni/android_common/android_common.h12
-rw-r--r--src/android/app/src/main/jni/applets/software_keyboard.cpp277
-rw-r--r--src/android/app/src/main/jni/applets/software_keyboard.h78
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp7
-rw-r--r--src/android/app/src/main/jni/native.cpp62
-rw-r--r--src/android/app/src/main/jni/native.h6
-rw-r--r--src/android/app/src/main/res/values/strings.xml5
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>