Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-22.04, macos-latest, windows-latest]
include:
- os: ubuntu-latest
- os: ubuntu-22.04
ARCH: "x86_64"
- os: macos-latest
ARCH: "aarch64"
Expand Down Expand Up @@ -56,11 +56,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java 11
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 11
java-version: 17

- name: Grant execute permission for gradlew
run: chmod +x gradlew
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ jobs:
fetch-depth: 5
persist-credentials: false

- name: Setup Java 11
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 11
java-version: 17

- name: Grant execute permission for gradlew
run: chmod +x gradlew
Expand Down
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
Expand Down Expand Up @@ -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;

Expand All @@ -60,6 +59,7 @@

import androidx.core.view.WindowCompat;

import java.util.HashMap;
import java.util.TimeZone;
import javafx.scene.input.KeyCode;

Expand All @@ -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);
Expand Down Expand Up @@ -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";
Comment on lines +235 to +236
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setActiveNodeId() logs currentActiveNodeId instead of the incoming id, 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.

Suggested change
Log.d(TAG, "setActiveNodeId: " + currentActiveNodeId);
currentActiveNodeId = (id != null) ? id : "default";
String newActiveNodeId = (id != null) ? id : "default";
Log.d(TAG, "setActiveNodeId: old=" + currentActiveNodeId + ", new=" + newActiveNodeId);
currentActiveNodeId = newActiveNodeId;

Copilot uses AI. Check for mistakes.
}

/**
* 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
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setKeyboardType() is never invoked anywhere in this repo, and there’s no corresponding JNI method handle/call from the native layer (unlike showIME/hideIME). As-is, currentInputType will always stay at the default, so the PR doesn’t actually enable keyboard-type switching. Please add the native-to-Java call path that sets this value (and triggers restartInput), or remove the unused API until it’s wired.

Copilot uses AI. Check for mistakes.

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);
Expand All @@ -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).
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "identifyed" should be "identified".

Suggested change
* 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 uses AI. Check for mistakes.
*/
private final HashMap<String, String> composedTexts = new HashMap<>();

public InternalSurfaceView(Context context) {
super(context);
setFocusableInTouchMode(true);
Expand Down Expand Up @@ -303,33 +377,56 @@ public boolean dispatchTouchEvent(MotionEvent event) {
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
Log.d(TAG, "onCreateInputConnection");
// Allows predictive text
outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
// Use the keyboard type set from the JavaFX layer. The types that include TYPE_CLASS_TEXT have
// predictive text features, and a custom BasicInputConnection implementation is needed.
outAttrs.inputType = currentInputType;
// Remove top textfield editor on landscape
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;
// A new IME session is starting (keyboard shown, field focused, etc.).
// Reset the composing text and the simulated text for the active node
currentComposingText = "";
composedTexts.putIfAbsent(currentActiveNodeId, "");

return new BaseInputConnection(this, true) {

@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
// remove old text
replaceText();
int deleteCount = currentComposingText.length();
currentComposingText = text.toString();
boolean result = super.setComposingText(text, newCursorPosition);
processText(text.toString());
// Send individual key events (backspaces + typed text).
resetText(deleteCount);
processAndroidKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), text.toString(), -1, 0));
// Update composed text with one call
updateComposedText(deleteCount, text.toString());
return result;
}

@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
// remove old text
replaceText();
int deleteCount = currentComposingText.length();
currentComposingText = "";
boolean result = super.commitText(text, newCursorPosition);
processText(text.toString());
if (ENTER_STRING.equals(text.toString())) {
// Clear composing region, then fire Enter.
resetText(deleteCount);
// Composed text is gone before the Enter key.
updateComposedText(deleteCount, "");
processAndroidKeyEvent(ENTER_DOWN_EVENT);
processAndroidKeyEvent(ENTER_UP_EVENT);
} else {
// backspaces to erase composing text, then insert committed word.
resetText(deleteCount);
processAndroidKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), text.toString(), -1, 0));
// committed word replaces the composing region at once.
updateComposedText(deleteCount, text.toString());
}
return result;
}

@Override
public boolean finishComposingText() {
currentComposingText = "";
boolean result = super.finishComposingText();
Editable content = getEditable();
if (content != null) {
Expand All @@ -340,52 +437,17 @@ public boolean finishComposingText() {

@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
// Explicit user deletion (e.g. hardware backspace).
boolean result = super.deleteSurroundingText(beforeLength, afterLength);
resetText(beforeLength - afterLength);
int length = beforeLength - afterLength;
// individual backspace key events.
resetText(length);
// sync with the explicit deletion at once.
updateComposedText(length, "");
Comment on lines +442 to +446
Copy link

Copilot AI Apr 10, 2026

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.

Suggested change
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 uses AI. Check for mistakes.
return result;
}

private void processText(String text) {
if (ENTER_STRING.equals(text)) {
// send enter
processAndroidKeyEvent(ENTER_DOWN_EVENT);
processAndroidKeyEvent(ENTER_UP_EVENT);
} else {
// send action_multiple with new text
processAndroidKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), text, -1, 0));
}
}

private void replaceText() {
Editable content = getEditable();
if (content == null) {
return;
}

int a = getComposingSpanStart(content);
int b = getComposingSpanEnd(content);
if (b < a) {
int tmp = a;
a = b;
b = tmp;
}

if (a == -1 || b == -1) {
a = Selection.getSelectionStart(content);
b = Selection.getSelectionEnd(content);
if (a < 0) a = 0;
if (b < 0) b = 0;
if (b < a) {
int tmp = a;
a = b;
b = tmp;
}
}
resetText(b - a);
}

private void resetText(int length) {
// clear the old text
for (int i = 0; i < length; i++) {
processAndroidKeyEvent(BACK_DOWN_EVENT);
processAndroidKeyEvent(BACK_UP_EVENT);
Expand All @@ -405,10 +467,23 @@ public boolean dispatchKeyEvent(final KeyEvent event) {
// let Android OS handle volume events
consume = false;
}
processAndroidKeyEvent (event);
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
// sync after direct backspace key presses.
updateComposedText(1, "");
}
processAndroidKeyEvent(event);
return consume;
}

private void updateComposedText(int deleteCount, String text) {
String id = currentActiveNodeId;
String current = composedTexts.getOrDefault(id, "");
String updated = current.substring(0, current.length() - Math.min(deleteCount, current.length())) + text;
composedTexts.put(id, updated);
Log.d(TAG, "updateComposedText [" + id + "]: TextInputControl shows: '" + updated + "'");
Copy link

Copilot AI Apr 10, 2026

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.

Suggested change
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 uses AI. Check for mistakes.
nativeNotifyComposingText(id, updated);
}
Comment on lines +478 to +485
Copy link

Copilot AI Apr 10, 2026

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.

Copilot uses AI. Check for mistakes.

private final Handler handler = new Handler();
private final LongPress longPress = new LongPress();

Expand Down
13 changes: 12 additions & 1 deletion src/main/resources/native/android/c/attach_adapter.c
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
Expand Down Expand Up @@ -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);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logs the full composed text (text=%s) at ERROR level, which can expose sensitive user input in logcat. Consider removing/redacting the text (log length/id only) and/or downgrading to DEBUG behind a build-time flag.

Suggested change
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);

Copilot uses AI. Check for mistakes.
attach_setComposingText(idChars, textChars);
(*env)->ReleaseStringUTFChars(env, text, textChars);
(*env)->ReleaseStringUTFChars(env, id, idChars);
}
3 changes: 2 additions & 1 deletion src/main/resources/native/android/c/grandroid.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2021, 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
Expand Down Expand Up @@ -41,6 +41,7 @@

extern jmethodID activity_showIME;
extern jmethodID activity_hideIME;
extern jmethodID activity_setActiveNodeId;

extern ANativeWindow *window;
extern jfloat density;
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/native/android/c/grandroid_ext.h
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
Expand Down Expand Up @@ -58,9 +58,11 @@ jobject substrateGetActivity();
#ifdef SUBSTRATE
void __attribute__((weak)) attach_setActivityResult(jint requestCode, jint resultCode, jobject intent) {}
void __attribute__((weak)) attach_setLifecycleEvent(const char *event) {}
void __attribute__((weak)) attach_setComposingText(const char *id, const char *text) {}
#else
void attach_setActivityResult(jint requestCode, jint resultCode, jobject intent);
void attach_setLifecycleEvent(const char *event);
void attach_setComposingText(const char *id, const char *text);
#endif

#define ATTACH_GRAAL() \
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/native/android/c/launcher.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2022, 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
Expand Down Expand Up @@ -38,6 +38,7 @@ jclass permissionActivityClass;
jobject activity;
jmethodID activity_showIME;
jmethodID activity_hideIME;
jmethodID activity_setActiveNodeId;

JavaVM *androidVM;
JNIEnv *androidEnv;
Expand Down Expand Up @@ -77,6 +78,7 @@ void registerMethodHandles(JNIEnv *aenv)
permissionActivityClass = (*aenv)->NewGlobalRef(aenv, (*aenv)->FindClass(aenv, "com/gluonhq/helloandroid/PermissionRequestActivity"));
activity_showIME = (*aenv)->GetStaticMethodID(aenv, activityClass, "showIME", "()V");
activity_hideIME = (*aenv)->GetStaticMethodID(aenv, activityClass, "hideIME", "()V");
activity_setActiveNodeId = (*aenv)->GetStaticMethodID(aenv, activityClass, "setActiveNodeId", "(Ljava/lang/String;)V");
registerJavaFXMethodHandles(aenv);
}

Expand Down
Loading