From 903e756dc03cf4d198e729cd4b2b73c172747498 Mon Sep 17 00:00:00 2001 From: sspanak Date: Wed, 11 Jun 2025 14:51:03 +0300 Subject: [PATCH] 10th attempt to fix the privileged options problem: InputConnection is no longer cached within the app --- .../github/sspanak/tt9/hacks/InputType.java | 10 +++--- .../sspanak/tt9/ime/AbstractHandler.java | 5 ++- .../sspanak/tt9/ime/MainViewHandler.java | 5 ++- .../github/sspanak/tt9/ime/TraditionalT9.java | 15 ++++---- .../github/sspanak/tt9/ime/TypingHandler.java | 29 +++++++-------- .../sspanak/tt9/ime/helpers/InputField.java | 16 ++++++--- .../tt9/ime/helpers/StandardInputType.java | 18 +++++++--- .../tt9/ime/helpers/SuggestionOps.java | 5 +-- .../sspanak/tt9/ime/helpers/TextField.java | 22 +++++++++--- .../tt9/ime/helpers/TextSelection.java | 35 ++++++++++++++----- 10 files changed, 104 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/io/github/sspanak/tt9/hacks/InputType.java b/app/src/main/java/io/github/sspanak/tt9/hacks/InputType.java index 3fcb5641..5e16e485 100644 --- a/app/src/main/java/io/github/sspanak/tt9/hacks/InputType.java +++ b/app/src/main/java/io/github/sspanak/tt9/hacks/InputType.java @@ -1,8 +1,10 @@ package io.github.sspanak.tt9.hacks; import android.content.Context; +import android.inputmethodservice.InputMethodService; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; + +import androidx.annotation.Nullable; import io.github.sspanak.tt9.ime.helpers.StandardInputType; import io.github.sspanak.tt9.util.sys.DeviceInfo; @@ -10,9 +12,9 @@ import io.github.sspanak.tt9.util.sys.DeviceInfo; public class InputType extends StandardInputType { private final boolean isUs; - public InputType(Context context, InputConnection inputConnection, EditorInfo inputField) { - super(inputConnection, inputField); - isUs = isAppField(context != null ? context.getPackageName() : "", EditorInfo.TYPE_NULL); + public InputType(@Nullable InputMethodService ims, EditorInfo inputField) { + super(ims, inputField); + isUs = isAppField(ims != null ? ims.getPackageName() : null, EditorInfo.TYPE_NULL); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/AbstractHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/AbstractHandler.java index f6a25b86..3c02564e 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/AbstractHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/AbstractHandler.java @@ -2,7 +2,6 @@ package io.github.sspanak.tt9.ime; import android.inputmethodservice.InputMethodService; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; import io.github.sspanak.tt9.ime.helpers.SuggestionOps; import io.github.sspanak.tt9.ime.modes.InputMode; @@ -19,10 +18,10 @@ abstract public class AbstractHandler extends InputMethodService { // lifecycle abstract protected void onInit(); - abstract protected boolean onStart(InputConnection inputConnection, EditorInfo inputField); + abstract protected boolean onStart(EditorInfo inputField); abstract protected void onFinishTyping(); abstract protected void onStop(); - abstract protected void setInputField(InputConnection inputConnection, EditorInfo inputField); + abstract protected void setInputField(EditorInfo inputField); // UI abstract protected void createSuggestionBar(); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/MainViewHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/MainViewHandler.java index 3a1b9b8a..33189445 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/MainViewHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/MainViewHandler.java @@ -1,7 +1,6 @@ package io.github.sspanak.tt9.ime; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -37,10 +36,10 @@ abstract public class MainViewHandler extends HotkeyHandler { @Override - protected boolean onStart(InputConnection connection, EditorInfo field) { + protected boolean onStart(EditorInfo field) { resetNormalizedDimensions(); dragResize = settings.getDragResize(); - return super.onStart(connection, field); + return super.onStart(field); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java b/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java index 1688ee1f..15e2b66b 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -6,7 +6,6 @@ import android.os.Handler; import android.os.Looper; import android.view.View; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; @@ -39,7 +38,7 @@ public class TraditionalT9 extends MainViewHandler { return false; } - setInputField(getCurrentInputConnection(), getCurrentInputEditorInfo()); + setInputField(getCurrentInputEditorInfo()); return shouldBeVisible(); } @@ -80,13 +79,13 @@ public class TraditionalT9 extends MainViewHandler { LOG_TAG, "===> Start Up; packageName: " + inputField.packageName + " inputType: " + inputField.inputType + " actionId: " + inputField.actionId + " imeOptions: " + inputField.imeOptions + " privateImeOptions: " + inputField.privateImeOptions + " extras: " + inputField.extras ); - onStart(getCurrentInputConnection(), inputField); + onStart(inputField); } @Override public void onStartInputView(EditorInfo inputField, boolean restarting) { - onStart(getCurrentInputConnection(), inputField); + onStart(inputField); } @@ -130,13 +129,13 @@ public class TraditionalT9 extends MainViewHandler { @Override - protected boolean onStart(InputConnection connection, EditorInfo field) { + protected boolean onStart(EditorInfo field) { if (zombieChecks == 0 && !SystemSettings.isTT9Selected(this)) { startZombieCheck(); return false; } - if (isDead || !super.onStart(connection, field)) { + if (isDead || !super.onStart(field)) { setStatusIcon(mInputMode, mLanguage); return false; } @@ -151,7 +150,7 @@ public class TraditionalT9 extends MainViewHandler { initUi(mInputMode); } - InputType newInputType = new InputType(getApplicationContext(), connection, field); + InputType newInputType = new InputType(this, field); if (newInputType.isText()) { DataStore.loadWordPairs(DictionaryLoader.getInstance(this), LanguageCollection.getAll(settings.getEnabledLanguageIds())); @@ -241,7 +240,7 @@ public class TraditionalT9 extends MainViewHandler { protected void cleanUp() { super.cleanUp(); - setInputField(null, null); + setInputField(null); backgroundTasks.removeCallbacksAndMessages(null); zombieChecks = SettingsStore.ZOMBIE_CHECK_MAX; zombieDetector.removeCallbacksAndMessages(null); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java index fa38d16b..012a7e0e 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java @@ -1,7 +1,7 @@ package io.github.sspanak.tt9.ime; +import android.inputmethodservice.InputMethodService; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,10 +29,10 @@ import io.github.sspanak.tt9.util.Text; public abstract class TypingHandler extends KeyPadHandler { // internal settings/data @NonNull protected AppHacks appHacks = new AppHacks(null, null, null); - @NonNull protected InputType inputType = new InputType(null, null, null); + @NonNull protected InputType inputType = new InputType(null, null); @NonNull protected TextField textField = new TextField(null, null, null); - @NonNull protected TextSelection textSelection = new TextSelection(this,null); - @NonNull protected SuggestionOps suggestionOps = new SuggestionOps(null, null, null, null, null); + @NonNull protected TextSelection textSelection = new TextSelection(null); + @NonNull protected SuggestionOps suggestionOps = new SuggestionOps(null, null, null, null, null, null); // input @NonNull protected ArrayList allowedInputModes = new ArrayList<>(); @@ -44,7 +44,7 @@ public abstract class TypingHandler extends KeyPadHandler { protected void createSuggestionBar() { - suggestionOps = new SuggestionOps(settings, mainView, textField, this::onAcceptSuggestionsDelayed, this::onOK); + suggestionOps = new SuggestionOps(this, settings, mainView, textField, this::onAcceptSuggestionsDelayed, this::onOK); } @@ -53,10 +53,10 @@ public abstract class TypingHandler extends KeyPadHandler { } @Override - protected boolean onStart(InputConnection connection, EditorInfo field) { - boolean restart = textField.equals(connection, field); + protected boolean onStart(EditorInfo field) { + boolean restart = textField.equals(getCurrentInputConnection(), field); - setInputField(connection, field); + setInputField(field); // 1. In case we are back from Settings screen, update the language list // 2. If the connected app hints it is in a language different than the current one, @@ -79,14 +79,15 @@ public abstract class TypingHandler extends KeyPadHandler { } - protected void setInputField(InputConnection connection, EditorInfo field) { - if (textField.equals(connection, field)) { + protected void setInputField(EditorInfo field) { + if (textField.equals(getCurrentInputConnection(), field)) { return; } - inputType = new InputType(getApplicationContext(), connection, field); - textField = new TextField(settings, connection, field); - textSelection = new TextSelection(this, connection); + InputMethodService context = field != null ? this : null; + inputType = new InputType(context, field); + textField = new TextField(context, settings, field); + textSelection = new TextSelection(context); // changing the TextField and notifying all interested classes is an atomic operation appHacks = new AppHacks(inputType, textField, textSelection); @@ -105,7 +106,7 @@ public abstract class TypingHandler extends KeyPadHandler { protected void onFinishTyping() { suggestionOps.cancelDelayedAccept(); mInputMode = InputMode.getInstance(null, null, null, null, InputMode.MODE_PASSTHROUGH); - setInputField(null, null); + setInputField(null); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java index 393fc357..3e092bf5 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/InputField.java @@ -1,5 +1,6 @@ package io.github.sspanak.tt9.ime.helpers; +import android.inputmethodservice.InputMethodService; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -14,19 +15,25 @@ import io.github.sspanak.tt9.util.sys.DeviceInfo; public class InputField { public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1; - @Nullable protected final InputConnection connection; + @Nullable protected final InputMethodService ims; @Nullable protected final EditorInfo field; - protected InputField(@Nullable InputConnection inputConnection, @Nullable EditorInfo inputField) { - connection = inputConnection; + protected InputField(@Nullable InputMethodService ims, @Nullable EditorInfo inputField) { + this.ims = ims; field = inputField; } + @Nullable + protected InputConnection getConnection() { + return ims != null ? ims.getCurrentInputConnection() : null; + } + + public boolean equals(InputConnection inputConnection, EditorInfo inputField) { return - connection != null && connection == inputConnection + inputConnection != null && inputConnection == getConnection() && field != null && field == inputField; } @@ -73,6 +80,7 @@ public class InputField { * Note that it is up to the app to decide what to do or ignore the action ID. */ public boolean performAction(int actionId) { + InputConnection connection = getConnection(); return connection != null && actionId != EditorInfo.IME_ACTION_NONE && connection.performEditorAction(actionId); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/StandardInputType.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/StandardInputType.java index b74757f8..fd9928fc 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/StandardInputType.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/StandardInputType.java @@ -1,11 +1,13 @@ package io.github.sspanak.tt9.ime.helpers; import android.content.Context; +import android.inputmethodservice.InputMethodService; import android.text.InputType; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.HashSet; import java.util.Set; @@ -16,18 +18,24 @@ import io.github.sspanak.tt9.ime.modes.InputMode; abstract public class StandardInputType { private static final int TYPE_MULTILINE_TEXT = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; - protected final InputConnection connection; + @Nullable protected final InputMethodService ims; protected final EditorInfo field; - protected StandardInputType(InputConnection inputConnection, EditorInfo inputField) { - connection = inputConnection; + protected StandardInputType(@Nullable InputMethodService ims, EditorInfo inputField) { + this.ims = ims; field = inputField; } + @Nullable + protected InputConnection getConnection() { + return ims != null ? ims.getCurrentInputConnection() : null; + } + + public boolean isValid() { - return field != null && connection != null; + return field != null && getConnection() != null; } @@ -193,7 +201,7 @@ abstract public class StandardInputType { * editor state. */ public int determineTextCase() { - if (connection == null || field == null || field.inputType == InputType.TYPE_NULL) { + if (getConnection() == null || field == null || field.inputType == InputType.TYPE_NULL) { return InputMode.CASE_UNDEFINED; } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/SuggestionOps.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/SuggestionOps.java index 48a7d8c0..a8c8fcdf 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/SuggestionOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/SuggestionOps.java @@ -1,5 +1,6 @@ package io.github.sspanak.tt9.ime.helpers; +import android.inputmethodservice.InputMethodService; import android.os.Handler; import android.os.Looper; @@ -25,11 +26,11 @@ public class SuggestionOps { @NonNull private TextField textField; - public SuggestionOps(@Nullable SettingsStore settings, @Nullable ResizableMainView mainView, @Nullable TextField textField, @Nullable ConsumerCompat onDelayedAccept, @Nullable Runnable onSuggestionClick) { + public SuggestionOps(@Nullable InputMethodService ims, @Nullable SettingsStore settings, @Nullable ResizableMainView mainView, @Nullable TextField textField, @Nullable ConsumerCompat onDelayedAccept, @Nullable Runnable onSuggestionClick) { delayedAcceptHandler = new Handler(Looper.getMainLooper()); this.onDelayedAccept = onDelayedAccept != null ? onDelayedAccept : s -> {}; - this.textField = textField != null ? textField : new TextField(null, null, null); + this.textField = textField != null ? textField : new TextField(ims, null, null); if (settings != null && mainView != null && onSuggestionClick != null) { suggestionBar = new SuggestionsBar(settings, mainView, onSuggestionClick); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java index 7b386712..0df7aac5 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -1,6 +1,7 @@ package io.github.sspanak.tt9.ime.helpers; import android.graphics.Typeface; +import android.inputmethodservice.InputMethodService; import android.text.Spannable; import android.text.SpannableString; import android.text.style.StyleSpan; @@ -10,6 +11,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.github.sspanak.tt9.hacks.InputType; import io.github.sspanak.tt9.ime.modes.InputMode; @@ -25,10 +27,10 @@ public class TextField extends InputField { private final boolean isNonText; - public TextField(SettingsStore settings, InputConnection inputConnection, EditorInfo inputField) { - super(inputConnection, inputField); + public TextField(@Nullable InputMethodService ims, SettingsStore settings, EditorInfo inputField) { + super(ims, inputField); - InputType inputType = new InputType(null, inputConnection, inputField); + InputType inputType = new InputType(ims, inputField); isComposingSupported = !inputType.isNumeric() && !inputType.isLimited() && !inputType.isRustDesk() && (settings == null || settings.getAllowComposingText()); isNonText = !inputType.isText(); } @@ -40,12 +42,14 @@ public class TextField extends InputField { @NonNull public String getStringAfterCursor(int numberOfChars) { + InputConnection connection = getConnection(); CharSequence chars = connection != null && numberOfChars > 0 ? connection.getTextAfterCursor(numberOfChars, 0) : null; return chars != null ? chars.toString() : ""; } @NonNull public String getStringBeforeCursor(int numberOfChars) { + InputConnection connection = getConnection(); CharSequence chars = connection != null && numberOfChars > 0 ? connection.getTextBeforeCursor(numberOfChars, 0) : null; return chars != null ? chars.toString() : ""; } @@ -133,6 +137,7 @@ public class TextField extends InputField { * "deleteSurroundingText()" to delete a region of text or a Unicode character. */ public void deleteChars(int numberOfChars) { + InputConnection connection = getConnection(); if (numberOfChars <= 0 || connection == null) { return; } @@ -162,6 +167,7 @@ public class TextField extends InputField { * No action is taken when there is no such word. */ public void deletePrecedingSpace(String word) { + InputConnection connection = getConnection(); if (connection == null) { return; } @@ -187,6 +193,7 @@ public class TextField extends InputField { * there is no such word before the cursor. */ public void addPrecedingSpace(String word) { + InputConnection connection = getConnection(); if (connection == null) { return; } @@ -210,6 +217,7 @@ public class TextField extends InputField { * the given "text". Returns "true" if the operation was successful, "false" otherwise. */ public boolean recompose(String text) { + InputConnection connection = getConnection(); if (text == null || connection == null || !isComposingSupported) { return false; } @@ -227,6 +235,7 @@ public class TextField extends InputField { * A fail-safe setter that appends text to the field, ignoring NULL input. */ public void setText(String text) { + InputConnection connection = getConnection(); if (text != null && connection != null) { connection.commitText(text, 1); } @@ -245,6 +254,7 @@ public class TextField extends InputField { */ public void setComposingText(CharSequence text, int position) { composingText = text; + InputConnection connection = getConnection(); if (text != null && connection != null && isComposingSupported) { connection.setComposingText(text, position); } @@ -275,6 +285,7 @@ public class TextField extends InputField { * Finish composing text or do nothing if the text field is invalid. */ public void finishComposingText() { + InputConnection connection = getConnection(); if (connection == null) { return; } @@ -332,7 +343,7 @@ public class TextField extends InputField { public boolean moveCursor(boolean backward) { if ( - connection == null + getConnection() == null || (backward && getStringBeforeCursor(1).isEmpty()) || (!backward && getStringAfterCursor(1).isEmpty()) ) { @@ -351,7 +362,8 @@ public class TextField extends InputField { public boolean sendDownUpKeyEvents(int keyCode, boolean shift, boolean ctrl) { - if (connection != null) { + InputConnection connection = getConnection(); + if (connection != null) { int metaState = shift ? KeyEvent.META_SHIFT_ON : 0; metaState |= ctrl ? KeyEvent.META_CTRL_ON : 0; KeyEvent downEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, metaState); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextSelection.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextSelection.java index 56506876..fbcc1cdc 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextSelection.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextSelection.java @@ -1,6 +1,6 @@ package io.github.sspanak.tt9.ime.helpers; -import android.content.Context; +import android.inputmethodservice.InputMethodService; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; @@ -11,19 +11,22 @@ import androidx.annotation.Nullable; import io.github.sspanak.tt9.util.sys.Clipboard; public class TextSelection { - @Nullable private final InputConnection connection; - private final Context context; + @Nullable private final InputMethodService ims; private int currentStart = 0; private int currentEnd = 0; - public TextSelection(Context context, @Nullable InputConnection connection) { - this.context = context; - this.connection = connection; + public TextSelection(@Nullable InputMethodService ims) { + this.ims = ims; detectCursorPosition(); } + private InputConnection getConnection() { + return ims != null ? ims.getCurrentInputConnection() : null; + } + + public void onSelectionUpdate(int start, int end) { currentStart = start; currentEnd = end; @@ -36,6 +39,7 @@ public class TextSelection { public void clear() { + InputConnection connection = getConnection(); if (connection != null) { connection.setSelection(currentEnd, currentEnd); } @@ -43,6 +47,7 @@ public class TextSelection { public void clear(boolean backward) { + InputConnection connection = getConnection(); if (connection != null) { connection.setSelection( backward ? Math.min(currentStart, currentEnd) : Math.max(currentStart, currentEnd), @@ -58,6 +63,7 @@ public class TextSelection { public void selectAll() { + InputConnection connection = getConnection(); if (connection != null) { connection.performContextMenuAction(android.R.id.selectAll); } @@ -65,6 +71,7 @@ public class TextSelection { public void selectNextChar(boolean backward) { + InputConnection connection = getConnection(); if (connection != null) { connection.setSelection(currentStart, currentEnd + (backward ? -1 : 1)); } @@ -72,6 +79,7 @@ public class TextSelection { public void selectNextWord(boolean backward) { + InputConnection connection = getConnection(); if (connection == null) { return; } @@ -81,12 +89,16 @@ public class TextSelection { public boolean copy() { + if (ims == null) { + return false; + } + CharSequence selectedText = getSelectedText(); if (selectedText.length() == 0) { return false; } - Clipboard.copy(context, selectedText); + Clipboard.copy(ims, selectedText); return true; } @@ -99,7 +111,11 @@ public class TextSelection { public void paste(@NonNull TextField textField) { - String clipboardText = Clipboard.paste(context); + if (ims == null) { + return; + } + + String clipboardText = Clipboard.paste(ims); if (!clipboardText.isEmpty()) { textField.setText(clipboardText); } @@ -107,6 +123,7 @@ public class TextSelection { private int getNextWordPosition(boolean backward) { + InputConnection connection = getConnection(); if (connection == null) { return currentEnd + (backward ? -1 : 1); } @@ -135,6 +152,7 @@ public class TextSelection { private void detectCursorPosition() { + InputConnection connection = getConnection(); if (connection == null) { return; } @@ -148,6 +166,7 @@ public class TextSelection { private CharSequence getSelectedText() { + InputConnection connection = getConnection(); if (connection == null) { return ""; }