android: SoftwareKeyboard implementation
This commit is contained in:
parent
b521a57f2f
commit
042cca532a
@ -0,0 +1,230 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.applets;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spanned;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
public final class SoftwareKeyboard {
|
||||
/// Corresponds to Frontend::ButtonConfig
|
||||
private interface ButtonConfig {
|
||||
int Single = 0; /// Ok button
|
||||
int Dual = 1; /// Cancel | Ok buttons
|
||||
int Triple = 2; /// Cancel | I Forgot | Ok buttons
|
||||
int None = 3; /// No button (returned by swkbdInputText in special cases)
|
||||
}
|
||||
|
||||
/// Corresponds to Frontend::ValidationError
|
||||
public enum ValidationError {
|
||||
None,
|
||||
// Button Selection
|
||||
ButtonOutOfRange,
|
||||
// Configured Filters
|
||||
MaxDigitsExceeded,
|
||||
AtSignNotAllowed,
|
||||
PercentNotAllowed,
|
||||
BackslashNotAllowed,
|
||||
ProfanityNotAllowed,
|
||||
CallbackFailed,
|
||||
// Allowed Input Type
|
||||
FixedLengthRequired,
|
||||
MaxLengthExceeded,
|
||||
BlankInputNotAllowed,
|
||||
EmptyInputNotAllowed,
|
||||
}
|
||||
|
||||
public static class KeyboardConfig {
|
||||
public int button_config;
|
||||
public int max_text_length;
|
||||
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
|
||||
public String hint_text; /// Displayed in the field as a hint before
|
||||
@Nullable public String[] button_text; /// Contains the button text that the caller provides
|
||||
}
|
||||
|
||||
/// Corresponds to Frontend::KeyboardData
|
||||
public static class KeyboardData {
|
||||
public int button;
|
||||
public String text;
|
||||
|
||||
private KeyboardData(int button, String text) {
|
||||
this.button = button;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyboardData data;
|
||||
private static final Object finishLock = new Object();
|
||||
|
||||
private static void ExecuteImpl(KeyboardConfig config) {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
|
||||
data = new KeyboardData(0, "");
|
||||
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = params.rightMargin =
|
||||
CitraApplication.getAppContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.dialog_margin);
|
||||
|
||||
// Set up the input
|
||||
EditText editText = new EditText(CitraApplication.getAppContext());
|
||||
editText.setHint(config.hint_text);
|
||||
editText.setSingleLine(!config.multiline_mode);
|
||||
editText.setLayoutParams(params);
|
||||
editText.setFilters(
|
||||
new InputFilter[] {new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
|
||||
|
||||
FrameLayout container = new FrameLayout(emulationActivity);
|
||||
container.addView(editText);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity)
|
||||
.setTitle(R.string.software_keyboard)
|
||||
.setView(container)
|
||||
.setCancelable(false);
|
||||
|
||||
switch (config.button_config) {
|
||||
case ButtonConfig.Triple: {
|
||||
final String text = config.button_text == null
|
||||
? emulationActivity.getString(R.string.i_forgot)
|
||||
: config.button_text[1];
|
||||
builder.setNeutralButton(text, null);
|
||||
}
|
||||
// fallthrough
|
||||
case ButtonConfig.Dual: {
|
||||
final String text = config.button_text == null
|
||||
? emulationActivity.getString(android.R.string.cancel)
|
||||
: config.button_text[0];
|
||||
builder.setNegativeButton(text, null);
|
||||
}
|
||||
// fallthrough
|
||||
case ButtonConfig.Single: {
|
||||
final String text = config.button_text == null
|
||||
? emulationActivity.getString(android.R.string.ok)
|
||||
: config.button_text[config.button_config];
|
||||
builder.setPositiveButton(text, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
|
||||
data.button = config.button_config;
|
||||
data.text = editText.getText().toString();
|
||||
|
||||
final ValidationError error = ValidateInput(data.text);
|
||||
if (error != ValidationError.None) {
|
||||
HandleValidationError(config, error);
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
|
||||
data.button = 1;
|
||||
dialog.dismiss();
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
|
||||
data.button = 0;
|
||||
dialog.dismiss();
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
|
||||
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 AlertDialog.Builder(emulationActivity)
|
||||
.setTitle(R.string.software_keyboard)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static KeyboardData Execute(KeyboardConfig config) {
|
||||
if (config.button_config == ButtonConfig.None) {
|
||||
Log.error("Unexpected button config None");
|
||||
return new KeyboardData(0, "");
|
||||
}
|
||||
|
||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
|
||||
|
||||
synchronized (finishLock) {
|
||||
try {
|
||||
finishLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void ShowError(String error) {
|
||||
NativeLibrary.displayAlertMsg(
|
||||
CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
|
||||
error, false);
|
||||
}
|
||||
|
||||
private static native ValidationError ValidateFilters(String text);
|
||||
private static native ValidationError ValidateInput(String text);
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
add_library(main SHARED
|
||||
applets/swkbd.cpp
|
||||
applets/swkbd.h
|
||||
button_manager.cpp
|
||||
button_manager.h
|
||||
config.cpp
|
||||
|
153
src/android/app/src/main/jni/applets/swkbd.cpp
Normal file
153
src/android/app/src/main/jni/applets/swkbd.cpp
Normal file
@ -0,0 +1,153 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <map>
|
||||
#include <jni.h>
|
||||
#include "core/core.h"
|
||||
#include "jni/applets/swkbd.h"
|
||||
#include "jni/id_cache.h"
|
||||
|
||||
static std::string GetJString(JNIEnv* env, jstring jstr) {
|
||||
if (!jstr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* s = env->GetStringUTFChars(jstr, nullptr);
|
||||
std::string result = s;
|
||||
env->ReleaseStringUTFChars(jstr, s);
|
||||
return result;
|
||||
}
|
||||
|
||||
static jclass s_software_keyboard_class;
|
||||
static jclass s_keyboard_config_class;
|
||||
static jclass s_keyboard_data_class;
|
||||
static jclass s_validation_error_class;
|
||||
static jmethodID s_swkbd_execute;
|
||||
static jmethodID s_swkbd_show_error;
|
||||
|
||||
namespace SoftwareKeyboard {
|
||||
|
||||
static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
jobject object = env->AllocObject(s_keyboard_config_class);
|
||||
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "button_config", "I"),
|
||||
static_cast<jint>(config.button_config));
|
||||
env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
|
||||
static_cast<jint>(config.max_text_length));
|
||||
env->SetBooleanField(object, env->GetFieldID(s_keyboard_config_class, "multiline_mode", "Z"),
|
||||
static_cast<jboolean>(config.multiline_mode));
|
||||
env->SetObjectField(object,
|
||||
env->GetFieldID(s_keyboard_config_class, "hint_text", "Ljava/lang/String;"),
|
||||
env->NewStringUTF(config.hint_text.c_str()));
|
||||
if (config.has_custom_button_text) {
|
||||
const jclass string_class =
|
||||
reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("java/lang/String")));
|
||||
const jobjectArray array =
|
||||
env->NewObjectArray(static_cast<jsize>(config.button_text.size()), string_class,
|
||||
env->NewStringUTF(config.button_text[0].c_str()));
|
||||
for (std::size_t i = 1; i < config.button_text.size(); ++i) {
|
||||
env->SetObjectArrayElement(array, static_cast<jsize>(i),
|
||||
env->NewStringUTF(config.button_text[i].c_str()));
|
||||
}
|
||||
env->SetObjectField(
|
||||
object, env->GetFieldID(s_keyboard_config_class, "button_text", "[Ljava/lang/String;"),
|
||||
array);
|
||||
env->DeleteGlobalRef(string_class);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
static Frontend::KeyboardData ToFrontendKeyboardData(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 Frontend::KeyboardData{
|
||||
GetJString(env, string),
|
||||
static_cast<u8>(
|
||||
env->GetIntField(object, env->GetFieldID(s_keyboard_data_class, "button", "I")))};
|
||||
}
|
||||
|
||||
AndroidKeyboard::~AndroidKeyboard() = default;
|
||||
|
||||
void AndroidKeyboard::Execute(const Frontend::KeyboardConfig& config) {
|
||||
SoftwareKeyboard::Execute(config);
|
||||
|
||||
const auto data = ToFrontendKeyboardData(IDCache::GetEnvForThread()->CallStaticObjectMethod(
|
||||
s_software_keyboard_class, s_swkbd_execute, ToJavaKeyboardConfig(config)));
|
||||
Finalize(data.text, data.button);
|
||||
}
|
||||
|
||||
void AndroidKeyboard::ShowError(const std::string& error) {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
env->CallStaticVoidMethod(s_software_keyboard_class, s_swkbd_show_error,
|
||||
env->NewStringUTF(error.c_str()));
|
||||
}
|
||||
|
||||
void InitJNI(JNIEnv* env) {
|
||||
s_software_keyboard_class = reinterpret_cast<jclass>(
|
||||
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard")));
|
||||
s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef(
|
||||
env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard$KeyboardConfig")));
|
||||
s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef(
|
||||
env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard$KeyboardData")));
|
||||
s_validation_error_class = reinterpret_cast<jclass>(env->NewGlobalRef(
|
||||
env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard$ValidationError")));
|
||||
|
||||
s_swkbd_execute = env->GetStaticMethodID(
|
||||
s_software_keyboard_class, "Execute",
|
||||
"(Lorg/citra/citra_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/citra/citra_emu/"
|
||||
"applets/SoftwareKeyboard$KeyboardData;");
|
||||
s_swkbd_show_error =
|
||||
env->GetStaticMethodID(s_software_keyboard_class, "ShowError", "(Ljava/lang/String;)V");
|
||||
}
|
||||
|
||||
void CleanupJNI(JNIEnv* env) {
|
||||
env->DeleteGlobalRef(s_software_keyboard_class);
|
||||
env->DeleteGlobalRef(s_keyboard_config_class);
|
||||
env->DeleteGlobalRef(s_keyboard_data_class);
|
||||
env->DeleteGlobalRef(s_validation_error_class);
|
||||
}
|
||||
|
||||
} // namespace SoftwareKeyboard
|
||||
|
||||
jobject ToJavaValidationError(Frontend::ValidationError error) {
|
||||
static const std::map<Frontend::ValidationError, const char*> ValidationErrorNameMap{{
|
||||
{Frontend::ValidationError::None, "None"},
|
||||
{Frontend::ValidationError::ButtonOutOfRange, "ButtonOutOfRange"},
|
||||
{Frontend::ValidationError::MaxDigitsExceeded, "MaxDigitsExceeded"},
|
||||
{Frontend::ValidationError::AtSignNotAllowed, "AtSignsNotAllowed"},
|
||||
{Frontend::ValidationError::PercentNotAllowed, "PercentNotAllowed"},
|
||||
{Frontend::ValidationError::BackslashNotAllowed, "BackslashNotAllowed"},
|
||||
{Frontend::ValidationError::ProfanityNotAllowed, "ProfanityNotAllowed"},
|
||||
{Frontend::ValidationError::CallbackFailed, "CallbackFailed"},
|
||||
{Frontend::ValidationError::FixedLengthRequired, "FixedLengthRequired"},
|
||||
{Frontend::ValidationError::MaxLengthExceeded, "MaxLengthExceeded"},
|
||||
{Frontend::ValidationError::BlankInputNotAllowed, "BlankInputNotAllowed"},
|
||||
{Frontend::ValidationError::EmptyInputNotAllowed, "EmptyInputNotAllowed"},
|
||||
}};
|
||||
ASSERT(ValidationErrorNameMap.count(error));
|
||||
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
return env->GetStaticObjectField(
|
||||
s_validation_error_class,
|
||||
env->GetStaticFieldID(s_validation_error_class, ValidationErrorNameMap.at(error),
|
||||
"Lorg/citra/citra_emu/applets/SoftwareKeyboard$ValidationError;"));
|
||||
}
|
||||
|
||||
jobject Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(JNIEnv* env,
|
||||
jclass clazz,
|
||||
jstring text) {
|
||||
|
||||
const auto ret =
|
||||
Core::System::GetInstance().GetSoftwareKeyboard()->ValidateFilters(GetJString(env, text));
|
||||
return ToJavaValidationError(ret);
|
||||
}
|
||||
|
||||
jobject Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(JNIEnv* env, jclass clazz,
|
||||
jstring text) {
|
||||
|
||||
const auto ret =
|
||||
Core::System::GetInstance().GetSoftwareKeyboard()->ValidateInput(GetJString(env, text));
|
||||
return ToJavaValidationError(ret);
|
||||
}
|
35
src/android/app/src/main/jni/applets/swkbd.h
Normal file
35
src/android/app/src/main/jni/applets/swkbd.h
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include "core/frontend/applets/swkbd.h"
|
||||
|
||||
namespace SoftwareKeyboard {
|
||||
|
||||
class AndroidKeyboard final : public Frontend::SoftwareKeyboard {
|
||||
public:
|
||||
~AndroidKeyboard();
|
||||
|
||||
void Execute(const Frontend::KeyboardConfig& config) override;
|
||||
void ShowError(const std::string& error) override;
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
#include "common/logging/filter.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/settings.h"
|
||||
#include "jni/applets/swkbd.h"
|
||||
#include "jni/id_cache.h"
|
||||
|
||||
#include <jni.h>
|
||||
@ -111,6 +112,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
s_exit_emulation_activity =
|
||||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
||||
|
||||
SoftwareKeyboard::InitJNI(env);
|
||||
|
||||
return JNI_VERSION;
|
||||
}
|
||||
|
||||
@ -121,6 +124,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||
}
|
||||
|
||||
env->DeleteGlobalRef(s_native_library_class);
|
||||
SoftwareKeyboard::CleanupJNI(env);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "core/frontend/scope_acquire_context.h"
|
||||
#include "core/hle/service/am/am.h"
|
||||
#include "core/settings.h"
|
||||
#include "jni/applets/swkbd.h"
|
||||
#include "jni/button_manager.h"
|
||||
#include "jni/config.h"
|
||||
#include "jni/emu_window/emu_window.h"
|
||||
@ -83,18 +84,6 @@ static int AlertPromptButton() {
|
||||
IDCache::GetAlertPromptButton()));
|
||||
}
|
||||
|
||||
class AndroidKeyboard final : public Frontend::SoftwareKeyboard {
|
||||
public:
|
||||
void Execute(const Frontend::KeyboardConfig& config) override {
|
||||
SoftwareKeyboard::Execute(config);
|
||||
Finalize(DisplayAlertPrompt("Enter text", config.hint_text.c_str(),
|
||||
static_cast<int>(this->config.button_config)),
|
||||
AlertPromptButton());
|
||||
}
|
||||
|
||||
void ShowError(const std::string& error) override {}
|
||||
};
|
||||
|
||||
static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
||||
// Citra core only supports a single running instance
|
||||
std::lock_guard<std::mutex> lock(running_mutex);
|
||||
@ -118,7 +107,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
||||
|
||||
// Register frontend applets
|
||||
Frontend::RegisterDefaultApplets();
|
||||
system.RegisterSoftwareKeyboard(std::make_shared<AndroidKeyboard>());
|
||||
system.RegisterSoftwareKeyboard(std::make_shared<SoftwareKeyboard::AndroidKeyboard>());
|
||||
|
||||
InputManager::Init();
|
||||
|
||||
|
@ -137,4 +137,12 @@
|
||||
<string name="external_storage_not_mounted">The external storage needs to be available in order to use Citra</string>
|
||||
|
||||
<string name="select_dir">Select This Directory</string>
|
||||
|
||||
<!-- Software Keyboard -->
|
||||
<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>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user