android: SoftwareKeyboard implementation

This commit is contained in:
zhupengfei 2020-03-25 22:18:07 +08:00 committed by bunnei
parent fd2d87f8d7
commit 6aa04a3bd3
7 changed files with 434 additions and 13 deletions

View File

@ -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);
}

View File

@ -1,4 +1,6 @@
add_library(main SHARED
applets/swkbd.cpp
applets/swkbd.h
button_manager.cpp
button_manager.h
config.cpp

View 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);
}

View 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);
}

View File

@ -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

View File

@ -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();

View File

@ -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>