-
Notifications
You must be signed in to change notification settings - Fork 54
Add support for keyboard types and composed text property on Android #1346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7eac3c3
eab70b1
4c60092
c204e1b
e54fe9b
ba105a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||
| /* | ||||||||||||||||||||||
| * Copyright (c) 2019, 2025, Gluon | ||||||||||||||||||||||
| * Copyright (c) 2019, 2026, Gluon | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * This program is free software: you can redistribute it and/or modify | ||||||||||||||||||||||
| * it under the terms of the GNU General Public License as published by | ||||||||||||||||||||||
|
|
@@ -38,7 +38,6 @@ | |||||||||||||||||||||
| import android.os.SystemClock; | ||||||||||||||||||||||
| import android.text.Editable; | ||||||||||||||||||||||
| import android.text.InputType; | ||||||||||||||||||||||
| import android.text.Selection; | ||||||||||||||||||||||
| import android.util.DisplayMetrics; | ||||||||||||||||||||||
| import android.util.Log; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -60,6 +59,7 @@ | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| import androidx.core.view.WindowCompat; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.util.HashMap; | ||||||||||||||||||||||
| import java.util.TimeZone; | ||||||||||||||||||||||
| import javafx.scene.input.KeyCode; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -78,6 +78,12 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback, | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static InputMethodManager imm; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** Android InputType to use the next time the IME connects. Defaults to plain text. */ | ||||||||||||||||||||||
| private static int currentInputType = InputType.TYPE_CLASS_TEXT; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** The id of the JavaFX Node that currently has focus. Set from Attach via keyboard.c. */ | ||||||||||||||||||||||
| private static String currentActiveNodeId = "default"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Override | ||||||||||||||||||||||
| protected void onCreate(Bundle savedInstanceState) { | ||||||||||||||||||||||
| super.onCreate(savedInstanceState); | ||||||||||||||||||||||
|
|
@@ -221,13 +227,74 @@ public void run() { | |||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * External call that passes the JavaFX text control that is currently active, so composing text | ||||||||||||||||||||||
| * can be tagged with the correct id. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| static void setActiveNodeId(String id) { | ||||||||||||||||||||||
| Log.d(TAG, "setActiveNodeId: " + currentActiveNodeId); | ||||||||||||||||||||||
| currentActiveNodeId = (id != null) ? id : "default"; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * External call that sets the Android InputType keyboard. Triggers a restartInput so the change takes | ||||||||||||||||||||||
| * effect immediately if the keyboard is already visible. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| static void setKeyboardType(int keyboardTypeValue) { | ||||||||||||||||||||||
| currentInputType = mapKeyboardTypeToInputType(keyboardTypeValue); | ||||||||||||||||||||||
| Log.d(TAG, "setKeyboardType: keyboardTypeValue=" + keyboardTypeValue + " -> inputType=" + currentInputType); | ||||||||||||||||||||||
| if (imm != null && mView != null) { | ||||||||||||||||||||||
| instance.runOnUiThread(() -> { | ||||||||||||||||||||||
| imm.restartInput(mView); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+239
to
+251
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static int mapKeyboardTypeToInputType(int keyboardTypeValue) { | ||||||||||||||||||||||
| switch (keyboardTypeValue) { | ||||||||||||||||||||||
| case 3: // URL | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI; | ||||||||||||||||||||||
| case 4: // NUMBER_PAD | ||||||||||||||||||||||
| case 11: // ASCII_NUMBER_PAD | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_NUMBER; | ||||||||||||||||||||||
| case 5: // PHONE_PAD | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_PHONE; | ||||||||||||||||||||||
| case 6: // NAME_PHONE_PAD | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME; | ||||||||||||||||||||||
| case 7: // EMAIL | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; | ||||||||||||||||||||||
| case 8: // DECIMAL_PAD | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL; | ||||||||||||||||||||||
| case 0: // DEFAULT | ||||||||||||||||||||||
| case 1: // ASCII | ||||||||||||||||||||||
| case 2: // NUMBERS_AND_PUNCTUATION | ||||||||||||||||||||||
| case 9: // TWITTER | ||||||||||||||||||||||
| case 10: // WEB_SEARCH | ||||||||||||||||||||||
| default: | ||||||||||||||||||||||
| return InputType.TYPE_CLASS_TEXT; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private native void startGraalApp(String[] launchArgs); | ||||||||||||||||||||||
| private native long surfaceReady(Surface surface, float density); | ||||||||||||||||||||||
| private native void nativeSetSurface(Surface surface); | ||||||||||||||||||||||
| private native void nativeSurfaceRedrawNeeded(); | ||||||||||||||||||||||
| private native void nativeGotTouchEvent(int pcount, int[] actions, int[] ids, int[] touchXs, int[] touchYs); | ||||||||||||||||||||||
| private native void nativeDispatchKeyEvent(int type, int key, char[] chars, int charCount, int modifiers); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Forwards the final composed text to the JavaFX layer via attach_adapter.c, | ||||||||||||||||||||||
| * without the internals of the BasicInputConnection (like deleting chars and typing | ||||||||||||||||||||||
| * them all over again while composing the resulting text). | ||||||||||||||||||||||
| * <p>Note that listeners attached to the JavaFX text input control {@code textProperty()} will still | ||||||||||||||||||||||
| * catch all those internals, so this is a convenient method to export to the JavaFX layer | ||||||||||||||||||||||
| * only the final composed text instead</p> | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * @param id the JavaFX Node id of the active text control | ||||||||||||||||||||||
| * @param text the full text content for that control | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| private native void nativeNotifyComposingText(String id, String text); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private native void nativeDispatchLifecycleEvent(String event); | ||||||||||||||||||||||
| private native void nativeDispatchActivityResult(int requestCode, int resultCode, Intent intent); | ||||||||||||||||||||||
| private native void nativeNotifyMenu(int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger); | ||||||||||||||||||||||
|
|
@@ -240,6 +307,13 @@ class InternalSurfaceView extends SurfaceView { | |||||||||||||||||||||
| private final KeyEvent ENTER_DOWN_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER); | ||||||||||||||||||||||
| private final KeyEvent ENTER_UP_EVENT = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private String currentComposingText = ""; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Map of currently composed text per Text Input control (identifyed by its id). | ||||||||||||||||||||||
|
||||||||||||||||||||||
| * Map of currently composed text per Text Input control (identifyed by its id). | |
| * Map of currently composed text per Text Input control (identified by its id). |
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In deleteSurroundingText(), computing length = beforeLength - afterLength can be negative when the IME deletes characters after the cursor. Passing a negative deleteCount into resetText/updateComposedText can lead to StringIndexOutOfBoundsException in updateComposedText (because Math.min(deleteCount, current.length()) becomes negative). Handle before/after deletions separately (e.g., treat deleteCount as max(beforeLength, 0) for backspaces) and ensure deleteCount is never negative before calling updateComposedText.
| int length = beforeLength - afterLength; | |
| // individual backspace key events. | |
| resetText(length); | |
| // sync with the explicit deletion at once. | |
| updateComposedText(length, ""); | |
| int deleteCount = Math.max(beforeLength, 0); | |
| // Send individual backspace key events for deletions before the cursor only. | |
| resetText(deleteCount); | |
| // Sync the removed composing text without ever passing a negative delete count. | |
| updateComposedText(deleteCount, ""); |
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both the Java and native layers log the full composed text content (updated / text=%s). This can leak sensitive user input into logcat (and it’s currently logged at ERROR level in native code). Consider removing the text from logs or logging only metadata (length/id) behind a debug flag.
| Log.d(TAG, "updateComposedText [" + id + "]: TextInputControl shows: '" + updated + "'"); | |
| if (Log.isLoggable(TAG, Log.DEBUG)) { | |
| Log.d(TAG, "updateComposedText [" + id + "]: deleteCount=" + deleteCount | |
| + ", insertedLength=" + text.length() | |
| + ", composedLength=" + updated.length()); | |
| } |
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updateComposedText() assumes deleteCount is non-negative; if it becomes negative (e.g., from deleteSurroundingText’s before/after calculation), the substring end index can exceed current.length() and crash. Consider clamping deleteCount to [0, current.length()] (and/or early-return) before computing the substring.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||
| /* | ||||||||
| * Copyright (c) 2020, Gluon | ||||||||
| * Copyright (c) 2020, 2026, Gluon | ||||||||
| * | ||||||||
| * This program is free software: you can redistribute it and/or modify | ||||||||
| * it under the terms of the GNU General Public License as published by | ||||||||
|
|
@@ -42,3 +42,14 @@ JNIEXPORT void JNICALL Java_com_gluonhq_helloandroid_MainActivity_nativeDispatch | |||||||
| LOGE(stderr, "Dispatching activity result from native Dalvik layer: %d %d", requestCode, resultCode); | ||||||||
| attach_setActivityResult(requestCode, resultCode, intent); | ||||||||
| } | ||||||||
|
|
||||||||
| // keyboard: Composing text (IME predictive text for node with id) | ||||||||
| JNIEXPORT void JNICALL Java_com_gluonhq_helloandroid_MainActivity_nativeNotifyComposingText(JNIEnv *env, jobject activity, jstring id, jstring text) | ||||||||
| { | ||||||||
| const char *idChars = (*env)->GetStringUTFChars(env, id, NULL); | ||||||||
| const char *textChars = (*env)->GetStringUTFChars(env, text, NULL); | ||||||||
| LOGE(stderr, "Dispatching composing text from native Dalvik layer: id=%s, text=%s", idChars, textChars); | ||||||||
|
||||||||
| LOGE(stderr, "Dispatching composing text from native Dalvik layer: id=%s, text=%s", idChars, textChars); | |
| jsize textLength = (*env)->GetStringUTFLength(env, text); | |
| LOGE(stderr, "Dispatching composing text from native Dalvik layer: id=%s, text_len=%d", idChars, (int) textLength); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setActiveNodeId() logs
currentActiveNodeIdinstead of the incomingid, which makes the log misleading (it prints the previous active id). Log the parameter (or both old/new values) so debugging focus changes is accurate.