diff options
| author | 2023-03-25 00:28:45 -0700 | |
|---|---|---|
| committer | 2023-06-03 00:05:47 -0700 | |
| commit | d5ebfc8e211c0c72a130079621f3e98532ef7f68 (patch) | |
| tree | 659c26500993c9cb7c44eded406ef67997b5a966 /src | |
| parent | android: config: Disable shader cache by default on Android. (diff) | |
| download | yuzu-d5ebfc8e211c0c72a130079621f3e98532ef7f68.tar.gz yuzu-d5ebfc8e211c0c72a130079621f3e98532ef7f68.tar.xz yuzu-d5ebfc8e211c0c72a130079621f3e98532ef7f68.zip | |
android: Implement basic software keyboard applet.
Diffstat (limited to 'src')
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 | |||
| 8 | import android.content.Intent | 8 | import android.content.Intent |
| 9 | import android.graphics.Rect | 9 | import android.graphics.Rect |
| 10 | import android.os.Bundle | 10 | import android.os.Bundle |
| 11 | import android.view.KeyEvent | ||
| 11 | import android.view.View | 12 | import android.view.View |
| 12 | import android.view.WindowManager | 13 | import android.view.WindowManager |
| 14 | import android.view.inputmethod.InputMethodManager | ||
| 13 | import androidx.appcompat.app.AppCompatActivity | 15 | import androidx.appcompat.app.AppCompatActivity |
| 14 | import androidx.fragment.app.FragmentActivity | 16 | import androidx.fragment.app.FragmentActivity |
| 15 | import androidx.preference.PreferenceManager | 17 | import 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 | ||
| 5 | package org.yuzu.yuzu_emu.applets; | 4 | package org.yuzu.yuzu_emu.applets; |
| 6 | 5 | ||
| 7 | import android.app.Activity; | 6 | import android.app.Activity; |
| 8 | import android.app.Dialog; | 7 | import android.app.Dialog; |
| 8 | import android.content.Context; | ||
| 9 | import android.content.DialogInterface; | 9 | import android.content.DialogInterface; |
| 10 | import android.graphics.Rect; | ||
| 10 | import android.os.Bundle; | 11 | import android.os.Bundle; |
| 12 | import android.os.Handler; | ||
| 13 | import android.os.ResultReceiver; | ||
| 11 | import android.text.InputFilter; | 14 | import android.text.InputFilter; |
| 12 | import android.text.Spanned; | 15 | import android.text.InputType; |
| 13 | import android.view.ViewGroup; | 16 | import android.view.ViewGroup; |
| 17 | import android.view.ViewTreeObserver; | ||
| 18 | import android.view.WindowInsets; | ||
| 19 | import android.view.inputmethod.InputMethodManager; | ||
| 14 | import android.widget.EditText; | 20 | import android.widget.EditText; |
| 15 | import android.widget.FrameLayout; | 21 | import android.widget.FrameLayout; |
| 16 | 22 | ||
| 17 | import androidx.annotation.NonNull; | 23 | import androidx.annotation.NonNull; |
| 18 | import androidx.annotation.Nullable; | ||
| 19 | import androidx.appcompat.app.AlertDialog; | 24 | import androidx.appcompat.app.AlertDialog; |
| 25 | import androidx.core.view.ViewCompat; | ||
| 20 | import androidx.fragment.app.DialogFragment; | 26 | import androidx.fragment.app.DialogFragment; |
| 21 | 27 | ||
| 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; | 28 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; |
| @@ -25,72 +31,66 @@ import org.yuzu.yuzu_emu.YuzuApplication; | |||
| 25 | import org.yuzu.yuzu_emu.NativeLibrary; | 31 | import org.yuzu.yuzu_emu.NativeLibrary; |
| 26 | import org.yuzu.yuzu_emu.R; | 32 | import org.yuzu.yuzu_emu.R; |
| 27 | import org.yuzu.yuzu_emu.activities.EmulationActivity; | 33 | import org.yuzu.yuzu_emu.activities.EmulationActivity; |
| 28 | import org.yuzu.yuzu_emu.utils.Log; | ||
| 29 | 34 | ||
| 30 | import java.util.Objects; | 35 | import java.util.Objects; |
| 31 | 36 | ||
| 32 | public final class SoftwareKeyboard { | 37 | public 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 @@ | |||
| 1 | add_library(yuzu-android SHARED | 1 | add_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 | |||
| 13 | std::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 | |||
| 27 | jstring 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 | |||
| 33 | jstring 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 | |||
| 10 | std::string GetJString(JNIEnv* env, jstring jstr); | ||
| 11 | jstring ToJString(JNIEnv* env, std::string_view str); | ||
| 12 | 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 @@ | |||
| 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 | |||
| 16 | static jclass s_software_keyboard_class; | ||
| 17 | static jclass s_keyboard_config_class; | ||
| 18 | static jclass s_keyboard_data_class; | ||
| 19 | static jmethodID s_swkbd_execute_normal; | ||
| 20 | static jmethodID s_swkbd_execute_inline; | ||
| 21 | |||
| 22 | namespace SoftwareKeyboard { | ||
| 23 | |||
| 24 | static 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 | |||
| 80 | AndroidKeyboard::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 | |||
| 89 | AndroidKeyboard::~AndroidKeyboard() = default; | ||
| 90 | |||
| 91 | void 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 | |||
| 137 | void 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 | |||
| 151 | void 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 | |||
| 157 | void 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 | |||
| 191 | void AndroidKeyboard::HideInlineKeyboard() const { | ||
| 192 | LOG_WARNING(Frontend, | ||
| 193 | "(STUBBED) called, backend requested to hide the inline software keyboard."); | ||
| 194 | } | ||
| 195 | |||
| 196 | void 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 | |||
| 211 | void AndroidKeyboard::ExitKeyboard() const { | ||
| 212 | LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard."); | ||
| 213 | } | ||
| 214 | |||
| 215 | void 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 | |||
| 226 | void 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 | |||
| 250 | void AndroidKeyboard::SubmitNormalText(const ResultData& data) const { | ||
| 251 | submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true); | ||
| 252 | } | ||
| 253 | |||
| 254 | void 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 | |||
| 271 | void 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 | |||
| 10 | namespace SoftwareKeyboard { | ||
| 11 | |||
| 12 | class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet { | ||
| 13 | public: | ||
| 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 | |||
| 43 | private: | ||
| 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 | |||
| 58 | private: | ||
| 59 | mutable bool m_is_inline_active{}; | ||
| 60 | std::u16string m_current_text; | ||
| 61 | }; | ||
| 62 | |||
| 63 | // Should be called in JNI_Load | ||
| 64 | void InitJNI(JNIEnv* env); | ||
| 65 | |||
| 66 | // Should be called in JNI_Unload | ||
| 67 | void CleanupJNI(JNIEnv* env); | ||
| 68 | |||
| 69 | } // namespace SoftwareKeyboard | ||
| 70 | |||
| 71 | // Native function calls | ||
| 72 | extern "C" { | ||
| 73 | JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters( | ||
| 74 | JNIEnv* env, jclass clazz, jstring text); | ||
| 75 | |||
| 76 | JNIEXPORT 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 | ||
| 9 | static JavaVM* s_java_vm; | 10 | static 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 | |||
| 236 | private: | 267 | private: |
| 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 | ||
| 293 | std::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 | |||
| 298 | std::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 | ||
| 314 | static Core::SystemResultStatus RunEmulation(const std::string& filepath) { | 327 | static 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 | ||
| 621 | void 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 | |||
| 627 | void 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 | |||
| 133 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, | 133 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, |
| 134 | jclass clazz); | 134 | jclass clazz); |
| 135 | 135 | ||
| 136 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText( | ||
| 137 | JNIEnv* env, jclass clazz, jstring j_text); | ||
| 138 | |||
| 139 | JNIEXPORT 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> |