From 34d32e0f721886020cf4d2ad144fa322704db5fa Mon Sep 17 00:00:00 2001 From: sspanak Date: Wed, 8 Jan 2025 14:32:12 +0200 Subject: [PATCH] fixed slow typing on the Language Selection screen, on the Delete Custom Words screen etc, caused by a main thread deadlock --- .../preferences/settings/SettingsStore.java | 2 +- .../tt9/util/InputConnectionTools.java | 88 +++++++++++++------ 2 files changed, 62 insertions(+), 28 deletions(-) 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 5a8fbc15..77178189 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 @@ -19,7 +19,7 @@ public class SettingsStore extends SettingsUI { 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 INPUT_CONNECTION_OPERATIONS_TIMEOUT = 100; // 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/util/InputConnectionTools.java b/app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java index 73c7cc16..46396499 100644 --- a/app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java +++ b/app/src/main/java/io/github/sspanak/tt9/util/InputConnectionTools.java @@ -3,26 +3,34 @@ package io.github.sspanak.tt9.util; import android.os.Build; import android.view.inputmethod.InputConnection; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import io.github.sspanak.tt9.preferences.settings.SettingsStore; public class InputConnectionTools { + private static final String LOG_TAG = InputConnectionTools.class.getSimpleName(); + @Nullable private final InputConnection connection; - private int connectionErrors; - private boolean isErrorReported; + + @Nullable CompletableFuture future; + @NonNull ExecutorService executor = Executors.newSingleThreadExecutor(); + private int connectionErrors = 0; + private boolean isErrorReported = false; + + private CharSequence result; public InputConnectionTools(@Nullable InputConnection connection) { this.connection = connection; - connectionErrors = 0; - isErrorReported = false; } @@ -39,19 +47,20 @@ public class InputConnectionTools { @Nullable private CharSequence getTextNextToCursor(int i, int ii, boolean after) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return getTextNextToCursorModern(i, ii, after); + getTextNextToCursorModern(i, ii, after); } else { - return getTextNextToCursorClassic(i, ii, after); + getTextNextToCursorClassic(i, ii, after); } + + return result; } - private CharSequence getTextNextToCursorClassic(int i, int ii, boolean after) { - if (connection == null) { - return null; + private void getTextNextToCursorClassic(int i, int ii, boolean after) { + result = null; + if (connection != null) { + result = after ? connection.getTextAfterCursor(i, ii) : connection.getTextBeforeCursor(i, ii); } - - return after ? connection.getTextAfterCursor(i, ii) : connection.getTextBeforeCursor(i, ii); } @@ -62,29 +71,54 @@ public class InputConnectionTools { * 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) - ); + private void getTextNextToCursorModern(int i, int ii, boolean after) { + // CompletableFuture is supported only in Android 24 and above, so we initialize it here. + future = new CompletableFuture<>(); + + // Start only the watchdog in a separate thread. If we start the main operation there too, + // it will causes a deadlock, because main would be waiting for the thread, which will be waiting + // for main to provide text. + executor.submit(this::awaitTextResult); try { - return future.get(SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { + getTextNextToCursorClassic(i, ii, after); + future.complete(true); + } catch (Exception 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; + logError("getStringBeforeCursor() failed " + connectionErrors + " times so far. Current error: " + e); } } + + @RequiresApi(api = Build.VERSION_CODES.N) + private void awaitTextResult() { + if (future == null) { + logError("No future. Cannot call getStringBeforeCursor()."); + return; + } + + try { + if (!future.get(SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT, TimeUnit.MILLISECONDS)) { + logError("Future is not true. InputConnection.getTextBeforeCursor() probably failed."); + } + } catch (TimeoutException e) { + connectionErrors++; + logError( + "getStringBeforeCursor() timed out after " + SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT + "ms. " + connectionErrors + " errors so far." + ); + } catch (InterruptedException | ExecutionException e) { + connectionErrors++; + logError("getStringBeforeCursor() failed " + connectionErrors + " times so far. Current error: " + e); + } + } + + + private void logError(@NonNull String error) { + Logger.e(LOG_TAG, error); + } + + public boolean shouldReportTimeout() { if (!isErrorReported && connectionErrors > SettingsStore.INPUT_CONNECTION_ERRORS_MAX) { isErrorReported = true;