diff --git a/src/io/github/sspanak/tt9/ConsumerCompat.java b/src/io/github/sspanak/tt9/ConsumerCompat.java new file mode 100644 index 00000000..f0cf742f --- /dev/null +++ b/src/io/github/sspanak/tt9/ConsumerCompat.java @@ -0,0 +1,10 @@ +package io.github.sspanak.tt9; + +/** + * ConsumerCompat + * A fallback interface for Consumer in API < 24 + */ +public interface ConsumerCompat{ + void accept(T t); + default ConsumerCompat andThen(ConsumerCompat after) {return null;} +} diff --git a/src/io/github/sspanak/tt9/Logger.java b/src/io/github/sspanak/tt9/Logger.java index 20fc723c..b19cbd0f 100644 --- a/src/io/github/sspanak/tt9/Logger.java +++ b/src/io/github/sspanak/tt9/Logger.java @@ -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); diff --git a/src/io/github/sspanak/tt9/db/DictionaryDb.java b/src/io/github/sspanak/tt9/db/DictionaryDb.java index b23388c2..92d8cb3c 100644 --- a/src/io/github/sspanak/tt9/db/DictionaryDb.java +++ b/src/io/github/sspanak/tt9/db/DictionaryDb.java @@ -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 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 languageIds) { + public static void deleteWords(Runnable notification, ArrayList 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 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 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 loadWordsExact(Language language, String sequence, String filter, int maximumWords) { long start = System.currentTimeMillis(); - List 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 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 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 loadWordsFuzzy(Language language, String sequence, String filter, int maximumWords) { long start = System.currentTimeMillis(); - List 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 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 data) { - Bundle bundle = new Bundle(); - bundle.putStringArrayList("suggestions", data); - Message msg = new Message(); - msg.setData(bundle); - handler.sendMessage(msg); + private static void sendWords(ConsumerCompat> dataHandler, ArrayList 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> 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 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 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(); } diff --git a/src/io/github/sspanak/tt9/db/DictionaryLoader.java b/src/io/github/sspanak/tt9/db/DictionaryLoader.java index b9adad52..1f310a9e 100644 --- a/src/io/github/sspanak/tt9/db/DictionaryLoader.java +++ b/src/io/github/sspanak/tt9/db/DictionaryLoader.java @@ -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 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 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 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)); } } diff --git a/src/io/github/sspanak/tt9/db/migrations/DB8.java b/src/io/github/sspanak/tt9/db/migrations/DB8.java new file mode 100644 index 00000000..12334ab9 --- /dev/null +++ b/src/io/github/sspanak/tt9/db/migrations/DB8.java @@ -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(); + } + } + }; +} diff --git a/src/io/github/sspanak/tt9/db/migrations/DB9.java b/src/io/github/sspanak/tt9/db/migrations/DB9.java new file mode 100644 index 00000000..68675718 --- /dev/null +++ b/src/io/github/sspanak/tt9/db/migrations/DB9.java @@ -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(); + } + } + }; +} diff --git a/src/io/github/sspanak/tt9/db/room/TT9Room.java b/src/io/github/sspanak/tt9/db/room/TT9Room.java index 49429f05..db75aadd 100644 --- a/src/io/github/sspanak/tt9/db/room/TT9Room.java +++ b/src/io/github/sspanak/tt9/db/room/TT9Room.java @@ -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); + } } diff --git a/src/io/github/sspanak/tt9/db/room/Word.java b/src/io/github/sspanak/tt9/db/room/Word.java index 8ca8d0c3..17ff2d83 100644 --- a/src/io/github/sspanak/tt9/db/room/Word.java +++ b/src/io/github/sspanak/tt9/db/room/Word.java @@ -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; } diff --git a/src/io/github/sspanak/tt9/db/room/WordList.java b/src/io/github/sspanak/tt9/db/room/WordList.java new file mode 100644 index 00000000..c8b972a2 --- /dev/null +++ b/src/io/github/sspanak/tt9/db/room/WordList.java @@ -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 { + public WordList() { + super(); + } + + public WordList(List 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 toStringList() { + ArrayList strings = new ArrayList<>(); + for (int i = 0; i < size(); i++) { + strings.add(get(i).word); + } + return strings; + } +} diff --git a/src/io/github/sspanak/tt9/db/room/WordsDao.java b/src/io/github/sspanak/tt9/db/room/WordsDao.java index bcffb548..35feafc5 100644 --- a/src/io/github/sspanak/tt9/db/room/WordsDao.java +++ b/src/io/github/sspanak/tt9/db/room/WordsDao.java @@ -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 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 getFuzzy(int langId, int limit, String sequence, String word); + @RawQuery(observedEntities = Word.class) + List getCustom(SimpleSQLiteQuery query); @Insert void insert(Word word); diff --git a/src/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java b/src/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java index 146ef728..fbd99392 100644 --- a/src/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java +++ b/src/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java @@ -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 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); - } - } - }; } diff --git a/src/io/github/sspanak/tt9/ime/TraditionalT9.java b/src/io/github/sspanak/tt9/ime/TraditionalT9.java index 76c72cf9..60951a5f 100644 --- a/src/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/src/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -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 suggestions) { setSuggestions(suggestions, 0); } diff --git a/src/io/github/sspanak/tt9/ime/modes/InputMode.java b/src/io/github/sspanak/tt9/ime/modes/InputMode.java index 56946bd8..52d46ada 100644 --- a/src/io/github/sspanak/tt9/ime/modes/InputMode.java +++ b/src/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -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 getSuggestions() { ArrayList newSuggestions = new ArrayList<>(); diff --git a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java index 1bf96e1e..c40101ac 100644 --- a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java +++ b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -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(); + } /** diff --git a/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java b/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java index 714c60f6..e953df2e 100644 --- a/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java +++ b/src/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java @@ -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 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 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 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 dbWords) { + if (dbWords.size() == 0 && digitSequence.length() > 0) { + emptyDbWarning.emitOnce(language); + dbWords = generatePossibleCompletions(inputWord); } - }; + + words.clear(); + suggestStem(); + suggestMissingWords(generatePossibleStemVariations(dbWords)); + suggestMissingWords(dbWords); + + onWordsChanged.run(); + } /** diff --git a/src/io/github/sspanak/tt9/languages/Characters.java b/src/io/github/sspanak/tt9/languages/Characters.java index d20f2c1b..6197de1b 100644 --- a/src/io/github/sspanak/tt9/languages/Characters.java +++ b/src/io/github/sspanak/tt9/languages/Characters.java @@ -46,7 +46,7 @@ public class Characters { public static ArrayList 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); } } diff --git a/src/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java b/src/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java index d91afefb..ffdcaed9 100644 --- a/src/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java +++ b/src/io/github/sspanak/tt9/preferences/items/ItemLoadDictionary.java @@ -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 diff --git a/src/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java b/src/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java index 4798cf13..b7949c5a 100644 --- a/src/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java +++ b/src/io/github/sspanak/tt9/preferences/items/ItemTruncateAll.java @@ -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; } diff --git a/src/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java b/src/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java index bbd9f72f..37fd717d 100644 --- a/src/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java +++ b/src/io/github/sspanak/tt9/preferences/items/ItemTruncateUnselected.java @@ -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; } diff --git a/src/io/github/sspanak/tt9/ui/AddWordAct.java b/src/io/github/sspanak/tt9/ui/AddWordAct.java index 005208c7..5357f907 100644 --- a/src/io/github/sspanak/tt9/ui/AddWordAct.java +++ b/src/io/github/sspanak/tt9/ui/AddWordAct.java @@ -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(); } } diff --git a/src/io/github/sspanak/tt9/ui/UI.java b/src/io/github/sspanak/tt9/ui/UI.java index c679595a..acb996b1 100644 --- a/src/io/github/sspanak/tt9/ui/UI.java +++ b/src/io/github/sspanak/tt9/ui/UI.java @@ -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(); + } }