Speed Optimizations (#250)
* emoji micro optimizations * Predictive Mode: reduced post-load processing time from 20 ms to <1 ms * EmptyDatabaseWarning does not run unnecessary queries, when the time for the next warning has not come yet * replaced unnecessary Handler with lambdas in EmptyDatabaseWarning and Add Word dialog * slightly simplified the word searching, importing and clearing code * word length is now stored in the database, instead of being calculated on-the-fly, while searching * created sepearate indexes and queries for 2-letter and longer words; suggestion loading time reduced from 50-60 ms, peaking to: 200 ms, down to: <20 ms with peaks to: 50-60 ms. * imporved dictionary loading speed by temporarily disabling the database indexes
This commit is contained in:
parent
4d67c02340
commit
c3787138fa
21 changed files with 507 additions and 316 deletions
10
src/io/github/sspanak/tt9/ConsumerCompat.java
Normal file
10
src/io/github/sspanak/tt9/ConsumerCompat.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package io.github.sspanak.tt9;
|
||||
|
||||
/**
|
||||
* ConsumerCompat
|
||||
* A fallback interface for Consumer in API < 24
|
||||
*/
|
||||
public interface ConsumerCompat<T>{
|
||||
void accept(T t);
|
||||
default ConsumerCompat<T> andThen(ConsumerCompat<? super T> after) {return null;}
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@ import android.util.Log;
|
|||
public class Logger {
|
||||
public static final int LEVEL = BuildConfig.DEBUG ? Log.DEBUG : Log.ERROR;
|
||||
|
||||
static public boolean isDebugLevel() {
|
||||
return LEVEL == Log.DEBUG;
|
||||
}
|
||||
|
||||
static public void v(String tag, String msg) {
|
||||
if (LEVEL <= Log.VERBOSE) {
|
||||
Log.v(tag, msg);
|
||||
|
|
|
|||
|
|
@ -2,17 +2,20 @@ package io.github.sspanak.tt9.db;
|
|||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.github.sspanak.tt9.ConsumerCompat;
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException;
|
||||
import io.github.sspanak.tt9.db.room.TT9Room;
|
||||
import io.github.sspanak.tt9.db.room.Word;
|
||||
import io.github.sspanak.tt9.db.room.WordList;
|
||||
import io.github.sspanak.tt9.db.room.WordsDao;
|
||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||
import io.github.sspanak.tt9.languages.InvalidLanguageException;
|
||||
import io.github.sspanak.tt9.languages.Language;
|
||||
|
|
@ -21,6 +24,7 @@ import io.github.sspanak.tt9.preferences.SettingsStore;
|
|||
public class DictionaryDb {
|
||||
private static TT9Room dbInstance;
|
||||
|
||||
private static final Handler asyncHandler = new Handler();
|
||||
|
||||
public static synchronized void init(Context context) {
|
||||
if (dbInstance == null) {
|
||||
|
|
@ -41,13 +45,51 @@ public class DictionaryDb {
|
|||
}
|
||||
|
||||
|
||||
private static void printDebug(String tag, String title, WordList words, long startTime) {
|
||||
if (!Logger.isDebugLevel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder debugText = new StringBuilder(title);
|
||||
debugText
|
||||
.append("\n")
|
||||
.append("Word Count: ").append(words.size())
|
||||
.append(". Time: ").append(System.currentTimeMillis() - startTime).append(" ms");
|
||||
if (words.size() > 0) {
|
||||
debugText.append("\n").append(words);
|
||||
}
|
||||
|
||||
Logger.d(tag, debugText.toString());
|
||||
}
|
||||
|
||||
|
||||
public static void runInTransaction(Runnable r) {
|
||||
getInstance().runInTransaction(r);
|
||||
}
|
||||
|
||||
|
||||
public static void createShortWordIndexSync() {
|
||||
getInstance().wordsDao().rawQuery(TT9Room.createShortWordsIndexQuery());
|
||||
}
|
||||
|
||||
public static void createLongWordIndexSync() {
|
||||
getInstance().wordsDao().rawQuery(TT9Room.createLongWordsIndexQuery());
|
||||
}
|
||||
|
||||
public static void dropShortWordIndexSync() {
|
||||
getInstance().wordsDao().rawQuery(TT9Room.dropShortWordsIndexQuery());
|
||||
}
|
||||
|
||||
public static void dropLongWordIndexSync() {
|
||||
getInstance().wordsDao().rawQuery(TT9Room.dropLongWordsIndexQuery());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* normalizeWordFrequencies
|
||||
* Normalizes the word frequencies for all languages that have reached the maximum, as defined in
|
||||
* the settings.
|
||||
*
|
||||
* This query will finish immediately, if there is nothing to do. It's safe to run it often.
|
||||
*
|
||||
*/
|
||||
public static void normalizeWordFrequencies(SettingsStore settings) {
|
||||
new Thread() {
|
||||
|
|
@ -55,7 +97,7 @@ public class DictionaryDb {
|
|||
public void run() {
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
int affectedRows = dbInstance.wordsDao().normalizeFrequencies(
|
||||
int affectedRows = getInstance().wordsDao().normalizeFrequencies(
|
||||
settings.getWordFrequencyNormalizationDivider(),
|
||||
settings.getWordFrequencyMax()
|
||||
);
|
||||
|
|
@ -69,17 +111,12 @@ public class DictionaryDb {
|
|||
}
|
||||
|
||||
|
||||
public static void runInTransaction(Runnable r) {
|
||||
getInstance().runInTransaction(r);
|
||||
}
|
||||
|
||||
|
||||
public static void areThereWords(Handler handler, Language language) {
|
||||
public static void areThereWords(ConsumerCompat<Boolean> notification, Language language) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
int langId = language != null ? language.getId() : -1;
|
||||
handler.sendEmptyMessage(getInstance().wordsDao().count(langId) > 0 ? 1 : 0);
|
||||
notification.accept(getInstance().wordsDao().count(langId) > 0);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
|
@ -94,12 +131,12 @@ public class DictionaryDb {
|
|||
}
|
||||
|
||||
|
||||
public static void deleteWords(Handler handler) {
|
||||
deleteWords(handler, null);
|
||||
public static void deleteWords(Runnable notification) {
|
||||
deleteWords(notification, null);
|
||||
}
|
||||
|
||||
|
||||
public static void deleteWords(Handler handler, ArrayList<Integer> languageIds) {
|
||||
public static void deleteWords(Runnable notification, ArrayList<Integer> languageIds) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
|
@ -108,13 +145,13 @@ public class DictionaryDb {
|
|||
} else if (languageIds.size() > 0) {
|
||||
getInstance().wordsDao().deleteByLanguage(languageIds);
|
||||
}
|
||||
handler.sendEmptyMessage(0);
|
||||
notification.run();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
|
||||
public static void insertWord(Handler handler, Language language, String word) throws Exception {
|
||||
public static void insertWord(ConsumerCompat<Integer> statusHandler, Language language, String word) throws Exception {
|
||||
if (language == null) {
|
||||
throw new InvalidLanguageException();
|
||||
}
|
||||
|
|
@ -127,6 +164,7 @@ public class DictionaryDb {
|
|||
dbWord.langId = language.getId();
|
||||
dbWord.sequence = language.getDigitSequenceForWord(word);
|
||||
dbWord.word = word.toLowerCase(language.getLocale());
|
||||
dbWord.length = word.length();
|
||||
dbWord.frequency = 1;
|
||||
|
||||
new Thread() {
|
||||
|
|
@ -135,15 +173,16 @@ public class DictionaryDb {
|
|||
try {
|
||||
getInstance().wordsDao().insert(dbWord);
|
||||
getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence);
|
||||
handler.sendEmptyMessage(0);
|
||||
statusHandler.accept(0);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
String msg = "Constraint violation when inserting a word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId;
|
||||
String msg = "Constraint violation when inserting a word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId
|
||||
+ ". " + e.getMessage();
|
||||
Logger.e("tt9/insertWord", msg);
|
||||
handler.sendEmptyMessage(1);
|
||||
statusHandler.accept(1);
|
||||
} catch (Exception e) {
|
||||
String msg = "Failed inserting word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId;
|
||||
String msg = "Failed inserting word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId + ". " + e.getMessage();
|
||||
Logger.e("tt9/insertWord", msg);
|
||||
handler.sendEmptyMessage(2);
|
||||
statusHandler.accept(2);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
|
@ -211,101 +250,97 @@ public class DictionaryDb {
|
|||
}
|
||||
|
||||
|
||||
private static ArrayList<String> getSuggestionsExact(Language language, String sequence, String word, int maximumWords) {
|
||||
/**
|
||||
* loadWordsExact
|
||||
* Loads words that match exactly the "sequence" and the optional "filter".
|
||||
* For example: "7655" gets "roll".
|
||||
*/
|
||||
private static ArrayList<String> loadWordsExact(Language language, String sequence, String filter, int maximumWords) {
|
||||
long start = System.currentTimeMillis();
|
||||
List<Word> exactMatches = getInstance().wordsDao().getMany(
|
||||
WordList matches = new WordList(getInstance().wordsDao().getMany(
|
||||
language.getId(),
|
||||
maximumWords,
|
||||
sequence,
|
||||
word == null || word.equals("") ? null : word
|
||||
);
|
||||
Logger.d(
|
||||
"db.getSuggestionsExact",
|
||||
"Exact matches: " + exactMatches.size() + ". Time: " + (System.currentTimeMillis() - start) + " ms"
|
||||
);
|
||||
filter == null || filter.equals("") ? null : filter
|
||||
));
|
||||
|
||||
ArrayList<String> suggestions = new ArrayList<>();
|
||||
for (Word w : exactMatches) {
|
||||
Logger.d("db.getSuggestions", "exact match: " + w.word + " | priority: " + w.frequency);
|
||||
suggestions.add(w.word);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
printDebug("loadWordsExact", "===== Exact Word Matches =====", matches, start);
|
||||
return matches.toStringList();
|
||||
}
|
||||
|
||||
|
||||
private static ArrayList<String> getSuggestionsFuzzy(Language language, String sequence, String word, int maximumWords) {
|
||||
/**
|
||||
* loadWordsFuzzy
|
||||
* Loads words that start with "sequence" and optionally match the "filter".
|
||||
* For example: "7655" -> "roll", but also: "rolled", "roller", "rolling", ...
|
||||
*/
|
||||
private static ArrayList<String> loadWordsFuzzy(Language language, String sequence, String filter, int maximumWords) {
|
||||
long start = System.currentTimeMillis();
|
||||
List<Word> extraWords = getInstance().wordsDao().getFuzzy(
|
||||
language.getId(),
|
||||
maximumWords,
|
||||
sequence,
|
||||
word == null || word.equals("") ? null : word
|
||||
);
|
||||
Logger.d(
|
||||
"db.getSuggestionsFuzzy",
|
||||
"Fuzzy matches: " + extraWords.size() + ". Time: " + (System.currentTimeMillis() - start) + " ms"
|
||||
);
|
||||
|
||||
ArrayList<String> suggestions = new ArrayList<>();
|
||||
for (Word w : extraWords) {
|
||||
Logger.d(
|
||||
"db.getSuggestions",
|
||||
"fuzzy match: " + w.word + " | sequence: " + w.sequence + " | priority: " + w.frequency
|
||||
// fuzzy queries are heavy, so we must restrict the search range as much as possible
|
||||
boolean noFilter = (filter == null || filter.equals(""));
|
||||
int maxWordLength = noFilter && sequence.length() <= 2 ? 5 : 1000;
|
||||
String index = sequence.length() <= 2 ? WordsDao.indexShortWords : WordsDao.indexLongWords;
|
||||
SimpleSQLiteQuery sql = TT9Room.getFuzzyQuery(index, language.getId(), maximumWords, sequence, sequence.length(), maxWordLength, filter);
|
||||
|
||||
WordList matches = new WordList(getInstance().wordsDao().getCustom(sql));
|
||||
|
||||
// In some cases, searching for words starting with "digitSequence" and limited to "maxWordLength" of 5,
|
||||
// may yield too few results. If so, we expand the search range a bit.
|
||||
if (noFilter && matches.size() < maximumWords) {
|
||||
sql = TT9Room.getFuzzyQuery(
|
||||
WordsDao.indexLongWords,
|
||||
language.getId(),
|
||||
maximumWords - matches.size(),
|
||||
sequence,
|
||||
5,
|
||||
1000
|
||||
);
|
||||
suggestions.add(w.word);
|
||||
matches.addAll(getInstance().wordsDao().getCustom(sql));
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
printDebug("loadWordsFuzzy", "~=~=~=~ Fuzzy Word Matches ~=~=~=~", matches, start);
|
||||
return matches.toStringList();
|
||||
}
|
||||
|
||||
|
||||
private static void sendSuggestions(Handler handler, ArrayList<String> data) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putStringArrayList("suggestions", data);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
handler.sendMessage(msg);
|
||||
private static void sendWords(ConsumerCompat<ArrayList<String>> dataHandler, ArrayList<String> wordList) {
|
||||
asyncHandler.post(() -> dataHandler.accept(wordList));
|
||||
}
|
||||
|
||||
|
||||
public static void getSuggestions(Handler handler, Language language, String sequence, String word, int minimumWords, int maximumWords) {
|
||||
public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minimumWords, int maximumWords) {
|
||||
final int minWords = Math.max(minimumWords, 0);
|
||||
final int maxWords = Math.max(maximumWords, minimumWords);
|
||||
|
||||
ArrayList<String> wordList = new ArrayList<>(maxWords);
|
||||
|
||||
if (sequence == null || sequence.length() == 0) {
|
||||
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for an empty sequence.");
|
||||
sendSuggestions(handler, new ArrayList<>());
|
||||
Logger.w("tt9/db.getWords", "Attempting to get words for an empty sequence.");
|
||||
sendWords(dataHandler, wordList);
|
||||
return;
|
||||
}
|
||||
|
||||
if (language == null) {
|
||||
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for NULL language.");
|
||||
sendSuggestions(handler, new ArrayList<>());
|
||||
Logger.w("tt9/db.getWords", "Attempting to get words for NULL language.");
|
||||
sendWords(dataHandler, wordList);
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// get exact sequence matches, for example: "9422" -> "what"
|
||||
ArrayList<String> suggestions = getSuggestionsExact(language, sequence, word, maxWords);
|
||||
|
||||
|
||||
// if the exact matches are too few, add some more words that start with the same characters,
|
||||
// for example: "rol" -> "roll", "roller", "rolling", ...
|
||||
if (suggestions.size() < minWords && sequence.length() >= 2) {
|
||||
suggestions.addAll(
|
||||
getSuggestionsFuzzy(language, sequence, word, minWords - suggestions.size())
|
||||
);
|
||||
if (sequence.length() == 1) {
|
||||
wordList.addAll(loadWordsExact(language, sequence, filter, maxWords));
|
||||
} else {
|
||||
wordList.addAll(loadWordsFuzzy(language, sequence, filter, minWords));
|
||||
}
|
||||
|
||||
if (suggestions.size() == 0) {
|
||||
Logger.i("db.getSuggestions", "No suggestions for sequence: " + sequence);
|
||||
if (wordList.size() == 0) {
|
||||
Logger.i("db.getWords", "No suggestions for sequence: " + sequence);
|
||||
}
|
||||
|
||||
// pack the words in a message and send it to the calling thread
|
||||
sendSuggestions(handler, suggestions);
|
||||
sendWords(dataHandler, wordList);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import android.content.Context;
|
|||
import android.content.res.AssetManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
|
@ -12,6 +11,7 @@ import java.io.LineNumberReader;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.ConsumerCompat;
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException;
|
||||
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAlreadyRunningException;
|
||||
|
|
@ -28,7 +28,8 @@ public class DictionaryLoader {
|
|||
private final AssetManager assets;
|
||||
private final SettingsStore settings;
|
||||
|
||||
private Handler statusHandler = null;
|
||||
private static final Handler asyncHandler = new Handler();
|
||||
private ConsumerCompat<Bundle> onStatusChange;
|
||||
private Thread loadThread;
|
||||
|
||||
private long importStartTime = 0;
|
||||
|
|
@ -53,8 +54,8 @@ public class DictionaryLoader {
|
|||
}
|
||||
|
||||
|
||||
public void setStatusHandler(Handler handler) {
|
||||
statusHandler = handler;
|
||||
public void setOnStatusChange(ConsumerCompat<Bundle> callback) {
|
||||
onStatusChange = callback;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -68,11 +69,19 @@ public class DictionaryLoader {
|
|||
throw new DictionaryImportAlreadyRunningException();
|
||||
}
|
||||
|
||||
if (languages.size() == 0) {
|
||||
Logger.d("DictionaryLoader", "Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
loadThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
currentFile = 0;
|
||||
importStartTime = System.currentTimeMillis();
|
||||
|
||||
dropIndexes();
|
||||
|
||||
// SQLite does not support parallel queries, so let's import them one by one
|
||||
for (Language lang : languages) {
|
||||
if (isInterrupted()) {
|
||||
|
|
@ -81,6 +90,8 @@ public class DictionaryLoader {
|
|||
importAll(lang);
|
||||
currentFile++;
|
||||
}
|
||||
|
||||
createIndexes();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -157,6 +168,28 @@ public class DictionaryLoader {
|
|||
}
|
||||
|
||||
|
||||
private void dropIndexes() {
|
||||
long start = System.currentTimeMillis();
|
||||
DictionaryDb.dropLongWordIndexSync();
|
||||
Logger.d("dropIndexes", "Index 1: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
start = System.currentTimeMillis();
|
||||
DictionaryDb.dropShortWordIndexSync();
|
||||
Logger.d("dropIndexes", "Index 2: " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
|
||||
|
||||
private void createIndexes() {
|
||||
long start = System.currentTimeMillis();
|
||||
DictionaryDb.createLongWordIndexSync();
|
||||
Logger.d("createIndexes", "Index 1: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
start = System.currentTimeMillis();
|
||||
DictionaryDb.createShortWordIndexSync();
|
||||
Logger.d("createIndexes", "Index 2: " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
|
||||
|
||||
private void importLetters(Language language) {
|
||||
ArrayList<Word> letters = new ArrayList<>();
|
||||
|
||||
|
|
@ -176,6 +209,7 @@ public class DictionaryLoader {
|
|||
Word word = new Word();
|
||||
word.langId = language.getId();
|
||||
word.frequency = 0;
|
||||
word.length = 1;
|
||||
word.sequence = String.valueOf(key);
|
||||
word.word = langChar;
|
||||
|
||||
|
|
@ -280,6 +314,7 @@ public class DictionaryLoader {
|
|||
Word dbWord = new Word();
|
||||
dbWord.langId = language.getId();
|
||||
dbWord.frequency = frequency;
|
||||
dbWord.length = word.length();
|
||||
dbWord.sequence = language.getDigitSequenceForWord(word);
|
||||
dbWord.word = word;
|
||||
|
||||
|
|
@ -288,7 +323,7 @@ public class DictionaryLoader {
|
|||
|
||||
|
||||
private void sendProgressMessage(Language language, int progress, int progressUpdateInterval) {
|
||||
if (statusHandler == null) {
|
||||
if (onStatusChange == null) {
|
||||
Logger.w(
|
||||
"tt9/DictionaryLoader.sendProgressMessage",
|
||||
"Cannot send progress without a status Handler. Ignoring message.");
|
||||
|
|
@ -302,45 +337,39 @@ public class DictionaryLoader {
|
|||
|
||||
lastProgressUpdate = now;
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("languageId", language.getId());
|
||||
bundle.putLong("time", getImportTime());
|
||||
bundle.putInt("progress", progress);
|
||||
bundle.putInt("currentFile", currentFile);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
statusHandler.sendMessage(msg);
|
||||
Bundle progressMsg = new Bundle();
|
||||
progressMsg.putInt("languageId", language.getId());
|
||||
progressMsg.putLong("time", getImportTime());
|
||||
progressMsg.putInt("progress", progress);
|
||||
progressMsg.putInt("currentFile", currentFile);
|
||||
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
|
||||
}
|
||||
|
||||
|
||||
private void sendError(String message, int langId) {
|
||||
if (statusHandler == null) {
|
||||
if (onStatusChange == null) {
|
||||
Logger.w("tt9/DictionaryLoader.sendError", "Cannot send an error without a status Handler. Ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("error", message);
|
||||
bundle.putInt("languageId", langId);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
statusHandler.sendMessage(msg);
|
||||
Bundle errorMsg = new Bundle();
|
||||
errorMsg.putString("error", message);
|
||||
errorMsg.putInt("languageId", langId);
|
||||
asyncHandler.post(() -> onStatusChange.accept(errorMsg));
|
||||
}
|
||||
|
||||
|
||||
private void sendImportError(String message, int langId, long fileLine, String word) {
|
||||
if (statusHandler == null) {
|
||||
if (onStatusChange == null) {
|
||||
Logger.w("tt9/DictionaryLoader.sendError", "Cannot send an import error without a status Handler. Ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("error", message);
|
||||
bundle.putLong("fileLine", fileLine + 1);
|
||||
bundle.putInt("languageId", langId);
|
||||
bundle.putString("word", word);
|
||||
Message msg = new Message();
|
||||
msg.setData(bundle);
|
||||
statusHandler.sendMessage(msg);
|
||||
Bundle errorMsg = new Bundle();
|
||||
errorMsg.putString("error", message);
|
||||
errorMsg.putLong("fileLine", fileLine + 1);
|
||||
errorMsg.putInt("languageId", langId);
|
||||
errorMsg.putString("word", word);
|
||||
asyncHandler.post(() -> onStatusChange.accept(errorMsg));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/io/github/sspanak/tt9/db/migrations/DB8.java
Normal file
27
src/io/github/sspanak/tt9/db/migrations/DB8.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package io.github.sspanak.tt9.db.migrations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
import io.github.sspanak.tt9.db.room.TT9Room;
|
||||
|
||||
public class DB8 {
|
||||
public static final Migration MIGRATION = new Migration(7, 8) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
try {
|
||||
database.beginTransaction();
|
||||
database.execSQL("ALTER TABLE words ADD COLUMN len INTEGER NOT NULL DEFAULT 0");
|
||||
database.execSQL("UPDATE words SET len = LENGTH(seq)");
|
||||
database.execSQL(TT9Room.createShortWordsIndexQuery().getSql());
|
||||
database.setTransactionSuccessful();
|
||||
} catch (Exception e) {
|
||||
Logger.e("Migrate to DB8", "Migration failed. " + e.getMessage());
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
27
src/io/github/sspanak/tt9/db/migrations/DB9.java
Normal file
27
src/io/github/sspanak/tt9/db/migrations/DB9.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package io.github.sspanak.tt9.db.migrations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
import io.github.sspanak.tt9.db.room.TT9Room;
|
||||
import io.github.sspanak.tt9.db.room.WordsDao;
|
||||
|
||||
public class DB9 {
|
||||
public static final Migration MIGRATION = new Migration(8, 9) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
try {
|
||||
database.beginTransaction();
|
||||
database.execSQL("DROP INDEX " + WordsDao.indexLongWords);
|
||||
database.execSQL(TT9Room.createLongWordsIndexQuery().getSql());
|
||||
database.setTransactionSuccessful();
|
||||
} catch (Exception e) {
|
||||
Logger.e("Migrate to DB9", "Migration failed. " + e.getMessage());
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -5,18 +5,63 @@ import android.content.Context;
|
|||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||
|
||||
import io.github.sspanak.tt9.db.migrations.DB6;
|
||||
import io.github.sspanak.tt9.db.migrations.DB7;
|
||||
import io.github.sspanak.tt9.db.migrations.DB8;
|
||||
import io.github.sspanak.tt9.db.migrations.DB9;
|
||||
|
||||
@Database(version = 7, entities = Word.class, exportSchema = false)
|
||||
@Database(version = 9, entities = Word.class, exportSchema = false)
|
||||
public abstract class TT9Room extends RoomDatabase {
|
||||
public abstract WordsDao wordsDao();
|
||||
|
||||
public static synchronized TT9Room getInstance(Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context, TT9Room.class, "t9dict.db")
|
||||
.addMigrations(DB6.MIGRATION, new DB7().getMigration(context))
|
||||
.addMigrations(
|
||||
DB6.MIGRATION,
|
||||
new DB7().getMigration(context),
|
||||
DB8.MIGRATION,
|
||||
DB9.MIGRATION
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static SimpleSQLiteQuery getFuzzyQuery(String index, int langId, int limit, String sequence, int minWordLength, int maxWordLength, String word) {
|
||||
String sql = "SELECT *" +
|
||||
" FROM words INDEXED BY " + index + " " +
|
||||
" WHERE 1" +
|
||||
" AND lang = " + langId +
|
||||
" AND len BETWEEN " + minWordLength + " AND " + maxWordLength +
|
||||
" AND seq BETWEEN " + sequence + " AND " + sequence + "99 " +
|
||||
" ORDER BY len ASC, freq DESC " +
|
||||
" LIMIT " + limit;
|
||||
|
||||
if (word != null) {
|
||||
sql = sql.replace("WHERE 1", "WHERE 1 AND word LIKE '" + word + "%'");
|
||||
}
|
||||
|
||||
return new SimpleSQLiteQuery(sql);
|
||||
}
|
||||
|
||||
public static SimpleSQLiteQuery getFuzzyQuery(String index, int langId, int limit, String sequence, int minWordLength, int maxWordLength) {
|
||||
return getFuzzyQuery(index, langId, limit, sequence, minWordLength, maxWordLength, null);
|
||||
}
|
||||
|
||||
public static SimpleSQLiteQuery createShortWordsIndexQuery() {
|
||||
return new SimpleSQLiteQuery("CREATE INDEX " + WordsDao.indexShortWords + " ON words (lang ASC, len ASC, seq ASC)");
|
||||
}
|
||||
|
||||
public static SimpleSQLiteQuery createLongWordsIndexQuery() {
|
||||
return new SimpleSQLiteQuery("CREATE INDEX " + WordsDao.indexLongWords + " ON words (lang ASC, seq ASC, freq DESC)");
|
||||
}
|
||||
|
||||
public static SimpleSQLiteQuery dropShortWordsIndexQuery() {
|
||||
return new SimpleSQLiteQuery("DROP INDEX IF EXISTS " + WordsDao.indexShortWords);
|
||||
}
|
||||
|
||||
public static SimpleSQLiteQuery dropLongWordsIndexQuery() {
|
||||
return new SimpleSQLiteQuery("DROP INDEX IF EXISTS " + WordsDao.indexLongWords);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import androidx.room.PrimaryKey;
|
|||
@Entity(
|
||||
indices = {
|
||||
@Index(value = {"lang", "word"}, unique = true),
|
||||
@Index(value = {"lang", "seq", "freq"})
|
||||
@Index(value = {"lang", "seq", "freq"}, orders = {Index.Order.ASC, Index.Order.ASC, Index.Order.DESC}),
|
||||
@Index(value = {"lang", "len", "seq"}, orders = {Index.Order.ASC, Index.Order.ASC, Index.Order.ASC})
|
||||
},
|
||||
tableName = "words"
|
||||
)
|
||||
|
|
@ -26,4 +27,7 @@ public class Word {
|
|||
|
||||
@ColumnInfo(name = "freq")
|
||||
public int frequency;
|
||||
|
||||
@ColumnInfo(name = "len")
|
||||
public int length;
|
||||
}
|
||||
|
|
|
|||
41
src/io/github/sspanak/tt9/db/room/WordList.java
Normal file
41
src/io/github/sspanak/tt9/db/room/WordList.java
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package io.github.sspanak.tt9.db.room;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WordList extends ArrayList<Word> {
|
||||
public WordList() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WordList(List<Word> words) {
|
||||
addAll(words);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < size(); i++) {
|
||||
sb
|
||||
.append("word: ").append(get(i).word)
|
||||
.append(" | sequence: ").append(get(i).sequence)
|
||||
.append(" | priority: ").append(get(i).frequency)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ArrayList<String> toStringList() {
|
||||
ArrayList<String> strings = new ArrayList<>();
|
||||
for (int i = 0; i < size(); i++) {
|
||||
strings.add(get(i).word);
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,20 @@ import androidx.room.Dao;
|
|||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.RawQuery;
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface WordsDao {
|
||||
String indexLongWords = "index_words_lang_seq_freq";
|
||||
String indexShortWords = "index_words_lang_len_seq";
|
||||
|
||||
@RawQuery()
|
||||
Object rawQuery(SimpleSQLiteQuery sql);
|
||||
|
||||
@Query("SELECT COUNT(id) FROM words WHERE :langId < 0 OR lang = :langId")
|
||||
int count(int langId);
|
||||
|
||||
|
|
@ -31,17 +39,8 @@ public interface WordsDao {
|
|||
)
|
||||
List<Word> getMany(int langId, int limit, String sequence, String word);
|
||||
|
||||
@Query(
|
||||
"SELECT * " +
|
||||
"FROM words " +
|
||||
"WHERE " +
|
||||
"lang = :langId " +
|
||||
"AND seq > :sequence AND seq <= :sequence || '99' " +
|
||||
"AND (:word IS NULL OR word LIKE :word || '%') " +
|
||||
"ORDER BY LENGTH(seq) ASC, freq DESC, seq ASC " +
|
||||
"LIMIT :limit"
|
||||
)
|
||||
List<Word> getFuzzy(int langId, int limit, String sequence, String word);
|
||||
@RawQuery(observedEntities = Word.class)
|
||||
List<Word> getCustom(SimpleSQLiteQuery query);
|
||||
|
||||
@Insert
|
||||
void insert(Word word);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package io.github.sspanak.tt9.ime;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
|
|
@ -18,6 +15,7 @@ public class EmptyDatabaseWarning {
|
|||
final int WARNING_INTERVAL;
|
||||
private static final HashMap<Integer, Long> warningDisplayedTime = new HashMap<>();
|
||||
|
||||
private Context context;
|
||||
private Language language;
|
||||
|
||||
public EmptyDatabaseWarning(SettingsStore settings) {
|
||||
|
|
@ -31,37 +29,33 @@ public class EmptyDatabaseWarning {
|
|||
}
|
||||
|
||||
public void emitOnce(Language language) {
|
||||
if (language == null) {
|
||||
context = context == null ? TraditionalT9.getMainContext() : context;
|
||||
this.language = language;
|
||||
|
||||
if (isItTimeAgain(TraditionalT9.getMainContext())) {
|
||||
DictionaryDb.areThereWords(this::show, language);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isItTimeAgain(Context context) {
|
||||
if (this.language == null || context == null || !warningDisplayedTime.containsKey(language.getId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
Long lastWarningTime = warningDisplayedTime.get(language.getId());
|
||||
return lastWarningTime != null && now - lastWarningTime > WARNING_INTERVAL;
|
||||
}
|
||||
|
||||
private void show(boolean areThereWords) {
|
||||
if (areThereWords) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.language = language;
|
||||
DictionaryDb.areThereWords(handleWordCount, language);
|
||||
warningDisplayedTime.put(language.getId(), System.currentTimeMillis());
|
||||
UI.toastLongFromAsync(
|
||||
context,
|
||||
context.getString(R.string.dictionary_missing_go_load_it, language.getName())
|
||||
);
|
||||
}
|
||||
|
||||
private final Handler handleWordCount = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
boolean areThereWords = msg.what == 1;
|
||||
|
||||
if (areThereWords) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = TraditionalT9.getMainContext();
|
||||
if (context == null || !warningDisplayedTime.containsKey(language.getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
Long lastWarningTime = warningDisplayedTime.get(language.getId());
|
||||
boolean isItWarningTimeAgain = lastWarningTime != null && now - lastWarningTime > WARNING_INTERVAL;
|
||||
|
||||
if (isItWarningTimeAgain) {
|
||||
String message = context.getString(R.string.dictionary_missing_go_load_it, language.getName());
|
||||
UI.toastLong(context, message);
|
||||
warningDisplayedTime.put(language.getId(), now);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@ package io.github.sspanak.tt9.ime;
|
|||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
|
@ -251,7 +248,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
|
||||
protected boolean onLeft() {
|
||||
if (mInputMode.clearWordStem()) {
|
||||
mInputMode.loadSuggestions(handleSuggestionsAsync, getComposingText());
|
||||
mInputMode.loadSuggestions(this::getSuggestions, getComposingText());
|
||||
} else {
|
||||
jumpBeforeComposingText();
|
||||
}
|
||||
|
|
@ -269,7 +266,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
}
|
||||
|
||||
if (mInputMode.setWordStem(filter, repeat)) {
|
||||
mInputMode.loadSuggestions(handleSuggestionsAsync, filter);
|
||||
mInputMode.loadSuggestions(this::getSuggestions, filter);
|
||||
} else if (filter.length() == 0) {
|
||||
mInputMode.reset();
|
||||
}
|
||||
|
|
@ -469,9 +466,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
|
||||
|
||||
private void getSuggestions() {
|
||||
if (!mInputMode.loadSuggestions(handleSuggestionsAsync, suggestionBar.getCurrentSuggestion())) {
|
||||
handleSuggestions();
|
||||
}
|
||||
mInputMode.loadSuggestions(this::handleSuggestions, suggestionBar.getCurrentSuggestion());
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -500,14 +495,6 @@ public class TraditionalT9 extends KeyPadHandler {
|
|||
}
|
||||
|
||||
|
||||
private final Handler handleSuggestionsAsync = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message m) {
|
||||
handleSuggestions();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private void setSuggestions(List<String> suggestions) {
|
||||
setSuggestions(suggestions, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package io.github.sspanak.tt9.ime.modes;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
|
|
@ -50,10 +48,18 @@ abstract public class InputMode {
|
|||
public boolean onBackspace() { return false; }
|
||||
abstract public boolean onNumber(int number, boolean hold, int repeat);
|
||||
|
||||
// Predictions
|
||||
// Suggestions
|
||||
public void onAcceptSuggestion(String suggestion) {}
|
||||
protected void onSuggestionsUpdated(Handler handler) { handler.sendEmptyMessage(0); }
|
||||
public boolean loadSuggestions(Handler handler, String currentWord) { return false; }
|
||||
|
||||
/**
|
||||
* loadSuggestions
|
||||
* Loads the suggestions based on the current state, with optional "currentWord" filter.
|
||||
* Once loading is finished the respective InputMode child will call "notification", notifying it
|
||||
* the suggestions are available using "getSuggestions()".
|
||||
*/
|
||||
public void loadSuggestions(Runnable notification, String currentWord) {
|
||||
notification.run();
|
||||
}
|
||||
|
||||
public ArrayList<String> getSuggestions() {
|
||||
ArrayList<String> newSuggestions = new ArrayList<>();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
package io.github.sspanak.tt9.ime.modes;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.github.sspanak.tt9.Logger;
|
||||
|
|
@ -30,7 +26,8 @@ public class ModePredictive extends InputMode {
|
|||
private String stem = "";
|
||||
|
||||
// async suggestion handling
|
||||
private static Handler handleSuggestionsExternal;
|
||||
private boolean disablePredictions = false;
|
||||
private Runnable onSuggestionsUpdated;
|
||||
|
||||
// text analysis tools
|
||||
private final AutoSpace autoSpace;
|
||||
|
|
@ -73,10 +70,12 @@ public class ModePredictive extends InputMode {
|
|||
// hold to type any digit
|
||||
reset();
|
||||
autoAcceptTimeout = 0;
|
||||
disablePredictions = true;
|
||||
suggestions.add(String.valueOf(number));
|
||||
} else {
|
||||
// words
|
||||
super.reset();
|
||||
disablePredictions = false;
|
||||
digitSequence += number;
|
||||
if (number == 0 && repeat > 0) {
|
||||
autoAcceptTimeout = 0;
|
||||
|
|
@ -104,6 +103,7 @@ public class ModePredictive extends InputMode {
|
|||
public void reset() {
|
||||
super.reset();
|
||||
digitSequence = "";
|
||||
disablePredictions = false;
|
||||
stem = "";
|
||||
}
|
||||
|
||||
|
|
@ -191,36 +191,35 @@ public class ModePredictive extends InputMode {
|
|||
* See: Predictions.generatePossibleCompletions()
|
||||
*/
|
||||
@Override
|
||||
public boolean loadSuggestions(Handler handler, String currentWord) {
|
||||
public void loadSuggestions(Runnable handler, String currentWord) {
|
||||
if (disablePredictions) {
|
||||
super.loadSuggestions(handler, currentWord);
|
||||
return;
|
||||
}
|
||||
|
||||
onSuggestionsUpdated = handler;
|
||||
predictions
|
||||
.setDigitSequence(digitSequence)
|
||||
.setIsStemFuzzy(isStemFuzzy)
|
||||
.setStem(stem)
|
||||
.setLanguage(language)
|
||||
.setInputWord(currentWord)
|
||||
.setWordsChangedHandler(handleSuggestions);
|
||||
|
||||
handleSuggestionsExternal = handler;
|
||||
|
||||
return predictions.load();
|
||||
.setWordsChangedHandler(this::getPredictions)
|
||||
.load();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* handleSuggestions
|
||||
* Extracts the suggestions from the Message object and passes them to the actual external Handler.
|
||||
* If there were no matches in the database, they will be generated based on the "lastInputFieldWord".
|
||||
* getPredictions
|
||||
* Gets the currently available Predictions and sends them over to the external caller.
|
||||
*/
|
||||
private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message m) {
|
||||
digitSequence = predictions.getDigitSequence();
|
||||
suggestions.clear();
|
||||
suggestions.addAll(predictions.getList());
|
||||
private void getPredictions() {
|
||||
digitSequence = predictions.getDigitSequence();
|
||||
suggestions.clear();
|
||||
suggestions.addAll(predictions.getList());
|
||||
|
||||
onSuggestionsUpdated(handleSuggestionsExternal);
|
||||
}
|
||||
};
|
||||
onSuggestionsUpdated.run();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
package io.github.sspanak.tt9.ime.modes.helpers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
|
@ -24,7 +20,7 @@ public class Predictions {
|
|||
private String inputWord;
|
||||
|
||||
// async operations
|
||||
private Handler wordsChangedHandler;
|
||||
private Runnable onWordsChanged = () -> {};
|
||||
|
||||
// data
|
||||
private final ArrayList<String> words = new ArrayList<>();
|
||||
|
|
@ -77,8 +73,9 @@ public class Predictions {
|
|||
return this;
|
||||
}
|
||||
|
||||
public void setWordsChangedHandler(Handler handler) {
|
||||
wordsChangedHandler = handler;
|
||||
public Predictions setWordsChangedHandler(Runnable handler) {
|
||||
onWordsChanged = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ArrayList<String> getList() {
|
||||
|
|
@ -111,35 +108,23 @@ public class Predictions {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* onWordsChanged
|
||||
* Notify the external handler the word list has changed, so they can get the new ones using getList().
|
||||
*/
|
||||
private void onWordsChanged() {
|
||||
wordsChangedHandler.sendEmptyMessage(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* load
|
||||
* Queries the dictionary database for a list of words matching the current language and
|
||||
* sequence or loads the static ones.
|
||||
*
|
||||
* Returns "false" on invalid digitSequence.
|
||||
*/
|
||||
public boolean load() {
|
||||
public void load() {
|
||||
if (digitSequence == null || digitSequence.length() == 0) {
|
||||
words.clear();
|
||||
onWordsChanged();
|
||||
return false;
|
||||
onWordsChanged.run();
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadStatic()) {
|
||||
onWordsChanged();
|
||||
onWordsChanged.run();
|
||||
} else {
|
||||
DictionaryDb.getSuggestions(
|
||||
dbWordsHandler,
|
||||
DictionaryDb.getWords(
|
||||
this::onDbWords,
|
||||
language,
|
||||
digitSequence,
|
||||
stem,
|
||||
|
|
@ -147,8 +132,6 @@ public class Predictions {
|
|||
settings.getSuggestionsMax()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -160,20 +143,20 @@ public class Predictions {
|
|||
private boolean loadStatic() {
|
||||
// whitespace/special/math characters
|
||||
if (digitSequence.equals("0")) {
|
||||
words.clear();
|
||||
stem = "";
|
||||
words.clear();
|
||||
words.addAll(language.getKeyCharacters(0, false));
|
||||
}
|
||||
// "00" is a shortcut for the preferred character
|
||||
else if (digitSequence.equals("00")) {
|
||||
words.clear();
|
||||
stem = "";
|
||||
words.clear();
|
||||
words.add(settings.getDoubleZeroChar());
|
||||
}
|
||||
// emoji
|
||||
else if (containsOnly1Regex.matcher(digitSequence).matches()) {
|
||||
words.clear();
|
||||
stem = "";
|
||||
words.clear();
|
||||
if (digitSequence.length() == 1) {
|
||||
words.addAll(language.getKeyCharacters(1, false));
|
||||
} else {
|
||||
|
|
@ -190,29 +173,23 @@ public class Predictions {
|
|||
|
||||
/**
|
||||
* dbWordsHandler
|
||||
* Extracts the words from the Message object, generates extra words, if necessary, then
|
||||
* notifies the external handler it is now possible to use "getList()".
|
||||
* If there were no matches in the database, they will be generated based on the "inputWord".
|
||||
* Callback for when the database has finished loading words. If there were no matches in the database,
|
||||
* they will be generated based on the "inputWord". After the word list is compiled, it notifies the
|
||||
* external handler it is now possible to use it with "getList()".
|
||||
*/
|
||||
private final Handler dbWordsHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
ArrayList<String> dbWords = msg.getData().getStringArrayList("suggestions");
|
||||
dbWords = dbWords != null ? dbWords : new ArrayList<>();
|
||||
|
||||
if (dbWords.size() == 0 && digitSequence.length() > 0) {
|
||||
emptyDbWarning.emitOnce(language);
|
||||
dbWords = generatePossibleCompletions(inputWord);
|
||||
}
|
||||
|
||||
words.clear();
|
||||
suggestStem();
|
||||
suggestMissingWords(generatePossibleStemVariations(dbWords));
|
||||
suggestMissingWords(dbWords);
|
||||
|
||||
onWordsChanged();
|
||||
private void onDbWords (ArrayList<String> dbWords) {
|
||||
if (dbWords.size() == 0 && digitSequence.length() > 0) {
|
||||
emptyDbWarning.emitOnce(language);
|
||||
dbWords = generatePossibleCompletions(inputWord);
|
||||
}
|
||||
};
|
||||
|
||||
words.clear();
|
||||
suggestStem();
|
||||
suggestMissingWords(generatePossibleStemVariations(dbWords));
|
||||
suggestMissingWords(dbWords);
|
||||
|
||||
onWordsChanged.run();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public class Characters {
|
|||
|
||||
public static ArrayList<String> getEmoji(int level) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return TextEmoticons;
|
||||
return new ArrayList<>(TextEmoticons);
|
||||
}
|
||||
|
||||
level = (Emoji.size() > level) ? level : Emoji.size() - 1;
|
||||
|
|
@ -59,6 +59,6 @@ public class Characters {
|
|||
}
|
||||
}
|
||||
|
||||
return availableEmoji.size() > 0 ? availableEmoji : TextEmoticons;
|
||||
return availableEmoji.size() > 0 ? availableEmoji : new ArrayList<>(TextEmoticons);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package io.github.sspanak.tt9.preferences.items;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
|
|
@ -36,7 +34,7 @@ public class ItemLoadDictionary extends ItemClickable {
|
|||
this.progressBar = progressBar;
|
||||
this.settings = settings;
|
||||
|
||||
loader.setStatusHandler(onDictionaryLoading);
|
||||
loader.setOnStatusChange(this::onLoadingStatusChange);
|
||||
|
||||
if (!progressBar.isCompleted() && !progressBar.isFailed()) {
|
||||
changeToCancelButton();
|
||||
|
|
@ -46,23 +44,20 @@ public class ItemLoadDictionary extends ItemClickable {
|
|||
}
|
||||
|
||||
|
||||
private final Handler onDictionaryLoading = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
progressBar.show(msg.getData());
|
||||
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
|
||||
private void onLoadingStatusChange(Bundle status) {
|
||||
progressBar.show(status);
|
||||
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
|
||||
|
||||
if (progressBar.isCancelled()) {
|
||||
changeToLoadButton();
|
||||
} else if (progressBar.isFailed()) {
|
||||
changeToLoadButton();
|
||||
UI.toast(context, progressBar.getMessage());
|
||||
} else if (progressBar.isCompleted()) {
|
||||
changeToLoadButton();
|
||||
UI.toast(context, R.string.dictionary_loaded);
|
||||
}
|
||||
if (progressBar.isCancelled()) {
|
||||
changeToLoadButton();
|
||||
} else if (progressBar.isFailed()) {
|
||||
changeToLoadButton();
|
||||
UI.toastFromAsync(context, progressBar.getMessage());
|
||||
} else if (progressBar.isCompleted()) {
|
||||
changeToLoadButton();
|
||||
UI.toastFromAsync(context, R.string.dictionary_loaded);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package io.github.sspanak.tt9.preferences.items;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
|
|
@ -28,12 +25,6 @@ public class ItemTruncateAll extends ItemClickable {
|
|||
this.loader = loader;
|
||||
}
|
||||
|
||||
private final Handler onDictionaryTruncated = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
UI.toast(context, R.string.dictionary_truncated);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected boolean onClick(Preference p) {
|
||||
|
|
@ -42,7 +33,7 @@ public class ItemTruncateAll extends ItemClickable {
|
|||
loadItem.changeToLoadButton();
|
||||
}
|
||||
|
||||
DictionaryDb.deleteWords(onDictionaryTruncated);
|
||||
DictionaryDb.deleteWords(() -> UI.toastFromAsync(context, R.string.dictionary_truncated));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package io.github.sspanak.tt9.preferences.items;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
|
|
@ -35,12 +32,6 @@ public class ItemTruncateUnselected extends ItemClickable {
|
|||
this.loader = loader;
|
||||
}
|
||||
|
||||
private final Handler onDictionaryTruncated = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
UI.toast(context, R.string.dictionary_truncated);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected boolean onClick(Preference p) {
|
||||
|
|
@ -57,7 +48,10 @@ public class ItemTruncateUnselected extends ItemClickable {
|
|||
}
|
||||
}
|
||||
|
||||
DictionaryDb.deleteWords(onDictionaryTruncated, unselectedLanguageIds);
|
||||
DictionaryDb.deleteWords(
|
||||
() -> UI.toastFromAsync(context, R.string.dictionary_truncated),
|
||||
unselectedLanguageIds
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@ package io.github.sspanak.tt9.ui;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
|
|
@ -50,30 +47,27 @@ public class AddWordAct extends AppCompatActivity {
|
|||
}
|
||||
|
||||
|
||||
private final Handler onAddedWord = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case 0:
|
||||
Logger.d("onAddedWord", "Added word: '" + word + "'...");
|
||||
settings.saveLastWord(word);
|
||||
break;
|
||||
private void onAddedWord(int statusCode) {
|
||||
switch (statusCode) {
|
||||
case 0:
|
||||
Logger.d("onAddedWord", "Successfully added word: '" + word + '"');
|
||||
settings.saveLastWord(word);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
UI.toastLong(
|
||||
main.getContext(),
|
||||
getResources().getString(R.string.add_word_exist, word)
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
UI.toastLongFromAsync(
|
||||
main.getContext(),
|
||||
getResources().getString(R.string.add_word_exist, word)
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
UI.toastLong(main.getContext(), R.string.error_unexpected);
|
||||
break;
|
||||
default:
|
||||
UI.toastLongFromAsync(main.getContext(), R.string.error_unexpected);
|
||||
break;
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
};
|
||||
finish();
|
||||
}
|
||||
|
||||
public void addWord(View v) {
|
||||
try {
|
||||
|
|
@ -81,7 +75,7 @@ public class AddWordAct extends AppCompatActivity {
|
|||
word = ((EditText) main.findViewById(R.id.add_word_text)).getText().toString();
|
||||
Logger.d("addWord", "Attempting to add word: '" + word + "'...");
|
||||
|
||||
DictionaryDb.insertWord(onAddedWord, LanguageCollection.getLanguage(lang), word);
|
||||
DictionaryDb.insertWord(this::onAddedWord, LanguageCollection.getLanguage(lang), word);
|
||||
} catch (InsertBlankWordException e) {
|
||||
Logger.e("AddWordAct.addWord", e.getMessage());
|
||||
UI.toastLong(this, R.string.add_word_blank);
|
||||
|
|
@ -96,6 +90,6 @@ public class AddWordAct extends AppCompatActivity {
|
|||
|
||||
|
||||
public void cancelAddingWord(View v) {
|
||||
this.finish();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package io.github.sspanak.tt9.ui;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
|
||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||
|
|
@ -30,15 +31,47 @@ public class UI {
|
|||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public static void toastFromAsync(Context context, CharSequence msg) {
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
toast(context, msg);
|
||||
Looper.loop();
|
||||
}
|
||||
|
||||
public static void toast(Context context, int resourceId) {
|
||||
Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public static void toastFromAsync(Context context, int resourceId) {
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
toast(context, resourceId);
|
||||
Looper.loop();
|
||||
}
|
||||
|
||||
public static void toastLong(Context context, int resourceId) {
|
||||
Toast.makeText(context, resourceId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public static void toastLongFromAsync(Context context, int resourceId) {
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
toast(context, resourceId);
|
||||
Looper.loop();
|
||||
}
|
||||
|
||||
public static void toastLong(Context context, CharSequence msg) {
|
||||
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public static void toastLongFromAsync(Context context, CharSequence msg) {
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
toastLong(context, msg);
|
||||
Looper.loop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue