diff --git a/android/KMEA/app/src/main/assets/keyboard.html b/android/KMEA/app/src/main/assets/keyboard.html index 9c3fee21d4..88cccf60d0 100644 --- a/android/KMEA/app/src/main/assets/keyboard.html +++ b/android/KMEA/app/src/main/assets/keyboard.html @@ -216,12 +216,14 @@ kmw['executePopupKey'](keyID); } - function executeHardwareKeystroke(code, shift, lstates) { + function executeHardwareKeystroke(code, shift, lstates, eventModifiers) { var kmw=window['keyman']; //window.console.log('executeHardwareKeystroke:('+code+', ' + shift + ', ' + lstates + ');'); try { - var r = kmw['executeHardwareKeystroke'](code, shift, lstates); - //window.console.log('executeHardwareKeystroke completed with '+r); + if (!kmw['executeHardwareKeystroke'](code, shift, lstates)) { + // KMW didn't process the key, so have the Android app dispatch the key with the original event modifiers + window.jsInterface.dispatchKey(code, eventModifiers); + } } catch(e) { window.console.log('executeHardwareKeystroke exception: '+e); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHardwareKeyboardInterpreter.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHardwareKeyboardInterpreter.java index 8346efa266..ab3f0c536d 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHardwareKeyboardInterpreter.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHardwareKeyboardInterpreter.java @@ -4,119 +4,13 @@ package com.tavultesoft.kmea; +import com.tavultesoft.kmea.KMScanCodeMap; import android.content.Context; import android.view.KeyEvent; public class KMHardwareKeyboardInterpreter implements KeyEvent.Callback { - /** - * Maps relevant keycodes from android.view.keyevent to the standard keycodes used by Keyman - * keyboards. Is conveniently based at 0, else we might need to HashMap it instead. - */ - /** - * From reference: https://source.android.com/devices/input#understanding-hid-usages-and-event-codes - * The Android API sometimes refers to the Linux key code associated with a key as its "scan code". - * This is technically incorrect in but it helps to distinguish Linux key codes from Android - * key codes in the API. - * - * Maps relevant Linux key code (LKC) to the standard key codes used by Keyman keyboards - * Table: https://source.android.com/devices/input/keyboard-devices - * - * Limitations: Intentionally not assigning number pad keys - */ - private static final - int scanCodeMap[] = { - 0, // padding = 0x00; - 0, // public static final int KEY_ESC = 0x01; - '1', // public static final int KEY_1 = 0x02; - '2', // public static final int KEY_2 = 0x03; - '3', // public static final int KEY_3 = 0x04; - '4', // public static final int KEY_4 = 0x05; - '5', // public static final int KEY_5 = 0x06; - '6', // public static final int KEY_6 = 0x07; - '7', // public static final int KEY_7 = 0x08; - '8', // public static final int KEY_8 = 0x09; - '9', // public static final int KEY_9 = 0x0A; - '0', // public static final int KEY_0 = 0x0B; - 189, // public static final int KEY_MINUS = 0x0C; - 187, // public static final int KEY_EQUALS = 0x0D; - 8, // public static final int KEY_BACKSPACE = 0x0E; - 9, // public static final int KEY_TAB = 0x0F; - 'Q', // public static final int KEY_Q = 0x10; - 'W', // public static final int KEY_W = 0x11; - 'E', // public static final int KEY_E = 0x12; - 'R', // public static final int KEY_R = 0x13; - 'T', // public static final int KEY_T = 0x14; - 'Y', // public static final int KEY_Y = 0x15; - 'U', // public static final int KEY_U = 0x16; - 'I', // public static final int KEY_I = 0x17; - 'O', // public static final int KEY_O = 0x18; - 'P', // public static final int KEY_P = 0x19; - 219, // public static final int KEY_LEFTBRACE = 0x1A; - 221, // public static final int KEY_RIGHTBRACE = 0x1B; - 13, // public static final int KEY_ENTER = 0x1C; - 0, // public static final int KEY_LEFTCTRL = 0x1D; - 'A', // public static final int KEY_A = 0x1E; - 'S', // public static final int KEY_S = 0x1F; - 'D', // public static final int KEY_D = 0x20; - 'F', // public static final int KEY_F = 0x21; - 'G', // public static final int KEY_G = 0x22; - 'H', // public static final int KEY_H = 0x23; - 'J', // public static final int KEY_J = 0x24; - 'K', // public static final int KEY_K = 0x25; - 'L', // public static final int KEY_L = 0x26; - 186, // public static final int KEY_SEMICOLON = 0x27; - 222, // public static final int KEY_APOSTROPHE = 0x28; - 192, // public static final int KEY_GRAVE = 0x29; - 0, // public static final int KEY_LEFTSHIFT = 0x2A; - 220, // public static final int KEY_BACKSLASH = 0x2B; - 'Z', // public static final int KEY_Z = 0x2C; - 'X', // public static final int KEY_X = 0x2D; - 'C', // public static final int KEY_C = 0x2E; - 'V', // public static final int KEY_V = 0x2F; - 'B', // public static final int KEY_B = 0x30; - 'N', // public static final int KEY_N = 0x31; - 'M', // public static final int KEY_M = 0x32; - 188, // public static final int KEY_COMMA = 0x33; - 190, // public static final int KEY_DOT = 0x34; - 191, // public static final int KEY_SLASH = 0x35; - 0, // public static final int KEY_RIGHTSHIFT = 0x36; - 0, // public static final int KEY_KPASTERISK = 0x37; - 0, // public static final int KEY_LEFTALT = 0x38; - 32, // public static final int KEY_SPACE = 0x39; - 0, // public static final int KEY_CAPSLOCK = 0x3A; - 0, // public static final int KEY_F1 = 0x3B; - 0, // public static final int KEY_F2 = 0x3C; - 0, // public static final int KEY_F3 = 0x3D; - 0, // public static final int KEY_F4 = 0x3E; - 0, // public static final int KEY_F5 = 0x3F; - 0, // public static final int KEY_F6 = 0x40; - 0, // public static final int KEY_F7 = 0x41; - 0, // public static final int KEY_F8 = 0x42; - 0, // public static final int KEY_F9 = 0x43; - 0, // public static final int KEY_F10 = 0x44; - 0, // public static final int KEY_NUMLOCK = 0x45; - 0, // public static final int KEY_SCROLLLOCK = 0x46; - 0, // public static final int KEY_KP7 = 0x47; - 0, // public static final int KEY_KP8 = 0x48; - 0, // public static final int KEY_KP9 = 0x49; - 0, // public static final int KEY_KPMINUS = 0x4A; - 0, // public static final int KEY_KP4 = 0x4B; - 0, // public static final int KEY_KP5 = 0x4C; - 0, // public static final int KEY_KP6 = 0x4D; - 0, // public static final int KEY_KPPLUS = 0x4E; - 0, // public static final int KEY_KP1 = 0x4F; - 0, // public static final int KEY_KP2 = 0x50; - 0, // public static final int KEY_KP3 = 0x51; - 0, // public static final int KEY_KP0 = 0x52; - 0, // public static final int KEY_KPDOT = 0x53; - 0, // padding 0x54; - 0, // public static final int KEY_ZENKAKUHANKAKU = 0x55; - 226 // public static final int KEY_102ND = 0x56; - - // Many more KEYS currently not used by KMW... - }; private final Context context; private final KMManager.KeyboardType keyboardType; @@ -169,14 +63,14 @@ public class KMHardwareKeyboardInterpreter implements KeyEvent.Callback { // Range check scanCode. If it's beyond our expected range, just use "0" int scanCode = event.getScanCode(); - int code = (scanCode >= 0 && scanCode < scanCodeMap.length) ? scanCodeMap[scanCode] : 0 ; + int code = (scanCode >= 0 && scanCode < KMScanCodeMap.scanCodeMap.length) ? KMScanCodeMap.scanCodeMap[scanCode] : 0 ; if (code == 0) { // Not an alphanumeric, punctuation or enter/tab/space key return false; } // Send keystroke to KeymanWeb for processing: will return true to swallow the keystroke - return KMManager.executeHardwareKeystroke(code, keymanModifiers, keyboardType, Lstates); + return KMManager.executeHardwareKeystroke(code, keymanModifiers, keyboardType, Lstates, androidModifiers); } @Override diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java index d7005a127f..b60612aed5 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java @@ -175,9 +175,9 @@ final class KMKeyboard extends WebView { loadUrl(jsString); } - public void executeHardwareKeystroke(int code, int shift, int lstates) { - String jsFormat = "javascript:executeHardwareKeystroke(%d,%d, %d)"; - String jsString = String.format(jsFormat, code, shift, lstates); + public void executeHardwareKeystroke(int code, int shift, int lstates, int eventModifiers) { + String jsFormat = "javascript:executeHardwareKeystroke(%d,%d, %d, %d)"; + String jsString = String.format(jsFormat, code, shift, lstates, eventModifiers); loadUrl(jsString); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardJSHandler.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardJSHandler.java index 9e3025f3e2..d766105f9f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardJSHandler.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardJSHandler.java @@ -63,6 +63,9 @@ public abstract class KMKeyboardJSHandler { } } + @JavascriptInterface + public abstract boolean dispatchKey(final int code, final int eventModifiers); + // Insert the selected string s @JavascriptInterface public abstract void insertText(final int dn, final String s); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java index fdd9b691ea..d0529b8e1e 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java @@ -52,6 +52,7 @@ import com.tavultesoft.kmea.KMKeyboardJSHandler; import com.tavultesoft.kmea.KeyboardEventHandler.EventType; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; import com.tavultesoft.kmea.packages.PackageProcessor; +import com.tavultesoft.kmea.KMScanCodeMap; import com.tavultesoft.kmea.util.FileUtils; import org.json.JSONObject; @@ -198,23 +199,23 @@ public final class KMManager { IMService = service; } - public static boolean executeHardwareKeystroke(int code, int shift, int lstates) { + public static boolean executeHardwareKeystroke(int code, int shift, int lstates, int eventModifiers) { if (SystemKeyboard != null) { - return executeHardwareKeystroke(code, shift, KeyboardType.KEYBOARD_TYPE_SYSTEM, lstates); + return executeHardwareKeystroke(code, shift, KeyboardType.KEYBOARD_TYPE_SYSTEM, lstates, eventModifiers); } else if (InAppKeyboard != null) { - return executeHardwareKeystroke(code, shift, KeyboardType.KEYBOARD_TYPE_INAPP, lstates); + return executeHardwareKeystroke(code, shift, KeyboardType.KEYBOARD_TYPE_INAPP, lstates, eventModifiers); } return false; } public static boolean executeHardwareKeystroke( - int code, int shift, KeyboardType keyboard, int lstates) { + int code, int shift, KeyboardType keyboard, int lstates, int eventModifiers) { if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP) { - InAppKeyboard.executeHardwareKeystroke(code, shift, lstates); + InAppKeyboard.executeHardwareKeystroke(code, shift, lstates, eventModifiers); return true; } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM) { - SystemKeyboard.executeHardwareKeystroke(code, shift, lstates); + SystemKeyboard.executeHardwareKeystroke(code, shift, lstates, eventModifiers); return true; } @@ -1461,6 +1462,33 @@ public final class KMManager { KMInAppKeyboardJSHandler(Context context, KMKeyboard k) { super(context, k); } + private static final String HANDLER_TAG = "IAK: JS Handler"; + + @JavascriptInterface + public boolean dispatchKey(final int code, final int eventModifiers) { + Handler mainLoop = new Handler(Looper.getMainLooper()); + mainLoop.post(new Runnable() { + public void run() { + if (InAppKeyboard.subKeysWindow != null || KMTextView.activeView == null || KMTextView.activeView.getClass() != KMTextView.class) { + if ((KMTextView.activeView == null) && isDebugMode()) { + Log.w(HANDLER_TAG, "dispatchKey failed: activeView is null"); + } + return; + } + + // Handle tab or enter since KMW didn't process it + KMTextView textView = (KMTextView) KMTextView.activeView; + if (code == KMScanCodeMap.scanCodeMap[KMScanCodeMap.KEY_TAB]) { + KeyEvent event = new KeyEvent(0, 0, 0, KeyEvent.KEYCODE_TAB, 0, eventModifiers, 0, 0, 0); + textView.dispatchKeyEvent(event); + } else if (code == KMScanCodeMap.scanCodeMap[KMScanCodeMap.KEY_ENTER]) { + KeyEvent event = new KeyEvent(0, 0, 0, KeyEvent.KEYCODE_ENTER, 0, eventModifiers, 0, 0, 0); + textView.dispatchKeyEvent(event); + } + } + }); + return true; + } // This annotation is required in Jelly Bean and later: @JavascriptInterface @@ -1552,6 +1580,40 @@ public final class KMManager { KMSystemKeyboardJSHandler(Context context, KMKeyboard k) { super(context, k); } + private static final String HANDLER_TAG = "SWK: JS Handler"; + + @JavascriptInterface + public boolean dispatchKey(final int code, final int eventModifiers) { + Handler mainLoop = new Handler(Looper.getMainLooper()); + mainLoop.post(new Runnable() { + public void run() { + if (SystemKeyboard.subKeysWindow != null) { + return; + } + + InputConnection ic = IMService.getCurrentInputConnection(); + if (ic == null) { + if (isDebugMode()) { + Log.w(HANDLER_TAG, "insertText failed: InputConnection is null"); + } + return; + } + + SystemKeyboard.dismissHelpBubble(); + + // Handle tab or enter since KMW didn't process it + Log.d(HANDLER_TAG, "dispatchKey called with code: " + code + ", eventModifiers: " + eventModifiers); + if (code == KMScanCodeMap.scanCodeMap[KMScanCodeMap.KEY_TAB]) { + KeyEvent event = new KeyEvent(0, 0, 0, KeyEvent.KEYCODE_TAB, 0, eventModifiers, 0, 0, 0); + ic.sendKeyEvent(event); + } else if (code == KMScanCodeMap.scanCodeMap[KMScanCodeMap.KEY_ENTER]) { + KeyEvent event = new KeyEvent(0, 0, 0, KeyEvent.KEYCODE_ENTER, 0, eventModifiers, 0, 0, 0); + ic.sendKeyEvent(event); + } + } + }); + return true; + } // This annotation is required in Jelly Bean and later: @JavascriptInterface @@ -1566,7 +1628,7 @@ public final class KMManager { InputConnection ic = IMService.getCurrentInputConnection(); if (ic == null) { if (isDebugMode()) { - Log.w("SWK: JS Handler", "insertText failed: InputConnection is null"); + Log.w(HANDLER_TAG, "insertText failed: InputConnection is null"); } return; } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMScanCodeMap.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMScanCodeMap.java new file mode 100644 index 0000000000..3ad32fee47 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMScanCodeMap.java @@ -0,0 +1,115 @@ +package com.tavultesoft.kmea; + +public final class KMScanCodeMap { + + /** + * Maps relevant keycodes from android.view.keyevent to the standard keycodes used by Keyman + * keyboards. Is conveniently based at 0, else we might need to HashMap it instead. + */ + + /** + * From reference: https://source.android.com/devices/input#understanding-hid-usages-and-event-codes + * The Android API sometimes refers to the Linux key code associated with a key as its "scan code". + * This is technically incorrect in but it helps to distinguish Linux key codes from Android + * key codes in the API. + * + * Maps relevant Linux key code (LKC) to the standard key codes used by Keyman keyboards + * Table: https://source.android.com/devices/input/keyboard-devices + * + * Limitations: Intentionally not assigning number pad keys + */ + final static int scanCodeMap[] = { + 0, // padding = 0x00; + 0, // public static final int KEY_ESC = 0x01; + '1', // public static final int KEY_1 = 0x02; + '2', // public static final int KEY_2 = 0x03; + '3', // public static final int KEY_3 = 0x04; + '4', // public static final int KEY_4 = 0x05; + '5', // public static final int KEY_5 = 0x06; + '6', // public static final int KEY_6 = 0x07; + '7', // public static final int KEY_7 = 0x08; + '8', // public static final int KEY_8 = 0x09; + '9', // public static final int KEY_9 = 0x0A; + '0', // public static final int KEY_0 = 0x0B; + 189, // public static final int KEY_MINUS = 0x0C; + 187, // public static final int KEY_EQUALS = 0x0D; + 8, // public static final int KEY_BACKSPACE = 0x0E; + 9, // public static final int KEY_TAB = 0x0F; + 'Q', // public static final int KEY_Q = 0x10; + 'W', // public static final int KEY_W = 0x11; + 'E', // public static final int KEY_E = 0x12; + 'R', // public static final int KEY_R = 0x13; + 'T', // public static final int KEY_T = 0x14; + 'Y', // public static final int KEY_Y = 0x15; + 'U', // public static final int KEY_U = 0x16; + 'I', // public static final int KEY_I = 0x17; + 'O', // public static final int KEY_O = 0x18; + 'P', // public static final int KEY_P = 0x19; + 219, // public static final int KEY_LEFTBRACE = 0x1A; + 221, // public static final int KEY_RIGHTBRACE = 0x1B; + 13, // public static final int KEY_ENTER = 0x1C; + 0, // public static final int KEY_LEFTCTRL = 0x1D; + 'A', // public static final int KEY_A = 0x1E; + 'S', // public static final int KEY_S = 0x1F; + 'D', // public static final int KEY_D = 0x20; + 'F', // public static final int KEY_F = 0x21; + 'G', // public static final int KEY_G = 0x22; + 'H', // public static final int KEY_H = 0x23; + 'J', // public static final int KEY_J = 0x24; + 'K', // public static final int KEY_K = 0x25; + 'L', // public static final int KEY_L = 0x26; + 186, // public static final int KEY_SEMICOLON = 0x27; + 222, // public static final int KEY_APOSTROPHE = 0x28; + 192, // public static final int KEY_GRAVE = 0x29; + 0, // public static final int KEY_LEFTSHIFT = 0x2A; + 220, // public static final int KEY_BACKSLASH = 0x2B; + 'Z', // public static final int KEY_Z = 0x2C; + 'X', // public static final int KEY_X = 0x2D; + 'C', // public static final int KEY_C = 0x2E; + 'V', // public static final int KEY_V = 0x2F; + 'B', // public static final int KEY_B = 0x30; + 'N', // public static final int KEY_N = 0x31; + 'M', // public static final int KEY_M = 0x32; + 188, // public static final int KEY_COMMA = 0x33; + 190, // public static final int KEY_DOT = 0x34; + 191, // public static final int KEY_SLASH = 0x35; + 0, // public static final int KEY_RIGHTSHIFT = 0x36; + 0, // public static final int KEY_KPASTERISK = 0x37; + 0, // public static final int KEY_LEFTALT = 0x38; + 32, // public static final int KEY_SPACE = 0x39; + 0, // public static final int KEY_CAPSLOCK = 0x3A; + 0, // public static final int KEY_F1 = 0x3B; + 0, // public static final int KEY_F2 = 0x3C; + 0, // public static final int KEY_F3 = 0x3D; + 0, // public static final int KEY_F4 = 0x3E; + 0, // public static final int KEY_F5 = 0x3F; + 0, // public static final int KEY_F6 = 0x40; + 0, // public static final int KEY_F7 = 0x41; + 0, // public static final int KEY_F8 = 0x42; + 0, // public static final int KEY_F9 = 0x43; + 0, // public static final int KEY_F10 = 0x44; + 0, // public static final int KEY_NUMLOCK = 0x45; + 0, // public static final int KEY_SCROLLLOCK = 0x46; + 0, // public static final int KEY_KP7 = 0x47; + 0, // public static final int KEY_KP8 = 0x48; + 0, // public static final int KEY_KP9 = 0x49; + 0, // public static final int KEY_KPMINUS = 0x4A; + 0, // public static final int KEY_KP4 = 0x4B; + 0, // public static final int KEY_KP5 = 0x4C; + 0, // public static final int KEY_KP6 = 0x4D; + 0, // public static final int KEY_KPPLUS = 0x4E; + 0, // public static final int KEY_KP1 = 0x4F; + 0, // public static final int KEY_KP2 = 0x50; + 0, // public static final int KEY_KP3 = 0x51; + 0, // public static final int KEY_KP0 = 0x52; + 0, // public static final int KEY_KPDOT = 0x53; + 0, // padding 0x54; + 0, // public static final int KEY_ZENKAKUHANKAKU = 0x55; + 226 // public static final int KEY_102ND = 0x56; + + // Many more KEYS currently not used by KMW... + }; + + final static int KEY_TAB = 0x0F; + final static int KEY_ENTER = 0x1C; +} \ No newline at end of file diff --git a/android/history.md b/android/history.md index b6461cad4a..3a8c33356d 100644 --- a/android/history.md +++ b/android/history.md @@ -2,6 +2,7 @@ ## 2019-01-14 11.0.2055 beta * Keyman for Android 11 requires a minimum version of Android 4.1 (Jelly Bean) (#1532) +* When KMW doesn't process external "tab" or "enter" keys, have the Android app dispatch the keys (#1526) ## 2019-01-10 11.0.2054 beta * Fix keyboard version comparison that was causing "Unable to contact Keyman server" notifications (#1520) diff --git a/web/source/kmwembedded.ts b/web/source/kmwembedded.ts index 6750afea84..c081c2ae3d 100644 --- a/web/source/kmwembedded.ts +++ b/web/source/kmwembedded.ts @@ -579,15 +579,12 @@ * @return {boolean} true if key code successfully processed */ keymanweb.processDefaultMapping = function(code, shift, Lelem, keyName) { + // Default handling for external keys. + // Intentionally not assigning K_TAB or K_ENTER so KMW will pass them back + // to the mobile apps to handle (insert characters or navigate forms). if (code == osk.keyCodes.K_SPACE) { kbdInterface.output(0, Lelem, ' '); return true; - } else if (code == osk.keyCodes.K_ENTER) { - kbdInterface.output(0, Lelem, '\n'); - return true; - } else if (code == osk.keyCodes.K_TAB) { - kbdInterface.output(0, Lelem, '\t'); - return true; } else if (code == osk.keyCodes.K_BKSP) { kbdInterface.defaultBackspace(); return true; @@ -601,6 +598,7 @@ return true; } + // Determine the character from the OSK var ch = osk.defaultKeyOutput(keyName, code, shift, false, undefined); if(ch) { kbdInterface.output(0, Lelem, ch);