From 10497af44db882ac1725543ee7c4d2d90f878af2 Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Fri, 6 Sep 2024 14:38:26 +0300 Subject: [PATCH] Word pair predictions (#616) --- .../{WordStoreAsync.java => DataStore.java} | 75 +++++-- .../sspanak/tt9/db/sqlite/DeleteOps.java | 4 + .../sspanak/tt9/db/sqlite/InsertOps.java | 13 ++ .../github/sspanak/tt9/db/sqlite/ReadOps.java | 22 +- .../sspanak/tt9/db/sqlite/SQLiteOpener.java | 15 +- .../github/sspanak/tt9/db/sqlite/Tables.java | 13 +- .../sspanak/tt9/db/wordPairs/WordPair.java | 75 +++++++ .../tt9/db/wordPairs/WordPairStore.java | 190 ++++++++++++++++++ .../tt9/db/{ => words}/DictionaryLoader.java | 5 +- .../sspanak/tt9/db/{ => words}/LegacyDb.java | 2 +- .../tt9/db/{ => words}/SlowQueryStats.java | 2 +- .../sspanak/tt9/db/{ => words}/WordStore.java | 4 +- .../sspanak/tt9/ime/CommandHandler.java | 10 +- .../github/sspanak/tt9/ime/HotkeyHandler.java | 2 +- .../github/sspanak/tt9/ime/TraditionalT9.java | 24 ++- .../github/sspanak/tt9/ime/TypingHandler.java | 10 +- .../sspanak/tt9/ime/helpers/TextField.java | 23 ++- .../sspanak/tt9/ime/modes/InputMode.java | 4 +- .../sspanak/tt9/ime/modes/ModePredictive.java | 10 +- .../tt9/ime/modes/helpers/Predictions.java | 138 ++++++++++--- .../tt9/preferences/PreferencesActivity.java | 6 +- .../preferences/screens/UsageStatsScreen.java | 67 ++++-- .../deleteWords/PreferenceDeletableWord.java | 4 +- .../deleteWords/PreferenceSearchWords.java | 10 +- .../LanguageSelectionScreen.java | 6 +- .../screens/languages/ItemLoadDictionary.java | 2 +- .../screens/languages/ItemTruncateAll.java | 4 +- .../languages/ItemTruncateUnselected.java | 4 +- .../screens/languages/LanguagesScreen.java | 2 +- .../preferences/settings/SettingsStore.java | 4 +- .../preferences/settings/SettingsTyping.java | 5 + .../sspanak/tt9/ui/dialogs/AddWordDialog.java | 4 +- .../tt9/ui/dialogs/AutoUpdateMonologue.java | 2 +- .../java/io/github/sspanak/tt9/util/Text.java | 10 + .../main/res/xml/prefs_screen_usage_stats.xml | 27 ++- 35 files changed, 667 insertions(+), 131 deletions(-) rename app/src/main/java/io/github/sspanak/tt9/db/{WordStoreAsync.java => DataStore.java} (51%) create mode 100644 app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPair.java create mode 100644 app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPairStore.java rename app/src/main/java/io/github/sspanak/tt9/db/{ => words}/DictionaryLoader.java (99%) rename app/src/main/java/io/github/sspanak/tt9/db/{ => words}/LegacyDb.java (96%) rename app/src/main/java/io/github/sspanak/tt9/db/{ => words}/SlowQueryStats.java (98%) rename app/src/main/java/io/github/sspanak/tt9/db/{ => words}/WordStore.java (99%) diff --git a/app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java b/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java similarity index 51% rename from app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java rename to app/src/main/java/io/github/sspanak/tt9/db/DataStore.java index 16c521a6..4352b932 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java @@ -8,39 +8,45 @@ import androidx.annotation.NonNull; import java.util.ArrayList; import io.github.sspanak.tt9.db.entities.AddWordResult; +import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; +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.util.ConsumerCompat; -public class WordStoreAsync { - private static WordStore store; +public class DataStore { private static final Handler asyncHandler = new Handler(); + private static WordPairStore pairs; + private static WordStore words; public static void init(Context context) { - store = new WordStore(context); + pairs = pairs == null ? new WordPairStore(context) : pairs; + words = words == null ? new WordStore(context) : words; } public static void destroy() { - if (store != null) { - store = null; - } + pairs = null; + words = null; + SQLiteOpener.destroyInstance(); } public static void normalizeNext() { - new Thread(() -> store.normalizeNext()).start(); + new Thread(() -> words.normalizeNext()).start(); } public static void getLastLanguageUpdateTime(ConsumerCompat notification, Language language) { - new Thread(() -> notification.accept(store.getLanguageFileHash(language))).start(); + new Thread(() -> notification.accept(words.getLanguageFileHash(language))).start(); } public static void deleteCustomWord(Runnable notification, Language language, String word) { new Thread(() -> { - store.removeCustomWord(language, word); + words.removeCustomWord(language, word); notification.run(); }).start(); } @@ -48,46 +54,83 @@ public class WordStoreAsync { public static void deleteWords(Runnable notification, @NonNull ArrayList languageIds) { new Thread(() -> { - store.remove(languageIds); + words.remove(languageIds); notification.run(); }).start(); } public static void put(ConsumerCompat statusHandler, Language language, String word) { - new Thread(() -> statusHandler.accept(store.put(language, word))).start(); + new Thread(() -> statusHandler.accept(words.put(language, word))).start(); } public static void makeTopWord(@NonNull Language language, @NonNull String word, @NonNull String sequence) { - new Thread(() -> store.makeTopWord(language, word, sequence)).start(); + new Thread(() -> words.makeTopWord(language, word, sequence)).start(); } public static void getWords(ConsumerCompat> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) { new Thread(() -> asyncHandler.post(() -> dataHandler.accept( - store.getSimilar(language, sequence, filter, minWords, maxWords))) + words.getSimilar(language, sequence, filter, minWords, maxWords))) ).start(); } public static void getCustomWords(ConsumerCompat> dataHandler, String wordFilter, int maxWords) { new Thread(() -> asyncHandler.post(() -> dataHandler.accept( - store.getSimilarCustom(wordFilter, maxWords))) + words.getSimilarCustom(wordFilter, maxWords))) ).start(); } public static void countCustomWords(ConsumerCompat dataHandler) { new Thread(() -> asyncHandler.post(() -> dataHandler.accept( - store.countCustom())) + words.countCustom())) ).start(); } public static void exists(ConsumerCompat> dataHandler, ArrayList languages) { new Thread(() -> asyncHandler.post(() -> dataHandler.accept( - store.exists(languages)) + words.exists(languages)) )).start(); } + + + public static void addWordPair(Language language, String word1, String word2, String sequence2) { + pairs.add(language, word1, word2, sequence2); + } + + + public static String getWord2(Language language, String word1, String sequence2) { + return pairs.getWord2(language, word1, sequence2); + } + + + public static void saveWordPairs() { + new Thread(() -> pairs.save()).start(); + } + + + public static void loadWordPairs(DictionaryLoader dictionaryLoader, ArrayList languages) { + new Thread(() -> pairs.load(dictionaryLoader, languages)).start(); + } + + + public static void clearWordPairCache() { + pairs.clearCache(); + } + + public static void deleteWordPairs(@NonNull ArrayList languages, @NonNull Runnable onDeleted) { + new Thread(() -> { + pairs.delete(languages); + onDeleted.run(); + }).start(); + } + + + public static String getWordPairStats() { + return pairs.toString(); + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/DeleteOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/DeleteOps.java index 28b74a23..179addbe 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/DeleteOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/DeleteOps.java @@ -25,4 +25,8 @@ public class DeleteOps { db.delete(Tables.CUSTOM_WORDS, "ROWID IN (" + repeatingWords + ")", null); } + + public static void deleteWordPairs(@NonNull SQLiteDatabase db, int languageId) { + db.delete(Tables.getWordPairs(languageId), null, null); + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java index 1b8dd1ca..92208809 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java @@ -6,8 +6,11 @@ import android.database.sqlite.SQLiteStatement; import androidx.annotation.NonNull; +import java.util.Collection; + import io.github.sspanak.tt9.db.entities.Word; import io.github.sspanak.tt9.db.entities.WordPosition; +import io.github.sspanak.tt9.db.wordPairs.WordPair; import io.github.sspanak.tt9.languages.Language; @@ -77,4 +80,14 @@ public class InsertOps { "SELECT -id, word FROM " + Tables.CUSTOM_WORDS + " WHERE langId = " + language.getId() ); } + + public static void insertWordPairs(@NonNull SQLiteDatabase db, int langId, Collection pairs) { + if (langId <= 0 || pairs == null || pairs.isEmpty()) { + return; + } + + for (WordPair pair : pairs) { + db.insert(Tables.getWordPairs(langId), null, pair.toContentValues()); + } + } } 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 82ea1b5c..4c16565b 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 @@ -9,10 +9,11 @@ import androidx.annotation.NonNull; import java.util.ArrayList; -import io.github.sspanak.tt9.db.SlowQueryStats; import io.github.sspanak.tt9.db.entities.NormalizationList; import io.github.sspanak.tt9.db.entities.WordList; import io.github.sspanak.tt9.db.entities.WordPositionsStringBuilder; +import io.github.sspanak.tt9.db.wordPairs.WordPair; +import io.github.sspanak.tt9.db.words.SlowQueryStats; import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.util.Logger; @@ -288,4 +289,23 @@ public class ReadOps { return new NormalizationList(res); } + + + @NonNull public ArrayList getWordPairs(@NonNull SQLiteDatabase db, @NonNull Language language, int limit) { + ArrayList pairs = new ArrayList<>(); + + if (limit <= 0) { + return pairs; + } + + String[] select = new String[]{"word1", "word2", "sequence2"}; + + try (Cursor cursor = db.query(Tables.getWordPairs(language.getId()), select, null, null, null, null, null, String.valueOf(limit))) { + while (cursor.moveToNext()) { + pairs.add(new WordPair(language, cursor.getString(0), cursor.getString(1), cursor.getString(2))); + } + } + + return pairs; + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java index 249104b2..2f14ff91 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java @@ -7,10 +7,10 @@ import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import io.github.sspanak.tt9.BuildConfig; -import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; +import io.github.sspanak.tt9.util.Logger; public class SQLiteOpener extends SQLiteOpenHelper { private static final String LOG_TAG = SQLiteOpener.class.getSimpleName(); @@ -38,6 +38,19 @@ public class SQLiteOpener extends SQLiteOpenHelper { } + public static void destroyInstance() { + try { + if (self != null) { + self.close(); + } + } catch (IllegalStateException e) { + Logger.e(LOG_TAG, e.getMessage() + ". Ignoring database state and setting reference to NULL."); + } finally { + self = null; + } + } + + @Override public void onCreate(SQLiteDatabase db) { for (String query : Tables.getCreateQueries(allLanguages)) { diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java index 5acd66a9..b71da3e5 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java @@ -14,14 +14,16 @@ public class Tables { static final String CUSTOM_WORDS = "custom_words"; private static final String POSITIONS_TABLE_BASE_NAME = "word_positions_"; private static final String WORDS_TABLE_BASE_NAME = "words_"; + private static final String WORD_PAIRS_TABLE_BASE_NAME = "word_pairs_"; static String getWords(int langId) { return WORDS_TABLE_BASE_NAME + langId; } static String getWordPositions(int langId) { return POSITIONS_TABLE_BASE_NAME + langId; } + static String getWordPairs(int langId) { return WORD_PAIRS_TABLE_BASE_NAME + langId; } static String[] getCreateQueries(ArrayList languages) { int languageCount = languages.size(); - String[] queries = new String[languageCount * 2 + 3]; + String[] queries = new String[languageCount * 3 + 3]; queries[0] = createCustomWords(); queries[1] = createCustomWordsIndex(); @@ -31,6 +33,7 @@ public class Tables { for (Language language : languages) { queries[queryId++] = createWordsTable(language.getId()); queries[queryId++] = createWordPositions(language.getId()); + queries[queryId++] = createWordPairs(language.getId()); } return queries; @@ -100,6 +103,14 @@ public class Tables { return "CREATE INDEX IF NOT EXISTS idx_langId_sequence ON " + CUSTOM_WORDS + " (langId, sequence)"; } + private static String createWordPairs(int langId) { + return "CREATE TABLE IF NOT EXISTS " + getWordPairs(langId) + " (" + + "word1 TEXT NOT NULL, " + + "word2 TEXT NOT NULL, " + + "sequence2 TEXT NOT NULL " + + ")"; + } + private static String createLanguagesMeta() { return "CREATE TABLE IF NOT EXISTS " + LANGUAGES_META + " (" + "langId INTEGER UNIQUE NOT NULL, " + diff --git a/app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPair.java b/app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPair.java new file mode 100644 index 00000000..60a90ece --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPair.java @@ -0,0 +1,75 @@ +package io.github.sspanak.tt9.db.wordPairs; + +import android.content.ContentValues; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.util.Text; + +public class WordPair { + private final Language language; + @NonNull private final String word1; + @NonNull private final String word2; + private final String sequence2; + private Integer hash = null; + + + public WordPair(Language language, String word1, String word2, String sequence2) { + this.language = language; + this.word1 = word1 != null ? word1.toLowerCase(language.getLocale()) : ""; + this.word2 = word2 != null ? word2.toLowerCase(language.getLocale()) : ""; + this.sequence2 = sequence2; + } + + + boolean isInvalid() { + return + language == null + || word1.isEmpty() || word2.isEmpty() + || (word1.length() > SettingsStore.WORD_PAIR_MAX_WORD_LENGTH && word2.length() > SettingsStore.WORD_PAIR_MAX_WORD_LENGTH) + || word1.equals(word2) + || sequence2 == null || word2.length() != sequence2.length() + || !(new Text(word1).isAlphabetic()) || !(new Text(word2).isAlphabetic()); + } + + + @NonNull + public String getWord2() { + return word2; + } + + + public ContentValues toContentValues() { + ContentValues values = new ContentValues(); + values.put("word1", word1); + values.put("word2", word2); + values.put("sequence2", sequence2); + return values; + } + + + @Override + public int hashCode() { + if (hash == null) { + hash = !word1.isEmpty() && sequence2 != null ? (word1 + "," + sequence2).hashCode() : 0; + } + + return hash; + } + + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof WordPair && obj.hashCode() == hashCode(); + } + + + @NonNull + @Override + public String toString() { + return "(" + word1 + "," + word2 + "," + sequence2 + ")"; + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPairStore.java b/app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPairStore.java new file mode 100644 index 00000000..02ef5fb1 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/wordPairs/WordPairStore.java @@ -0,0 +1,190 @@ +package io.github.sspanak.tt9.db.wordPairs; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.github.sspanak.tt9.db.sqlite.DeleteOps; +import io.github.sspanak.tt9.db.sqlite.InsertOps; +import io.github.sspanak.tt9.db.sqlite.ReadOps; +import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; +import io.github.sspanak.tt9.db.words.DictionaryLoader; +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.util.Logger; +import io.github.sspanak.tt9.util.Timer; + +public class WordPairStore { + // data + private final SQLiteOpener sqlite; + private final ConcurrentHashMap> pairs = new ConcurrentHashMap<>(); + + // timing + private long slowestAddTime = 0; + private long slowestLoadTime = 0; + private long slowestSaveTime = 0; + private long slowestSearchTime = 0; + + + public WordPairStore(Context context) { + sqlite = SQLiteOpener.getInstance(context); + } + + + public void add(Language language, String word1, String word2, String sequence2) { + String ADD_TIMER_NAME = "word_pair_add"; + Timer.start(ADD_TIMER_NAME); + + WordPair pair = new WordPair(language, word1, word2, sequence2); + if (pair.isInvalid()) { + return; + } + + HashMap languagePairs = pairs.get(language.getId()); + if (languagePairs == null) { + languagePairs = new HashMap<>(); + pairs.put(language.getId(), languagePairs); + } + + if (languagePairs.size() >= SettingsStore.WORD_PAIR_MAX) { + languagePairs.remove(languagePairs.keySet().iterator().next()); + } + + languagePairs.put(pair, pair); + + slowestAddTime = Math.max(slowestAddTime, Timer.stop(ADD_TIMER_NAME)); + } + + + public void clearCache() { + pairs.clear(); + slowestAddTime = 0; + slowestSearchTime = 0; + slowestSaveTime = 0; + slowestLoadTime = 0; + } + + + @Nullable + public String getWord2(Language language, String word1, String sequence2) { + String SEARCH_TIMER_NAME = "word_pair_search"; + Timer.start(SEARCH_TIMER_NAME); + + HashMap languagePairs = pairs.get(language.getId()); + + if (languagePairs == null) { + slowestSearchTime = Math.max(slowestSearchTime, Timer.stop(SEARCH_TIMER_NAME)); + return null; + } + + WordPair pair = languagePairs.get(new WordPair(language, word1, null, sequence2)); + String word2 = pair == null || pair.getWord2().isEmpty() ? null : pair.getWord2(); + + slowestSearchTime = Math.max(slowestSearchTime, Timer.stop(SEARCH_TIMER_NAME)); + return word2; + } + + + public void save() { + String SAVE_TIMER_NAME = "word_pair_save"; + Timer.start(SAVE_TIMER_NAME); + + for (Map.Entry> entry : pairs.entrySet()) { + int langId = entry.getKey(); + HashMap languagePairs = entry.getValue(); + + sqlite.beginTransaction(); + DeleteOps.deleteWordPairs(sqlite.getDb(), langId); + InsertOps.insertWordPairs(sqlite.getDb(), langId, languagePairs.values()); + sqlite.finishTransaction(); + } + + + long currentTime = Timer.stop(SAVE_TIMER_NAME); + slowestSaveTime = Math.max(slowestSaveTime, currentTime); + Logger.d(getClass().getSimpleName(), "Saved all word pairs in: " + currentTime + " ms"); + } + + + public void load(@NonNull DictionaryLoader dictionaryLoader, ArrayList languages) { + if (dictionaryLoader.isRunning()) { + Logger.e(getClass().getSimpleName(), "Cannot load word pairs while the DictionaryLoader is working."); + return; + } + + if (languages == null) { + Logger.e(getClass().getSimpleName(), "Cannot load word pairs for NULL language list."); + return; + } + + String LOAD_TIMER_NAME = "word_pair_load"; + Timer.start(LOAD_TIMER_NAME); + + int totalPairs = 0; + for (Language language : languages) { + HashMap wordPairs = pairs.get(language.getId()); + if (wordPairs == null) { + wordPairs = new HashMap<>(); + pairs.put(language.getId(), wordPairs); + } else if (!wordPairs.isEmpty()) { + continue; + } + + int max = SettingsStore.WORD_PAIR_MAX - wordPairs.size(); + ArrayList dbPairs = new ReadOps().getWordPairs(sqlite.getDb(), language, max); + for (WordPair pair : dbPairs) { + wordPairs.put(pair, pair); + } + + Logger.d(getClass().getSimpleName(), "Loaded " + wordPairs.size() + " word pairs for language: " + language.getId()); + } + + long currentTime = Timer.stop(LOAD_TIMER_NAME); + slowestLoadTime = Math.max(slowestLoadTime, currentTime); + Logger.d(getClass().getSimpleName(), "Loaded " + totalPairs + " word pairs in " + currentTime + " ms"); + } + + + public void delete(@NonNull ArrayList languages) { + sqlite.beginTransaction(); + for (Language language : languages) { + DeleteOps.deleteWordPairs(sqlite.getDb(), language.getId()); + } + sqlite.finishTransaction(); + + slowestLoadTime = 0; + slowestSaveTime = 0; + } + + + @NonNull + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (Map.Entry> entry : pairs.entrySet()) { + int langId = entry.getKey(); + HashMap languagePairs = entry.getValue(); + + sb.append("Language ").append(langId).append(": "); + sb.append(languagePairs.size()).append("\n"); + } + + if (sb.length() == 0) { + sb.append("No word pairs.\n"); + } else { + sb.append("\nSlowest add-one: ").append(slowestAddTime).append(" ms\n"); + sb.append("Slowest search-one: ").append(slowestSearchTime).append(" ms\n"); + sb.append("Slowest save-all: ").append(slowestSaveTime).append(" ms\n"); + sb.append("Slowest load-all: ").append(slowestLoadTime).append(" ms\n"); + } + + return sb.toString(); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java b/app/src/main/java/io/github/sspanak/tt9/db/words/DictionaryLoader.java similarity index 99% rename from app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java rename to app/src/main/java/io/github/sspanak/tt9/db/words/DictionaryLoader.java index 2b0b7403..1b64bf98 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/words/DictionaryLoader.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db; +package io.github.sspanak.tt9.db.words; import android.content.Context; import android.content.res.AssetManager; @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.db.entities.WordBatch; import io.github.sspanak.tt9.db.entities.WordFile; import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException; @@ -122,7 +123,7 @@ public class DictionaryLoader { return false; } - WordStoreAsync.getLastLanguageUpdateTime( + DataStore.getLastLanguageUpdateTime( (hash) -> { self.lastAutoLoadAttemptTime.put(language.getId(), System.currentTimeMillis()); diff --git a/app/src/main/java/io/github/sspanak/tt9/db/LegacyDb.java b/app/src/main/java/io/github/sspanak/tt9/db/words/LegacyDb.java similarity index 96% rename from app/src/main/java/io/github/sspanak/tt9/db/LegacyDb.java rename to app/src/main/java/io/github/sspanak/tt9/db/words/LegacyDb.java index 9dbb82ba..acc93e2b 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/LegacyDb.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/words/LegacyDb.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db; +package io.github.sspanak.tt9.db.words; import android.app.Activity; import android.database.sqlite.SQLiteDatabase; diff --git a/app/src/main/java/io/github/sspanak/tt9/db/SlowQueryStats.java b/app/src/main/java/io/github/sspanak/tt9/db/words/SlowQueryStats.java similarity index 98% rename from app/src/main/java/io/github/sspanak/tt9/db/SlowQueryStats.java rename to app/src/main/java/io/github/sspanak/tt9/db/words/SlowQueryStats.java index a5878848..95239b66 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/SlowQueryStats.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/words/SlowQueryStats.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db; +package io.github.sspanak.tt9.db.words; import java.util.HashMap; diff --git a/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java b/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java similarity index 99% rename from app/src/main/java/io/github/sspanak/tt9/db/WordStore.java rename to app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java index 7bada810..9237e3c1 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java @@ -1,4 +1,4 @@ -package io.github.sspanak.tt9.db; +package io.github.sspanak.tt9.db.words; import android.content.Context; @@ -31,7 +31,7 @@ public class WordStore { private ReadOps readOps = null; - WordStore(@NonNull Context context) { + public WordStore(@NonNull Context context) { try { sqlite = SQLiteOpener.getInstance(context); sqlite.getDb(); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/CommandHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/CommandHandler.java index 38b1fc2d..2c6139d2 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/CommandHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/CommandHandler.java @@ -1,8 +1,8 @@ package io.github.sspanak.tt9.ime; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryLoader; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.ime.modes.ModeABC; import io.github.sspanak.tt9.languages.LanguageCollection; @@ -95,7 +95,7 @@ abstract public class CommandHandler extends TextEditingHandler { if (word.isEmpty()) { UI.toastLong(this, R.string.add_word_no_selection); } else if (settings.getAddWordsNoConfirmation()) { - WordStoreAsync.put((res) -> UI.toastLongFromAsync(this, res.toHumanFriendlyString(this)), mLanguage, word); + DataStore.put((res) -> UI.toastLongFromAsync(this, res.toHumanFriendlyString(this)), mLanguage, word); } else { AddWordDialog.show(this, mLanguage.getId(), word); } @@ -113,7 +113,7 @@ abstract public class CommandHandler extends TextEditingHandler { if (mInputMode.isPassthrough() || voiceInputOps.isListening()) { return; } else if (allowedInputModes.size() == 1 && allowedInputModes.contains(InputMode.MODE_123)) { - mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, InputMode.MODE_123) : mInputMode; + mInputMode = !mInputMode.is123() ? InputMode.getInstance(settings, mLanguage, inputType, textField, InputMode.MODE_123) : mInputMode; } // when typing a word or viewing scrolling the suggestions, only change the case else if (!suggestionOps.isEmpty()) { @@ -124,7 +124,7 @@ abstract public class CommandHandler extends TextEditingHandler { mInputMode.nextTextCase(); } else { int nextModeIndex = (allowedInputModes.indexOf(mInputMode.getId()) + 1) % allowedInputModes.size(); - mInputMode = InputMode.getInstance(settings, mLanguage, inputType, allowedInputModes.get(nextModeIndex)); + mInputMode = InputMode.getInstance(settings, mLanguage, inputType, textField, allowedInputModes.get(nextModeIndex)); mInputMode.setTextFieldCase(inputType.determineTextCase()); mInputMode.determineNextWordTextCase(textField.getStringBeforeCursor()); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java index d8c16c44..60cb0220 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/HotkeyHandler.java @@ -2,7 +2,7 @@ package io.github.sspanak.tt9.ime; import android.view.KeyEvent; -import io.github.sspanak.tt9.db.DictionaryLoader; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.ime.modes.ModePredictive; import io.github.sspanak.tt9.preferences.helpers.Hotkeys; 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 06db8126..8f3b3e35 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,10 +9,11 @@ import android.view.inputmethod.InputConnection; import androidx.annotation.NonNull; -import io.github.sspanak.tt9.db.DictionaryLoader; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.hacks.InputType; import io.github.sspanak.tt9.ime.modes.ModePredictive; +import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.ui.UI; import io.github.sspanak.tt9.ui.dialogs.PopupDialog; @@ -121,7 +122,7 @@ public class TraditionalT9 extends MainViewHandler { protected void onInit() { settings.setDemoMode(false); Logger.setLevel(settings.getLogLevel()); - WordStoreAsync.init(this); + DataStore.init(this); super.onInit(); } @@ -143,7 +144,13 @@ public class TraditionalT9 extends MainViewHandler { initUi(); } - if (new InputType(connection, field).isNotUs(this)) { + InputType newInputType = new InputType(connection, field); + + if (newInputType.isText()) { + DataStore.loadWordPairs(DictionaryLoader.getInstance(this), LanguageCollection.getAll(this, settings.getEnabledLanguageIds())); + } + + if (newInputType.isNotUs(this)) { DictionaryLoader.autoLoad(this, mLanguage); } @@ -165,8 +172,11 @@ public class TraditionalT9 extends MainViewHandler { normalizationHandler.removeCallbacksAndMessages(null); normalizationHandler.postDelayed( - () -> { if (!DictionaryLoader.getInstance(this).isRunning()) WordStoreAsync.normalizeNext(); }, - SettingsStore.WORD_NORMALIZATION_DELAY + () -> { + DataStore.saveWordPairs(); + if (!DictionaryLoader.getInstance(this).isRunning()) DataStore.normalizeNext(); + }, + SettingsStore.WORD_BACKGROUND_TASKS_DELAY ); } @@ -188,7 +198,7 @@ public class TraditionalT9 extends MainViewHandler { requestHideSelf(0); onStop(); normalizationHandler.removeCallbacksAndMessages(null); - WordStoreAsync.destroy(); + DataStore.destroy(); stopSelf(); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java index e3196508..70a15633 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TypingHandler.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; import java.util.ArrayList; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryLoader; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.hacks.AppHacks; import io.github.sspanak.tt9.hacks.InputType; import io.github.sspanak.tt9.ime.helpers.CursorOps; @@ -35,7 +35,7 @@ public abstract class TypingHandler extends KeyPadHandler { // input protected ArrayList allowedInputModes = new ArrayList<>(); @NonNull - protected InputMode mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH); + protected InputMode mInputMode = InputMode.getInstance(null, null, null, null, InputMode.MODE_PASSTHROUGH); // language protected ArrayList mEnabledLanguages; @@ -103,7 +103,7 @@ public abstract class TypingHandler extends KeyPadHandler { protected void onFinishTyping() { suggestionOps.cancelDelayedAccept(); - mInputMode = InputMode.getInstance(null, null, null, InputMode.MODE_PASSTHROUGH); + mInputMode = InputMode.getInstance(null, null, null, null, InputMode.MODE_PASSTHROUGH); setInputField(null, null); } @@ -135,7 +135,7 @@ public abstract class TypingHandler extends KeyPadHandler { } if (settings.getBackspaceRecomposing() && !hold && suggestionOps.isEmpty()) { - final String previousWord = textField.getWordBeforeCursor(mLanguage); + final String previousWord = textField.getWordBeforeCursor(mLanguage, 0, false); if (mInputMode.recompose(previousWord) && textField.recompose(previousWord)) { getSuggestions(); } else { @@ -283,7 +283,7 @@ public abstract class TypingHandler extends KeyPadHandler { * Same as getInputModeId(), but returns an actual InputMode. */ protected InputMode getInputMode() { - return InputMode.getInstance(settings, mLanguage, inputType, getInputModeId()); + return InputMode.getInstance(settings, mLanguage, inputType, textField, getInputModeId()); } 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 276ba9c7..a41acd50 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 @@ -107,7 +107,13 @@ public class TextField extends InputField { } - @NonNull public String getWordBeforeCursor(Language language) { + /** + * Returns a word (String containing of alphabetic) characters before the cursor, only if the cursor is + * not in the middle of that word. "skipWords" can be used to return the N-th word before the cursor. + * "stopAtPunctuation" can be used to stop searching at the first punctuation character. In case + * no complete word is found due to any reason, an empty string is returned. + */ + @NonNull public String getWordBeforeCursor(Language language, int skipWords, boolean stopAtPunctuation) { if (getTextAfterCursor(1).startsWithWord()) { return ""; } @@ -117,17 +123,28 @@ public class TextField extends InputField { return ""; } + int endIndex = before.length(); + for (int i = before.length() - 1; i >= 0; i--) { char currentLetter = before.charAt(i); + + if (stopAtPunctuation && language.getKeyCharacters(1).contains(currentLetter + "")) { + return ""; + } + if ( !Character.isAlphabetic(currentLetter) && !(currentLetter == '\'' && (LanguageKind.isHebrew(language) || LanguageKind.isUkrainian(language))) ) { - return before.substring(i + 1); + if (skipWords-- <= 0 || i == 0) { + return before.substring(i + 1, endIndex); + } else { + endIndex = i; + } } } - return before; + return endIndex == before.length() ? before : before.substring(0, endIndex); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java index 7f1abcfa..97883f62 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -37,10 +37,10 @@ abstract public class InputMode { protected int specialCharSelectedGroup = 0; - public static InputMode getInstance(SettingsStore settings, Language language, InputType inputType, int mode) { + public static InputMode getInstance(SettingsStore settings, Language language, InputType inputType, TextField textField, int mode) { switch (mode) { case MODE_PREDICTIVE: - return new ModePredictive(settings, inputType, language); + return new ModePredictive(settings, inputType, textField, language); case MODE_ABC: return new ModeABC(settings, inputType, language); case MODE_PASSTHROUGH: diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java index 937ac117..3a6fb591 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -4,7 +4,7 @@ import androidx.annotation.NonNull; import java.util.ArrayList; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.hacks.InputType; import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace; @@ -46,13 +46,13 @@ public class ModePredictive extends InputMode { private boolean isCursorDirectionForward = false; - ModePredictive(SettingsStore settings, InputType inputType, Language lang) { + ModePredictive(SettingsStore settings, InputType inputType, TextField textField, Language lang) { changeLanguage(lang); defaultTextCase(); autoSpace = new AutoSpace(settings); autoTextCase = new AutoTextCase(settings); - predictions = new Predictions(); + predictions = new Predictions(settings, textField); this.settings = settings; @@ -365,6 +365,7 @@ public class ModePredictive extends InputMode { return; } + // increment the frequency of the given word try { Language workingLanguage = TextTools.isGraphic(currentWord) ? new EmojiLanguage() : language; @@ -373,14 +374,13 @@ public class ModePredictive extends InputMode { // punctuation and special chars are not in the database, so there is no point in // running queries that would update nothing if (!sequence.equals(NaturalLanguage.PUNCTUATION_KEY) && !sequence.startsWith(NaturalLanguage.SPECIAL_CHARS_KEY)) { - WordStoreAsync.makeTopWord(workingLanguage, currentWord, sequence); + predictions.onAccept(currentWord, sequence); } } catch (Exception e) { Logger.e(LOG_TAG, "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage()); } } - @Override protected String adjustSuggestionTextCase(String word, int newTextCase) { return autoTextCase.adjustSuggestionTextCase(new Text(language, word), newTextCase); diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java index d582cb3b..bbe22d5b 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java @@ -2,19 +2,24 @@ package io.github.sspanak.tt9.ime.modes.helpers; import java.util.ArrayList; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; +import io.github.sspanak.tt9.ime.helpers.TextField; import io.github.sspanak.tt9.languages.EmojiLanguage; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.util.Characters; public class Predictions { + private final SettingsStore settings; + private final TextField textField; - private Language language; private String digitSequence; - private boolean isStemFuzzy; - private String stem; private String inputWord; + private boolean isStemFuzzy; + private Language language; + private String stem; + + private String lastEnforcedTopWord = ""; // async operations private Runnable onWordsChanged = () -> {}; @@ -24,6 +29,11 @@ public class Predictions { private boolean containsGeneratedWords = false; private ArrayList words = new ArrayList<>(); + public Predictions(SettingsStore settings, TextField textField) { + this.settings = settings; + this.textField = textField; + } + public Predictions setLanguage(Language language) { this.language = language; @@ -68,31 +78,6 @@ public class Predictions { } - /** - * suggestStem - * Add the current stem filter to the predictions list, when it has length of X and - * the user has pressed X keys (otherwise, it makes no sense to add it). - */ - private void suggestStem() { - if (!stem.isEmpty() && stem.length() == digitSequence.length()) { - words.add(stem); - } - } - - - /** - * suggestMissingWords - * Takes a list of words and appends them to the words list, if they are missing. - */ - private void suggestMissingWords(ArrayList newWords) { - for (String newWord : newWords) { - if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) { - words.add(newWord); - } - } - } - - /** * load * Queries the dictionary database for a list of words matching the current language and @@ -109,7 +94,7 @@ public class Predictions { boolean retryAllowed = !digitSequence.equals(EmojiLanguage.CUSTOM_EMOJI_SEQUENCE); - WordStoreAsync.getWords( + DataStore.getWords( (dbWords) -> onDbWords(dbWords, retryAllowed), language, digitSequence, @@ -121,7 +106,7 @@ public class Predictions { } private void loadWithoutLeadingPunctuation() { - WordStoreAsync.getWords( + DataStore.getWords( (dbWords) -> { char firstChar = inputWord.charAt(0); for (int i = 0; i < dbWords.size(); i++) { @@ -160,6 +145,7 @@ public class Predictions { words.addAll(dbWords); } else { suggestStem(); + dbWords = rearrangeByPairFrequency(dbWords); suggestMissingWords(generatePossibleStemVariations(dbWords)); suggestMissingWords(dbWords.isEmpty() ? generateWordVariations(inputWord) : dbWords); words = insertPunctuationCompletions(words); @@ -169,6 +155,31 @@ public class Predictions { } + /** + * suggestStem + * Add the current stem filter to the predictions list, when it has length of X and + * the user has pressed X keys (otherwise, it makes no sense to add it). + */ + private void suggestStem() { + if (!stem.isEmpty() && stem.length() == digitSequence.length()) { + words.add(stem); + } + } + + + /** + * suggestMissingWords + * Takes a list of words and appends them to the words list, if they are missing. + */ + private void suggestMissingWords(ArrayList newWords) { + for (String newWord : newWords) { + if (!words.contains(newWord) && !words.contains(newWord.toLowerCase(language.getLocale()))) { + words.add(newWord); + } + } + } + + /** * generateWordVariations * When there are no matching suggestions after the last key press, generate a list of possible @@ -288,4 +299,69 @@ public class Predictions { containsGeneratedWords = !variations.isEmpty(); return variations; } + + + /** + * onAccept + * This stores common word pairs, so they can be used in "rearrangeByPairFrequency()" method. + * For example, if the user types "I am an apple", the word "am" will be suggested after "I", + * and "an" after "am", even if "am" frequency was boosted right before typing "an". This both + * prevents from suggesting the same word twice in row and makes the suggestions more intuitive + * when there are many textonyms for a single sequence. + */ + public void onAccept(String word, String sequence) { + if ( + !settings.getPredictWordPairs() + // If the accepted word is longer than the sequence, it is some different word, not a textonym + // of the fist suggestion. We don't need to store it. + || word == null || digitSequence == null + || word.length() != digitSequence.length() + // If the word is the first suggestion, we have already guessed it right, and it makes no + // sense to store it as a popular pair. + || (!words.isEmpty() && words.get(0).equals(word)) + ) { + return; + } + + DataStore.addWordPair(language, textField.getWordBeforeCursor(language, 1, true), word, sequence); + if (!word.equals(lastEnforcedTopWord)) { + DataStore.makeTopWord(language, word, sequence); + } + } + + + /** + * rearrangeByPairFrequency + * Uses the last two words in the text field to rearrange the suggestions, so that the most popular + * one in a pair comes first. This is useful for typing phrases, like "I am an apple". Since, in + * "onAccept()", we have remembered the "am" comes after "I" and "an" comes after "am", we will + * not suggest the textonyms "am" or "an" twice (depending on which has the highest frequency). + */ + private ArrayList rearrangeByPairFrequency(ArrayList words) { + lastEnforcedTopWord = ""; + + if (!settings.getPredictWordPairs() || words.size() < 2) { + return words; + } + + ArrayList rearrangedWords = new ArrayList<>(); + String penultimateWord = textField.getWordBeforeCursor(language, 1, true); + + String word = DataStore.getWord2(language, penultimateWord, digitSequence); + int morePopularIndex = word == null ? -1 : words.indexOf(word); + if (morePopularIndex == -1) { + return words; + } + + lastEnforcedTopWord = word; + rearrangedWords.add(word); + + for (int i = 0; i < words.size(); i++) { + if (i != morePopularIndex) { + rearrangedWords.add(words.get(i)); + } + } + + return rearrangedWords; + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java b/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java index f1d0f8e2..526af702 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java @@ -14,8 +14,8 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.LegacyDb; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; +import io.github.sspanak.tt9.db.words.LegacyDb; import io.github.sspanak.tt9.ime.helpers.InputModeValidator; import io.github.sspanak.tt9.preferences.helpers.Hotkeys; import io.github.sspanak.tt9.preferences.screens.BaseScreenFragment; @@ -43,7 +43,7 @@ public class PreferencesActivity extends ActivityWithNavigation implements Prefe Logger.setLevel(settings.getLogLevel()); try (LegacyDb db = new LegacyDb(this)) { db.clear(); } - WordStoreAsync.init(this); + DataStore.init(this); InputModeValidator.validateEnabledLanguages(this, settings.getEnabledLanguageIds()); validateFunctionKeys(); diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/UsageStatsScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/UsageStatsScreen.java index 7990d5e5..f7cfe00a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/UsageStatsScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/UsageStatsScreen.java @@ -3,14 +3,21 @@ package io.github.sspanak.tt9.preferences.screens; import androidx.preference.Preference; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.SlowQueryStats; +import io.github.sspanak.tt9.db.DataStore; +import io.github.sspanak.tt9.db.words.SlowQueryStats; +import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemText; +import io.github.sspanak.tt9.ui.UI; public class UsageStatsScreen extends BaseScreenFragment { final public static String NAME = "UsageStats"; - final private static String RESET_BUTTON = "pref_slow_queries_reset_stats"; - final private static String SUMMARY_CONTAINER = "summary_container"; + final private static String RESET_SLOW_QUERIES_BUTTON = "slow_queries_clear_cache"; + final private static String RESET_WORD_PAIRS_CACHE_BUTTON = "word_pair_clear_cache"; + final private static String RESET_WORD_PAIRS_DB_BUTTON = "word_pair_clear_db"; + + final private static String SLOW_QUERY_STATS_CONTAINER = "summary_container"; + final private static String WORD_PAIRS_CONTAINER = "word_pairs_container"; private ItemText queryListContainer; public UsageStatsScreen() { init(); } @@ -22,26 +29,32 @@ public class UsageStatsScreen extends BaseScreenFragment { @Override protected void onCreate() { - printSummary(); + print(SLOW_QUERY_STATS_CONTAINER, SlowQueryStats.getSummary()); + print(WORD_PAIRS_CONTAINER, DataStore.getWordPairStats()); printSlowQueries(); - Preference resetButton = findPreference(RESET_BUTTON); - if (resetButton != null) { - resetButton.setOnPreferenceClickListener((Preference p) -> { - SlowQueryStats.clear(); - printSummary(); - printSlowQueries(); - return true; - }); + Preference slowQueriesButton = findPreference(RESET_SLOW_QUERIES_BUTTON); + if (slowQueriesButton != null) { + slowQueriesButton.setOnPreferenceClickListener(this::resetSlowQueries); + } + + Preference wordPairsCacheButton = findPreference(RESET_WORD_PAIRS_CACHE_BUTTON); + if (wordPairsCacheButton != null) { + wordPairsCacheButton.setOnPreferenceClickListener(this::resetWordPairsCache); + } + + Preference wordPairsDbButton = findPreference(RESET_WORD_PAIRS_DB_BUTTON); + if (wordPairsDbButton != null) { + wordPairsDbButton.setOnPreferenceClickListener(this::deleteWordPairs); } resetFontSize(false); } - private void printSummary() { - Preference logsContainer = findPreference(SUMMARY_CONTAINER); - if (logsContainer != null) { - logsContainer.setSummary(SlowQueryStats.getSummary()); + private void print(String containerName, String text) { + Preference container = findPreference(containerName); + if (container != null) { + container.setSummary(text); } } @@ -54,4 +67,26 @@ public class UsageStatsScreen extends BaseScreenFragment { String slowQueries = SlowQueryStats.getList(); queryListContainer.populate(slowQueries.isEmpty() ? "No slow queries." : slowQueries); } + + private boolean resetSlowQueries(Preference ignored) { + SlowQueryStats.clear(); + print(SLOW_QUERY_STATS_CONTAINER, SlowQueryStats.getSummary()); + printSlowQueries(); + return true; + } + + private boolean resetWordPairsCache(Preference ignored) { + DataStore.clearWordPairCache(); + print(WORD_PAIRS_CONTAINER, DataStore.getWordPairStats()); + return true; + } + + private boolean deleteWordPairs(Preference ignored) { + DataStore.deleteWordPairs( + LanguageCollection.getAll(activity), + () -> UI.toastLongFromAsync(activity, "Word pairs deleted. You must reopen the screen manually.") + ); + return true; + } + } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceDeletableWord.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceDeletableWord.java index fea6bfab..0490dab5 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceDeletableWord.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceDeletableWord.java @@ -9,7 +9,7 @@ import androidx.annotation.Nullable; import androidx.preference.PreferenceCategory; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.custom.ScreenPreference; import io.github.sspanak.tt9.preferences.settings.SettingsStore; @@ -62,7 +62,7 @@ public class PreferenceDeletableWord extends ScreenPreference { private void onDeletionConfirmed() { SettingsStore settings = new SettingsStore(getContext()); - WordStoreAsync.deleteCustomWord( + DataStore.deleteCustomWord( this::onWordDeleted, LanguageCollection.getLanguage(getContext(), settings.getInputLanguage()), word diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java index d35114b3..f9a3ea83 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java @@ -8,7 +8,7 @@ import androidx.annotation.Nullable; import java.util.ArrayList; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.preferences.items.ItemTextInput; import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.util.ConsumerCompat; @@ -49,11 +49,11 @@ public class PreferenceSearchWords extends ItemTextInput { if (onWords == null) { Logger.w(LOG_TAG, "No handler set for the word change event."); } else if (lastSearchTerm.isEmpty()) { - WordStoreAsync.countCustomWords(onTotalWords); - WordStoreAsync.getCustomWords(onWords, lastSearchTerm, SettingsStore.CUSTOM_WORDS_SEARCH_RESULTS_MAX); + DataStore.countCustomWords(onTotalWords); + DataStore.getCustomWords(onWords, lastSearchTerm, SettingsStore.CUSTOM_WORDS_SEARCH_RESULTS_MAX); } else { - WordStoreAsync.countCustomWords(onTotalWords); - WordStoreAsync.getCustomWords(onWords, lastSearchTerm, -1); + DataStore.countCustomWords(onTotalWords); + DataStore.getCustomWords(onWords, lastSearchTerm, -1); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/LanguageSelectionScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/LanguageSelectionScreen.java index 6c931519..c795922a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/LanguageSelectionScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/LanguageSelectionScreen.java @@ -6,8 +6,8 @@ import androidx.preference.PreferenceCategory; import java.util.ArrayList; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryLoader; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.languages.NaturalLanguage; @@ -49,7 +49,7 @@ public class LanguageSelectionScreen extends BaseScreenFragment { addLanguagesToCategory(languagesCategory, allLanguages); if (!DictionaryLoader.getInstance(activity).isRunning()) { - WordStoreAsync.exists(this::addLoadedStatus, allLanguages); + DataStore.exists(this::addLoadedStatus, allLanguages); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java index ef59d892..6e6e8a07 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java @@ -7,7 +7,7 @@ import androidx.preference.Preference; import java.util.ArrayList; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryLoader; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateAll.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateAll.java index 076f59d8..b1ba964a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateAll.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateAll.java @@ -5,7 +5,7 @@ import androidx.preference.Preference; import java.util.ArrayList; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; @@ -36,7 +36,7 @@ class ItemTruncateAll extends ItemClickable { for (Language lang : LanguageCollection.getAll(activity, false)) { languageIds.add(lang.getId()); } - WordStoreAsync.deleteWords(this::onFinishDeleting, languageIds); + DataStore.deleteWords(this::onFinishDeleting, languageIds); return true; } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateUnselected.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateUnselected.java index 65804200..586a992f 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateUnselected.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemTruncateUnselected.java @@ -4,7 +4,7 @@ import androidx.preference.Preference; import java.util.ArrayList; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.PreferencesActivity; @@ -30,7 +30,7 @@ class ItemTruncateUnselected extends ItemTruncateAll { } onStartDeleting(); - WordStoreAsync.deleteWords(this::onFinishDeleting, unselectedLanguageIds); + DataStore.deleteWords(this::onFinishDeleting, unselectedLanguageIds); return true; } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java index b37b7ef9..3073b925 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/LanguagesScreen.java @@ -8,10 +8,10 @@ import androidx.activity.result.contract.ActivityResultContracts; import java.util.ArrayList; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryLoader; import io.github.sspanak.tt9.db.customWords.CustomWordsExporter; import io.github.sspanak.tt9.db.customWords.CustomWordsImporter; import io.github.sspanak.tt9.db.customWords.DictionaryExporter; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.preferences.PreferencesActivity; import io.github.sspanak.tt9.preferences.items.ItemClickable; import io.github.sspanak.tt9.preferences.screens.BaseScreenFragment; 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 6fcddc12..abfc53c3 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 @@ -32,9 +32,11 @@ public class SettingsStore extends SettingsUI { public final static int SUGGESTIONS_SELECT_ANIMATION_DURATION = 66; public final static int SUGGESTIONS_TRANSLATE_ANIMATION_DURATION = 0; public final static int TEXT_INPUT_DEBOUNCE_TIME = 500; // ms + public final static int WORD_BACKGROUND_TASKS_DELAY = 15000; // ms public final static int WORD_FREQUENCY_MAX = 25500; public final static int WORD_FREQUENCY_NORMALIZATION_DIVIDER = 100; // normalized frequency = WORD_FREQUENCY_MAX / WORD_FREQUENCY_NORMALIZATION_DIVIDER - public final static int WORD_NORMALIZATION_DELAY = 15000; // ms + public final static int WORD_PAIR_MAX = 1000; + public final static int WORD_PAIR_MAX_WORD_LENGTH = 6; public final static int ZOMBIE_CHECK_INTERVAL = 666; // ms /************* hacks *************/ diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsTyping.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsTyping.java index 5d490b9f..f65347f1 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsTyping.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsTyping.java @@ -29,5 +29,10 @@ class SettingsTyping extends SettingsInput { // SharedPreferences return a corrupted string when using the real "\n"... :( return character.equals("\\n") ? "\n" : character; } + + public boolean getPredictWordPairs() { + return prefs.getBoolean("pref_predict_word_pairs", true); + } + public boolean getUpsideDownKeys() { return prefs.getBoolean("pref_upside_down_keys", false); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java index 4e14a48f..9e963087 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java @@ -7,7 +7,7 @@ import android.inputmethodservice.InputMethodService; import androidx.annotation.NonNull; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.db.DataStore; import io.github.sspanak.tt9.db.entities.AddWordResult; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; @@ -47,7 +47,7 @@ public class AddWordDialog extends PopupDialog { private void onOK() { if (language != null) { - WordStoreAsync.put(this::onAddingFinished, language, word); + DataStore.put(this::onAddingFinished, language, word); } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AutoUpdateMonologue.java b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AutoUpdateMonologue.java index afe35b31..8cba08e8 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AutoUpdateMonologue.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AutoUpdateMonologue.java @@ -5,7 +5,7 @@ import android.content.Intent; import androidx.annotation.NonNull; -import io.github.sspanak.tt9.db.DictionaryLoader; +import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.util.ConsumerCompat; diff --git a/app/src/main/java/io/github/sspanak/tt9/util/Text.java b/app/src/main/java/io/github/sspanak/tt9/util/Text.java index ed0965a1..5ba06e56 100644 --- a/app/src/main/java/io/github/sspanak/tt9/util/Text.java +++ b/app/src/main/java/io/github/sspanak/tt9/util/Text.java @@ -49,6 +49,16 @@ public class Text extends TextTools { } } + public boolean isAlphabetic() { + for (int i = 0, end = text == null ? 0 : text.length(); i < end; i++) { + if (!Character.isAlphabetic(text.charAt(i))) { + return false; + } + } + + return true; + } + public boolean isEmpty() { return text == null || text.isEmpty(); } diff --git a/app/src/main/res/xml/prefs_screen_usage_stats.xml b/app/src/main/res/xml/prefs_screen_usage_stats.xml index 30185f42..0952a4d1 100644 --- a/app/src/main/res/xml/prefs_screen_usage_stats.xml +++ b/app/src/main/res/xml/prefs_screen_usage_stats.xml @@ -2,21 +2,32 @@ + app:key="slow_queries_clear_cache" + app:title="Clear Query Cache" /> - + + + + + + + + + + - +