From d30103b69fe8081cbcd3f5e5f4a821db4d7a7c0a Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Fri, 31 Mar 2023 21:28:49 -0400 Subject: [PATCH] android: Convert keyboard applet to kotlin and refactor --- .../yuzu_emu/activities/EmulationActivity.kt | 14 +- .../yuzu_emu/applets/SoftwareKeyboard.java | 264 ------------------ .../applets/keyboard/SoftwareKeyboard.kt | 117 ++++++++ .../keyboard/ui/KeyboardDialogFragment.kt | 100 +++++++ .../main/jni/applets/software_keyboard.cpp | 16 +- .../src/main/res/layout/dialog_edit_text.xml | 23 ++ 6 files changed, 255 insertions(+), 279 deletions(-) delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt create mode 100644 src/android/app/src/main/res/layout/dialog_edit_text.xml 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 8304c2aa5..3589e7629 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 @@ -83,22 +83,22 @@ open class EmulationActivity : AppCompatActivity() { } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (event.action == android.view.KeyEvent.ACTION_DOWN) { - if (keyCode == android.view.KeyEvent.KEYCODE_ENTER) { + if (event.action == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { // Special case, we do not support multiline input, dismiss the keyboard. val overlayView: View = - this.findViewById(R.id.surface_input_overlay) + this.findViewById(R.id.surface_input_overlay) val im = overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - im.hideSoftInputFromWindow(overlayView.windowToken, 0); + im.hideSoftInputFromWindow(overlayView.windowToken, 0) } else { - val textChar = event.getUnicodeChar(); + val textChar = event.unicodeChar if (textChar == 0) { // No text, button input. - NativeLibrary.SubmitInlineKeyboardInput(keyCode); + NativeLibrary.SubmitInlineKeyboardInput(keyCode) } else { // Text submitted. - NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString()); + NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString()) } } } 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 deleted file mode 100644 index 8ad4b1e22..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/SoftwareKeyboard.java +++ /dev/null @@ -1,264 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.applets; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.Handler; -import android.os.ResultReceiver; -import android.text.InputFilter; -import android.text.InputType; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.view.ViewCompat; -import androidx.fragment.app.DialogFragment; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.yuzu.yuzu_emu.YuzuApplication; -import org.yuzu.yuzu_emu.NativeLibrary; -import org.yuzu.yuzu_emu.R; -import org.yuzu.yuzu_emu.activities.EmulationActivity; - -import java.util.Objects; - -public final class SoftwareKeyboard { - /// Corresponds to Service::AM::Applets::SwkbdType - private interface SwkbdType { - int Normal = 0; - int NumberPad = 1; - int Qwerty = 2; - int Unknown3 = 3; - int Latin = 4; - int SimplifiedChinese = 5; - int TraditionalChinese = 6; - int Korean = 7; - }; - - /// Corresponds to Service::AM::Applets::SwkbdPasswordMode - private interface SwkbdPasswordMode { - int Disabled = 0; - int Enabled = 1; - }; - - /// Corresponds to Service::AM::Applets::SwkbdResult - private interface SwkbdResult { - int Ok = 0; - int Cancel = 1; - }; - - public static class KeyboardConfig implements java.io.Serializable { - public String ok_text; - public String header_text; - public String sub_text; - public String guide_text; - public String initial_text; - public short left_optional_symbol_key; - public short right_optional_symbol_key; - public int max_text_length; - public int min_text_length; - public int initial_cursor_position; - public int type; - public int password_mode; - public int text_draw_type; - public int key_disable_flags; - public boolean use_blur_background; - public boolean enable_backspace_button; - public boolean enable_return_button; - public boolean disable_cancel_button; - } - - /// Corresponds to Frontend::KeyboardData - public static class KeyboardData { - public int result; - public String text; - - private KeyboardData(int result, String text) { - this.result = result; - this.text = text; - } - } - - public static class KeyboardDialogFragment extends DialogFragment { - static KeyboardDialogFragment newInstance(KeyboardConfig config) { - KeyboardDialogFragment frag = new KeyboardDialogFragment(); - Bundle args = new Bundle(); - args.putSerializable("config", config); - frag.setArguments(args); - return frag; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Activity emulationActivity = getActivity(); - assert emulationActivity != null; - - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.leftMargin = params.rightMargin = - YuzuApplication.getAppContext().getResources().getDimensionPixelSize( - R.dimen.dialog_margin); - - KeyboardConfig config = Objects.requireNonNull( - (KeyboardConfig) requireArguments().getSerializable("config")); - - // Set up the input - EditText editText = new EditText(YuzuApplication.getAppContext()); - editText.setHint(config.initial_text); - editText.setSingleLine(!config.enable_return_button); - editText.setLayoutParams(params); - editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(config.max_text_length)}); - - // Handle input type - int input_type = 0; - switch (config.type) - { - case SwkbdType.Normal: - case SwkbdType.Qwerty: - case SwkbdType.Unknown3: - case SwkbdType.Latin: - case SwkbdType.SimplifiedChinese: - case SwkbdType.TraditionalChinese: - case SwkbdType.Korean: - default: - input_type = InputType.TYPE_CLASS_TEXT; - if (config.password_mode == SwkbdPasswordMode.Enabled) - { - input_type |= InputType.TYPE_TEXT_VARIATION_PASSWORD; - } - break; - case SwkbdType.NumberPad: - input_type = InputType.TYPE_CLASS_NUMBER; - if (config.password_mode == SwkbdPasswordMode.Enabled) - { - input_type |= InputType.TYPE_NUMBER_VARIATION_PASSWORD; - } - break; - } - - // Apply input type - editText.setInputType(input_type); - - FrameLayout container = new FrameLayout(emulationActivity); - container.addView(editText); - - String headerText = config.header_text.isEmpty() ? emulationActivity.getString(R.string.software_keyboard) : config.header_text; - String okText = config.header_text.isEmpty() ? emulationActivity.getString(android.R.string.ok) : config.ok_text; - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity) - .setTitle(headerText) - .setView(container); - setCancelable(false); - - builder.setPositiveButton(okText, null); - builder.setNegativeButton(emulationActivity.getString(android.R.string.cancel), null); - - final AlertDialog dialog = builder.create(); - dialog.create(); - if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { - data.result = SwkbdResult.Ok; - data.text = editText.getText().toString(); - dialog.dismiss(); - - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { - dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { - data.result = SwkbdResult.Ok; - dialog.dismiss(); - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { - data.result = SwkbdResult.Cancel; - dialog.dismiss(); - synchronized (finishLock) { - finishLock.notifyAll(); - } - }); - } - - return dialog; - } - } - - private static KeyboardData data; - private static final Object finishLock = new Object(); - - private static void ExecuteNormalImpl(KeyboardConfig config) { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - - data = new KeyboardData(SwkbdResult.Cancel, ""); - - KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); - fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); - } - - private static void ExecuteInlineImpl(KeyboardConfig config) { - final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); - - var overlayView = emulationActivity.findViewById(R.id.surface_input_overlay); - InputMethodManager im = (InputMethodManager)overlayView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED); - - // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result. - final Handler handler = new Handler(); - final int delayMs = 500; - handler.postDelayed(new Runnable() { - public void run() { - var insets = ViewCompat.getRootWindowInsets(overlayView); - var isKeyboardVisible = insets.isVisible(WindowInsets.Type.ime()); - if (isKeyboardVisible) { - handler.postDelayed(this, delayMs); - return; - } - - // No longer visible, submit the result. - NativeLibrary.SubmitInlineKeyboardInput(android.view.KeyEvent.KEYCODE_ENTER); - } - }, delayMs); - } - - public static KeyboardData ExecuteNormal(KeyboardConfig config) { - NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteNormalImpl(config)); - - synchronized (finishLock) { - try { - finishLock.wait(); - } catch (Exception ignored) { - } - } - - return data; - } - - public static void ExecuteInline(KeyboardConfig config) { - NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteInlineImpl(config)); - } - - public static void ShowError(String error) { - NativeLibrary.displayAlertMsg( - YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard), - error, false); - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt new file mode 100644 index 000000000..704109ec0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard.kt @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.applets.keyboard + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.KeyEvent +import android.view.View +import android.view.WindowInsets +import android.view.inputmethod.InputMethodManager +import androidx.core.view.ViewCompat +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment +import java.io.Serializable + +object SoftwareKeyboard { + lateinit var data: KeyboardData + val dataLock = Object() + + private fun executeNormalImpl(config: KeyboardConfig) { + val emulationActivity = NativeLibrary.sEmulationActivity.get() + data = KeyboardData(SwkbdResult.Cancel.ordinal, "") + val fragment = KeyboardDialogFragment.newInstance(config) + fragment.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG) + } + + private fun executeInlineImpl(config: KeyboardConfig) { + val emulationActivity = NativeLibrary.sEmulationActivity.get() + + val overlayView = emulationActivity!!.findViewById(R.id.surface_input_overlay) + val im = + overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED) + + // There isn't a good way to know that the IMM is dismissed, so poll every 500ms to submit inline keyboard result. + val handler = Handler(Looper.myLooper()!!) + val delayMs = 500 + handler.postDelayed(object : Runnable { + override fun run() { + val insets = ViewCompat.getRootWindowInsets(overlayView) + val isKeyboardVisible = insets!!.isVisible(WindowInsets.Type.ime()) + if (isKeyboardVisible) { + handler.postDelayed(this, delayMs.toLong()) + return + } + + // No longer visible, submit the result. + NativeLibrary.SubmitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) + } + }, delayMs.toLong()) + } + + @JvmStatic + fun executeNormal(config: KeyboardConfig): KeyboardData { + NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeNormalImpl(config) } + synchronized(dataLock) { + dataLock.wait() + } + return data + } + + @JvmStatic + fun executeInline(config: KeyboardConfig) { + NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { executeInlineImpl(config) } + } + + // Corresponds to Service::AM::Applets::SwkbdType + enum class SwkbdType { + Normal, + NumberPad, + Qwerty, + Unknown3, + Latin, + SimplifiedChinese, + TraditionalChinese, + Korean + } + + // Corresponds to Service::AM::Applets::SwkbdPasswordMode + enum class SwkbdPasswordMode { + Disabled, + Enabled + } + + // Corresponds to Service::AM::Applets::SwkbdResult + enum class SwkbdResult { + Ok, + Cancel + } + + data class KeyboardConfig( + var ok_text: String? = null, + var header_text: String? = null, + var sub_text: String? = null, + var guide_text: String? = null, + var initial_text: String? = null, + var left_optional_symbol_key: Short = 0, + var right_optional_symbol_key: Short = 0, + var max_text_length: Int = 0, + var min_text_length: Int = 0, + var initial_cursor_position: Int = 0, + var type: Int = 0, + var password_mode: Int = 0, + var text_draw_type: Int = 0, + var key_disable_flags: Int = 0, + var use_blur_background: Boolean = false, + var enable_backspace_button: Boolean = false, + var enable_return_button: Boolean = false, + var disable_cancel_button: Boolean = false + ) : Serializable + + // Corresponds to Frontend::KeyboardData + data class KeyboardData(var result: Int, var text: String) +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt new file mode 100644 index 000000000..2428bd261 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/applets/keyboard/ui/KeyboardDialogFragment.kt @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.applets.keyboard.ui + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.text.InputFilter +import android.text.InputType +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard +import org.yuzu.yuzu_emu.applets.keyboard.SoftwareKeyboard.KeyboardConfig +import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding +import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable + +class KeyboardDialogFragment : DialogFragment() { + private lateinit var binding: DialogEditTextBinding + private lateinit var config: KeyboardConfig + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogEditTextBinding.inflate(layoutInflater) + config = requireArguments().serializable(CONFIG)!! + + // Set up the input + binding.editText.hint = config.initial_text + binding.editText.isSingleLine = !config.enable_return_button + binding.editText.filters = + arrayOf(InputFilter.LengthFilter(config.max_text_length)) + + // Handle input type + var inputType: Int + when (config.type) { + SoftwareKeyboard.SwkbdType.Normal.ordinal, + SoftwareKeyboard.SwkbdType.Qwerty.ordinal, + SoftwareKeyboard.SwkbdType.Unknown3.ordinal, + SoftwareKeyboard.SwkbdType.Latin.ordinal, + SoftwareKeyboard.SwkbdType.SimplifiedChinese.ordinal, + SoftwareKeyboard.SwkbdType.TraditionalChinese.ordinal, + SoftwareKeyboard.SwkbdType.Korean.ordinal -> { + inputType = InputType.TYPE_CLASS_TEXT + if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) { + inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + } + SoftwareKeyboard.SwkbdType.NumberPad.ordinal -> { + inputType = InputType.TYPE_CLASS_NUMBER + if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) { + inputType = inputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD + } + } + else -> { + inputType = InputType.TYPE_CLASS_TEXT + if (config.password_mode == SoftwareKeyboard.SwkbdPasswordMode.Enabled.ordinal) { + inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD + } + } + } + binding.editText.inputType = inputType + + val headerText = + config.header_text!!.ifEmpty { resources.getString(R.string.software_keyboard) } + val okText = + if (config.header_text!!.isEmpty()) resources.getString(android.R.string.ok) else config.ok_text!! + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(headerText) + .setView(binding.root) + .setPositiveButton(okText) { _, _ -> + SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Ok.ordinal + SoftwareKeyboard.data.text = binding.editText.text.toString() + } + .setNegativeButton(resources.getString(android.R.string.cancel)) { _, _ -> + SoftwareKeyboard.data.result = SoftwareKeyboard.SwkbdResult.Cancel.ordinal + } + .create() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + synchronized(SoftwareKeyboard.dataLock) { + SoftwareKeyboard.dataLock.notifyAll() + } + } + + companion object { + const val TAG = "KeyboardDialogFragment" + const val CONFIG = "keyboard_config" + + fun newInstance(config: KeyboardConfig?): KeyboardDialogFragment { + val frag = KeyboardDialogFragment() + val args = Bundle() + args.putSerializable(CONFIG, config) + frag.arguments = args + return frag + } + } +} diff --git a/src/android/app/src/main/jni/applets/software_keyboard.cpp b/src/android/app/src/main/jni/applets/software_keyboard.cpp index 278137b4c..c6fffbbab 100644 --- a/src/android/app/src/main/jni/applets/software_keyboard.cpp +++ b/src/android/app/src/main/jni/applets/software_keyboard.cpp @@ -253,19 +253,19 @@ void AndroidKeyboard::SubmitNormalText(const ResultData& data) const { void InitJNI(JNIEnv* env) { s_software_keyboard_class = reinterpret_cast( - env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard"))); + env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard"))); s_keyboard_config_class = reinterpret_cast(env->NewGlobalRef( - env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig"))); + env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig"))); s_keyboard_data_class = reinterpret_cast(env->NewGlobalRef( - env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardData"))); + env->FindClass("org/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardData"))); s_swkbd_execute_normal = env->GetStaticMethodID( - s_software_keyboard_class, "ExecuteNormal", - "(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/" - "applets/SoftwareKeyboard$KeyboardData;"); + s_software_keyboard_class, "executeNormal", + "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/" + "applets/keyboard/SoftwareKeyboard$KeyboardData;"); s_swkbd_execute_inline = - env->GetStaticMethodID(s_software_keyboard_class, "ExecuteInline", - "(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)V"); + env->GetStaticMethodID(s_software_keyboard_class, "executeInline", + "(Lorg/yuzu/yuzu_emu/applets/keyboard/SoftwareKeyboard$KeyboardConfig;)V"); } void CleanupJNI(JNIEnv* env) { diff --git a/src/android/app/src/main/res/layout/dialog_edit_text.xml b/src/android/app/src/main/res/layout/dialog_edit_text.xml new file mode 100644 index 000000000..58b905d71 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_edit_text.xml @@ -0,0 +1,23 @@ + + + + + + + + + +