summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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 {
633 public static native void LogDeviceInfo(); 633 public static native void LogDeviceInfo();
634 634
635 /** 635 /**
636 * Submits inline keyboard text. Called on input for buttons that result text.
637 * @param text Text to submit to the inline software keyboard implementation.
638 */
639 public static native void SubmitInlineKeyboardText(String text);
640
641 /**
642 * Submits inline keyboard input. Used to indicate keys pressed that are not text.
643 * @param key_code Android Key Code associated with the keyboard input.
644 */
645 public static native void SubmitInlineKeyboardInput(int key_code);
646
647 /**
636 * Button type for use in onTouchEvent 648 * Button type for use in onTouchEvent
637 */ 649 */
638 public static final class ButtonType { 650 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
8import android.content.Intent 8import android.content.Intent
9import android.graphics.Rect 9import android.graphics.Rect
10import android.os.Bundle 10import android.os.Bundle
11import android.view.KeyEvent
11import android.view.View 12import android.view.View
12import android.view.WindowManager 13import android.view.WindowManager
14import android.view.inputmethod.InputMethodManager
13import androidx.appcompat.app.AppCompatActivity 15import androidx.appcompat.app.AppCompatActivity
14import androidx.fragment.app.FragmentActivity 16import androidx.fragment.app.FragmentActivity
15import androidx.preference.PreferenceManager 17import androidx.preference.PreferenceManager
@@ -80,6 +82,29 @@ open class EmulationActivity : AppCompatActivity() {
80 //startForegroundService(foregroundService); 82 //startForegroundService(foregroundService);
81 } 83 }
82 84
85 override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
86 if (event.action == android.view.KeyEvent.ACTION_DOWN) {
87 if (keyCode == android.view.KeyEvent.KEYCODE_ENTER) {
88 // Special case, we do not support multiline input, dismiss the keyboard.
89 val overlayView: View =
90 this.findViewById<View>(R.id.surface_input_overlay)
91 val im =
92 overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
93 im.hideSoftInputFromWindow(overlayView.windowToken, 0);
94 } else {
95 val textChar = event.getUnicodeChar();
96 if (textChar == 0) {
97 // No text, button input.
98 NativeLibrary.SubmitInlineKeyboardInput(keyCode);
99 } else {
100 // Text submitted.
101 NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString());
102 }
103 }
104 }
105 return super.onKeyDown(keyCode, event)
106 }
107
83 override fun onSaveInstanceState(outState: Bundle) { 108 override fun onSaveInstanceState(outState: Bundle) {
84 outState.putParcelable(EXTRA_SELECTED_GAME, game) 109 outState.putParcelable(EXTRA_SELECTED_GAME, game)
85 super.onSaveInstanceState(outState) 110 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 @@
1// Copyright 2020 Citra Emulator Project 1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// Licensed under GPLv2 or any later version 2// SPDX-License-Identifier: GPL-2.0-or-later
3// Refer to the license.txt file included.
4 3
5package org.yuzu.yuzu_emu.applets; 4package org.yuzu.yuzu_emu.applets;
6 5
7import android.app.Activity; 6import android.app.Activity;
8import android.app.Dialog; 7import android.app.Dialog;
8import android.content.Context;
9import android.content.DialogInterface; 9import android.content.DialogInterface;
10import android.graphics.Rect;
10import android.os.Bundle; 11import android.os.Bundle;
12import android.os.Handler;
13import android.os.ResultReceiver;
11import android.text.InputFilter; 14import android.text.InputFilter;
12import android.text.Spanned; 15import android.text.InputType;
13import android.view.ViewGroup; 16import android.view.ViewGroup;
17import android.view.ViewTreeObserver;
18import android.view.WindowInsets;
19import android.view.inputmethod.InputMethodManager;
14import android.widget.EditText; 20import android.widget.EditText;
15import android.widget.FrameLayout; 21import android.widget.FrameLayout;
16 22
17import androidx.annotation.NonNull; 23import androidx.annotation.NonNull;
18import androidx.annotation.Nullable;
19import androidx.appcompat.app.AlertDialog; 24import androidx.appcompat.app.AlertDialog;
25import androidx.core.view.ViewCompat;
20import androidx.fragment.app.DialogFragment; 26import androidx.fragment.app.DialogFragment;
21 27
22import com.google.android.material.dialog.MaterialAlertDialogBuilder; 28import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -25,72 +31,66 @@ import org.yuzu.yuzu_emu.YuzuApplication;
25import org.yuzu.yuzu_emu.NativeLibrary; 31import org.yuzu.yuzu_emu.NativeLibrary;
26import org.yuzu.yuzu_emu.R; 32import org.yuzu.yuzu_emu.R;
27import org.yuzu.yuzu_emu.activities.EmulationActivity; 33import org.yuzu.yuzu_emu.activities.EmulationActivity;
28import org.yuzu.yuzu_emu.utils.Log;
29 34
30import java.util.Objects; 35import java.util.Objects;
31 36
32public final class SoftwareKeyboard { 37public final class SoftwareKeyboard {
33 /// Corresponds to Frontend::ButtonConfig 38 /// Corresponds to Service::AM::Applets::SwkbdType
34 private interface ButtonConfig { 39 private interface SwkbdType {
35 int Single = 0; /// Ok button 40 int Normal = 0;
36 int Dual = 1; /// Cancel | Ok buttons 41 int NumberPad = 1;
37 int Triple = 2; /// Cancel | I Forgot | Ok buttons 42 int Qwerty = 2;
38 int None = 3; /// No button (returned by swkbdInputText in special cases) 43 int Unknown3 = 3;
39 } 44 int Latin = 4;
40 45 int SimplifiedChinese = 5;
41 /// Corresponds to Frontend::ValidationError 46 int TraditionalChinese = 6;
42 public enum ValidationError { 47 int Korean = 7;
43 None, 48 };
44 // Button Selection 49
45 ButtonOutOfRange, 50 /// Corresponds to Service::AM::Applets::SwkbdPasswordMode
46 // Configured Filters 51 private interface SwkbdPasswordMode {
47 MaxDigitsExceeded, 52 int Disabled = 0;
48 AtSignNotAllowed, 53 int Enabled = 1;
49 PercentNotAllowed, 54 };
50 BackslashNotAllowed, 55
51 ProfanityNotAllowed, 56 /// Corresponds to Service::AM::Applets::SwkbdResult
52 CallbackFailed, 57 private interface SwkbdResult {
53 // Allowed Input Type 58 int Ok = 0;
54 FixedLengthRequired, 59 int Cancel = 1;
55 MaxLengthExceeded, 60 };
56 BlankInputNotAllowed,
57 EmptyInputNotAllowed,
58 }
59 61
60 public static class KeyboardConfig implements java.io.Serializable { 62 public static class KeyboardConfig implements java.io.Serializable {
61 public int button_config; 63 public String ok_text;
64 public String header_text;
65 public String sub_text;
66 public String guide_text;
67 public String initial_text;
68 public short left_optional_symbol_key;
69 public short right_optional_symbol_key;
62 public int max_text_length; 70 public int max_text_length;
63 public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input 71 public int min_text_length;
64 public String hint_text; /// Displayed in the field as a hint before 72 public int initial_cursor_position;
65 @Nullable 73 public int type;
66 public String[] button_text; /// Contains the button text that the caller provides 74 public int password_mode;
75 public int text_draw_type;
76 public int key_disable_flags;
77 public boolean use_blur_background;
78 public boolean enable_backspace_button;
79 public boolean enable_return_button;
80 public boolean disable_cancel_button;
67 } 81 }
68 82
69 /// Corresponds to Frontend::KeyboardData 83 /// Corresponds to Frontend::KeyboardData
70 public static class KeyboardData { 84 public static class KeyboardData {
71 public int button; 85 public int result;
72 public String text; 86 public String text;
73 87
74 private KeyboardData(int button, String text) { 88 private KeyboardData(int result, String text) {
75 this.button = button; 89 this.result = result;
76 this.text = text; 90 this.text = text;
77 } 91 }
78 } 92 }
79 93
80 private static class Filter implements InputFilter {
81 @Override
82 public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
83 int dstart, int dend) {
84 String text = new StringBuilder(dest)
85 .replace(dstart, dend, source.subSequence(start, end).toString())
86 .toString();
87 if (ValidateFilters(text) == ValidationError.None) {
88 return null; // Accept replacement
89 }
90 return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
91 }
92 }
93
94 public static class KeyboardDialogFragment extends DialogFragment { 94 public static class KeyboardDialogFragment extends DialogFragment {
95 static KeyboardDialogFragment newInstance(KeyboardConfig config) { 95 static KeyboardDialogFragment newInstance(KeyboardConfig config) {
96 KeyboardDialogFragment frag = new KeyboardDialogFragment(); 96 KeyboardDialogFragment frag = new KeyboardDialogFragment();
@@ -113,60 +113,65 @@ public final class SoftwareKeyboard {
113 R.dimen.dialog_margin); 113 R.dimen.dialog_margin);
114 114
115 KeyboardConfig config = Objects.requireNonNull( 115 KeyboardConfig config = Objects.requireNonNull(
116 (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); 116 (KeyboardConfig) requireArguments().getSerializable("config"));
117 117
118 // Set up the input 118 // Set up the input
119 EditText editText = new EditText(YuzuApplication.getAppContext()); 119 EditText editText = new EditText(YuzuApplication.getAppContext());
120 editText.setHint(config.hint_text); 120 editText.setHint(config.initial_text);
121 editText.setSingleLine(!config.multiline_mode); 121 editText.setSingleLine(!config.enable_return_button);
122 editText.setLayoutParams(params); 122 editText.setLayoutParams(params);
123 editText.setFilters(new InputFilter[]{ 123 editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(config.max_text_length)});
124 new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); 124
125 // Handle input type
126 int input_type = 0;
127 switch (config.type)
128 {
129 case SwkbdType.Normal:
130 case SwkbdType.Qwerty:
131 case SwkbdType.Unknown3:
132 case SwkbdType.Latin:
133 case SwkbdType.SimplifiedChinese:
134 case SwkbdType.TraditionalChinese:
135 case SwkbdType.Korean:
136 default:
137 input_type = InputType.TYPE_CLASS_TEXT;
138 if (config.password_mode == SwkbdPasswordMode.Enabled)
139 {
140 input_type |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
141 }
142 break;
143 case SwkbdType.NumberPad:
144 input_type = InputType.TYPE_CLASS_NUMBER;
145 if (config.password_mode == SwkbdPasswordMode.Enabled)
146 {
147 input_type |= InputType.TYPE_NUMBER_VARIATION_PASSWORD;
148 }
149 break;
150 }
151
152 // Apply input type
153 editText.setInputType(input_type);
125 154
126 FrameLayout container = new FrameLayout(emulationActivity); 155 FrameLayout container = new FrameLayout(emulationActivity);
127 container.addView(editText); 156 container.addView(editText);
128 157
158 String headerText = config.header_text.isEmpty() ? emulationActivity.getString(R.string.software_keyboard) : config.header_text;
159 String okText = config.header_text.isEmpty() ? emulationActivity.getString(android.R.string.ok) : config.ok_text;
160
129 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity) 161 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
130 .setTitle(R.string.software_keyboard) 162 .setTitle(headerText)
131 .setView(container); 163 .setView(container);
132 setCancelable(false); 164 setCancelable(false);
133 165
134 switch (config.button_config) { 166 builder.setPositiveButton(okText, null);
135 case ButtonConfig.Triple: { 167 builder.setNegativeButton(emulationActivity.getString(android.R.string.cancel), null);
136 final String text = config.button_text[1].isEmpty()
137 ? emulationActivity.getString(R.string.i_forgot)
138 : config.button_text[1];
139 builder.setNeutralButton(text, null);
140 }
141 // fallthrough
142 case ButtonConfig.Dual: {
143 final String text = config.button_text[0].isEmpty()
144 ? emulationActivity.getString(android.R.string.cancel)
145 : config.button_text[0];
146 builder.setNegativeButton(text, null);
147 }
148 // fallthrough
149 case ButtonConfig.Single: {
150 final String text = config.button_text[2].isEmpty()
151 ? emulationActivity.getString(android.R.string.ok)
152 : config.button_text[2];
153 builder.setPositiveButton(text, null);
154 break;
155 }
156 }
157 168
158 final AlertDialog dialog = builder.create(); 169 final AlertDialog dialog = builder.create();
159 dialog.create(); 170 dialog.create();
160 if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { 171 if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
161 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { 172 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
162 data.button = config.button_config; 173 data.result = SwkbdResult.Ok;
163 data.text = editText.getText().toString(); 174 data.text = editText.getText().toString();
164 final ValidationError error = ValidateInput(data.text);
165 if (error != ValidationError.None) {
166 HandleValidationError(config, error);
167 return;
168 }
169
170 dialog.dismiss(); 175 dialog.dismiss();
171 176
172 synchronized (finishLock) { 177 synchronized (finishLock) {
@@ -176,7 +181,7 @@ public final class SoftwareKeyboard {
176 } 181 }
177 if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { 182 if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
178 dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { 183 dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
179 data.button = 1; 184 data.result = SwkbdResult.Ok;
180 dialog.dismiss(); 185 dialog.dismiss();
181 synchronized (finishLock) { 186 synchronized (finishLock) {
182 finishLock.notifyAll(); 187 finishLock.notifyAll();
@@ -185,7 +190,7 @@ public final class SoftwareKeyboard {
185 } 190 }
186 if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { 191 if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
187 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { 192 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
188 data.button = 0; 193 data.result = SwkbdResult.Cancel;
189 dialog.dismiss(); 194 dialog.dismiss();
190 synchronized (finishLock) { 195 synchronized (finishLock) {
191 finishLock.notifyAll(); 196 finishLock.notifyAll();
@@ -200,49 +205,42 @@ public final class SoftwareKeyboard {
200 private static KeyboardData data; 205 private static KeyboardData data;
201 private static final Object finishLock = new Object(); 206 private static final Object finishLock = new Object();
202 207
203 private static void ExecuteImpl(KeyboardConfig config) { 208 private static void ExecuteNormalImpl(KeyboardConfig config) {
204 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); 209 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
205 210
206 data = new KeyboardData(0, ""); 211 data = new KeyboardData(SwkbdResult.Cancel, "");
207 212
208 KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); 213 KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
209 fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); 214 fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
210 } 215 }
211 216
212 private static void HandleValidationError(KeyboardConfig config, ValidationError error) { 217 private static void ExecuteInlineImpl(KeyboardConfig config) {
213 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); 218 final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
214 String message = "";
215 switch (error) {
216 case FixedLengthRequired:
217 message =
218 emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
219 break;
220 case MaxLengthExceeded:
221 message =
222 emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
223 break;
224 case BlankInputNotAllowed:
225 message = emulationActivity.getString(R.string.blank_input_not_allowed);
226 break;
227 case EmptyInputNotAllowed:
228 message = emulationActivity.getString(R.string.empty_input_not_allowed);
229 break;
230 }
231 219
232 new MaterialAlertDialogBuilder(emulationActivity) 220 var overlayView = emulationActivity.findViewById(R.id.surface_input_overlay);
233 .setTitle(R.string.software_keyboard) 221 InputMethodManager im = (InputMethodManager)overlayView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
234 .setMessage(message) 222 im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED);
235 .setPositiveButton(android.R.string.ok, null) 223
236 .show(); 224 // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result.
237 } 225 final Handler handler = new Handler();
226 final int delayMs = 500;
227 handler.postDelayed(new Runnable() {
228 public void run() {
229 var insets = ViewCompat.getRootWindowInsets(overlayView);
230 var isKeyboardVisible = insets.isVisible(WindowInsets.Type.ime());
231 if (isKeyboardVisible) {
232 handler.postDelayed(this, delayMs);
233 return;
234 }
238 235
239 public static KeyboardData Execute(KeyboardConfig config) { 236 // No longer visible, submit the result.
240 if (config.button_config == ButtonConfig.None) { 237 NativeLibrary.SubmitInlineKeyboardInput(android.view.KeyEvent.KEYCODE_ENTER);
241 Log.error("Unexpected button config None"); 238 }
242 return new KeyboardData(0, ""); 239 }, delayMs);
243 } 240 }
244 241
245 NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); 242 public static KeyboardData ExecuteNormal(KeyboardConfig config) {
243 NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteNormalImpl(config));
246 244
247 synchronized (finishLock) { 245 synchronized (finishLock) {
248 try { 246 try {
@@ -254,13 +252,13 @@ public final class SoftwareKeyboard {
254 return data; 252 return data;
255 } 253 }
256 254
255 public static void ExecuteInline(KeyboardConfig config) {
256 NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteInlineImpl(config));
257 }
258
257 public static void ShowError(String error) { 259 public static void ShowError(String error) {
258 NativeLibrary.displayAlertMsg( 260 NativeLibrary.displayAlertMsg(
259 YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard), 261 YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard),
260 error, false); 262 error, false);
261 } 263 }
262
263 private static native ValidationError ValidateFilters(String text);
264
265 private static native ValidationError ValidateInput(String text);
266} 264}
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 @@
1add_library(yuzu-android SHARED 1add_library(yuzu-android SHARED
2 android_common/android_common.cpp
3 android_common/android_common.h
4 applets/software_keyboard.cpp
5 applets/software_keyboard.h
2 config.cpp 6 config.cpp
3 config.h 7 config.h
4 default_ini.h 8 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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "jni/android_common/android_common.h"
5
6#include <string>
7#include <string_view>
8
9#include <jni.h>
10
11#include "common/string_util.h"
12
13std::string GetJString(JNIEnv* env, jstring jstr) {
14 if (!jstr) {
15 return {};
16 }
17
18 const jchar* jchars = env->GetStringChars(jstr, nullptr);
19 const jsize length = env->GetStringLength(jstr);
20 const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
21 const std::string converted_string = Common::UTF16ToUTF8(string_view);
22 env->ReleaseStringChars(jstr, jchars);
23
24 return converted_string;
25}
26
27jstring ToJString(JNIEnv* env, std::string_view str) {
28 const std::u16string converted_string = Common::UTF8ToUTF16(str);
29 return env->NewString(reinterpret_cast<const jchar*>(converted_string.data()),
30 static_cast<jint>(converted_string.size()));
31}
32
33jstring ToJString(JNIEnv* env, std::u16string_view str) {
34 return ToJString(env, Common::UTF16ToUTF8(str));
35}
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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <string>
7
8#include <jni.h>
9
10std::string GetJString(JNIEnv* env, jstring jstr);
11jstring ToJString(JNIEnv* env, std::string_view str);
12jstring 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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <map>
5#include <thread>
6
7#include <jni.h>
8
9#include "common/logging/log.h"
10#include "common/string_util.h"
11#include "core/core.h"
12#include "jni/android_common/android_common.h"
13#include "jni/applets/software_keyboard.h"
14#include "jni/id_cache.h"
15
16static jclass s_software_keyboard_class;
17static jclass s_keyboard_config_class;
18static jclass s_keyboard_data_class;
19static jmethodID s_swkbd_execute_normal;
20static jmethodID s_swkbd_execute_inline;
21
22namespace SoftwareKeyboard {
23
24static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) {
25 JNIEnv* env = IDCache::GetEnvForThread();
26 jobject object = env->AllocObject(s_keyboard_config_class);
27
28 env->SetObjectField(object,
29 env->GetFieldID(s_keyboard_config_class, "ok_text", "Ljava/lang/String;"),
30 ToJString(env, config.ok_text));
31 env->SetObjectField(
32 object, env->GetFieldID(s_keyboard_config_class, "header_text", "Ljava/lang/String;"),
33 ToJString(env, config.header_text));
34 env->SetObjectField(object,
35 env->GetFieldID(s_keyboard_config_class, "sub_text", "Ljava/lang/String;"),
36 ToJString(env, config.sub_text));
37 env->SetObjectField(
38 object, env->GetFieldID(s_keyboard_config_class, "guide_text", "Ljava/lang/String;"),
39 ToJString(env, config.guide_text));
40 env->SetObjectField(
41 object, env->GetFieldID(s_keyboard_config_class, "initial_text", "Ljava/lang/String;"),
42 ToJString(env, config.initial_text));
43 env->SetShortField(object,
44 env->GetFieldID(s_keyboard_config_class, "left_optional_symbol_key", "S"),
45 static_cast<jshort>(config.left_optional_symbol_key));
46 env->SetShortField(object,
47 env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"),
48 static_cast<jshort>(config.right_optional_symbol_key));
49 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
50 static_cast<jint>(config.max_text_length));
51 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"),
52 static_cast<jint>(config.min_text_length));
53 env->SetIntField(object,
54 env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"),
55 static_cast<jint>(config.initial_cursor_position));
56 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"),
57 static_cast<jint>(config.type));
58 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"),
59 static_cast<jint>(config.password_mode));
60 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"),
61 static_cast<jint>(config.text_draw_type));
62 env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"),
63 static_cast<jint>(config.key_disable_flags.raw));
64 env->SetBooleanField(object,
65 env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"),
66 static_cast<jboolean>(config.use_blur_background));
67 env->SetBooleanField(object,
68 env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"),
69 static_cast<jboolean>(config.enable_backspace_button));
70 env->SetBooleanField(object,
71 env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"),
72 static_cast<jboolean>(config.enable_return_button));
73 env->SetBooleanField(object,
74 env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"),
75 static_cast<jboolean>(config.disable_cancel_button));
76
77 return object;
78}
79
80AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) {
81 JNIEnv* env = IDCache::GetEnvForThread();
82 const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
83 object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
84 return ResultData{GetJString(env, string),
85 static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField(
86 object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
87}
88
89AndroidKeyboard::~AndroidKeyboard() = default;
90
91void AndroidKeyboard::InitializeKeyboard(
92 bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters,
93 SubmitNormalCallback submit_normal_callback_, SubmitInlineCallback submit_inline_callback_) {
94 if (is_inline) {
95 LOG_WARNING(
96 Frontend,
97 "(STUBBED) called, backend requested to initialize the inline software keyboard.");
98
99 submit_inline_callback = std::move(submit_inline_callback_);
100 } else {
101 LOG_WARNING(
102 Frontend,
103 "(STUBBED) called, backend requested to initialize the normal software keyboard.");
104
105 submit_normal_callback = std::move(submit_normal_callback_);
106 }
107
108 parameters = std::move(initialize_parameters);
109
110 LOG_INFO(Frontend,
111 "\nKeyboardInitializeParameters:"
112 "\nok_text={}"
113 "\nheader_text={}"
114 "\nsub_text={}"
115 "\nguide_text={}"
116 "\ninitial_text={}"
117 "\nmax_text_length={}"
118 "\nmin_text_length={}"
119 "\ninitial_cursor_position={}"
120 "\ntype={}"
121 "\npassword_mode={}"
122 "\ntext_draw_type={}"
123 "\nkey_disable_flags={}"
124 "\nuse_blur_background={}"
125 "\nenable_backspace_button={}"
126 "\nenable_return_button={}"
127 "\ndisable_cancel_button={}",
128 Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text),
129 Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text),
130 Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length,
131 parameters.min_text_length, parameters.initial_cursor_position, parameters.type,
132 parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw,
133 parameters.use_blur_background, parameters.enable_backspace_button,
134 parameters.enable_return_button, parameters.disable_cancel_button);
135}
136
137void AndroidKeyboard::ShowNormalKeyboard() const {
138 LOG_DEBUG(Frontend, "called, backend requested to show the normal software keyboard.");
139
140 ResultData data{};
141
142 // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
143 std::thread([&] {
144 data = ResultData::CreateFromFrontend(IDCache::GetEnvForThread()->CallStaticObjectMethod(
145 s_software_keyboard_class, s_swkbd_execute_normal, ToJKeyboardParams(parameters)));
146 }).join();
147
148 SubmitNormalText(data);
149}
150
151void AndroidKeyboard::ShowTextCheckDialog(
152 Service::AM::Applets::SwkbdTextCheckResult text_check_result,
153 std::u16string text_check_message) const {
154 LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
155}
156
157void AndroidKeyboard::ShowInlineKeyboard(
158 Core::Frontend::InlineAppearParameters appear_parameters) const {
159 LOG_WARNING(Frontend,
160 "(STUBBED) called, backend requested to show the inline software keyboard.");
161
162 LOG_INFO(Frontend,
163 "\nInlineAppearParameters:"
164 "\nmax_text_length={}"
165 "\nmin_text_length={}"
166 "\nkey_top_scale_x={}"
167 "\nkey_top_scale_y={}"
168 "\nkey_top_translate_x={}"
169 "\nkey_top_translate_y={}"
170 "\ntype={}"
171 "\nkey_disable_flags={}"
172 "\nkey_top_as_floating={}"
173 "\nenable_backspace_button={}"
174 "\nenable_return_button={}"
175 "\ndisable_cancel_button={}",
176 appear_parameters.max_text_length, appear_parameters.min_text_length,
177 appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y,
178 appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y,
179 appear_parameters.type, appear_parameters.key_disable_flags.raw,
180 appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button,
181 appear_parameters.enable_return_button, appear_parameters.disable_cancel_button);
182
183 // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
184 m_is_inline_active = true;
185 std::thread([&] {
186 IDCache::GetEnvForThread()->CallStaticVoidMethod(
187 s_software_keyboard_class, s_swkbd_execute_inline, ToJKeyboardParams(parameters));
188 }).join();
189}
190
191void AndroidKeyboard::HideInlineKeyboard() const {
192 LOG_WARNING(Frontend,
193 "(STUBBED) called, backend requested to hide the inline software keyboard.");
194}
195
196void AndroidKeyboard::InlineTextChanged(
197 Core::Frontend::InlineTextParameters text_parameters) const {
198 LOG_WARNING(Frontend,
199 "(STUBBED) called, backend requested to change the inline keyboard text.");
200
201 LOG_INFO(Frontend,
202 "\nInlineTextParameters:"
203 "\ninput_text={}"
204 "\ncursor_position={}",
205 Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
206
207 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString,
208 text_parameters.input_text, text_parameters.cursor_position);
209}
210
211void AndroidKeyboard::ExitKeyboard() const {
212 LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard.");
213}
214
215void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
216 if (!m_is_inline_active) {
217 return;
218 }
219
220 m_current_text += submitted_text;
221
222 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
223 m_current_text.size());
224}
225
226void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
227 static constexpr int KEYCODE_BACK = 4;
228 static constexpr int KEYCODE_ENTER = 66;
229 static constexpr int KEYCODE_DEL = 67;
230
231 if (!m_is_inline_active) {
232 return;
233 }
234
235 switch (key_code) {
236 case KEYCODE_BACK:
237 case KEYCODE_ENTER:
238 m_is_inline_active = false;
239 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text,
240 static_cast<s32>(m_current_text.size()));
241 break;
242 case KEYCODE_DEL:
243 m_current_text.pop_back();
244 submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
245 m_current_text.size());
246 break;
247 }
248}
249
250void AndroidKeyboard::SubmitNormalText(const ResultData& data) const {
251 submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true);
252}
253
254void InitJNI(JNIEnv* env) {
255 s_software_keyboard_class = reinterpret_cast<jclass>(
256 env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard")));
257 s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
258 env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig")));
259 s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
260 env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardData")));
261
262 s_swkbd_execute_normal = env->GetStaticMethodID(
263 s_software_keyboard_class, "ExecuteNormal",
264 "(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/"
265 "applets/SoftwareKeyboard$KeyboardData;");
266 s_swkbd_execute_inline =
267 env->GetStaticMethodID(s_software_keyboard_class, "ExecuteInline",
268 "(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)V");
269}
270
271void CleanupJNI(JNIEnv* env) {
272 env->DeleteGlobalRef(s_software_keyboard_class);
273 env->DeleteGlobalRef(s_keyboard_config_class);
274 env->DeleteGlobalRef(s_keyboard_data_class);
275}
276
277} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <jni.h>
7
8#include "core/frontend/applets/software_keyboard.h"
9
10namespace SoftwareKeyboard {
11
12class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet {
13public:
14 ~AndroidKeyboard() override;
15
16 void Close() const override {
17 ExitKeyboard();
18 }
19
20 void InitializeKeyboard(bool is_inline,
21 Core::Frontend::KeyboardInitializeParameters initialize_parameters,
22 SubmitNormalCallback submit_normal_callback_,
23 SubmitInlineCallback submit_inline_callback_) override;
24
25 void ShowNormalKeyboard() const override;
26
27 void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result,
28 std::u16string text_check_message) const override;
29
30 void ShowInlineKeyboard(
31 Core::Frontend::InlineAppearParameters appear_parameters) const override;
32
33 void HideInlineKeyboard() const override;
34
35 void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
36
37 void ExitKeyboard() const override;
38
39 void SubmitInlineKeyboardText(std::u16string submitted_text);
40
41 void SubmitInlineKeyboardInput(int key_code);
42
43private:
44 struct ResultData {
45 static ResultData CreateFromFrontend(jobject object);
46
47 std::string text;
48 Service::AM::Applets::SwkbdResult result{};
49 };
50
51 void SubmitNormalText(const ResultData& result) const;
52
53 Core::Frontend::KeyboardInitializeParameters parameters{};
54
55 mutable SubmitNormalCallback submit_normal_callback;
56 mutable SubmitInlineCallback submit_inline_callback;
57
58private:
59 mutable bool m_is_inline_active{};
60 std::u16string m_current_text;
61};
62
63// Should be called in JNI_Load
64void InitJNI(JNIEnv* env);
65
66// Should be called in JNI_Unload
67void CleanupJNI(JNIEnv* env);
68
69} // namespace SoftwareKeyboard
70
71// Native function calls
72extern "C" {
73JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(
74 JNIEnv* env, jclass clazz, jstring text);
75
76JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(
77 JNIEnv* env, jclass clazz, jstring text);
78}
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 @@
4#include <jni.h> 4#include <jni.h>
5 5
6#include "common/fs/fs_android.h" 6#include "common/fs/fs_android.h"
7#include "jni/applets/software_keyboard.h"
7#include "jni/id_cache.h" 8#include "jni/id_cache.h"
8 9
9static JavaVM* s_java_vm; 10static JavaVM* s_java_vm;
@@ -63,6 +64,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
63 // Initialize Android Storage 64 // Initialize Android Storage
64 Common::FS::Android::RegisterCallbacks(env, s_native_library_class); 65 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
65 66
67 // Initialize applets
68 SoftwareKeyboard::InitJNI(env);
69
66 return JNI_VERSION; 70 return JNI_VERSION;
67} 71}
68 72
@@ -75,6 +79,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
75 // UnInitialize Android Storage 79 // UnInitialize Android Storage
76 Common::FS::Android::UnRegisterCallbacks(); 80 Common::FS::Android::UnRegisterCallbacks();
77 env->DeleteGlobalRef(s_native_library_class); 81 env->DeleteGlobalRef(s_native_library_class);
82
83 // UnInitialze applets
84 SoftwareKeyboard::CleanupJNI(env);
78} 85}
79 86
80#ifdef __cplusplus 87#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 @@
23#include "common/scm_rev.h" 23#include "common/scm_rev.h"
24#include "common/scope_exit.h" 24#include "common/scope_exit.h"
25#include "common/settings.h" 25#include "common/settings.h"
26#include "common/string_util.h"
26#include "core/core.h" 27#include "core/core.h"
27#include "core/cpu_manager.h" 28#include "core/cpu_manager.h"
28#include "core/crypto/key_manager.h" 29#include "core/crypto/key_manager.h"
29#include "core/file_sys/registered_cache.h" 30#include "core/file_sys/registered_cache.h"
30#include "core/file_sys/vfs_real.h" 31#include "core/file_sys/vfs_real.h"
32#include "core/frontend/applets/cabinet.h"
33#include "core/frontend/applets/controller.h"
34#include "core/frontend/applets/error.h"
35#include "core/frontend/applets/general_frontend.h"
36#include "core/frontend/applets/mii_edit.h"
37#include "core/frontend/applets/profile_select.h"
38#include "core/frontend/applets/software_keyboard.h"
39#include "core/frontend/applets/web_browser.h"
31#include "core/hid/hid_core.h" 40#include "core/hid/hid_core.h"
41#include "core/hle/service/am/applet_ae.h"
42#include "core/hle/service/am/applet_oe.h"
43#include "core/hle/service/am/applets/applets.h"
32#include "core/hle/service/filesystem/filesystem.h" 44#include "core/hle/service/filesystem/filesystem.h"
33#include "core/loader/loader.h" 45#include "core/loader/loader.h"
34#include "core/perf_stats.h" 46#include "core/perf_stats.h"
47#include "jni/android_common/android_common.h"
48#include "jni/applets/software_keyboard.h"
35#include "jni/config.h" 49#include "jni/config.h"
36#include "jni/emu_window/emu_window.h" 50#include "jni/emu_window/emu_window.h"
37#include "jni/id_cache.h" 51#include "jni/id_cache.h"
@@ -135,11 +149,24 @@ public:
135 m_vulkan_library); 149 m_vulkan_library);
136 150
137 // Initialize system. 151 // Initialize system.
152 auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
153 m_software_keyboard = android_keyboard.get();
138 m_system.SetShuttingDown(false); 154 m_system.SetShuttingDown(false);
139 m_system.ApplySettings(); 155 m_system.ApplySettings();
140 m_system.HIDCore().ReloadInputDevices(); 156 m_system.HIDCore().ReloadInputDevices();
141 m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); 157 m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
142 m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); 158 m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
159 m_system.SetAppletFrontendSet({
160 nullptr, // Amiibo Settings
161 nullptr, // Controller Selector
162 nullptr, // Error Display
163 nullptr, // Mii Editor
164 nullptr, // Parental Controls
165 nullptr, // Photo Viewer
166 nullptr, // Profile Selector
167 std::move(android_keyboard), // Software Keyboard
168 nullptr, // Web Browser
169 });
143 m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem()); 170 m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem());
144 171
145 // Load the ROM. 172 // Load the ROM.
@@ -233,6 +260,10 @@ public:
233 m_rom_metadata_cache.clear(); 260 m_rom_metadata_cache.clear();
234 } 261 }
235 262
263 SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard() {
264 return m_software_keyboard;
265 }
266
236private: 267private:
237 struct RomMetadata { 268 struct RomMetadata {
238 std::string title; 269 std::string title;
@@ -278,6 +309,7 @@ private:
278 std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs; 309 std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs;
279 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; 310 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
280 bool m_is_running{}; 311 bool m_is_running{};
312 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
281 313
282 // GPU driver parameters 314 // GPU driver parameters
283 std::shared_ptr<Common::DynamicLibrary> m_vulkan_library; 315 std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
@@ -290,25 +322,6 @@ private:
290 322
291/*static*/ EmulationSession EmulationSession::s_instance; 323/*static*/ EmulationSession EmulationSession::s_instance;
292 324
293std::string UTF16ToUTF8(std::u16string_view input) {
294 std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
295 return convert.to_bytes(input.data(), input.data() + input.size());
296}
297
298std::string GetJString(JNIEnv* env, jstring jstr) {
299 if (!jstr) {
300 return {};
301 }
302
303 const jchar* jchars = env->GetStringChars(jstr, nullptr);
304 const jsize length = env->GetStringLength(jstr);
305 const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
306 const std::string converted_string = UTF16ToUTF8(string_view);
307 env->ReleaseStringChars(jstr, jchars);
308
309 return converted_string;
310}
311
312} // Anonymous namespace 325} // Anonymous namespace
313 326
314static Core::SystemResultStatus RunEmulation(const std::string& filepath) { 327static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
@@ -605,4 +618,15 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo([[maybe_unused]] JNIEnv
605 LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); 618 LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
606} 619}
607 620
621void Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(JNIEnv* env, jclass clazz,
622 jstring j_text) {
623 const std::u16string input = Common::UTF8ToUTF16(GetJString(env, j_text));
624 EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input);
625}
626
627void Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(JNIEnv* env, jclass clazz,
628 jint j_key_code) {
629 EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
630}
631
608} // extern "C" 632} // 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
133JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, 133JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
134 jclass clazz); 134 jclass clazz);
135 135
136JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(
137 JNIEnv* env, jclass clazz, jstring j_text);
138
139JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(
140 JNIEnv* env, jclass clazz, jint j_key_code);
141
136#ifdef __cplusplus 142#ifdef __cplusplus
137} 143}
138#endif 144#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 @@
101 101
102 <!-- Software keyboard --> 102 <!-- Software keyboard -->
103 <string name="software_keyboard">Software Keyboard</string> 103 <string name="software_keyboard">Software Keyboard</string>
104 <string name="i_forgot">I Forgot</string>
105 <string name="fixed_length_required">Text length is not correct (should be %d characters)</string>
106 <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string>
107 <string name="blank_input_not_allowed">Blank input is not allowed</string>
108 <string name="empty_input_not_allowed">Empty input is not allowed</string>
109 104
110 <!-- Errors and warnings --> 105 <!-- Errors and warnings -->
111 <string name="abort_button">Abort</string> 106 <string name="abort_button">Abort</string>