diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java b/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java index e9e1bb2e..3666d881 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java @@ -1,22 +1,35 @@ package io.github.sspanak.tt9.db; import android.content.Context; +import android.os.CancellationSignal; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import io.github.sspanak.tt9.db.entities.AddWordResult; import io.github.sspanak.tt9.db.wordPairs.WordPairStore; import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.db.words.WordStore; import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.util.ConsumerCompat; import io.github.sspanak.tt9.util.Logger; public class DataStore { - private static final Handler asyncHandler = new Handler(); + private final static String LOG_TAG = DataStore.class.getSimpleName(); + + private static final Handler asyncReturn = new Handler(); + private static final ExecutorService executor = Executors.newCachedThreadPool(); + + private static Future getWordsTask; + private static CancellationSignal getWordsCancellationSignal = new CancellationSignal(); + private static WordPairStore pairs; private static WordStore words; @@ -28,7 +41,7 @@ public class DataStore { private static void runInThread(@NonNull Runnable action) { - new Thread(action).start(); + executor.submit(action); } @@ -40,7 +53,7 @@ public class DataStore { words.finishTransaction(); } catch (Exception e) { words.failTransaction(); - Logger.e(DataStore.class.getSimpleName(), errorMessagePrefix + " " + e.getMessage()); + Logger.e(LOG_TAG, errorMessagePrefix + " " + e.getMessage()); } onFinish.run(); }); @@ -85,17 +98,42 @@ public class DataStore { public static void getWords(ConsumerCompat> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) { - runInThread(() -> { - ArrayList data = words.getSimilar(language, sequence, filter, minWords, maxWords); - asyncHandler.post(() -> dataHandler.accept(data)); - }); + if (getWordsTask != null && !getWordsTask.isDone()) { + dataHandler.accept(new ArrayList<>()); + getWordsCancellationSignal.cancel(); + return; + } + + getWordsCancellationSignal = new CancellationSignal(); + getWordsTask = executor.submit(() -> getWordsSync(dataHandler, language, sequence, filter, minWords, maxWords)); + executor.submit(DataStore::setGetWordsTimeout); + } + + + private static void getWordsSync(ConsumerCompat> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) { + try { + ArrayList data = words.getSimilar(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords); + asyncReturn.post(() -> dataHandler.accept(data)); + } catch (Exception e) { + Logger.e(LOG_TAG, "Error fetching words: " + e.getMessage()); + } + } + + + private static void setGetWordsTimeout() { + try { + getWordsTask.get(SettingsStore.SLOW_QUERY_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (Exception e) { + getWordsCancellationSignal.cancel(); + Logger.e(LOG_TAG, "Word loading timed out after " + SettingsStore.SLOW_QUERY_TIMEOUT + " ms."); + } } public static void getCustomWords(ConsumerCompat> dataHandler, String wordFilter, int maxWords) { runInThread(() -> { ArrayList data = words.getSimilarCustom(wordFilter, maxWords); - asyncHandler.post(() -> dataHandler.accept(data)); + asyncReturn.post(() -> dataHandler.accept(data)); }); } @@ -103,7 +141,7 @@ public class DataStore { public static void countCustomWords(ConsumerCompat dataHandler) { runInThread(() -> { long data = words.countCustom(); - asyncHandler.post(() -> dataHandler.accept(data)); + asyncReturn.post(() -> dataHandler.accept(data)); }); } @@ -111,7 +149,7 @@ public class DataStore { public static void exists(ConsumerCompat> dataHandler, ArrayList languages) { runInThread(() -> { ArrayList data = words.exists(languages); - asyncHandler.post(() -> dataHandler.accept(data)); + asyncReturn.post(() -> dataHandler.accept(data)); }); } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java index 5aca4991..67caa242 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java @@ -4,6 +4,8 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteStatement; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; import androidx.annotation.NonNull; @@ -118,19 +120,19 @@ public class ReadOps { @NonNull - public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { + public WordList getWords(@NonNull SQLiteDatabase db, CancellationSignal cancel, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { if (positions.isEmpty()) { Logger.d(LOG_TAG, "No word positions. Not searching words."); return new WordList(); } String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput); - if (wordsQuery.isEmpty()) { + if (wordsQuery.isEmpty() || cancel.isCanceled()) { return new WordList(); } WordList words = new WordList(); - try (Cursor cursor = db.rawQuery(wordsQuery, null)) { + try (Cursor cursor = db.rawQuery(wordsQuery, null, cancel)) { while (cursor.moveToNext()) { words.add( cursor.getString(0), @@ -138,26 +140,29 @@ public class ReadOps { fullOutput ? cursor.getInt(2) : 0 ); } + } catch (OperationCanceledException e) { + Logger.d(LOG_TAG, "Words query cancelled!"); + return words; } return words; } - public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) { + public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) { int generations = switch (sequence.length()) { case 2 -> wordFilter.isEmpty() ? 1 : 10; case 3, 4 -> wordFilter.isEmpty() ? 2 : 10; default -> 10; }; - return getWordPositions(db, language, sequence, generations, minPositions, wordFilter); + return getWordPositions(db, cancel, language, sequence, generations, minPositions, wordFilter); } @NonNull - public String getWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) { - if (sequence.length() == 1) { + public String getWordPositions(@NonNull SQLiteDatabase db, CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) { + if (sequence.length() == 1 || cancel.isCanceled()) { return sequence; } @@ -165,18 +170,24 @@ public class ReadOps { String cachedFactoryPositions = SlowQueryStats.getCachedIfSlow(SlowQueryStats.generateKey(language, sequence, wordFilter, minPositions)); if (cachedFactoryPositions != null) { - String customWordPositions = getCustomWordPositions(db, language, sequence, generations); + String customWordPositions = getCustomWordPositions(db, cancel, language, sequence, generations); return customWordPositions.isEmpty() ? cachedFactoryPositions : customWordPositions + "," + cachedFactoryPositions; } - try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null)) { + try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null, cancel)) { positions.appendFromDbRanges(cursor); + } catch (OperationCanceledException ignored) { + Logger.d(LOG_TAG, "Word positions query cancelled!"); + return sequence; } if (positions.size < minPositions && generations < Integer.MAX_VALUE) { Logger.d(LOG_TAG, "Not enough positions: " + positions.size + " < " + minPositions + ". Searching for more."); - try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null)) { + try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null, cancel)) { positions.appendFromDbRanges(cursor); + } catch (OperationCanceledException ignored) { + Logger.d(LOG_TAG, "Word positions query cancelled!"); + return sequence; } } @@ -184,9 +195,12 @@ public class ReadOps { } - @NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, Language language, String sequence, int generations) { - try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null)) { + @NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, CancellationSignal cancel, Language language, String sequence, int generations) { + try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null, cancel)) { return new WordPositionsStringBuilder().appendFromDbRanges(cursor).toString(); + } catch (OperationCanceledException e) { + Logger.d(LOG_TAG, "Custom word positions query cancelled."); + return ""; } } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java b/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java index 8d8ff151..6bbff4fd 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java @@ -1,6 +1,7 @@ package io.github.sspanak.tt9.db.words; import android.content.Context; +import android.os.CancellationSignal; import androidx.annotation.NonNull; @@ -60,7 +61,7 @@ public class WordStore extends BaseSyncStore { * For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ... * and other similar. */ - public ArrayList getSimilar(Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) { + public ArrayList getSimilar(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) { if (!checkOrNotify()) { return new ArrayList<>(); } @@ -80,15 +81,17 @@ public class WordStore extends BaseSyncStore { final String filter = wordFilter == null ? "" : wordFilter; Timer.start("get_positions"); - String positions = readOps.getSimilarWordPositions(sqlite.getDb(), language, sequence, filter, minWords); + String positions = readOps.getSimilarWordPositions(sqlite.getDb(), cancel, language, sequence, filter, minWords); long positionsTime = Timer.stop("get_positions"); Timer.start("get_words"); - ArrayList words = readOps.getWords(sqlite.getDb(), language, positions, filter, maxWords, false).toStringList(); + ArrayList words = readOps.getWords(sqlite.getDb(), cancel, language, positions, filter, maxWords, false).toStringList(); long wordsTime = Timer.stop("get_words"); printLoadingSummary(sequence, words, positionsTime, wordsTime); - SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions); + if (!cancel.isCanceled()) { // do not store empty results from aborted queries in the cache + SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions); + } return words; } @@ -188,8 +191,8 @@ public class WordStore extends BaseSyncStore { try { Timer.start(LOG_TAG); - String topWordPositions = readOps.getWordPositions(sqlite.getDb(), language, sequence, 0, 0, ""); - WordList topWords = readOps.getWords(sqlite.getDb(), language, topWordPositions, "", 9999, true); + String topWordPositions = readOps.getWordPositions(sqlite.getDb(), null, language, sequence, 0, 0, ""); + WordList topWords = readOps.getWords(sqlite.getDb(), null, language, topWordPositions, "", 9999, true); if (topWords.isEmpty()) { throw new Exception("No such word"); } 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 d04ebbd2..dcecfe42 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 @@ -20,6 +20,7 @@ public class SettingsStore extends SettingsUI { public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // 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 public final static int SOFT_KEY_DOUBLE_CLICK_DELAY = 500; // ms public final static int SOFT_KEY_REPEAT_DELAY = 40; // ms public final static int SOFT_KEY_TITLE_MAX_CHARS = 5;