From 2b2b8e9a1c705f9bc5757f69234d5964a46a3c20 Mon Sep 17 00:00:00 2001 From: sspanak Date: Fri, 3 Jan 2025 21:28:03 +0200 Subject: [PATCH] workaround for getTextBeforeCursor() taking too much time and causing unresponsive UI --- .../github/sspanak/tt9/ime/TraditionalT9.java | 15 +++ .../sspanak/tt9/ime/helpers/InputField.java | 10 +- .../sspanak/tt9/ime/helpers/TextField.java | 22 +++-- .../preferences/settings/SettingsStore.java | 2 + .../java/io/github/sspanak/tt9/ui/UI.java | 13 ++- .../tt9/util/InputConnectionTools.java | 95 +++++++++++++++++++ app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 1 + app/src/main/res/values-lt/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 19 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java 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 9b8eaa2c..6a596249 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 @@ -9,6 +9,7 @@ import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; +import io.github.sspanak.tt9.R; import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.hacks.InputType; @@ -237,21 +238,35 @@ public class TraditionalT9 extends MainViewHandler { Logger.d(LOG_TAG, "===> Shutdown completed"); } + @Override public void onTimeout(int startId) { onZombie(); super.onTimeout(startId); } + @Override protected boolean onNumber(int key, boolean hold, int repeat) { if (InputModeKind.isPredictive(mInputMode) && DictionaryLoader.autoLoad(this, mLanguage)) { return true; } + if (textField.shouldReportConnectionErrors()) { + UI.toastLongSingle(getApplicationContext(), R.string.error_unstable_input_connection); + } return super.onNumber(key, hold, repeat); } + @Override + public boolean onBackspace(int repeat) { + if (textField.shouldReportConnectionErrors()) { + UI.toastLongSingle(getApplicationContext(), R.string.error_unstable_input_connection); + } + return super.onBackspace(repeat); + } + + @Override protected TraditionalT9 getFinalContext() { return this; 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 dcee51f1..c8863c22 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 @@ -14,11 +14,11 @@ import io.github.sspanak.tt9.languages.LanguageCollection; public class InputField { public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1; - protected final InputConnection connection; - protected final EditorInfo field; + @Nullable protected final InputConnection connection; + @Nullable protected final EditorInfo field; - protected InputField(InputConnection inputConnection, EditorInfo inputField) { + protected InputField(@Nullable InputConnection inputConnection, @Nullable EditorInfo inputField) { connection = inputConnection; field = inputField; } @@ -85,11 +85,11 @@ public class InputField { */ @Nullable public Language getLanguage(ArrayList allowedLanguageIds) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || field == null || field.hintLocales == null) { return null; } - for (int i = 0; field.hintLocales != null && i < field.hintLocales.size(); i++) { + for (int i = 0; i < field.hintLocales.size(); i++) { Language lang = LanguageCollection.getByLanguageCode(field.hintLocales.get(i).getLanguage()); if (lang != null && allowedLanguageIds.contains(lang.getId())) { return lang; 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 da1b582e..6e336f25 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 @@ -18,10 +18,13 @@ import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageKind; import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.util.InputConnectionTools; import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.Text; public class TextField extends InputField { + @NonNull private final InputConnectionTools connectionTools; + private CharSequence composingText = ""; private final boolean isComposingSupported; private final boolean isNonText; @@ -30,6 +33,8 @@ public class TextField extends InputField { public TextField(InputConnection inputConnection, EditorInfo inputField) { super(inputConnection, inputField); + connectionTools = new InputConnectionTools(inputConnection); + InputType inputType = new InputType(inputConnection, inputField); isComposingSupported = !inputType.isNumeric() && !inputType.isLimited() && !inputType.isRustDesk() && !inputType.isDeezerSearchBar(); isNonText = !inputType.isText(); @@ -37,14 +42,14 @@ public class TextField extends InputField { public String getStringAfterCursor(int numberOfChars) { - CharSequence character = connection != null ? connection.getTextAfterCursor(numberOfChars, 0) : null; - return character != null ? character.toString() : ""; + CharSequence chars = connectionTools.getTextAfterCursor(numberOfChars, 0); + return chars != null ? chars.toString() : ""; } public String getStringBeforeCursor(int numberOfChars) { - CharSequence character = connection != null ? connection.getTextBeforeCursor(numberOfChars, 0) : null; - return character != null ? character.toString() : ""; + CharSequence chars = connectionTools.getTextBeforeCursor(numberOfChars, 0); + return chars != null ? chars.toString() : ""; } @@ -206,7 +211,7 @@ public class TextField extends InputField { String searchText = " " + word; connection.beginBatchEdit(); - CharSequence beforeText = connection.getTextBeforeCursor(searchText.length(), 0); + String beforeText = getStringBeforeCursor(searchText.length()); if (beforeText == null || !beforeText.equals(searchText)) { connection.endBatchEdit(); return; @@ -229,7 +234,7 @@ public class TextField extends InputField { } connection.beginBatchEdit(); - CharSequence beforeText = connection.getTextBeforeCursor(word.length(), 0); + String beforeText = getStringBeforeCursor(word.length()); if (beforeText == null || !beforeText.equals(word)) { connection.endBatchEdit(); return; @@ -391,4 +396,9 @@ public class TextField extends InputField { return false; } + + + public boolean shouldReportConnectionErrors() { + return connectionTools.shouldReportTimeout(); + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java index b1953e3c..5a8fbc15 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java @@ -18,6 +18,8 @@ public class SettingsStore extends SettingsUI { public final static int DICTIONARY_DOWNLOAD_READ_TIMEOUT = 10000; // ms public final static int DICTIONARY_IMPORT_BATCH_SIZE = 5000; // words public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms + public final static int INPUT_CONNECTION_ERRORS_MAX = 3; + public final static int INPUT_CONNECTION_OPERATIONS_TIMEOUT = 150; // ms public final static int RESIZE_THROTTLING_TIME = 60; // ms public final static byte SLOW_QUERY_TIME = 50; // ms public final static int SLOW_QUERY_TIMEOUT = 3000; // ms diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/UI.java b/app/src/main/java/io/github/sspanak/tt9/ui/UI.java index 08ff731c..f5cc6346 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/UI.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/UI.java @@ -97,7 +97,7 @@ public class UI { toastLong(context, msg); } - public static void toastShortSingle(@NonNull Context context, @NonNull String uniqueId, @NonNull String message) { + public static void toastSingle(@NonNull Context context, @NonNull String uniqueId, @NonNull String message, boolean isShort) { Toast toast = singleToasts.get(uniqueId); if (toast != null) { @@ -105,14 +105,21 @@ public class UI { } // we recreate the toast, because if set new text, when it is fading out, it is ignored - toast = Toast.makeText(context, message, Toast.LENGTH_SHORT); + toast = Toast.makeText(context, message, isShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); toast.show(); singleToasts.put(uniqueId, toast); } + public static void toastShortSingle(@NonNull Context context, @NonNull String uniqueId, @NonNull String message) { + toastSingle(context, uniqueId, message, true); + } public static void toastShortSingle(@NonNull Context context, int resourceId) { - toastShortSingle(context, String.valueOf(resourceId), context.getString(resourceId)); + toastSingle(context, String.valueOf(resourceId), context.getString(resourceId), true); + } + + public static void toastLongSingle(@NonNull Context context, int resourceId) { + toastSingle(context, String.valueOf(resourceId), context.getString(resourceId), false); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java b/app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java new file mode 100644 index 00000000..73c7cc16 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java @@ -0,0 +1,95 @@ +package io.github.sspanak.tt9.util; + +import android.os.Build; +import android.view.inputmethod.InputConnection; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.github.sspanak.tt9.preferences.settings.SettingsStore; + +public class InputConnectionTools { + @Nullable private final InputConnection connection; + private int connectionErrors; + private boolean isErrorReported; + + + public InputConnectionTools(@Nullable InputConnection connection) { + this.connection = connection; + connectionErrors = 0; + isErrorReported = false; + } + + + public CharSequence getTextAfterCursor(int i, int ii) { + return getTextNextToCursor(i, ii, true); + } + + + public CharSequence getTextBeforeCursor(int i, int ii) { + return getTextNextToCursor(i, ii, false); + } + + + @Nullable + private CharSequence getTextNextToCursor(int i, int ii, boolean after) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getTextNextToCursorModern(i, ii, after); + } else { + return getTextNextToCursorClassic(i, ii, after); + } + } + + + private CharSequence getTextNextToCursorClassic(int i, int ii, boolean after) { + if (connection == null) { + return null; + } + + return after ? connection.getTextAfterCursor(i, ii) : connection.getTextBeforeCursor(i, ii); + } + + + /** + * getTextNextToCursorModern + * On some devices with Android >= 11, getTextBeforeCursor() sometimes takes too long to execute, + * blocking the UI thread and ultimately causing ANR. This method is a wrapper around + * getTextBeforeCursor() that terminates the operations after a certain timeout. Just in case we + * handle getTextAfterCursor() the same way. + */ + @Nullable + @RequiresApi(api = Build.VERSION_CODES.N) + private CharSequence getTextNextToCursorModern(int i, int ii, boolean after) { + CompletableFuture future = CompletableFuture.supplyAsync( + () -> getTextNextToCursorClassic(i, ii, after) + ); + + try { + return future.get(SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + connectionErrors++; + Logger.e( + getClass().getSimpleName(), + "getStringBeforeCursor() timed out after " + SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT + "ms. " + connectionErrors + " errors so far." + ); + return null; + } catch (InterruptedException | ExecutionException e) { + connectionErrors++; + Logger.e(getClass().getSimpleName(), "getStringBeforeCursor() failed " + connectionErrors + " times so far. Current error: " + e); + return null; + } + } + + public boolean shouldReportTimeout() { + if (!isErrorReported && connectionErrors > SettingsStore.INPUT_CONNECTION_ERRORS_MAX) { + isErrorReported = true; + return true; + } + return false; + } +} diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 2c7238b6..d21819d8 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -5,6 +5,7 @@ Завършено Зареждане… Възникна неочаквана грешка. + Нестабилна връзка с приложението! Добави Преместете показалеца върху дума, за да я добавите към речника. Не може да се въведе празна дума. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6b25da14..095f385c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -4,6 +4,7 @@ Laden… Unerwarteter Fehler aufgetreten. + Instabile Anwendungsverbindung! Hinzufügen Wort \"%1$s\" zu %2$s hinzufügen? Bewegen Sie den Cursor innerhalb eines Wortes, um es hinzuzufügen. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4cf3f1e5..5bffef61 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,6 +1,7 @@ Configuración de Traditional T9 + ¡Conexión inestable con la aplicación! Agregar ¿Agregar la palabra \"%1$s\" a %2$s? Mueve el cursor dentro de una palabra para añadirla. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cf60a554..da9304a0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -5,6 +5,7 @@ Fini Chargement… Une erreur inattendue s\'est produite. + Connexion instable à l\'application ! Ajouter Déplacez le curseur dans un mot pour l\'ajouter. Mot vide non ajouté. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5f354d27..c52669b3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -6,6 +6,7 @@ Caricamento… Si è verificato un errore imprevisto. + Connessione instabile all\'applicazione! Aggiungere Aggiungi la parola \"%1$s\" a %2$s? Sposta il cursore dentro un parola per aggiungerla. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 51d3ffc3..ddddf8f5 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -6,6 +6,7 @@ טוען… אירעה שגיאה לא צפויה. + חיבור לא יציב לאפליקציה! הוסף האם להוסיף את המילה \"%1$s\" ל-%2$s? הזיזו את הסמן בתוך מילה כדי להוסיף אותה. diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 580f0526..89b8f16e 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -7,6 +7,7 @@ Įvyko netikėta klaida. + Nestabilus programėlės ryšys! Pridėti Pridėti žodį \"%1$s\" į \"%2$s\" žodyną? Perkelkite žymeklį prie žodžio kurį norite pridėti. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6ffef5a5..3d4f97a6 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -4,6 +4,7 @@ Laden… Er is een onverwachte fout opgetreden. + Onstabiele applicatieverbinding! Toevoegen Voeg woord \"%1$s\" toe aan %2$s? Verplaats de cursor binnen een woord om het toe te voegen. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index dbeed628..1caa986b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -6,6 +6,7 @@ Carregando… Um erro inesperado aconteceu. + Conexão instável com o aplicativo! Adicionar Adicionar a palavra \"%1$s\" a %2$s? Mova o cursor dentro de uma palavra para adicioná-la. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 96e13d18..1f9f5400 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -5,6 +5,7 @@ Выполнено Загрузка… Произошла непредвиденная ошибка. + Нестабильное соединение с приложением! Добавить Переместите курсор внутрь слова, чтобы добавить его. Невозможно добавить слово. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index fc56a5c6..c88b4a31 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -4,6 +4,7 @@ Yükleniyor… Beklenmeyen bir hata ile karşılaşıldı. + Uygulama bağlantısı kararsız! Ekle \"%1$s\" kelimesi %2$s sözlüğe eklensin mi? Ekleme yapmak için imleci eklemek istediğiniz kelimeye götürün. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 881646cb..3b50abf7 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -7,6 +7,7 @@ Сталася неочікувана помилка. + Нестабільне з\'єднання з додатком! Додати Додати слово \"%1$s\" в %2$s? Перемістіть курсор у слово, щоб додати його. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3015a4e..6ef14769 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ No results. Unexpected error occurred. + Unstable application connection! Add Add word \"%1$s\" to %2$s?