android: Implement basic software keyboard applet.

master
bunnei 2023-03-25 00:28:45 +07:00
parent 58ede89c60
commit d5ebfc8e21
12 changed files with 624 additions and 151 deletions

@ -632,6 +632,18 @@ public final class NativeLibrary {
*/ */
public static native void LogDeviceInfo(); public static native void LogDeviceInfo();
/**
* Submits inline keyboard text. Called on input for buttons that result text.
* @param text Text to submit to the inline software keyboard implementation.
*/
public static native void SubmitInlineKeyboardText(String text);
/**
* Submits inline keyboard input. Used to indicate keys pressed that are not text.
* @param key_code Android Key Code associated with the keyboard input.
*/
public static native void SubmitInlineKeyboardInput(int key_code);
/** /**
* Button type for use in onTouchEvent * Button type for use in onTouchEvent
*/ */

@ -8,8 +8,10 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -80,6 +82,29 @@ open class EmulationActivity : AppCompatActivity() {
//startForegroundService(foregroundService); //startForegroundService(foregroundService);
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (event.action == android.view.KeyEvent.ACTION_DOWN) {
if (keyCode == android.view.KeyEvent.KEYCODE_ENTER) {
// Special case, we do not support multiline input, dismiss the keyboard.
val overlayView: View =
this.findViewById<View>(R.id.surface_input_overlay)
val im =
overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
im.hideSoftInputFromWindow(overlayView.windowToken, 0);
} else {
val textChar = event.getUnicodeChar();
if (textChar == 0) {
// No text, button input.
NativeLibrary.SubmitInlineKeyboardInput(keyCode);
} else {
// Text submitted.
NativeLibrary.SubmitInlineKeyboardText(textChar.toChar().toString());
}
}
}
return super.onKeyDown(keyCode, event)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelable(EXTRA_SELECTED_GAME, game) outState.putParcelable(EXTRA_SELECTED_GAME, game)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)

@ -1,22 +1,28 @@
// Copyright 2020 Citra Emulator Project // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// Licensed under GPLv2 or any later version // SPDX-License-Identifier: GPL-2.0-or-later
// Refer to the license.txt file included.
package org.yuzu.yuzu_emu.applets; package org.yuzu.yuzu_emu.applets;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.graphics.Rect;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.Spanned; import android.text.InputType;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText; import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -25,72 +31,66 @@ import org.yuzu.yuzu_emu.YuzuApplication;
import org.yuzu.yuzu_emu.NativeLibrary; import org.yuzu.yuzu_emu.NativeLibrary;
import org.yuzu.yuzu_emu.R; import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.activities.EmulationActivity; import org.yuzu.yuzu_emu.activities.EmulationActivity;
import org.yuzu.yuzu_emu.utils.Log;
import java.util.Objects; import java.util.Objects;
public final class SoftwareKeyboard { public final class SoftwareKeyboard {
/// Corresponds to Frontend::ButtonConfig /// Corresponds to Service::AM::Applets::SwkbdType
private interface ButtonConfig { private interface SwkbdType {
int Single = 0; /// Ok button int Normal = 0;
int Dual = 1; /// Cancel | Ok buttons int NumberPad = 1;
int Triple = 2; /// Cancel | I Forgot | Ok buttons int Qwerty = 2;
int None = 3; /// No button (returned by swkbdInputText in special cases) int Unknown3 = 3;
} int Latin = 4;
int SimplifiedChinese = 5;
int TraditionalChinese = 6;
int Korean = 7;
};
/// Corresponds to Frontend::ValidationError /// Corresponds to Service::AM::Applets::SwkbdPasswordMode
public enum ValidationError { private interface SwkbdPasswordMode {
None, int Disabled = 0;
// Button Selection int Enabled = 1;
ButtonOutOfRange, };
// Configured Filters
MaxDigitsExceeded, /// Corresponds to Service::AM::Applets::SwkbdResult
AtSignNotAllowed, private interface SwkbdResult {
PercentNotAllowed, int Ok = 0;
BackslashNotAllowed, int Cancel = 1;
ProfanityNotAllowed, };
CallbackFailed,
// Allowed Input Type
FixedLengthRequired,
MaxLengthExceeded,
BlankInputNotAllowed,
EmptyInputNotAllowed,
}
public static class KeyboardConfig implements java.io.Serializable { public static class KeyboardConfig implements java.io.Serializable {
public int button_config; public String ok_text;
public String header_text;
public String sub_text;
public String guide_text;
public String initial_text;
public short left_optional_symbol_key;
public short right_optional_symbol_key;
public int max_text_length; public int max_text_length;
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input public int min_text_length;
public String hint_text; /// Displayed in the field as a hint before public int initial_cursor_position;
@Nullable public int type;
public String[] button_text; /// Contains the button text that the caller provides 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 /// Corresponds to Frontend::KeyboardData
public static class KeyboardData { public static class KeyboardData {
public int button; public int result;
public String text; public String text;
private KeyboardData(int button, String text) { private KeyboardData(int result, String text) {
this.button = button; this.result = result;
this.text = text; this.text = text;
} }
} }
private static class Filter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
int dstart, int dend) {
String text = new StringBuilder(dest)
.replace(dstart, dend, source.subSequence(start, end).toString())
.toString();
if (ValidateFilters(text) == ValidationError.None) {
return null; // Accept replacement
}
return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
}
}
public static class KeyboardDialogFragment extends DialogFragment { public static class KeyboardDialogFragment extends DialogFragment {
static KeyboardDialogFragment newInstance(KeyboardConfig config) { static KeyboardDialogFragment newInstance(KeyboardConfig config) {
KeyboardDialogFragment frag = new KeyboardDialogFragment(); KeyboardDialogFragment frag = new KeyboardDialogFragment();
@ -113,60 +113,65 @@ public final class SoftwareKeyboard {
R.dimen.dialog_margin); R.dimen.dialog_margin);
KeyboardConfig config = Objects.requireNonNull( KeyboardConfig config = Objects.requireNonNull(
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); (KeyboardConfig) requireArguments().getSerializable("config"));
// Set up the input // Set up the input
EditText editText = new EditText(YuzuApplication.getAppContext()); EditText editText = new EditText(YuzuApplication.getAppContext());
editText.setHint(config.hint_text); editText.setHint(config.initial_text);
editText.setSingleLine(!config.multiline_mode); editText.setSingleLine(!config.enable_return_button);
editText.setLayoutParams(params); editText.setLayoutParams(params);
editText.setFilters(new InputFilter[]{ editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(config.max_text_length)});
new Filter(), 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); FrameLayout container = new FrameLayout(emulationActivity);
container.addView(editText); 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) MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(R.string.software_keyboard) .setTitle(headerText)
.setView(container); .setView(container);
setCancelable(false); setCancelable(false);
switch (config.button_config) { builder.setPositiveButton(okText, null);
case ButtonConfig.Triple: { builder.setNegativeButton(emulationActivity.getString(android.R.string.cancel), null);
final String text = config.button_text[1].isEmpty()
? emulationActivity.getString(R.string.i_forgot)
: config.button_text[1];
builder.setNeutralButton(text, null);
}
// fallthrough
case ButtonConfig.Dual: {
final String text = config.button_text[0].isEmpty()
? emulationActivity.getString(android.R.string.cancel)
: config.button_text[0];
builder.setNegativeButton(text, null);
}
// fallthrough
case ButtonConfig.Single: {
final String text = config.button_text[2].isEmpty()
? emulationActivity.getString(android.R.string.ok)
: config.button_text[2];
builder.setPositiveButton(text, null);
break;
}
}
final AlertDialog dialog = builder.create(); final AlertDialog dialog = builder.create();
dialog.create(); dialog.create();
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
data.button = config.button_config; data.result = SwkbdResult.Ok;
data.text = editText.getText().toString(); data.text = editText.getText().toString();
final ValidationError error = ValidateInput(data.text);
if (error != ValidationError.None) {
HandleValidationError(config, error);
return;
}
dialog.dismiss(); dialog.dismiss();
synchronized (finishLock) { synchronized (finishLock) {
@ -176,7 +181,7 @@ public final class SoftwareKeyboard {
} }
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
data.button = 1; data.result = SwkbdResult.Ok;
dialog.dismiss(); dialog.dismiss();
synchronized (finishLock) { synchronized (finishLock) {
finishLock.notifyAll(); finishLock.notifyAll();
@ -185,7 +190,7 @@ public final class SoftwareKeyboard {
} }
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
data.button = 0; data.result = SwkbdResult.Cancel;
dialog.dismiss(); dialog.dismiss();
synchronized (finishLock) { synchronized (finishLock) {
finishLock.notifyAll(); finishLock.notifyAll();
@ -200,49 +205,42 @@ public final class SoftwareKeyboard {
private static KeyboardData data; private static KeyboardData data;
private static final Object finishLock = new Object(); private static final Object finishLock = new Object();
private static void ExecuteImpl(KeyboardConfig config) { private static void ExecuteNormalImpl(KeyboardConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
data = new KeyboardData(0, ""); data = new KeyboardData(SwkbdResult.Cancel, "");
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
} }
private static void HandleValidationError(KeyboardConfig config, ValidationError error) { private static void ExecuteInlineImpl(KeyboardConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
String message = "";
switch (error) {
case FixedLengthRequired:
message =
emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
break;
case MaxLengthExceeded:
message =
emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
break;
case BlankInputNotAllowed:
message = emulationActivity.getString(R.string.blank_input_not_allowed);
break;
case EmptyInputNotAllowed:
message = emulationActivity.getString(R.string.empty_input_not_allowed);
break;
}
new MaterialAlertDialogBuilder(emulationActivity) var overlayView = emulationActivity.findViewById(R.id.surface_input_overlay);
.setTitle(R.string.software_keyboard) InputMethodManager im = (InputMethodManager)overlayView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
.setMessage(message) im.showSoftInput(overlayView, InputMethodManager.SHOW_FORCED);
.setPositiveButton(android.R.string.ok, null)
.show(); // 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 Execute(KeyboardConfig config) { public static KeyboardData ExecuteNormal(KeyboardConfig config) {
if (config.button_config == ButtonConfig.None) { NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteNormalImpl(config));
Log.error("Unexpected button config None");
return new KeyboardData(0, "");
}
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
synchronized (finishLock) { synchronized (finishLock) {
try { try {
@ -254,13 +252,13 @@ public final class SoftwareKeyboard {
return data; return data;
} }
public static void ExecuteInline(KeyboardConfig config) {
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteInlineImpl(config));
}
public static void ShowError(String error) { public static void ShowError(String error) {
NativeLibrary.displayAlertMsg( NativeLibrary.displayAlertMsg(
YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard), YuzuApplication.getAppContext().getResources().getString(R.string.software_keyboard),
error, false); error, false);
} }
private static native ValidationError ValidateFilters(String text);
private static native ValidationError ValidateInput(String text);
} }

@ -1,4 +1,8 @@
add_library(yuzu-android SHARED add_library(yuzu-android SHARED
android_common/android_common.cpp
android_common/android_common.h
applets/software_keyboard.cpp
applets/software_keyboard.h
config.cpp config.cpp
config.h config.h
default_ini.h default_ini.h

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "jni/android_common/android_common.h"
#include <string>
#include <string_view>
#include <jni.h>
#include "common/string_util.h"
std::string GetJString(JNIEnv* env, jstring jstr) {
if (!jstr) {
return {};
}
const jchar* jchars = env->GetStringChars(jstr, nullptr);
const jsize length = env->GetStringLength(jstr);
const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
const std::string converted_string = Common::UTF16ToUTF8(string_view);
env->ReleaseStringChars(jstr, jchars);
return converted_string;
}
jstring ToJString(JNIEnv* env, std::string_view str) {
const std::u16string converted_string = Common::UTF8ToUTF16(str);
return env->NewString(reinterpret_cast<const jchar*>(converted_string.data()),
static_cast<jint>(converted_string.size()));
}
jstring ToJString(JNIEnv* env, std::u16string_view str) {
return ToJString(env, Common::UTF16ToUTF8(str));
}

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include <jni.h>
std::string GetJString(JNIEnv* env, jstring jstr);
jstring ToJString(JNIEnv* env, std::string_view str);
jstring ToJString(JNIEnv* env, std::u16string_view str);

@ -0,0 +1,277 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <thread>
#include <jni.h>
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/core.h"
#include "jni/android_common/android_common.h"
#include "jni/applets/software_keyboard.h"
#include "jni/id_cache.h"
static jclass s_software_keyboard_class;
static jclass s_keyboard_config_class;
static jclass s_keyboard_data_class;
static jmethodID s_swkbd_execute_normal;
static jmethodID s_swkbd_execute_inline;
namespace SoftwareKeyboard {
static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) {
JNIEnv* env = IDCache::GetEnvForThread();
jobject object = env->AllocObject(s_keyboard_config_class);
env->SetObjectField(object,
env->GetFieldID(s_keyboard_config_class, "ok_text", "Ljava/lang/String;"),
ToJString(env, config.ok_text));
env->SetObjectField(
object, env->GetFieldID(s_keyboard_config_class, "header_text", "Ljava/lang/String;"),
ToJString(env, config.header_text));
env->SetObjectField(object,
env->GetFieldID(s_keyboard_config_class, "sub_text", "Ljava/lang/String;"),
ToJString(env, config.sub_text));
env->SetObjectField(
object, env->GetFieldID(s_keyboard_config_class, "guide_text", "Ljava/lang/String;"),
ToJString(env, config.guide_text));
env->SetObjectField(
object, env->GetFieldID(s_keyboard_config_class, "initial_text", "Ljava/lang/String;"),
ToJString(env, config.initial_text));
env->SetShortField(object,
env->GetFieldID(s_keyboard_config_class, "left_optional_symbol_key", "S"),
static_cast<jshort>(config.left_optional_symbol_key));
env->SetShortField(object,
env->GetFieldID(s_keyboard_config_class, "right_optional_symbol_key", "S"),
static_cast<jshort>(config.right_optional_symbol_key));
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
static_cast<jint>(config.max_text_length));
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "min_text_length", "I"),
static_cast<jint>(config.min_text_length));
env->SetIntField(object,
env->GetFieldID(s_keyboard_config_class, "initial_cursor_position", "I"),
static_cast<jint>(config.initial_cursor_position));
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "type", "I"),
static_cast<jint>(config.type));
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "password_mode", "I"),
static_cast<jint>(config.password_mode));
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "text_draw_type", "I"),
static_cast<jint>(config.text_draw_type));
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "key_disable_flags", "I"),
static_cast<jint>(config.key_disable_flags.raw));
env->SetBooleanField(object,
env->GetFieldID(s_keyboard_config_class, "use_blur_background", "Z"),
static_cast<jboolean>(config.use_blur_background));
env->SetBooleanField(object,
env->GetFieldID(s_keyboard_config_class, "enable_backspace_button", "Z"),
static_cast<jboolean>(config.enable_backspace_button));
env->SetBooleanField(object,
env->GetFieldID(s_keyboard_config_class, "enable_return_button", "Z"),
static_cast<jboolean>(config.enable_return_button));
env->SetBooleanField(object,
env->GetFieldID(s_keyboard_config_class, "disable_cancel_button", "Z"),
static_cast<jboolean>(config.disable_cancel_button));
return object;
}
AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobject object) {
JNIEnv* env = IDCache::GetEnvForThread();
const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
return ResultData{GetJString(env, string),
static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField(
object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
}
AndroidKeyboard::~AndroidKeyboard() = default;
void AndroidKeyboard::InitializeKeyboard(
bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters,
SubmitNormalCallback submit_normal_callback_, SubmitInlineCallback submit_inline_callback_) {
if (is_inline) {
LOG_WARNING(
Frontend,
"(STUBBED) called, backend requested to initialize the inline software keyboard.");
submit_inline_callback = std::move(submit_inline_callback_);
} else {
LOG_WARNING(
Frontend,
"(STUBBED) called, backend requested to initialize the normal software keyboard.");
submit_normal_callback = std::move(submit_normal_callback_);
}
parameters = std::move(initialize_parameters);
LOG_INFO(Frontend,
"\nKeyboardInitializeParameters:"
"\nok_text={}"
"\nheader_text={}"
"\nsub_text={}"
"\nguide_text={}"
"\ninitial_text={}"
"\nmax_text_length={}"
"\nmin_text_length={}"
"\ninitial_cursor_position={}"
"\ntype={}"
"\npassword_mode={}"
"\ntext_draw_type={}"
"\nkey_disable_flags={}"
"\nuse_blur_background={}"
"\nenable_backspace_button={}"
"\nenable_return_button={}"
"\ndisable_cancel_button={}",
Common::UTF16ToUTF8(parameters.ok_text), Common::UTF16ToUTF8(parameters.header_text),
Common::UTF16ToUTF8(parameters.sub_text), Common::UTF16ToUTF8(parameters.guide_text),
Common::UTF16ToUTF8(parameters.initial_text), parameters.max_text_length,
parameters.min_text_length, parameters.initial_cursor_position, parameters.type,
parameters.password_mode, parameters.text_draw_type, parameters.key_disable_flags.raw,
parameters.use_blur_background, parameters.enable_backspace_button,
parameters.enable_return_button, parameters.disable_cancel_button);
}
void AndroidKeyboard::ShowNormalKeyboard() const {
LOG_DEBUG(Frontend, "called, backend requested to show the normal software keyboard.");
ResultData data{};
// Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
std::thread([&] {
data = ResultData::CreateFromFrontend(IDCache::GetEnvForThread()->CallStaticObjectMethod(
s_software_keyboard_class, s_swkbd_execute_normal, ToJKeyboardParams(parameters)));
}).join();
SubmitNormalText(data);
}
void AndroidKeyboard::ShowTextCheckDialog(
Service::AM::Applets::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const {
LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
}
void AndroidKeyboard::ShowInlineKeyboard(
Core::Frontend::InlineAppearParameters appear_parameters) const {
LOG_WARNING(Frontend,
"(STUBBED) called, backend requested to show the inline software keyboard.");
LOG_INFO(Frontend,
"\nInlineAppearParameters:"
"\nmax_text_length={}"
"\nmin_text_length={}"
"\nkey_top_scale_x={}"
"\nkey_top_scale_y={}"
"\nkey_top_translate_x={}"
"\nkey_top_translate_y={}"
"\ntype={}"
"\nkey_disable_flags={}"
"\nkey_top_as_floating={}"
"\nenable_backspace_button={}"
"\nenable_return_button={}"
"\ndisable_cancel_button={}",
appear_parameters.max_text_length, appear_parameters.min_text_length,
appear_parameters.key_top_scale_x, appear_parameters.key_top_scale_y,
appear_parameters.key_top_translate_x, appear_parameters.key_top_translate_y,
appear_parameters.type, appear_parameters.key_disable_flags.raw,
appear_parameters.key_top_as_floating, appear_parameters.enable_backspace_button,
appear_parameters.enable_return_button, appear_parameters.disable_cancel_button);
// Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
m_is_inline_active = true;
std::thread([&] {
IDCache::GetEnvForThread()->CallStaticVoidMethod(
s_software_keyboard_class, s_swkbd_execute_inline, ToJKeyboardParams(parameters));
}).join();
}
void AndroidKeyboard::HideInlineKeyboard() const {
LOG_WARNING(Frontend,
"(STUBBED) called, backend requested to hide the inline software keyboard.");
}
void AndroidKeyboard::InlineTextChanged(
Core::Frontend::InlineTextParameters text_parameters) const {
LOG_WARNING(Frontend,
"(STUBBED) called, backend requested to change the inline keyboard text.");
LOG_INFO(Frontend,
"\nInlineTextParameters:"
"\ninput_text={}"
"\ncursor_position={}",
Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString,
text_parameters.input_text, text_parameters.cursor_position);
}
void AndroidKeyboard::ExitKeyboard() const {
LOG_WARNING(Frontend, "(STUBBED) called, backend requested to exit the software keyboard.");
}
void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
if (!m_is_inline_active) {
return;
}
m_current_text += submitted_text;
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
m_current_text.size());
}
void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
static constexpr int KEYCODE_BACK = 4;
static constexpr int KEYCODE_ENTER = 66;
static constexpr int KEYCODE_DEL = 67;
if (!m_is_inline_active) {
return;
}
switch (key_code) {
case KEYCODE_BACK:
case KEYCODE_ENTER:
m_is_inline_active = false;
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text,
static_cast<s32>(m_current_text.size()));
break;
case KEYCODE_DEL:
m_current_text.pop_back();
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
m_current_text.size());
break;
}
}
void AndroidKeyboard::SubmitNormalText(const ResultData& data) const {
submit_normal_callback(data.result, Common::UTF8ToUTF16(data.text), true);
}
void InitJNI(JNIEnv* env) {
s_software_keyboard_class = reinterpret_cast<jclass>(
env->NewGlobalRef(env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard")));
s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig")));
s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
env->FindClass("org/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardData")));
s_swkbd_execute_normal = env->GetStaticMethodID(
s_software_keyboard_class, "ExecuteNormal",
"(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/yuzu/yuzu_emu/"
"applets/SoftwareKeyboard$KeyboardData;");
s_swkbd_execute_inline =
env->GetStaticMethodID(s_software_keyboard_class, "ExecuteInline",
"(Lorg/yuzu/yuzu_emu/applets/SoftwareKeyboard$KeyboardConfig;)V");
}
void CleanupJNI(JNIEnv* env) {
env->DeleteGlobalRef(s_software_keyboard_class);
env->DeleteGlobalRef(s_keyboard_config_class);
env->DeleteGlobalRef(s_keyboard_data_class);
}
} // namespace SoftwareKeyboard

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <jni.h>
#include "core/frontend/applets/software_keyboard.h"
namespace SoftwareKeyboard {
class AndroidKeyboard final : public Core::Frontend::SoftwareKeyboardApplet {
public:
~AndroidKeyboard() override;
void Close() const override {
ExitKeyboard();
}
void InitializeKeyboard(bool is_inline,
Core::Frontend::KeyboardInitializeParameters initialize_parameters,
SubmitNormalCallback submit_normal_callback_,
SubmitInlineCallback submit_inline_callback_) override;
void ShowNormalKeyboard() const override;
void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const override;
void ShowInlineKeyboard(
Core::Frontend::InlineAppearParameters appear_parameters) const override;
void HideInlineKeyboard() const override;
void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override;
void ExitKeyboard() const override;
void SubmitInlineKeyboardText(std::u16string submitted_text);
void SubmitInlineKeyboardInput(int key_code);
private:
struct ResultData {
static ResultData CreateFromFrontend(jobject object);
std::string text;
Service::AM::Applets::SwkbdResult result{};
};
void SubmitNormalText(const ResultData& result) const;
Core::Frontend::KeyboardInitializeParameters parameters{};
mutable SubmitNormalCallback submit_normal_callback;
mutable SubmitInlineCallback submit_inline_callback;
private:
mutable bool m_is_inline_active{};
std::u16string m_current_text;
};
// Should be called in JNI_Load
void InitJNI(JNIEnv* env);
// Should be called in JNI_Unload
void CleanupJNI(JNIEnv* env);
} // namespace SoftwareKeyboard
// Native function calls
extern "C" {
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(
JNIEnv* env, jclass clazz, jstring text);
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(
JNIEnv* env, jclass clazz, jstring text);
}

@ -4,6 +4,7 @@
#include <jni.h> #include <jni.h>
#include "common/fs/fs_android.h" #include "common/fs/fs_android.h"
#include "jni/applets/software_keyboard.h"
#include "jni/id_cache.h" #include "jni/id_cache.h"
static JavaVM* s_java_vm; static JavaVM* s_java_vm;
@ -63,6 +64,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
// Initialize Android Storage // Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class); Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
// Initialize applets
SoftwareKeyboard::InitJNI(env);
return JNI_VERSION; return JNI_VERSION;
} }
@ -75,6 +79,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
// UnInitialize Android Storage // UnInitialize Android Storage
Common::FS::Android::UnRegisterCallbacks(); Common::FS::Android::UnRegisterCallbacks();
env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_native_library_class);
// UnInitialze applets
SoftwareKeyboard::CleanupJNI(env);
} }
#ifdef __cplusplus #ifdef __cplusplus

@ -23,15 +23,29 @@
#include "common/scm_rev.h" #include "common/scm_rev.h"
#include "common/scope_exit.h" #include "common/scope_exit.h"
#include "common/settings.h" #include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h" #include "core/core.h"
#include "core/cpu_manager.h" #include "core/cpu_manager.h"
#include "core/crypto/key_manager.h" #include "core/crypto/key_manager.h"
#include "core/file_sys/registered_cache.h" #include "core/file_sys/registered_cache.h"
#include "core/file_sys/vfs_real.h" #include "core/file_sys/vfs_real.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h"
#include "core/frontend/applets/general_frontend.h"
#include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
#include "core/hid/hid_core.h" #include "core/hid/hid_core.h"
#include "core/hle/service/am/applet_ae.h"
#include "core/hle/service/am/applet_oe.h"
#include "core/hle/service/am/applets/applets.h"
#include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "core/perf_stats.h" #include "core/perf_stats.h"
#include "jni/android_common/android_common.h"
#include "jni/applets/software_keyboard.h"
#include "jni/config.h" #include "jni/config.h"
#include "jni/emu_window/emu_window.h" #include "jni/emu_window/emu_window.h"
#include "jni/id_cache.h" #include "jni/id_cache.h"
@ -135,11 +149,24 @@ public:
m_vulkan_library); m_vulkan_library);
// Initialize system. // Initialize system.
auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>();
m_software_keyboard = android_keyboard.get();
m_system.SetShuttingDown(false); m_system.SetShuttingDown(false);
m_system.ApplySettings(); m_system.ApplySettings();
m_system.HIDCore().ReloadInputDevices(); m_system.HIDCore().ReloadInputDevices();
m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
m_system.SetAppletFrontendSet({
nullptr, // Amiibo Settings
nullptr, // Controller Selector
nullptr, // Error Display
nullptr, // Mii Editor
nullptr, // Parental Controls
nullptr, // Photo Viewer
nullptr, // Profile Selector
std::move(android_keyboard), // Software Keyboard
nullptr, // Web Browser
});
m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem()); m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem());
// Load the ROM. // Load the ROM.
@ -233,6 +260,10 @@ public:
m_rom_metadata_cache.clear(); m_rom_metadata_cache.clear();
} }
SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard() {
return m_software_keyboard;
}
private: private:
struct RomMetadata { struct RomMetadata {
std::string title; std::string title;
@ -278,6 +309,7 @@ private:
std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs; std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{}; bool m_is_running{};
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
// GPU driver parameters // GPU driver parameters
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library; std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
@ -290,25 +322,6 @@ private:
/*static*/ EmulationSession EmulationSession::s_instance; /*static*/ EmulationSession EmulationSession::s_instance;
std::string UTF16ToUTF8(std::u16string_view input) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
return convert.to_bytes(input.data(), input.data() + input.size());
}
std::string GetJString(JNIEnv* env, jstring jstr) {
if (!jstr) {
return {};
}
const jchar* jchars = env->GetStringChars(jstr, nullptr);
const jsize length = env->GetStringLength(jstr);
const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
const std::string converted_string = UTF16ToUTF8(string_view);
env->ReleaseStringChars(jstr, jchars);
return converted_string;
}
} // Anonymous namespace } // Anonymous namespace
static Core::SystemResultStatus RunEmulation(const std::string& filepath) { static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
@ -605,4 +618,15 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo([[maybe_unused]] JNIEnv
LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level()); LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(JNIEnv* env, jclass clazz,
jstring j_text) {
const std::u16string input = Common::UTF8ToUTF16(GetJString(env, j_text));
EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(JNIEnv* env, jclass clazz,
jint j_key_code) {
EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
}
} // extern "C" } // extern "C"

@ -133,6 +133,12 @@ JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStat
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
jclass clazz); jclass clazz);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardText(
JNIEnv* env, jclass clazz, jstring j_text);
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SubmitInlineKeyboardInput(
JNIEnv* env, jclass clazz, jint j_key_code);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

@ -101,11 +101,6 @@
<!-- Software keyboard --> <!-- Software keyboard -->
<string name="software_keyboard">Software Keyboard</string> <string name="software_keyboard">Software Keyboard</string>
<string name="i_forgot">I Forgot</string>
<string name="fixed_length_required">Text length is not correct (should be %d characters)</string>
<string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string>
<string name="blank_input_not_allowed">Blank input is not allowed</string>
<string name="empty_input_not_allowed">Empty input is not allowed</string>
<!-- Errors and warnings --> <!-- Errors and warnings -->
<string name="abort_button">Abort</string> <string name="abort_button">Abort</string>