android: Implement basic software keyboard applet.
This commit is contained in:
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));
|
||||||
|
}
|
12
src/android/app/src/main/jni/android_common/android_common.h
Normal file
12
src/android/app/src/main/jni/android_common/android_common.h
Normal file
|
@ -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);
|
277
src/android/app/src/main/jni/applets/software_keyboard.cpp
Normal file
277
src/android/app/src/main/jni/applets/software_keyboard.cpp
Normal file
|
@ -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
|
78
src/android/app/src/main/jni/applets/software_keyboard.h
Normal file
78
src/android/app/src/main/jni/applets/software_keyboard.h
Normal file
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue