diff --git a/app/src/main/cpp/lorie/activity.c b/app/src/main/cpp/lorie/activity.c index 6e50b23d8..6c6418261 100644 --- a/app/src/main/cpp/lorie/activity.c +++ b/app/src/main/cpp/lorie/activity.c @@ -26,7 +26,7 @@ extern volatile int conn_fd; // The only variable from shared with X server code static struct { jclass self; - jmethodID getInstance, clientConnectedStateChanged; + jmethodID getInstance, clientConnectedStateChanged, flushComposingView; } MainActivity = {0}; static struct { @@ -115,6 +115,7 @@ static void nativeInit(JNIEnv *env, jobject thiz) { MainActivity.self = FindClassOrDie(env, "com/termux/x11/MainActivity"); MainActivity.getInstance = FindMethodOrDie(env, MainActivity.self, "getInstance", "()Lcom/termux/x11/MainActivity;", JNI_TRUE); MainActivity.clientConnectedStateChanged = FindMethodOrDie(env, MainActivity.self, "clientConnectedStateChanged", "()V", JNI_FALSE); + MainActivity.flushComposingView = FindMethodOrDie(env, (*env)->GetObjectClass(env, thiz), "flushComposingView", "()V", JNI_FALSE); } (*env)->GetJavaVM(env, &vm); @@ -123,6 +124,13 @@ static void nativeInit(JNIEnv *env, jobject thiz) { connect_(NULL, NULL, -1); } +static void flushComposingView(JNIEnv* env) { + if (!env || !globalThiz) + return; + + (*env)->CallVoidMethod(env, globalThiz, MainActivity.flushComposingView); +} + static int xcallback(int fd, int events, __unused void* data) { JNIEnv *env = guienv; jobject thiz = globalThiz; @@ -260,6 +268,7 @@ static void setClipboardSyncEnabled(__unused JNIEnv* env, __unused jobject cls, static void sendClipboardAnnounce(__unused JNIEnv *env, __unused jobject thiz) { if (conn_fd != -1) { + flushComposingView(env); lorieEvent e = { .type = EVENT_CLIPBOARD_ANNOUNCE }; write(conn_fd, &e, sizeof(e)); } @@ -267,6 +276,7 @@ static void sendClipboardAnnounce(__unused JNIEnv *env, __unused jobject thiz) { static void 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 } }; @@ -278,6 +288,7 @@ static void sendClipboardEvent(JNIEnv *env, __unused jobject thiz, jbyteArray te static void sendWindowChange(__unused JNIEnv* env, __unused jobject cls, jint width, jint height, jint framerate, jstring jname) { if (conn_fd != -1) { + flushComposingView(env); const char *name = (!jname || width <= 0 || height <= 0) ? NULL : (*env)->GetStringUTFChars(env, jname, JNI_FALSE); lorieEvent e = { .screenSize = { .t = EVENT_SCREEN_SIZE, .width = width, .height = height, .framerate = framerate, .name_size = (name ? strlen(name) : 0) } }; write(conn_fd, &e, sizeof(e)); @@ -290,6 +301,7 @@ static void sendWindowChange(__unused JNIEnv* env, __unused jobject cls, jint wi static void 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)); } @@ -297,6 +309,7 @@ static void sendMouseEvent(__unused JNIEnv* env, __unused jobject cls, jfloat x, static void 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)); } @@ -306,6 +319,7 @@ static void sendStylusEvent(__unused JNIEnv *env, __unused jobject thiz, jfloat 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)); } @@ -320,6 +334,7 @@ static void requestStylusEnabled(__unused JNIEnv *env, __unused jclass clazz, jb static jboolean 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 } }; @@ -358,7 +373,7 @@ static void sendTextEvent(JNIEnv *env, __unused jobject thiz, jbyteArray text) { p += len; if (p - (char*) str >= length) break; - usleep(30000); + usleep(10000); } (*env)->ReleaseByteArrayElements(env, text, str, JNI_ABORT); diff --git a/app/src/main/java/com/termux/x11/LorieView.java b/app/src/main/java/com/termux/x11/LorieView.java index bc28f8c81..fab115e34 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; + import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -53,12 +55,106 @@ 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 MainActivity a = MainActivity.getInstance(); + 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; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + for (int i=0; i= 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*/ + @Keep + private void flushComposingView() { + synchronized (mEditable) { + if (mEditable.length() <= 0) + return; } + + mConnection.finishComposingText(); } static native boolean renderingInActivity(); diff --git a/app/src/main/java/com/termux/x11/MainActivity.java b/app/src/main/java/com/termux/x11/MainActivity.java index 8204e2b29..2917cddcf 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; @@ -90,7 +91,7 @@ public class MainActivity extends AppCompatActivity implements View.OnApplyWindo private static boolean externalKeyboardConnected = false; private View.OnKeyListener mLorieKeyListener; private boolean filterOutWinKey = false; - private boolean useTermuxEKBarBehaviour = false; + boolean useTermuxEKBarBehaviour = false; private boolean isInPictureInPictureMode = false; public static Prefs prefs = null; @@ -190,6 +191,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) -> { String name; @@ -232,7 +234,6 @@ else if (SamsungDexUtils.checkDeXEnabled(this)) onPreferencesChanged(""); toggleExtraKeys(false, false); - checkRestartInput(); initStylusAuxButtons(); initMouseAuxButtons(); @@ -245,6 +246,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 @@ -879,15 +886,6 @@ public static boolean isConnected() { return LorieView.connected(); } - private void checkRestartInput() { - // 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::checkRestartInput, 300); - } - public static void getRealMetrics(DisplayMetrics m) { if (getInstance() != null && getInstance().getLorieView() != null && 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"/> + + META for META key, \n Pause key intercepting with Esc key Filter out intercepted Win (Meta/Mod4) key. Allows you to use Dex shortcuts while intercepting. Requires Accessibility service to work. - Workaround to enable CJK Gboard - May require Android 14 and Gboard 14 Clipboard sharing Request notification permission diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 0015779ba..6520cffd1 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -49,7 +49,6 @@ -