1
0
Fork 0

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:
Dimo Karaivanov 2023-05-10 09:50:41 +03:00 committed by GitHub
parent 4d67c02340
commit c3787138fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 507 additions and 316 deletions

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

View file

@ -5,6 +5,10 @@ import android.util.Log;
public class Logger { public class Logger {
public static final int LEVEL = BuildConfig.DEBUG ? Log.DEBUG : Log.ERROR; 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) { static public void v(String tag, String msg) {
if (LEVEL <= Log.VERBOSE) { if (LEVEL <= Log.VERBOSE) {
Log.v(tag, msg); Log.v(tag, msg);

View file

@ -2,17 +2,20 @@ package io.github.sspanak.tt9.db;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteConstraintException;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message;
import androidx.sqlite.db.SimpleSQLiteQuery;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.github.sspanak.tt9.ConsumerCompat;
import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException; import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException;
import io.github.sspanak.tt9.db.room.TT9Room; import io.github.sspanak.tt9.db.room.TT9Room;
import io.github.sspanak.tt9.db.room.Word; 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.ime.TraditionalT9;
import io.github.sspanak.tt9.languages.InvalidLanguageException; import io.github.sspanak.tt9.languages.InvalidLanguageException;
import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.Language;
@ -21,6 +24,7 @@ import io.github.sspanak.tt9.preferences.SettingsStore;
public class DictionaryDb { public class DictionaryDb {
private static TT9Room dbInstance; private static TT9Room dbInstance;
private static final Handler asyncHandler = new Handler();
public static synchronized void init(Context context) { public static synchronized void init(Context context) {
if (dbInstance == null) { 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 * normalizeWordFrequencies
* Normalizes the word frequencies for all languages that have reached the maximum, as defined in * Normalizes the word frequencies for all languages that have reached the maximum, as defined in
* the settings. * the settings.
*
* This query will finish immediately, if there is nothing to do. It's safe to run it often. * This query will finish immediately, if there is nothing to do. It's safe to run it often.
*
*/ */
public static void normalizeWordFrequencies(SettingsStore settings) { public static void normalizeWordFrequencies(SettingsStore settings) {
new Thread() { new Thread() {
@ -55,7 +97,7 @@ public class DictionaryDb {
public void run() { public void run() {
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
int affectedRows = dbInstance.wordsDao().normalizeFrequencies( int affectedRows = getInstance().wordsDao().normalizeFrequencies(
settings.getWordFrequencyNormalizationDivider(), settings.getWordFrequencyNormalizationDivider(),
settings.getWordFrequencyMax() settings.getWordFrequencyMax()
); );
@ -69,17 +111,12 @@ public class DictionaryDb {
} }
public static void runInTransaction(Runnable r) { public static void areThereWords(ConsumerCompat<Boolean> notification, Language language) {
getInstance().runInTransaction(r);
}
public static void areThereWords(Handler handler, Language language) {
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
int langId = language != null ? language.getId() : -1; int langId = language != null ? language.getId() : -1;
handler.sendEmptyMessage(getInstance().wordsDao().count(langId) > 0 ? 1 : 0); notification.accept(getInstance().wordsDao().count(langId) > 0);
} }
}.start(); }.start();
} }
@ -94,12 +131,12 @@ public class DictionaryDb {
} }
public static void deleteWords(Handler handler) { public static void deleteWords(Runnable notification) {
deleteWords(handler, null); deleteWords(notification, null);
} }
public static void deleteWords(Handler handler, ArrayList<Integer> languageIds) { public static void deleteWords(Runnable notification, ArrayList<Integer> languageIds) {
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
@ -108,13 +145,13 @@ public class DictionaryDb {
} else if (languageIds.size() > 0) { } else if (languageIds.size() > 0) {
getInstance().wordsDao().deleteByLanguage(languageIds); getInstance().wordsDao().deleteByLanguage(languageIds);
} }
handler.sendEmptyMessage(0); notification.run();
} }
}.start(); }.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) { if (language == null) {
throw new InvalidLanguageException(); throw new InvalidLanguageException();
} }
@ -127,6 +164,7 @@ public class DictionaryDb {
dbWord.langId = language.getId(); dbWord.langId = language.getId();
dbWord.sequence = language.getDigitSequenceForWord(word); dbWord.sequence = language.getDigitSequenceForWord(word);
dbWord.word = word.toLowerCase(language.getLocale()); dbWord.word = word.toLowerCase(language.getLocale());
dbWord.length = word.length();
dbWord.frequency = 1; dbWord.frequency = 1;
new Thread() { new Thread() {
@ -135,15 +173,16 @@ public class DictionaryDb {
try { try {
getInstance().wordsDao().insert(dbWord); getInstance().wordsDao().insert(dbWord);
getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence); getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence);
handler.sendEmptyMessage(0); statusHandler.accept(0);
} catch (SQLiteConstraintException e) { } 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); Logger.e("tt9/insertWord", msg);
handler.sendEmptyMessage(1); statusHandler.accept(1);
} catch (Exception e) { } 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); Logger.e("tt9/insertWord", msg);
handler.sendEmptyMessage(2); statusHandler.accept(2);
} }
} }
}.start(); }.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(); long start = System.currentTimeMillis();
List<Word> exactMatches = getInstance().wordsDao().getMany( WordList matches = new WordList(getInstance().wordsDao().getMany(
language.getId(), language.getId(),
maximumWords, maximumWords,
sequence, sequence,
word == null || word.equals("") ? null : word filter == null || filter.equals("") ? null : filter
); ));
Logger.d(
"db.getSuggestionsExact",
"Exact matches: " + exactMatches.size() + ". Time: " + (System.currentTimeMillis() - start) + " ms"
);
ArrayList<String> suggestions = new ArrayList<>(); printDebug("loadWordsExact", "===== Exact Word Matches =====", matches, start);
for (Word w : exactMatches) { return matches.toStringList();
Logger.d("db.getSuggestions", "exact match: " + w.word + " | priority: " + w.frequency);
suggestions.add(w.word);
}
return suggestions;
} }
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(); 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<>(); // fuzzy queries are heavy, so we must restrict the search range as much as possible
for (Word w : extraWords) { boolean noFilter = (filter == null || filter.equals(""));
Logger.d( int maxWordLength = noFilter && sequence.length() <= 2 ? 5 : 1000;
"db.getSuggestions", String index = sequence.length() <= 2 ? WordsDao.indexShortWords : WordsDao.indexLongWords;
"fuzzy match: " + w.word + " | sequence: " + w.sequence + " | priority: " + w.frequency 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) { private static void sendWords(ConsumerCompat<ArrayList<String>> dataHandler, ArrayList<String> wordList) {
Bundle bundle = new Bundle(); asyncHandler.post(() -> dataHandler.accept(wordList));
bundle.putStringArrayList("suggestions", data);
Message msg = new Message();
msg.setData(bundle);
handler.sendMessage(msg);
} }
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 minWords = Math.max(minimumWords, 0);
final int maxWords = Math.max(maximumWords, minimumWords); final int maxWords = Math.max(maximumWords, minimumWords);
ArrayList<String> wordList = new ArrayList<>(maxWords);
if (sequence == null || sequence.length() == 0) { if (sequence == null || sequence.length() == 0) {
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for an empty sequence."); Logger.w("tt9/db.getWords", "Attempting to get words for an empty sequence.");
sendSuggestions(handler, new ArrayList<>()); sendWords(dataHandler, wordList);
return; return;
} }
if (language == null) { if (language == null) {
Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for NULL language."); Logger.w("tt9/db.getWords", "Attempting to get words for NULL language.");
sendSuggestions(handler, new ArrayList<>()); sendWords(dataHandler, wordList);
return; return;
} }
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
// get exact sequence matches, for example: "9422" -> "what" if (sequence.length() == 1) {
ArrayList<String> suggestions = getSuggestionsExact(language, sequence, word, maxWords); wordList.addAll(loadWordsExact(language, sequence, filter, maxWords));
} else {
wordList.addAll(loadWordsFuzzy(language, sequence, filter, minWords));
// 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 (suggestions.size() == 0) { if (wordList.size() == 0) {
Logger.i("db.getSuggestions", "No suggestions for sequence: " + sequence); Logger.i("db.getWords", "No suggestions for sequence: " + sequence);
} }
// pack the words in a message and send it to the calling thread sendWords(dataHandler, wordList);
sendSuggestions(handler, suggestions);
} }
}.start(); }.start();
} }

View file

@ -4,7 +4,6 @@ import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -12,6 +11,7 @@ import java.io.LineNumberReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import io.github.sspanak.tt9.ConsumerCompat;
import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException; import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException;
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAlreadyRunningException; import io.github.sspanak.tt9.db.exceptions.DictionaryImportAlreadyRunningException;
@ -28,7 +28,8 @@ public class DictionaryLoader {
private final AssetManager assets; private final AssetManager assets;
private final SettingsStore settings; private final SettingsStore settings;
private Handler statusHandler = null; private static final Handler asyncHandler = new Handler();
private ConsumerCompat<Bundle> onStatusChange;
private Thread loadThread; private Thread loadThread;
private long importStartTime = 0; private long importStartTime = 0;
@ -53,8 +54,8 @@ public class DictionaryLoader {
} }
public void setStatusHandler(Handler handler) { public void setOnStatusChange(ConsumerCompat<Bundle> callback) {
statusHandler = handler; onStatusChange = callback;
} }
@ -68,11 +69,19 @@ public class DictionaryLoader {
throw new DictionaryImportAlreadyRunningException(); throw new DictionaryImportAlreadyRunningException();
} }
if (languages.size() == 0) {
Logger.d("DictionaryLoader", "Nothing to do");
return;
}
loadThread = new Thread() { loadThread = new Thread() {
@Override @Override
public void run() { public void run() {
currentFile = 0; currentFile = 0;
importStartTime = System.currentTimeMillis(); importStartTime = System.currentTimeMillis();
dropIndexes();
// SQLite does not support parallel queries, so let's import them one by one // SQLite does not support parallel queries, so let's import them one by one
for (Language lang : languages) { for (Language lang : languages) {
if (isInterrupted()) { if (isInterrupted()) {
@ -81,6 +90,8 @@ public class DictionaryLoader {
importAll(lang); importAll(lang);
currentFile++; 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) { private void importLetters(Language language) {
ArrayList<Word> letters = new ArrayList<>(); ArrayList<Word> letters = new ArrayList<>();
@ -176,6 +209,7 @@ public class DictionaryLoader {
Word word = new Word(); Word word = new Word();
word.langId = language.getId(); word.langId = language.getId();
word.frequency = 0; word.frequency = 0;
word.length = 1;
word.sequence = String.valueOf(key); word.sequence = String.valueOf(key);
word.word = langChar; word.word = langChar;
@ -280,6 +314,7 @@ public class DictionaryLoader {
Word dbWord = new Word(); Word dbWord = new Word();
dbWord.langId = language.getId(); dbWord.langId = language.getId();
dbWord.frequency = frequency; dbWord.frequency = frequency;
dbWord.length = word.length();
dbWord.sequence = language.getDigitSequenceForWord(word); dbWord.sequence = language.getDigitSequenceForWord(word);
dbWord.word = word; dbWord.word = word;
@ -288,7 +323,7 @@ public class DictionaryLoader {
private void sendProgressMessage(Language language, int progress, int progressUpdateInterval) { private void sendProgressMessage(Language language, int progress, int progressUpdateInterval) {
if (statusHandler == null) { if (onStatusChange == null) {
Logger.w( Logger.w(
"tt9/DictionaryLoader.sendProgressMessage", "tt9/DictionaryLoader.sendProgressMessage",
"Cannot send progress without a status Handler. Ignoring message."); "Cannot send progress without a status Handler. Ignoring message.");
@ -302,45 +337,39 @@ public class DictionaryLoader {
lastProgressUpdate = now; lastProgressUpdate = now;
Bundle bundle = new Bundle(); Bundle progressMsg = new Bundle();
bundle.putInt("languageId", language.getId()); progressMsg.putInt("languageId", language.getId());
bundle.putLong("time", getImportTime()); progressMsg.putLong("time", getImportTime());
bundle.putInt("progress", progress); progressMsg.putInt("progress", progress);
bundle.putInt("currentFile", currentFile); progressMsg.putInt("currentFile", currentFile);
Message msg = new Message(); asyncHandler.post(() -> onStatusChange.accept(progressMsg));
msg.setData(bundle);
statusHandler.sendMessage(msg);
} }
private void sendError(String message, int langId) { 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."); Logger.w("tt9/DictionaryLoader.sendError", "Cannot send an error without a status Handler. Ignoring message.");
return; return;
} }
Bundle bundle = new Bundle(); Bundle errorMsg = new Bundle();
bundle.putString("error", message); errorMsg.putString("error", message);
bundle.putInt("languageId", langId); errorMsg.putInt("languageId", langId);
Message msg = new Message(); asyncHandler.post(() -> onStatusChange.accept(errorMsg));
msg.setData(bundle);
statusHandler.sendMessage(msg);
} }
private void sendImportError(String message, int langId, long fileLine, String word) { 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."); Logger.w("tt9/DictionaryLoader.sendError", "Cannot send an import error without a status Handler. Ignoring message.");
return; return;
} }
Bundle bundle = new Bundle(); Bundle errorMsg = new Bundle();
bundle.putString("error", message); errorMsg.putString("error", message);
bundle.putLong("fileLine", fileLine + 1); errorMsg.putLong("fileLine", fileLine + 1);
bundle.putInt("languageId", langId); errorMsg.putInt("languageId", langId);
bundle.putString("word", word); errorMsg.putString("word", word);
Message msg = new Message(); asyncHandler.post(() -> onStatusChange.accept(errorMsg));
msg.setData(bundle);
statusHandler.sendMessage(msg);
} }
} }

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

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

View file

@ -5,18 +5,63 @@ import android.content.Context;
import androidx.room.Database; import androidx.room.Database;
import androidx.room.Room; import androidx.room.Room;
import androidx.room.RoomDatabase; import androidx.room.RoomDatabase;
import androidx.sqlite.db.SimpleSQLiteQuery;
import io.github.sspanak.tt9.db.migrations.DB6; import io.github.sspanak.tt9.db.migrations.DB6;
import io.github.sspanak.tt9.db.migrations.DB7; 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 class TT9Room extends RoomDatabase {
public abstract WordsDao wordsDao(); public abstract WordsDao wordsDao();
public static synchronized TT9Room getInstance(Context context) { public static synchronized TT9Room getInstance(Context context) {
return Room return Room
.databaseBuilder(context, TT9Room.class, "t9dict.db") .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(); .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);
}
} }

View file

@ -8,7 +8,8 @@ import androidx.room.PrimaryKey;
@Entity( @Entity(
indices = { indices = {
@Index(value = {"lang", "word"}, unique = true), @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" tableName = "words"
) )
@ -26,4 +27,7 @@ public class Word {
@ColumnInfo(name = "freq") @ColumnInfo(name = "freq")
public int frequency; public int frequency;
@ColumnInfo(name = "len")
public int length;
} }

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

View file

@ -4,12 +4,20 @@ import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.OnConflictStrategy; import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.RawQuery;
import androidx.sqlite.db.SimpleSQLiteQuery;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Dao @Dao
public interface WordsDao { 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") @Query("SELECT COUNT(id) FROM words WHERE :langId < 0 OR lang = :langId")
int count(int langId); int count(int langId);
@ -31,17 +39,8 @@ public interface WordsDao {
) )
List<Word> getMany(int langId, int limit, String sequence, String word); List<Word> getMany(int langId, int limit, String sequence, String word);
@Query( @RawQuery(observedEntities = Word.class)
"SELECT * " + List<Word> getCustom(SimpleSQLiteQuery query);
"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);
@Insert @Insert
void insert(Word word); void insert(Word word);

View file

@ -1,9 +1,6 @@
package io.github.sspanak.tt9.ime; package io.github.sspanak.tt9.ime;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.util.HashMap; import java.util.HashMap;
@ -18,6 +15,7 @@ public class EmptyDatabaseWarning {
final int WARNING_INTERVAL; final int WARNING_INTERVAL;
private static final HashMap<Integer, Long> warningDisplayedTime = new HashMap<>(); private static final HashMap<Integer, Long> warningDisplayedTime = new HashMap<>();
private Context context;
private Language language; private Language language;
public EmptyDatabaseWarning(SettingsStore settings) { public EmptyDatabaseWarning(SettingsStore settings) {
@ -31,37 +29,33 @@ public class EmptyDatabaseWarning {
} }
public void emitOnce(Language language) { 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; return;
} }
this.language = language; warningDisplayedTime.put(language.getId(), System.currentTimeMillis());
DictionaryDb.areThereWords(handleWordCount, language); 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);
}
}
};
} }

View file

@ -2,9 +2,6 @@ package io.github.sspanak.tt9.ime;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
@ -251,7 +248,7 @@ public class TraditionalT9 extends KeyPadHandler {
protected boolean onLeft() { protected boolean onLeft() {
if (mInputMode.clearWordStem()) { if (mInputMode.clearWordStem()) {
mInputMode.loadSuggestions(handleSuggestionsAsync, getComposingText()); mInputMode.loadSuggestions(this::getSuggestions, getComposingText());
} else { } else {
jumpBeforeComposingText(); jumpBeforeComposingText();
} }
@ -269,7 +266,7 @@ public class TraditionalT9 extends KeyPadHandler {
} }
if (mInputMode.setWordStem(filter, repeat)) { if (mInputMode.setWordStem(filter, repeat)) {
mInputMode.loadSuggestions(handleSuggestionsAsync, filter); mInputMode.loadSuggestions(this::getSuggestions, filter);
} else if (filter.length() == 0) { } else if (filter.length() == 0) {
mInputMode.reset(); mInputMode.reset();
} }
@ -469,9 +466,7 @@ public class TraditionalT9 extends KeyPadHandler {
private void getSuggestions() { private void getSuggestions() {
if (!mInputMode.loadSuggestions(handleSuggestionsAsync, suggestionBar.getCurrentSuggestion())) { mInputMode.loadSuggestions(this::handleSuggestions, suggestionBar.getCurrentSuggestion());
handleSuggestions();
}
} }
@ -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) { private void setSuggestions(List<String> suggestions) {
setSuggestions(suggestions, 0); setSuggestions(suggestions, 0);
} }

View file

@ -1,7 +1,5 @@
package io.github.sspanak.tt9.ime.modes; package io.github.sspanak.tt9.ime.modes;
import android.os.Handler;
import java.util.ArrayList; import java.util.ArrayList;
import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.Logger;
@ -50,10 +48,18 @@ abstract public class InputMode {
public boolean onBackspace() { return false; } public boolean onBackspace() { return false; }
abstract public boolean onNumber(int number, boolean hold, int repeat); abstract public boolean onNumber(int number, boolean hold, int repeat);
// Predictions // Suggestions
public void onAcceptSuggestion(String suggestion) {} 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() { public ArrayList<String> getSuggestions() {
ArrayList<String> newSuggestions = new ArrayList<>(); ArrayList<String> newSuggestions = new ArrayList<>();

View file

@ -1,9 +1,5 @@
package io.github.sspanak.tt9.ime.modes; package io.github.sspanak.tt9.ime.modes;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.Logger;
@ -30,7 +26,8 @@ public class ModePredictive extends InputMode {
private String stem = ""; private String stem = "";
// async suggestion handling // async suggestion handling
private static Handler handleSuggestionsExternal; private boolean disablePredictions = false;
private Runnable onSuggestionsUpdated;
// text analysis tools // text analysis tools
private final AutoSpace autoSpace; private final AutoSpace autoSpace;
@ -73,10 +70,12 @@ public class ModePredictive extends InputMode {
// hold to type any digit // hold to type any digit
reset(); reset();
autoAcceptTimeout = 0; autoAcceptTimeout = 0;
disablePredictions = true;
suggestions.add(String.valueOf(number)); suggestions.add(String.valueOf(number));
} else { } else {
// words // words
super.reset(); super.reset();
disablePredictions = false;
digitSequence += number; digitSequence += number;
if (number == 0 && repeat > 0) { if (number == 0 && repeat > 0) {
autoAcceptTimeout = 0; autoAcceptTimeout = 0;
@ -104,6 +103,7 @@ public class ModePredictive extends InputMode {
public void reset() { public void reset() {
super.reset(); super.reset();
digitSequence = ""; digitSequence = "";
disablePredictions = false;
stem = ""; stem = "";
} }
@ -191,36 +191,35 @@ public class ModePredictive extends InputMode {
* See: Predictions.generatePossibleCompletions() * See: Predictions.generatePossibleCompletions()
*/ */
@Override @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 predictions
.setDigitSequence(digitSequence) .setDigitSequence(digitSequence)
.setIsStemFuzzy(isStemFuzzy) .setIsStemFuzzy(isStemFuzzy)
.setStem(stem) .setStem(stem)
.setLanguage(language) .setLanguage(language)
.setInputWord(currentWord) .setInputWord(currentWord)
.setWordsChangedHandler(handleSuggestions); .setWordsChangedHandler(this::getPredictions)
.load();
handleSuggestionsExternal = handler;
return predictions.load();
} }
/** /**
* handleSuggestions * getPredictions
* Extracts the suggestions from the Message object and passes them to the actual external Handler. * Gets the currently available Predictions and sends them over to the external caller.
* If there were no matches in the database, they will be generated based on the "lastInputFieldWord".
*/ */
private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) { private void getPredictions() {
@Override digitSequence = predictions.getDigitSequence();
public void handleMessage(Message m) { suggestions.clear();
digitSequence = predictions.getDigitSequence(); suggestions.addAll(predictions.getList());
suggestions.clear();
suggestions.addAll(predictions.getList());
onSuggestionsUpdated(handleSuggestionsExternal); onSuggestionsUpdated.run();
} }
};
/** /**

View file

@ -1,9 +1,5 @@
package io.github.sspanak.tt9.ime.modes.helpers; 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.ArrayList;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -24,7 +20,7 @@ public class Predictions {
private String inputWord; private String inputWord;
// async operations // async operations
private Handler wordsChangedHandler; private Runnable onWordsChanged = () -> {};
// data // data
private final ArrayList<String> words = new ArrayList<>(); private final ArrayList<String> words = new ArrayList<>();
@ -77,8 +73,9 @@ public class Predictions {
return this; return this;
} }
public void setWordsChangedHandler(Handler handler) { public Predictions setWordsChangedHandler(Runnable handler) {
wordsChangedHandler = handler; onWordsChanged = handler;
return this;
} }
public ArrayList<String> getList() { 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 * load
* Queries the dictionary database for a list of words matching the current language and * Queries the dictionary database for a list of words matching the current language and
* sequence or loads the static ones. * sequence or loads the static ones.
*
* Returns "false" on invalid digitSequence.
*/ */
public boolean load() { public void load() {
if (digitSequence == null || digitSequence.length() == 0) { if (digitSequence == null || digitSequence.length() == 0) {
words.clear(); words.clear();
onWordsChanged(); onWordsChanged.run();
return false; return;
} }
if (loadStatic()) { if (loadStatic()) {
onWordsChanged(); onWordsChanged.run();
} else { } else {
DictionaryDb.getSuggestions( DictionaryDb.getWords(
dbWordsHandler, this::onDbWords,
language, language,
digitSequence, digitSequence,
stem, stem,
@ -147,8 +132,6 @@ public class Predictions {
settings.getSuggestionsMax() settings.getSuggestionsMax()
); );
} }
return true;
} }
@ -160,20 +143,20 @@ public class Predictions {
private boolean loadStatic() { private boolean loadStatic() {
// whitespace/special/math characters // whitespace/special/math characters
if (digitSequence.equals("0")) { if (digitSequence.equals("0")) {
words.clear();
stem = ""; stem = "";
words.clear();
words.addAll(language.getKeyCharacters(0, false)); words.addAll(language.getKeyCharacters(0, false));
} }
// "00" is a shortcut for the preferred character // "00" is a shortcut for the preferred character
else if (digitSequence.equals("00")) { else if (digitSequence.equals("00")) {
words.clear();
stem = ""; stem = "";
words.clear();
words.add(settings.getDoubleZeroChar()); words.add(settings.getDoubleZeroChar());
} }
// emoji // emoji
else if (containsOnly1Regex.matcher(digitSequence).matches()) { else if (containsOnly1Regex.matcher(digitSequence).matches()) {
words.clear();
stem = ""; stem = "";
words.clear();
if (digitSequence.length() == 1) { if (digitSequence.length() == 1) {
words.addAll(language.getKeyCharacters(1, false)); words.addAll(language.getKeyCharacters(1, false));
} else { } else {
@ -190,29 +173,23 @@ public class Predictions {
/** /**
* dbWordsHandler * dbWordsHandler
* Extracts the words from the Message object, generates extra words, if necessary, then * Callback for when the database has finished loading words. If there were no matches in the database,
* notifies the external handler it is now possible to use "getList()". * they will be generated based on the "inputWord". After the word list is compiled, it notifies the
* If there were no matches in the database, they will be generated based on the "inputWord". * external handler it is now possible to use it with "getList()".
*/ */
private final Handler dbWordsHandler = new Handler(Looper.getMainLooper()) { private void onDbWords (ArrayList<String> dbWords) {
@Override if (dbWords.size() == 0 && digitSequence.length() > 0) {
public void handleMessage(Message msg) { emptyDbWarning.emitOnce(language);
ArrayList<String> dbWords = msg.getData().getStringArrayList("suggestions"); dbWords = generatePossibleCompletions(inputWord);
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();
} }
};
words.clear();
suggestStem();
suggestMissingWords(generatePossibleStemVariations(dbWords));
suggestMissingWords(dbWords);
onWordsChanged.run();
}
/** /**

View file

@ -46,7 +46,7 @@ public class Characters {
public static ArrayList<String> getEmoji(int level) { public static ArrayList<String> getEmoji(int level) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return TextEmoticons; return new ArrayList<>(TextEmoticons);
} }
level = (Emoji.size() > level) ? level : Emoji.size() - 1; 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);
} }
} }

View file

@ -1,9 +1,7 @@
package io.github.sspanak.tt9.preferences.items; package io.github.sspanak.tt9.preferences.items;
import android.content.Context; import android.content.Context;
import android.os.Handler; import android.os.Bundle;
import android.os.Looper;
import android.os.Message;
import androidx.preference.Preference; import androidx.preference.Preference;
@ -36,7 +34,7 @@ public class ItemLoadDictionary extends ItemClickable {
this.progressBar = progressBar; this.progressBar = progressBar;
this.settings = settings; this.settings = settings;
loader.setStatusHandler(onDictionaryLoading); loader.setOnStatusChange(this::onLoadingStatusChange);
if (!progressBar.isCompleted() && !progressBar.isFailed()) { if (!progressBar.isCompleted() && !progressBar.isFailed()) {
changeToCancelButton(); changeToCancelButton();
@ -46,23 +44,20 @@ public class ItemLoadDictionary extends ItemClickable {
} }
private final Handler onDictionaryLoading = new Handler(Looper.getMainLooper()) { private void onLoadingStatusChange(Bundle status) {
@Override progressBar.show(status);
public void handleMessage(Message msg) { item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
progressBar.show(msg.getData());
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
if (progressBar.isCancelled()) { if (progressBar.isCancelled()) {
changeToLoadButton(); changeToLoadButton();
} else if (progressBar.isFailed()) { } else if (progressBar.isFailed()) {
changeToLoadButton(); changeToLoadButton();
UI.toast(context, progressBar.getMessage()); UI.toastFromAsync(context, progressBar.getMessage());
} else if (progressBar.isCompleted()) { } else if (progressBar.isCompleted()) {
changeToLoadButton(); changeToLoadButton();
UI.toast(context, R.string.dictionary_loaded); UI.toastFromAsync(context, R.string.dictionary_loaded);
}
} }
}; }
@Override @Override

View file

@ -1,9 +1,6 @@
package io.github.sspanak.tt9.preferences.items; package io.github.sspanak.tt9.preferences.items;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.preference.Preference; import androidx.preference.Preference;
@ -28,12 +25,6 @@ public class ItemTruncateAll extends ItemClickable {
this.loader = loader; 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 @Override
protected boolean onClick(Preference p) { protected boolean onClick(Preference p) {
@ -42,7 +33,7 @@ public class ItemTruncateAll extends ItemClickable {
loadItem.changeToLoadButton(); loadItem.changeToLoadButton();
} }
DictionaryDb.deleteWords(onDictionaryTruncated); DictionaryDb.deleteWords(() -> UI.toastFromAsync(context, R.string.dictionary_truncated));
return true; return true;
} }

View file

@ -1,9 +1,6 @@
package io.github.sspanak.tt9.preferences.items; package io.github.sspanak.tt9.preferences.items;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.preference.Preference; import androidx.preference.Preference;
@ -35,12 +32,6 @@ public class ItemTruncateUnselected extends ItemClickable {
this.loader = loader; 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 @Override
protected boolean onClick(Preference p) { 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; return true;
} }

View file

@ -2,9 +2,6 @@ package io.github.sspanak.tt9.ui;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View; import android.view.View;
import android.widget.EditText; import android.widget.EditText;
@ -50,30 +47,27 @@ public class AddWordAct extends AppCompatActivity {
} }
private final Handler onAddedWord = new Handler(Looper.getMainLooper()) { private void onAddedWord(int statusCode) {
@Override switch (statusCode) {
public void handleMessage(Message msg) { case 0:
switch (msg.what) { Logger.d("onAddedWord", "Successfully added word: '" + word + '"');
case 0: settings.saveLastWord(word);
Logger.d("onAddedWord", "Added word: '" + word + "'..."); break;
settings.saveLastWord(word);
break;
case 1: case 1:
UI.toastLong( UI.toastLongFromAsync(
main.getContext(), main.getContext(),
getResources().getString(R.string.add_word_exist, word) getResources().getString(R.string.add_word_exist, word)
); );
break; break;
default: default:
UI.toastLong(main.getContext(), R.string.error_unexpected); UI.toastLongFromAsync(main.getContext(), R.string.error_unexpected);
break; break;
} }
finish(); finish();
} }
};
public void addWord(View v) { public void addWord(View v) {
try { try {
@ -81,7 +75,7 @@ public class AddWordAct extends AppCompatActivity {
word = ((EditText) main.findViewById(R.id.add_word_text)).getText().toString(); word = ((EditText) main.findViewById(R.id.add_word_text)).getText().toString();
Logger.d("addWord", "Attempting to add word: '" + word + "'..."); 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) { } catch (InsertBlankWordException e) {
Logger.e("AddWordAct.addWord", e.getMessage()); Logger.e("AddWordAct.addWord", e.getMessage());
UI.toastLong(this, R.string.add_word_blank); UI.toastLong(this, R.string.add_word_blank);
@ -96,6 +90,6 @@ public class AddWordAct extends AppCompatActivity {
public void cancelAddingWord(View v) { public void cancelAddingWord(View v) {
this.finish(); finish();
} }
} }

View file

@ -2,6 +2,7 @@ package io.github.sspanak.tt9.ui;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Looper;
import android.widget.Toast; import android.widget.Toast;
import io.github.sspanak.tt9.ime.TraditionalT9; import io.github.sspanak.tt9.ime.TraditionalT9;
@ -30,15 +31,47 @@ public class UI {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); 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) { public static void toast(Context context, int resourceId) {
Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show(); 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) { public static void toastLong(Context context, int resourceId) {
Toast.makeText(context, resourceId, Toast.LENGTH_LONG).show(); 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) { public static void toastLong(Context context, CharSequence msg) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); 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();
}
} }