1
0
Fork 0

Word pair predictions (#616)

This commit is contained in:
Dimo Karaivanov 2024-09-06 14:38:26 +03:00 committed by GitHub
parent 1300f8b517
commit 10497af44d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 667 additions and 131 deletions

View file

@ -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<String> 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<Integer> languageIds) {
new Thread(() -> {
store.remove(languageIds);
words.remove(languageIds);
notification.run();
}).start();
}
public static void put(ConsumerCompat<AddWordResult> 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<ArrayList<String>> 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<ArrayList<String>> 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<Long> dataHandler) {
new Thread(() -> asyncHandler.post(() -> dataHandler.accept(
store.countCustom()))
words.countCustom()))
).start();
}
public static void exists(ConsumerCompat<ArrayList<Integer>> dataHandler, ArrayList<Language> 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<Language> languages) {
new Thread(() -> pairs.load(dictionaryLoader, languages)).start();
}
public static void clearWordPairCache() {
pairs.clearCache();
}
public static void deleteWordPairs(@NonNull ArrayList<Language> languages, @NonNull Runnable onDeleted) {
new Thread(() -> {
pairs.delete(languages);
onDeleted.run();
}).start();
}
public static String getWordPairStats() {
return pairs.toString();
}
}

View file

@ -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);
}
}

View file

@ -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<WordPair> pairs) {
if (langId <= 0 || pairs == null || pairs.isEmpty()) {
return;
}
for (WordPair pair : pairs) {
db.insert(Tables.getWordPairs(langId), null, pair.toContentValues());
}
}
}

View file

@ -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<WordPair> getWordPairs(@NonNull SQLiteDatabase db, @NonNull Language language, int limit) {
ArrayList<WordPair> 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;
}
}

View file

@ -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)) {

View file

@ -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<Language> 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, " +

View file

@ -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 + ")";
}
}

View file

@ -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<Integer, HashMap<WordPair, WordPair>> 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<WordPair, WordPair> 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<WordPair, WordPair> 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<Integer, HashMap<WordPair, WordPair>> entry : pairs.entrySet()) {
int langId = entry.getKey();
HashMap<WordPair, WordPair> 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<Language> 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<WordPair, WordPair> 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<WordPair> 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<Language> 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<Integer, HashMap<WordPair, WordPair>> entry : pairs.entrySet()) {
int langId = entry.getKey();
HashMap<WordPair, WordPair> 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();
}
}

View file

@ -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());

View file

@ -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;

View file

@ -1,4 +1,4 @@
package io.github.sspanak.tt9.db;
package io.github.sspanak.tt9.db.words;
import java.util.HashMap;

View file

@ -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();

View file

@ -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());

View file

@ -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;

View file

@ -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();
}

View file

@ -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<Integer> 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<Integer> 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());
}

View file

@ -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);
}

View file

@ -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:

View file

@ -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);

View file

@ -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<String> 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<String> 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<String> 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<String> rearrangeByPairFrequency(ArrayList<String> words) {
lastEnforcedTopWord = "";
if (!settings.getPredictWordPairs() || words.size() < 2) {
return words;
}
ArrayList<String> 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;
}
}

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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 *************/

View file

@ -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); }
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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();
}

View file

@ -2,21 +2,32 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto" app:orderingFromXml="true">
<Preference
app:key="pref_slow_queries_reset_stats"
app:title="Clear DB Cache" />
app:key="slow_queries_clear_cache"
app:title="Clear Query Cache" />
<PreferenceCategory
app:title="Summary"
app:singleLineTitle="true">
<Preference
app:key="word_pair_clear_cache"
app:title="Clear Word Pair Memory Cache" />
<Preference
app:key="word_pair_clear_db"
app:title="Clear Word Pair DB" />
<PreferenceCategory app:title="Word Pair Stats" app:singleLineTitle="true">
<io.github.sspanak.tt9.preferences.custom.PreferencePlainText
app:key="word_pairs_container"
app:summary="--">
</io.github.sspanak.tt9.preferences.custom.PreferencePlainText>
</PreferenceCategory>
<PreferenceCategory app:title="DB Query Stats" app:singleLineTitle="true">
<io.github.sspanak.tt9.preferences.custom.PreferencePlainText
app:key="summary_container"
app:summary="--">
</io.github.sspanak.tt9.preferences.custom.PreferencePlainText>
</PreferenceCategory>
<PreferenceCategory
app:title="Slow Queries"
app:singleLineTitle="true">
<PreferenceCategory app:title="Slow Queries" app:singleLineTitle="true">
<io.github.sspanak.tt9.preferences.custom.PreferencePlainText
app:key="query_list_container"
app:summary="--">