Android basic keyboard input support (incl. virtual keyboard)

Normal English input works fine. Punctuation and navigation are partially supported.
IME from virtual keyboard doesn't work at all, seems like the native activity wrapper(android_app class) issues after Android 4.0.
This commit is contained in:
V. Khmelevskiy 2019-02-18 20:09:19 +07:00
parent 108d7ce74c
commit c79fca652a
6 changed files with 372 additions and 5 deletions

View File

@ -2,9 +2,16 @@ PLATFORM_DIR=armeabi-v7a
echo "\nLOCAL_MODULE: $LOCAL_MODULE"
OS_TYPE="unknown"
case "$OSTYPE" in
msys*|cygwin*) OS_TYPE="windows" ;;
linux*) OS_TYPE="linux" ;;
esac
LDC_PARAMS="-mtriple=armv7-none-linux-androideabi -relocation-model=pic "
export LD=$NDK/toolchains/llvm/prebuilt/linux-$NDK_ARCH/bin/llvm-link
export CC=$NDK/toolchains/llvm/prebuilt/linux-$NDK_ARCH/bin/clang
export LD=$NDK/toolchains/llvm/prebuilt/$OS_TYPE-$NDK_ARCH/bin/llvm-link
export CC=$NDK/toolchains/llvm/prebuilt/$OS_TYPE-$NDK_ARCH/bin/clang
SOURCES="$LOCAL_SRC_FILES $DLANGUI_SOURCES"
SOURCE_PATHS="-I./jni $DLANGUI_SOURCE_PATHS $DLANGUI_IMPORT_PATHS"
@ -24,7 +31,7 @@ LINK_OPTIONS="\
-Wl,-soname,libnative-activity.so \
-shared \
--sysroot=$NDK/platforms/$ANDROID_TARGET/arch-arm \
-gcc-toolchain $NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-$NDK_ARCH \
-gcc-toolchain $NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/$OS_TYPE-$NDK_ARCH \
-no-canonical-prefixes \
-target armv7-none-linux-androideabi \
-Wl,--fix-cortex-a8 \

View File

@ -4,6 +4,7 @@
DLANGUI_SOURCES="\
$DLANGUI_DIR/src/dlangui/platforms/android/androidapp.d \
$DLANGUI_DIR/src/dlangui/platforms/android/imm.d \
$DLANGUI_DIR/src/dlangui/platforms/common/startup.d \
$DLANGUI_DIR/src/dlangui/platforms/common/platform.d \
$DLANGUI_DIR/src/dlangui/dialogs/filedlg.d \

View File

@ -16,6 +16,7 @@ import dlangui.platforms.common.platform;
import android.input, android.looper : ALooper_pollAll;
import android.native_window : ANativeWindow_setBuffersGeometry;
import android.configuration;
import android.keycodes;
import android.log, android.android_native_app_glue;
/**
@ -139,6 +140,8 @@ class AndroidWindow : Window {
* Process the next input event.
*/
int handle_input(AInputEvent* event) {
import imm = dlangui.platforms.android.imm;
import std.conv : to;
Log.i("handle input, event=", AInputEvent_getType(event));
auto et = AInputEvent_getType(event);
if (et == AINPUT_EVENT_TYPE_MOTION) {
@ -173,7 +176,37 @@ class AndroidWindow : Window {
return 1;
} else if (et == AINPUT_EVENT_TYPE_KEY) {
Log.d("AINPUT_EVENT_TYPE_KEY");
return 0;
KeyEvent evt;
auto app = (cast(AndroidPlatform)platform)._appstate;
int _keyFlags = AKeyEvent_getMetaState(event).toKeyFlag();
int sysKeyCode = AKeyEvent_getKeyCode(event);
int sysMeta = AKeyEvent_getMetaState(event);
int keyCode = androidKeyMap.get(sysKeyCode, KeyCode.init);
auto action = toKeyAction(AKeyEvent_getAction(event));
int char_ = imm.GetUnicodeChar(app, action, sysKeyCode, sysMeta);
dchar[] text;
if (!isTextEditControl(sysKeyCode)) {
if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_MULTIPLE) {
// it's a string from IME
if (sysKeyCode == AKEYCODE_UNKNOWN) {
text = cast(dchar[]) to!dstring(imm.GetUnicodeString(app, event));
action = KeyAction.Text;
}
// else repeat character AKeyEvent_getRepeatCount() times
}
else if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_DOWN && (char_ || isASCIIChar(sysKeyCode))) {
text ~= cast(dchar)(char_ == 0 ? sysKeyCode : char_);
action = KeyAction.Text;
}
}
Log.d("ACTION: ", action, " syskeyCode: ", sysKeyCode, " sysMeta: ", sysMeta, "meta: ", _keyFlags, " char '", cast(dchar)char_, "' str:", cast(dstring)text);
if (action == KeyAction.Text)
evt = new KeyEvent(KeyAction.Text, 0, 0, cast(dstring)text);
else
evt = new KeyEvent(action, keyCode, _keyFlags);
if (evt && dispatchKeyEvent(evt))
update();
return 1;
}
return 0;
}
@ -225,6 +258,12 @@ class AndroidPlatform : Platform {
}
void showSoftKeyboard(bool shouldShow) {
import imm = dlangui.platforms.android.imm;
imm.showSoftKeyboard(_appstate, shouldShow);
}
/**
* Initialize an EGL context for the current display.
*/
@ -706,3 +745,82 @@ extern (C) void android_main(android_app* state) {
}
private KeyAction toKeyAction(int androidKeyAction) {
switch(androidKeyAction) {
case AKEY_EVENT_ACTION_DOWN: return KeyAction.KeyDown;
case AKEY_EVENT_ACTION_UP: return KeyAction.KeyUp;
case AKEY_EVENT_ACTION_MULTIPLE: return KeyAction.Repeat; // can also be text
default:
assert(0, "should never reach this");
}
}
private bool isASCIIChar(int ch) {
return 31 < ch && ch < 127;
}
// Text editor controls such as move caret or (de)select
private bool isTextEditControl(int keyCode) {
switch (keyCode){
case AKEYCODE_DEL: // backspace ("DEL" or "<x" arrow on soft keyboard)
case 112: // delete
case 59: // lshift
case 60: // rshift
case 113: // lcontrol
case 114: // rcontrol
case 57: // lalt
case 58: // ralt
case 19: // up arrow
case 20: // down arrow
case 21: // left arrow
case 22: // right arrow
case 92: // page up?
case 93: // page down?
case 122: // home
case 123: // end
case 124: // insert
return true;
static foreach(fn; 131..143) // f1-f12
case fn: return true;
default:
return false;
}
}
private int toKeyFlag(int keyMeta) {
int state;
if (keyMeta & AMETA_ALT_ON) state |= KeyFlag.Alt;
if (keyMeta & AMETA_SHIFT_ON) state |= KeyFlag.Shift;
return state;
}
/// Android to dlangui key mapping
private static immutable KeyCode[int] androidKeyMap;
static this() {
import std.conv : text;
androidKeyMap = [
AKEYCODE_DEL: KeyCode.BACK, // Delete(key code 67) on Android seems to work as backspace(key 112)
AKEYCODE_BACK: KeyCode.BACK,
AKEYCODE_SPACE: KeyCode.SPACE,
AKEYCODE_ENTER: KeyCode.RETURN,
AKEYCODE_TAB: KeyCode.TAB,
AKEYCODE_DPAD_LEFT: KeyCode.LEFT,
AKEYCODE_DPAD_RIGHT: KeyCode.RIGHT,
AKEYCODE_DPAD_UP: KeyCode.UP,
AKEYCODE_DPAD_DOWN: KeyCode.DOWN,
AKEYCODE_PAGE_UP: KeyCode.PAGEUP,
AKEYCODE_PAGE_DOWN: KeyCode.PAGEDOWN,
112: KeyCode.DEL,
122: KeyCode.HOME,
123: KeyCode.END,
];
static foreach(n; 0..10) // keys 0-9
androidKeyMap[mixin("AKEYCODE_" ~ n.text)] = mixin("KeyCode.KEY_" ~ n.text);
static foreach(char n; 'A'..'Z'+1) // A-Z
androidKeyMap[mixin("AKEYCODE_" ~ n)] = mixin("KeyCode.KEY_" ~ n);
}

View File

@ -0,0 +1,220 @@
module dlangui.platforms.android.imm;
import jni;
import android.android_native_app_glue;
import android.input;
import dlangui.core.logger;
alias IMMResult = int;
// values from InputMethodManager.java
private enum : IMMResult
{
RESULT_UNCHANGED_SHOWN = 0,
RESULT_UNCHANGED_HIDDEN = 1,
RESULT_SHOWN = 2,
RESULT_HIDDEN = 3,
}
alias IMMFlags = int;
// values from InputMethodManager.java
private enum : IMMFlags
{
SHOW_IMPLICIT = 0x0001,
SHOW_FORCED = 0x0002,
HIDE_IMPLICIT_ONLY = 0x0001,
HIDE_NOT_ALWAYS = 0x0002,
}
/**
* JNI wrapper used with native actitiy to show/hide software keyboard
* It relies on java reflection and it might be slow.
*/
version(Android)
void showSoftKeyboard(android_app* app, bool shouldShow)
{
// The code is based on https://stackoverflow.com/questions/5864790/how-to-show-the-soft-keyboard-on-native-activity
// Attaches the current thread to the JVM.
jint result;
IMMFlags flags;
auto javaVM = app.activity.vm;
auto env = app.activity.env;
JavaVMAttachArgs attachArgs;
attachArgs.version_ = JNI_VERSION_1_6;
attachArgs.name = "NativeThread";
attachArgs.group = null;
if ((*javaVM).AttachCurrentThread(javaVM, &env, &attachArgs) == JNI_ERR)
{
Log.e("showSoftKeyboard Unable to attach to JVM");
return;
}
// Retrieves NativeActivity.
jobject nativeActivity = app.activity.clazz;
jclass nativeActivityClass = (*env).GetObjectClass(env, nativeActivity);
// Retrieves Context.INPUT_METHOD_SERVICE.
jclass contextClass = (*env).FindClass(env, "android/content/Context");
jfieldID FieldINPUT_METHOD_SERVICE =
(*env).GetStaticFieldID(env, contextClass,
"INPUT_METHOD_SERVICE", "Ljava/lang/String;");
jobject INPUT_METHOD_SERVICE =
(*env).GetStaticObjectField(env, contextClass, FieldINPUT_METHOD_SERVICE);
//jniCheck(INPUT_METHOD_SERVICE);
// Runs getSystemService(Context.INPUT_METHOD_SERVICE).
jclass immClass = (*env).FindClass(
env, "android/view/inputmethod/InputMethodManager");
jmethodID MethodGetSystemService = (*env).GetMethodID(
env, nativeActivityClass, "getSystemService",
"(Ljava/lang/String;)Ljava/lang/Object;");
jobject imm = (*env).CallObjectMethod(
env, nativeActivity, MethodGetSystemService,
INPUT_METHOD_SERVICE);
// Runs getWindow().getDecorView().
jmethodID MethodGetWindow = (*env).GetMethodID(
env, nativeActivityClass, "getWindow", "()Landroid/view/Window;");
jobject window = (*env).CallObjectMethod(
env, nativeActivity, MethodGetWindow);
jclass windowClass = (*env).FindClass(
env, "android/view/Window");
jmethodID MethodGetDecorView = (*env).GetMethodID(
env, windowClass, "getDecorView", "()Landroid/view/View;");
jobject decorView = (*env).CallObjectMethod(
env, window, MethodGetDecorView);
if (shouldShow) {
// Runs imm.showSoftInput(...).
jmethodID MethodShowSoftInput = (*env).GetMethodID(
env, immClass, "showSoftInput", "(Landroid/view/View;I)Z");
jboolean res = (*env).CallBooleanMethod(
env, imm, MethodShowSoftInput, decorView, flags);
} else {
// Runs lWindow.getViewToken()
jclass viewClass = (*env).FindClass(
env, "android/view/View");
jmethodID MethodGetWindowToken = (*env).GetMethodID(
env, viewClass, "getWindowToken", "()Landroid/os/IBinder;");
jobject binder = (*env).CallObjectMethod(
env, decorView, MethodGetWindowToken);
// lInputMethodManager.hideSoftInput(...).
jmethodID MethodHideSoftInput = (*env).GetMethodID(
env, immClass, "hideSoftInputFromWindow",
"(Landroid/os/IBinder;I)Z");
jboolean res = (*env).CallBooleanMethod(
env, imm, MethodHideSoftInput, binder, flags);
}
// Finished with the JVM.
(*javaVM).DetachCurrentThread(javaVM);
}
int GetUnicodeChar(android_app* app, int eventType, int keyCode, int metaState)
{
auto javaVM = app.activity.vm;
auto env = app.activity.env;
JavaVMAttachArgs attachArgs;
attachArgs.version_ = JNI_VERSION_1_6;
attachArgs.name = "NativeThread";
attachArgs.group = null;
if ((*javaVM).AttachCurrentThread(javaVM, &env, &attachArgs) == JNI_ERR)
return 0;
jclass class_key_event = (*env).FindClass(env, "android/view/KeyEvent");
int unicodeKey;
if(metaState == 0)
{
jmethodID method_get_unicode_char = (*env).GetMethodID(env, class_key_event, "getUnicodeChar", "()I");
jmethodID eventConstructor = (*env).GetMethodID(env, class_key_event, "<init>", "(II)V");
jobject eventObj = (*env).NewObject(env, class_key_event, eventConstructor, eventType, keyCode);
unicodeKey = (*env).CallIntMethod(env, eventObj, method_get_unicode_char);
}
else
{
jmethodID method_get_unicode_char = (*env).GetMethodID(env, class_key_event, "getUnicodeChar", "(I)I");
jmethodID eventConstructor = (*env).GetMethodID(env, class_key_event, "<init>", "(II)V");
jobject eventObj = (*env).NewObject(env, class_key_event, eventConstructor, eventType, keyCode);
unicodeKey = (*env).CallIntMethod(env, eventObj, method_get_unicode_char, metaState);
}
(*javaVM).DetachCurrentThread(javaVM);
return unicodeKey;
}
// Issue: native app glue seems to mess up the input.
// It is clearly seen in debugger that initally key event do have real input,
// but second time it is called it is all messed up
string GetUnicodeString(android_app* app, AInputEvent* event)
{
string str;
auto javaVM = app.activity.vm;
auto env = app.activity.env;
JavaVMAttachArgs attachArgs;
attachArgs.version_ = JNI_VERSION_1_6;
attachArgs.name = "NativeThread";
attachArgs.group = null;
if ((*javaVM).AttachCurrentThread(javaVM, &env, &attachArgs) == JNI_ERR)
{
Log.e("showSoftKeyboard Unable to attach to JVM");
return null;
}
jclass class_key_event = (*env).FindClass(env, "android/view/KeyEvent");
jmethodID eventConstructor = (*env).GetMethodID(env, class_key_event, "<init>", "(JJIIIIIIII)V");
jobject eventObj = (*env).NewObject(env, class_key_event, eventConstructor,
AKeyEvent_getDownTime(event),
AKeyEvent_getEventTime(event),
AKeyEvent_getAction(event),
AKeyEvent_getKeyCode(event),
AKeyEvent_getRepeatCount(event),
AKeyEvent_getMetaState(event),
AInputEvent_getDeviceId(event),
AKeyEvent_getScanCode(event),
AKeyEvent_getFlags(event),
AInputEvent_getSource(event)
);
// this won't work because characters is a member passed on construction and getCharacter() is just a getter
jmethodID method_get_characters = (*env).GetMethodID(env, class_key_event, "getCharacters", "()Ljava/lang/String;");
if (auto jstr = (*env).CallObjectMethod(env, eventObj, method_get_characters)) {
str.length = (*env).GetStringUTFLength(env, jstr);
(*env).GetStringUTFRegion(env, jstr, 0, str.length, cast(char*)str.ptr);
}
{
jmethodID method_get_unicode_char = (*env).GetMethodID(env, class_key_event, "getUnicodeChar", "()I");
int unicodeKey = (*env).CallIntMethod(env, eventObj, method_get_unicode_char);
if (str.length == 0) {
import std.conv : to;
dchar[] tmp;
tmp ~= unicodeKey;
str = to!string(tmp);
}
}
(*javaVM).DetachCurrentThread(javaVM);
return str;
}

View File

@ -345,8 +345,10 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction
/// sets focus to this widget or suitable focusable child, returns previously focused widget
override Widget setFocus(FocusReason reason = FocusReason.Unspecified) {
Widget res = super.setFocus(reason);
if (focused)
if (focused) {
showSoftKeyboard();
handleEditorStateChange();
}
return res;
}

View File

@ -1099,6 +1099,7 @@ public:
// try to find focusable child
return window.focusedWidget;
}
hideSoftKeyboard();
return window.setFocus(this, reason);
}
/// searches children for first focusable item, returns null if not found
@ -1116,6 +1117,24 @@ public:
return null;
}
///
final void hideSoftKeyboard() {
version(Android) {
import dlangui.platforms.android.androidapp;
if (auto androidPlatform = cast(AndroidPlatform)platform)
androidPlatform.showSoftKeyboard(false);
}
}
/// Shows system virtual keyabord where applicable
final void showSoftKeyboard() {
version(Android) {
import dlangui.platforms.android.androidapp;
if (auto androidPlatform = cast(AndroidPlatform)platform)
androidPlatform.showSoftKeyboard(true);
}
}
// =======================================================
// Events