1
0
Fork 0

10th attempt to fix the privileged options problem: InputConnection is no longer cached within the app

This commit is contained in:
sspanak 2025-06-11 14:51:03 +03:00 committed by Dimo Karaivanov
parent 9c63e66a1d
commit 903e756dc0
10 changed files with 104 additions and 56 deletions

View file

@ -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);
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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);

View file

@ -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<Integer> 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);
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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<String> onDelayedAccept, @Nullable Runnable onSuggestionClick) {
public SuggestionOps(@Nullable InputMethodService ims, @Nullable SettingsStore settings, @Nullable ResizableMainView mainView, @Nullable TextField textField, @Nullable ConsumerCompat<String> 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);

View file

@ -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,6 +362,7 @@ public class TextField extends InputField {
public boolean sendDownUpKeyEvents(int keyCode, boolean shift, boolean ctrl) {
InputConnection connection = getConnection();
if (connection != null) {
int metaState = shift ? KeyEvent.META_SHIFT_ON : 0;
metaState |= ctrl ? KeyEvent.META_CTRL_ON : 0;

View file

@ -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 "";
}