diff --git a/app/src/main/cpp/lorie/android.c b/app/src/main/cpp/lorie/android.c index 0fc453deb..52b384fb9 100644 --- a/app/src/main/cpp/lorie/android.c +++ b/app/src/main/cpp/lorie/android.c @@ -567,7 +567,6 @@ Java_com_termux_x11_CmdEntryPoint_listenForConnections(JNIEnv *env, jobject thiz } } close(client); - client = -1; } } @@ -589,8 +588,11 @@ static inline void checkConnection(JNIEnv* env) { } } +static jobject mLorieView = NULL; +static jmethodID mFlushComposingView = NULL; + JNIEXPORT void JNICALL -Java_com_termux_x11_LorieView_connect(__unused JNIEnv* env, __unused jobject cls, jint fd) { +Java_com_termux_x11_LorieView_nativeInit(JNIEnv *env, jobject thiz) { if (!Charset.self) { // Init clipboard-related JNI stuff Charset.self = FindClassOrDie(env, "java/nio/charset/Charset"); @@ -601,6 +603,19 @@ Java_com_termux_x11_LorieView_connect(__unused JNIEnv* env, __unused jobject cls CharBuffer.toString = FindMethodOrDie(env, CharBuffer.self, "toString", "()Ljava/lang/String;", JNI_FALSE); } + mLorieView = (*env)->NewGlobalRef(env, thiz); + mFlushComposingView = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, thiz), "flushComposingView", "()V"); +} + +static void flushComposingView(JNIEnv* env) { + if (!env || !mLorieView || !mFlushComposingView) + return; + + (*env)->CallVoidMethod(env, mLorieView, mFlushComposingView); +} + +JNIEXPORT void JNICALL +Java_com_termux_x11_LorieView_connect(__unused JNIEnv* env, __unused jobject cls, jint fd) { conn_fd = fd; fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); checkConnection(env); @@ -679,6 +694,7 @@ Java_com_termux_x11_LorieView_setClipboardSyncEnabled(__unused JNIEnv* env, __un JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_sendClipboardAnnounce(JNIEnv *env, __unused jobject thiz) { if (conn_fd != -1) { + flushComposingView(env); lorieEvent e = { .type = EVENT_CLIPBOARD_ANNOUNCE }; write(conn_fd, &e, sizeof(e)); checkConnection(env); @@ -688,6 +704,7 @@ Java_com_termux_x11_LorieView_sendClipboardAnnounce(JNIEnv *env, __unused jobjec JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_sendClipboardEvent(JNIEnv *env, __unused jobject thiz, jbyteArray text) { if (conn_fd != -1 && text) { + flushComposingView(env); jsize length = (*env)->GetArrayLength(env, text); jbyte* str = (*env)->GetByteArrayElements(env, text, NULL); lorieEvent e = { .clipboardSend = { .t = EVENT_CLIPBOARD_SEND, .count = length } }; @@ -701,6 +718,7 @@ Java_com_termux_x11_LorieView_sendClipboardEvent(JNIEnv *env, __unused jobject t JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_sendWindowChange(__unused JNIEnv* env, __unused jobject cls, jint width, jint height, jint framerate) { if (conn_fd != -1) { + flushComposingView(env); lorieEvent e = { .screenSize = { .t = EVENT_SCREEN_SIZE, .width = width, .height = height, .framerate = framerate } }; write(conn_fd, &e, sizeof(e)); checkConnection(env); @@ -710,6 +728,7 @@ Java_com_termux_x11_LorieView_sendWindowChange(__unused JNIEnv* env, __unused jo JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_sendMouseEvent(__unused JNIEnv* env, __unused jobject cls, jfloat x, jfloat y, jint which_button, jboolean button_down, jboolean relative) { if (conn_fd != -1) { + flushComposingView(env); lorieEvent e = { .mouse = { .t = EVENT_MOUSE, .x = x, .y = y, .detail = which_button, .down = button_down, .relative = relative } }; write(conn_fd, &e, sizeof(e)); checkConnection(env); @@ -719,6 +738,7 @@ Java_com_termux_x11_LorieView_sendMouseEvent(__unused JNIEnv* env, __unused jobj JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_sendTouchEvent(__unused JNIEnv* env, __unused jobject cls, jint action, jint id, jint x, jint y) { if (conn_fd != -1 && action != -1) { + flushComposingView(env); lorieEvent e = { .touch = { .t = EVENT_TOUCH, .type = action, .id = id, .x = x, .y = y } }; write(conn_fd, &e, sizeof(e)); checkConnection(env); @@ -730,6 +750,7 @@ Java_com_termux_x11_LorieView_sendStylusEvent(JNIEnv *env, __unused jobject thiz jint pressure, jint tilt_x, jint tilt_y, jint orientation, jint buttons, jboolean eraser, jboolean mouse) { if (conn_fd != -1) { + flushComposingView(env); lorieEvent e = { .stylus = { .t = EVENT_STYLUS, .x = x, .y = y, .pressure = pressure, .tilt_x = tilt_x, .tilt_y = tilt_y, .orientation = orientation, .buttons = buttons, .eraser = eraser, .mouse = mouse } }; write(conn_fd, &e, sizeof(e)); checkConnection(env); @@ -739,6 +760,7 @@ Java_com_termux_x11_LorieView_sendStylusEvent(JNIEnv *env, __unused jobject thiz JNIEXPORT void JNICALL Java_com_termux_x11_LorieView_requestStylusEnabled(JNIEnv *env, __unused jclass clazz, jboolean enabled) { if (conn_fd != -1) { + flushComposingView(env); lorieEvent e = { .stylusEnable = { .t = EVENT_STYLUS_ENABLE, .enable = enabled } }; write(conn_fd, &e, sizeof(e)); checkConnection(env); @@ -748,6 +770,7 @@ Java_com_termux_x11_LorieView_requestStylusEnabled(JNIEnv *env, __unused jclass JNIEXPORT jboolean JNICALL Java_com_termux_x11_LorieView_sendKeyEvent(__unused JNIEnv* env, __unused jobject cls, jint scan_code, jint key_code, jboolean key_down) { if (conn_fd != -1) { + flushComposingView(env); int code = (scan_code) ?: android_to_linux_keycode[key_code]; log(DEBUG, "Sending key: %d (%d %d %d)", code + 8, scan_code, key_code, key_down); lorieEvent e = { .key = { .t = EVENT_KEY, .key = code + 8, .state = key_down } }; diff --git a/app/src/main/java/com/termux/x11/LorieView.java b/app/src/main/java/com/termux/x11/LorieView.java index afbc24942..93603cfc7 100644 --- a/app/src/main/java/com/termux/x11/LorieView.java +++ b/app/src/main/java/com/termux/x11/LorieView.java @@ -11,8 +11,8 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; -import android.os.Build; import android.text.Editable; +import android.text.InputFilter; import android.text.InputType; import android.util.AttributeSet; import android.util.Log; @@ -23,8 +23,7 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; +import android.widget.TextView; import androidx.annotation.Keep; import androidx.annotation.NonNull; @@ -32,9 +31,12 @@ import com.termux.x11.input.InputStub; import com.termux.x11.input.TouchInputHandler; -import java.nio.charset.StandardCharsets; +import java.nio.CharBuffer; +import java.util.Arrays; import java.util.regex.PatternSyntaxException; +import static java.nio.charset.StandardCharsets.UTF_8; + @Keep @SuppressLint("WrongConstant") @SuppressWarnings("deprecation") public class LorieView extends SurfaceView implements InputStub { @@ -50,12 +52,95 @@ interface PixelFormat { private long lastClipboardTimestamp = System.currentTimeMillis(); private static boolean clipboardSyncEnabled = false; private static boolean hardwareKbdScancodesWorkaround = false; - private final InputMethodManager mIMM = (InputMethodManager)getContext().getSystemService( Context.INPUT_METHOD_SERVICE); - private String mImeLang; - private boolean mImeCJK; - public boolean enableGboardCJK; private Callback mCallback; private final Point p = new Point(); + private TextView composingView; + private final Editable mEditable = new Editable() { + // Wrap editable methods and hook and intercept changes + void onChange() { + int len = thiz.length(); + char[] chars = new char[len]; + thiz.getChars(0, len, chars, 0); + onComposingTextChange(chars, len); + } + private final Editable thiz = Editable.Factory.getInstance().newEditable(""); + @Override public int length() { return thiz.length(); } + @Override public char charAt(int index) { return thiz.charAt(index); } + @NonNull @Override public CharSequence subSequence(int start, int end) { return thiz.subSequence(start, end); } + @Override public T[] getSpans(int start, int end, Class type) { return thiz.getSpans(start, end, type); } + @Override public int getSpanStart(Object tag) { return thiz.getSpanStart(tag); } + @Override public int getSpanEnd(Object tag) { return thiz.getSpanEnd(tag); } + @Override public int getSpanFlags(Object tag) { return thiz.getSpanFlags(tag); } + @Override public int nextSpanTransition(int start, int limit, Class type) { return thiz.nextSpanTransition(start, limit, type); } + @Override public void setSpan(Object what, int start, int end, int flags) { thiz.setSpan(what, start, end, flags); onChange(); } + @Override public void removeSpan(Object what) { thiz.removeSpan(what); onChange(); } + @Override public void getChars(int start, int end, char[] dest, int destoff) { thiz.getChars(start, end, dest, destoff); } + @NonNull @Override public Editable replace(int st, int en, CharSequence source, int start, int end) { Editable e = thiz.replace(st, en, source, start, end); onChange(); android.util.Log.d("EDITABLE", "replace0"); return e; } + @NonNull @Override public Editable replace(int st, int en, CharSequence text) { Editable e = thiz.replace(st, en, text); onChange(); android.util.Log.d("EDITABLE", "replace1" + st + " " + en + " " + text); return e; } + @NonNull @Override public Editable insert(int where, CharSequence text, int start, int end) { Editable e = thiz.insert(where, text, start, end); onChange(); android.util.Log.d("EDITABLE", "insert0"); return e; } + @NonNull @Override public Editable insert(int where, CharSequence text) { Editable e = thiz.insert(where, text); onChange(); android.util.Log.d("EDITABLE", "insert1"); return e; } + @NonNull @Override public Editable delete(int st, int en) { Editable e = thiz.delete(st, en); onChange(); android.util.Log.d("EDITABLE", "delete"); return e; } + @NonNull @Override public Editable append(CharSequence text) { Editable e = thiz.append(text); onChange(); android.util.Log.d("EDITABLE", "append0"); return e; } + @NonNull @Override public Editable append(CharSequence text, int start, int end) { Editable e = thiz.append(text, start, end); onChange(); android.util.Log.d("EDITABLE", "append1"); return e; } + @NonNull @Override public Editable append(char text) { Editable e = thiz.append(text); onChange(); android.util.Log.d("EDITABLE", "append2"); return e; } + @Override public void clear() { thiz.clear(); onChange(); } + @Override public void clearSpans() { thiz.clearSpans(); onChange(); } + @Override public void setFilters(InputFilter[] filters) { thiz.setFilters(filters); onChange(); } + @Override public InputFilter[] getFilters() { return thiz.getFilters(); } + }; + private final InputConnection mConnection = new BaseInputConnection(this, false) { + private final CharSequence seq = " "; + + @Override + public Editable getEditable() { + return mEditable; + } + + // Needed to send arrow keys with IME's cursor control feature + @Override + public CharSequence getTextBeforeCursor(int length, int flags) { + return seq; + } + + // Needed to send arrow keys with IME's cursor control feature + @Override + public CharSequence getTextAfterCursor(int length, int flags) { + return seq; + } + + // There is no real editor, so no need to delete surrounding text + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + return true; + } + + // Needed to send arrow keys with IME's cursor control feature + @Override + public boolean setComposingRegion(int start, int end) { + return true; + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + sendTextEvent(text.toString().getBytes(UTF_8)); + return true; + } + + @Override + public boolean finishComposingText() { + synchronized (mEditable) { + if (mEditable.length() <= 0) + return true; + + char[] t = new char[mEditable.length()]; + mEditable.getChars(0, mEditable.length(), t, 0); + mEditable.clear(); + sendTextEvent(UTF_8.encode(CharBuffer.wrap(t)).array()); + } + + return true; + } + }; private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { holder.setFormat(PixelFormat.BGRA_8888); @@ -88,6 +173,7 @@ interface PixelFormat { private void init() { getHolder().addCallback(mSurfaceCallback); clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + nativeInit(); } public void setCallback(Callback callback) { @@ -235,8 +321,6 @@ public void reloadPreferences(Prefs p) { clipboardSyncEnabled = p.clipboardEnable.get(); setClipboardSyncEnabled(clipboardSyncEnabled, clipboardSyncEnabled); TouchInputHandler.refreshInputDevices(); - enableGboardCJK = p.enableGboardCJK.get(); - mIMM.restartInput(this); } // It is used in native code @@ -252,14 +336,14 @@ void setClipboardText(String text) { /** @noinspection unused*/ // It is used in native code void requestClipboard() { if (!clipboardSyncEnabled) { - sendClipboardEvent("".getBytes(StandardCharsets.UTF_8)); + sendClipboardEvent("".getBytes(UTF_8)); return; } CharSequence clip = clipboard.getText(); if (clip != null) { String text = String.valueOf(clipboard.getText()); - sendClipboardEvent(text.getBytes(StandardCharsets.UTF_8)); + sendClipboardEvent(text.getBytes(UTF_8)); Log.d("CLIP", "sending clipboard contents: " + text); } } @@ -298,69 +382,41 @@ public void onWindowFocusChanged(boolean hasFocus) { TouchInputHandler.refreshInputDevices(); } - public void checkRestartInput(boolean recheck) { - if (!enableGboardCJK) + public void setComposingView(TextView v) { + composingView = v; + } + + private void onComposingTextChange(char[] chars, int len) { + if (composingView == null) return; - InputMethodSubtype methodSubtype = mIMM.getCurrentInputMethodSubtype(); - String languageTag = methodSubtype == null ? null : methodSubtype.getLanguageTag(); - if (languageTag != null && languageTag.length() >= 2 && !languageTag.substring(0, 2).equals(mImeLang)) - mIMM.restartInput(this); - else if (recheck) { // recheck needed because sometimes requestCursorUpdates() is called too fast, before InputMethodManager detect change in IM subtype - MainActivity.handler.postDelayed(() -> checkRestartInput(false), 40); - } + composingView.setText(String.format(" %s ", new String(chars, 0, len))); + composingView.setVisibility(len == 0 ? LorieView.INVISIBLE : LorieView.VISIBLE); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_NORMAL; + outAttrs.actionLabel = "↵"; // Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen // keyboard on Android TV (see https://github.com/termux/termux-app/issues/221). outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + return mConnection; + } - if (enableGboardCJK) { - InputMethodSubtype methodSubtype = mIMM.getCurrentInputMethodSubtype(); - mImeLang = methodSubtype == null ? null : methodSubtype.getLanguageTag(); - if (mImeLang != null && mImeLang.length() > 2) - mImeLang = mImeLang.substring(0, 2); - mImeCJK = mImeLang != null && (mImeLang.equals("zh") || mImeLang.equals("ko") || mImeLang.equals("ja")); - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | - (mImeCJK ? InputType.TYPE_TEXT_VARIATION_NORMAL : InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); - return new BaseInputConnection(this, false) { - // workaround for Gboard - // Gboard calls requestCursorUpdates() whenever switching language - // check and then restart keyboard in different inputType when needed - @Override - public Editable getEditable() { - checkRestartInput(true); - return super.getEditable(); - } - @Override - public boolean requestCursorUpdates(int cursorUpdateMode) { - checkRestartInput(true); - return super.requestCursorUpdates(cursorUpdateMode); - } - - @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - boolean result = super.commitText(text, newCursorPosition); - if (mImeCJK) - // suppress Gboard CJK keyboard suggestion - // this workaround does not work well for non-CJK keyboards - // , when typing fast and two keypresses (commitText) are close in time - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - mIMM.invalidateInput(LorieView.this); - else - mIMM.restartInput(LorieView.this); - return result; - } - }; - } else { - return super.onCreateInputConnection(outAttrs); + // Used in native method + /** @noinspection unused*/ + private void flushComposingView() { + synchronized (mEditable) { + if (mEditable.length() <= 0) + return; } + + mConnection.finishComposingText(); } + private native void nativeInit(); static native void connect(int fd); native void handleXEvents(); static native void startLogcat(int fd); diff --git a/app/src/main/java/com/termux/x11/MainActivity.java b/app/src/main/java/com/termux/x11/MainActivity.java index d431e1cb6..ac9d0a681 100644 --- a/app/src/main/java/com/termux/x11/MainActivity.java +++ b/app/src/main/java/com/termux/x11/MainActivity.java @@ -24,6 +24,7 @@ import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Build.VERSION_CODES; @@ -185,6 +186,7 @@ protected void onCreate(Bundle savedInstanceState) { lorieView.setOnCapturedPointerListener((v, e) -> mInputHandler.handleTouchEvent(lorieView, lorieView, e)); lorieParent.setOnCapturedPointerListener((v, e) -> mInputHandler.handleTouchEvent(lorieView, lorieView, e)); lorieView.setOnKeyListener(mLorieKeyListener); + lorieView.setComposingView(findViewById(R.id.composingView)); lorieView.setCallback((sfc, surfaceWidth, surfaceHeight, screenWidth, screenHeight) -> { int framerate = (int) ((lorieView.getDisplay() != null) ? lorieView.getDisplay().getRefreshRate() : 30); @@ -240,6 +242,12 @@ && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PERMISSION_GRA onReceiveConnection(getIntent()); findViewById(android.R.id.content).addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> makeSureHelpersAreVisibleAndInScreenBounds()); + findViewById(android.R.id.content).getViewTreeObserver().addOnGlobalLayoutListener(() -> { + View composingView = findViewById(R.id.composingView); + ViewGroup parent = (ViewGroup) composingView.getParent(); + if (parent.getChildAt(parent.getChildCount() - 1) != composingView) + composingView.bringToFront(); + }); } @Override @@ -893,11 +901,6 @@ public static boolean isConnected() { private void checkXEvents() { getLorieView().handleXEvents(); - // an imperfect workaround for Gboard CJK keyboard in DeX soft keyboard mode - // in that particular mode during language switching, InputConnection#requestCursorUpdates() is not called and no signal can be picked up. - // therefore, check to activate CJK keyboard is done upon a keypress. - if (getLorieView().enableGboardCJK && SamsungDexUtils.checkDeXEnabled(this)) - getLorieView().checkRestartInput(false); handler.postDelayed(this::checkXEvents, 300); } diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index c429f1ff5..ded03bd2c 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -276,6 +276,18 @@ android:background="@android:color/black" android:layout_gravity="bottom|center"/> + + -