fixed slow typing on the Language Selection screen, on the Delete Custom Words screen etc, caused by a main thread deadlock
This commit is contained in:
parent
7aa3ab5102
commit
34d32e0f72
2 changed files with 62 additions and 28 deletions
|
|
@ -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_BATCH_SIZE = 5000; // words
|
||||||
public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms
|
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_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 int RESIZE_THROTTLING_TIME = 60; // ms
|
||||||
public final static byte SLOW_QUERY_TIME = 50; // ms
|
public final static byte SLOW_QUERY_TIME = 50; // ms
|
||||||
public final static int SLOW_QUERY_TIMEOUT = 3000; // ms
|
public final static int SLOW_QUERY_TIMEOUT = 3000; // ms
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,34 @@ package io.github.sspanak.tt9.util;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
|
||||||
|
|
||||||
public class InputConnectionTools {
|
public class InputConnectionTools {
|
||||||
|
private static final String LOG_TAG = InputConnectionTools.class.getSimpleName();
|
||||||
|
|
||||||
@Nullable private final InputConnection connection;
|
@Nullable private final InputConnection connection;
|
||||||
private int connectionErrors;
|
|
||||||
private boolean isErrorReported;
|
@Nullable CompletableFuture<Boolean> future;
|
||||||
|
@NonNull ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private int connectionErrors = 0;
|
||||||
|
private boolean isErrorReported = false;
|
||||||
|
|
||||||
|
private CharSequence result;
|
||||||
|
|
||||||
|
|
||||||
public InputConnectionTools(@Nullable InputConnection connection) {
|
public InputConnectionTools(@Nullable InputConnection connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
connectionErrors = 0;
|
|
||||||
isErrorReported = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,19 +47,20 @@ public class InputConnectionTools {
|
||||||
@Nullable
|
@Nullable
|
||||||
private CharSequence getTextNextToCursor(int i, int ii, boolean after) {
|
private CharSequence getTextNextToCursor(int i, int ii, boolean after) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
return getTextNextToCursorModern(i, ii, after);
|
getTextNextToCursorModern(i, ii, after);
|
||||||
} else {
|
} else {
|
||||||
return getTextNextToCursorClassic(i, ii, after);
|
getTextNextToCursorClassic(i, ii, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private CharSequence getTextNextToCursorClassic(int i, int ii, boolean after) {
|
private void getTextNextToCursorClassic(int i, int ii, boolean after) {
|
||||||
if (connection == null) {
|
result = null;
|
||||||
return 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
|
* getTextBeforeCursor() that terminates the operations after a certain timeout. Just in case we
|
||||||
* handle getTextAfterCursor() the same way.
|
* handle getTextAfterCursor() the same way.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
private CharSequence getTextNextToCursorModern(int i, int ii, boolean after) {
|
private void getTextNextToCursorModern(int i, int ii, boolean after) {
|
||||||
CompletableFuture<CharSequence> future = CompletableFuture.supplyAsync(
|
// CompletableFuture is supported only in Android 24 and above, so we initialize it here.
|
||||||
() -> getTextNextToCursorClassic(i, ii, after)
|
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 {
|
try {
|
||||||
return future.get(SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT, TimeUnit.MILLISECONDS);
|
getTextNextToCursorClassic(i, ii, after);
|
||||||
|
future.complete(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
connectionErrors++;
|
||||||
|
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) {
|
} catch (TimeoutException e) {
|
||||||
connectionErrors++;
|
connectionErrors++;
|
||||||
Logger.e(
|
logError(
|
||||||
getClass().getSimpleName(),
|
|
||||||
"getStringBeforeCursor() timed out after " + SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT + "ms. " + connectionErrors + " errors so far."
|
"getStringBeforeCursor() timed out after " + SettingsStore.INPUT_CONNECTION_OPERATIONS_TIMEOUT + "ms. " + connectionErrors + " errors so far."
|
||||||
);
|
);
|
||||||
return null;
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
connectionErrors++;
|
connectionErrors++;
|
||||||
Logger.e(getClass().getSimpleName(), "getStringBeforeCursor() failed " + connectionErrors + " times so far. Current error: " + e);
|
logError("getStringBeforeCursor() failed " + connectionErrors + " times so far. Current error: " + e);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void logError(@NonNull String error) {
|
||||||
|
Logger.e(LOG_TAG, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean shouldReportTimeout() {
|
public boolean shouldReportTimeout() {
|
||||||
if (!isErrorReported && connectionErrors > SettingsStore.INPUT_CONNECTION_ERRORS_MAX) {
|
if (!isErrorReported && connectionErrors > SettingsStore.INPUT_CONNECTION_ERRORS_MAX) {
|
||||||
isErrorReported = true;
|
isErrorReported = true;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue