diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b1c53ff..3c5869ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: - name: Validate Dictionaries run: ./gradlew validateLanguages - name: Build Languages - run: ./gradlew copyLanguages calculateDictionarySizes + run: ./gradlew copyLanguages writeDictionaryProperties - name: Lint run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks - name: Build Release APK diff --git a/app/build.gradle b/app/build.gradle index 920cb282..d9326f9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,12 +25,14 @@ tasks.register('copyLanguages', Copy) { into LANGUAGES_OUTPUT_DIR } -tasks.register('calculateDictionarySizes') { +tasks.register('writeDictionaryProperties') { inputs.dir fileTree(dir: DICTIONARIES_INPUT_DIR) outputs.dir DICTIONARIES_OUTPUT_DIR doLast { - getDictionarySizes(DICTIONARIES_INPUT_DIR, DICTIONARIES_OUTPUT_DIR) + [getDictionarySizes, getDictionaryHashes].parallelStream().forEach { action -> + action(DICTIONARIES_INPUT_DIR, DICTIONARIES_OUTPUT_DIR) + } } } @@ -89,12 +91,12 @@ android { applicationVariants.configureEach { variant -> tasks.named("generate${variant.name.capitalize()}Assets")?.configure { - dependsOn(validateLanguages, copyLanguages, calculateDictionarySizes) + dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties) } ["lintAnalyzeDebug", "generateDebugLintReportModel", "lintVitalAnalyzeRelease", "generateReleaseLintVitalReportModel"].each { taskName -> tasks.named(taskName)?.configure { - dependsOn(validateLanguages, copyLanguages, calculateDictionarySizes) + dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties) } } diff --git a/app/dictionary-tools.gradle b/app/dictionary-tools.gradle index bfa04cae..d3a01f5b 100644 --- a/app/dictionary-tools.gradle +++ b/app/dictionary-tools.gradle @@ -5,9 +5,9 @@ ext.getDictionarySizes = { dictionariesDir, sizesDir -> } } -ext.getDictionaryTimestamps = { dictionariesDir, timestampsDir -> +ext.getDictionaryHashes = { dictionariesDir, timestampsDir -> fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary -> - def dictionaryTimestamp = dictionary.exists() ? dictionary.lastModified() : 0 - new File(timestampsDir, "${dictionary.getName()}.timestamp").text = dictionaryTimestamp + def hash = dictionary.exists() ? dictionary.text.digest("SHA-1") : "" + new File(timestampsDir, "${dictionary.getName()}.hash").text = hash } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9128e049..08db6328 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,8 +31,8 @@ + android:label="" + android:name="io.github.sspanak.tt9.ui.PopupDialogActivity" + android:theme="@style/alertDialog" /> diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java index 090a078e..c97e946f 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java @@ -6,24 +6,26 @@ import android.os.Bundle; import android.os.Handler; import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Locale; import io.github.sspanak.tt9.ConsumerCompat; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.db.entities.WordBatch; +import io.github.sspanak.tt9.db.entities.WordFile; import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException; import io.github.sspanak.tt9.db.exceptions.DictionaryImportException; import io.github.sspanak.tt9.db.sqlite.DeleteOps; import io.github.sspanak.tt9.db.sqlite.InsertOps; import io.github.sspanak.tt9.db.sqlite.SQLiteOpener; import io.github.sspanak.tt9.db.sqlite.Tables; +import io.github.sspanak.tt9.ime.TraditionalT9; import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException; import io.github.sspanak.tt9.languages.InvalidLanguageException; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.SettingsStore; +import io.github.sspanak.tt9.ui.DictionaryLoadingBar; +import io.github.sspanak.tt9.ui.UI; public class DictionaryLoader { private static final String LOG_TAG = "DictionaryLoader"; @@ -101,6 +103,34 @@ public class DictionaryLoader { } + public static void load(Context context, Language language) { + DictionaryLoadingBar progressBar = new DictionaryLoadingBar(context); + getInstance(context).setOnStatusChange(status -> progressBar.show(context, status)); + self.load(new ArrayList() {{ add(language); }}); + } + + + public static void autoLoad(TraditionalT9 context, Language language) { + if (getInstance(context).isRunning()) { + return; + } + + WordStoreAsync.getLastLanguageUpdateTime( + (hash) -> { + // no words at all, load without confirmation + if (hash.isEmpty()) { + load(context, language); + } + // or if the database is outdated, compared to the dictionary file, ask for confirmation and load + else if (!hash.equals(new WordFile(language.getDictionaryFile(), self.assets).getHash())) { + UI.showConfirmDictionaryUpdateDialog(context, language.getId()); + } + }, + language + ); + } + + public void stop() { loadThread.interrupt(); } @@ -210,22 +240,21 @@ public class DictionaryLoader { private void importWordFile(Language language, int positionShift, float minProgress, float maxProgress) throws Exception { + WordFile wordFile = new WordFile(language.getDictionaryFile(), assets); + WordBatch batch = new WordBatch(language, wordFile.getTotalLines()); int currentLine = 1; - int totalLines = getFileSize(language.getDictionaryFile()); - float progressRatio = (maxProgress - minProgress) / totalLines; + float progressRatio = (maxProgress - minProgress) / wordFile.getTotalLines(); - WordBatch batch = new WordBatch(language, totalLines); - - try (BufferedReader br = new BufferedReader(new InputStreamReader(assets.open(language.getDictionaryFile()), StandardCharsets.UTF_8))) { + try (BufferedReader br = wordFile.getReader()) { for (String line; (line = br.readLine()) != null; currentLine++) { if (loadThread.isInterrupted()) { sendProgressMessage(language, 0, 0); throw new DictionaryImportAbortedException(); } - String[] parts = splitLine(line); + String[] parts = WordFile.splitLine(line); String word = parts[0]; - short frequency = getFrequency(parts); + short frequency = WordFile.getFrequencyFromLineParts(parts); try { boolean isFinalized = batch.add(word, frequency, currentLine + positionShift); @@ -237,14 +266,14 @@ public class DictionaryLoader { throw new DictionaryImportException(word, currentLine); } - if (totalLines > 0) { + if (wordFile.getTotalLines() > 0) { sendProgressMessage(language, minProgress + progressRatio * currentLine, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME); } } } saveWordBatch(batch); - InsertOps.insertLanguageMeta(sqlite.getDb(), language.getId()); + InsertOps.replaceLanguageMeta(sqlite.getDb(), language.getId(), wordFile.getHash()); } @@ -261,44 +290,6 @@ public class DictionaryLoader { } - private String[] splitLine(String line) { - String[] parts = { line, "" }; - - // This is faster than String.split() by around 10%, so it's worth having it. - // It runs very often, so any other optimizations are welcome. - for (int i = 0 ; i < line.length(); i++) { - if (line.charAt(i) == ' ') { // the delimiter is TAB - parts[0] = line.substring(0, i); - parts[1] = i < line.length() - 1 ? line.substring(i + 1) : ""; - break; - } - } - - return parts; - } - - - private int getFileSize(String filename) { - String sizeFilename = filename + ".size"; - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(sizeFilename), StandardCharsets.UTF_8))) { - return Integer.parseInt(reader.readLine()); - } catch (Exception e) { - Logger.w(LOG_TAG, "Could not read the size of: " + filename + " from: " + sizeFilename + ". " + e.getMessage()); - return 0; - } - } - - - private short getFrequency(String[] lineParts) { - try { - return Short.parseShort(lineParts[1]); - } catch (Exception e) { - return 0; - } - } - - private void sendStartMessage(int fileCount) { if (onStatusChange == null) { Logger.w(LOG_TAG, "Cannot send file count without a status Handler. Ignoring message."); diff --git a/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java b/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java index 71252921..955f9ccc 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/WordStore.java @@ -18,7 +18,7 @@ import io.github.sspanak.tt9.ime.TraditionalT9; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.Text; import io.github.sspanak.tt9.preferences.SettingsStore; -import io.github.sspanak.tt9.ui.AddWordAct; +import io.github.sspanak.tt9.ui.dialogs.AddWordDialog; public class WordStore { @@ -90,6 +90,11 @@ public class WordStore { } + @NonNull public String getLanguageFileHash(Language language) { + return language != null && checkOrNotify() ? readOps.getLanguageFileHash(sqlite.getDb(), language.getId()) : ""; + } + + public boolean exists(Language language) { return language != null && checkOrNotify() && readOps.exists(sqlite.getDb(), language.getId()); } @@ -120,20 +125,20 @@ public class WordStore { public int put(Language language, String word) { if (word == null || word.isEmpty()) { - return AddWordAct.CODE_BLANK_WORD; + return AddWordDialog.CODE_BLANK_WORD; } if (language == null) { - return AddWordAct.CODE_INVALID_LANGUAGE; + return AddWordDialog.CODE_INVALID_LANGUAGE; } if (!checkOrNotify()) { - return AddWordAct.CODE_GENERAL_ERROR; + return AddWordDialog.CODE_GENERAL_ERROR; } try { if (readOps.exists(sqlite.getDb(), language, word)) { - return AddWordAct.CODE_WORD_EXISTS; + return AddWordDialog.CODE_WORD_EXISTS; } String sequence = language.getDigitSequenceForWord(word); @@ -146,10 +151,10 @@ public class WordStore { } catch (Exception e) { String msg = "Failed inserting word: '" + word + "' for language: " + language.getId() + ". " + e.getMessage(); Logger.e("insertWord", msg); - return AddWordAct.CODE_GENERAL_ERROR; + return AddWordDialog.CODE_GENERAL_ERROR; } - return AddWordAct.CODE_SUCCESS; + return AddWordDialog.CODE_SUCCESS; } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java b/app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java index f36d23c8..bbec4339 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/WordStoreAsync.java @@ -39,6 +39,10 @@ public class WordStoreAsync { new Thread(() -> notification.accept(getStore().exists(language))).start(); } + public static void getLastLanguageUpdateTime(ConsumerCompat notification, Language language) { + new Thread(() -> notification.accept(getStore().getLanguageFileHash(language))).start(); + } + public static void deleteWords(Runnable notification, @NonNull ArrayList languageIds) { new Thread(() -> { diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java new file mode 100644 index 00000000..5ac732bc --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java @@ -0,0 +1,85 @@ +package io.github.sspanak.tt9.db.entities; + +import android.content.res.AssetManager; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import io.github.sspanak.tt9.Logger; + +public class WordFile { + private static final String LOG_TAG = WordFile.class.getSimpleName(); + + private final AssetManager assets; + private final String name; + private String hash = null; + private int totalLines = -1; + + public WordFile(String name, AssetManager assets) { + this.assets = assets; + this.name = name; + } + + public static String[] splitLine(String line) { + String[] parts = { line, "" }; + + // This is faster than String.split() by around 10%, so it's worth having it. + // It runs very often, so any other optimizations are welcome. + for (int i = 0 ; i < line.length(); i++) { + if (line.charAt(i) == ' ') { // the delimiter is TAB + parts[0] = line.substring(0, i); + parts[1] = i < line.length() - 1 ? line.substring(i + 1) : ""; + break; + } + } + + return parts; + } + + public static short getFrequencyFromLineParts(String[] frequencyParts) { + try { + return Short.parseShort(frequencyParts[1]); + } catch (Exception e) { + return 0; + } + } + + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(assets.open(name), StandardCharsets.UTF_8)); + } + + public int getTotalLines() { + if (totalLines < 0) { + String rawTotalLines = getProperty("size"); + try { + totalLines = Integer.parseInt(rawTotalLines); + } catch (Exception e) { + Logger.w(LOG_TAG, "Invalid 'size' property of: " + name + ". Expecting an integer, got: '" + rawTotalLines + "'."); + totalLines = 0; + } + } + + return totalLines; + } + + public String getHash() { + if (hash == null) { + hash = getProperty("hash"); + } + + return hash; + } + + private String getProperty(String propertyName) { + String propertyFilename = name + "." + propertyName; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(propertyFilename)))) { + return reader.readLine(); + } catch (Exception e) { + Logger.w(LOG_TAG, "Could not read the '" + propertyName + "' property of: " + name + " from: " + propertyFilename + ". " + e.getMessage()); + return ""; + } + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java index e747f4fe..1b8dd1ca 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/InsertOps.java @@ -39,9 +39,10 @@ public class InsertOps { } - public static void insertLanguageMeta(@NonNull SQLiteDatabase db, int langId) { - SQLiteStatement query = CompiledQueryCache.get(db, "REPLACE INTO " + Tables.LANGUAGES_META + " (langId) VALUES (?)"); + public static void replaceLanguageMeta(@NonNull SQLiteDatabase db, int langId, String fileHash) { + SQLiteStatement query = CompiledQueryCache.get(db, "REPLACE INTO " + Tables.LANGUAGES_META + " (langId, fileHash) VALUES (?, ?)"); query.bindLong(1, langId); + query.bindString(2, fileHash); query.execute(); } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Migration.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Migration.java new file mode 100644 index 00000000..78bf5d0b --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Migration.java @@ -0,0 +1,17 @@ +package io.github.sspanak.tt9.db.sqlite; + +class Migration { + static final Migration[] LIST = { + new Migration( + "ALTER TABLE " + Tables.LANGUAGES_META + " ADD COLUMN fileHash TEXT NOT NULL DEFAULT 0", + true + ) + }; + + String query; + boolean mayFail; + private Migration(String query, boolean mayFail) { + this.query = query; + this.mayFail = mayFail; + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java index 7322d154..f41bdfaa 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java @@ -48,6 +48,20 @@ public class ReadOps { } + /** + * Gets the timestamp of the language file at the time of the last import into the database. + */ + public String getLanguageFileHash(@NonNull SQLiteDatabase db, int langId) { + SQLiteStatement query = CompiledQueryCache.get(db, "SELECT fileHash FROM " + Tables.LANGUAGES_META + " WHERE langId = ?"); + query.bindLong(1, langId); + try { + return query.simpleQueryForString(); + } catch (SQLiteDoneException e) { + return ""; + } + } + + @NonNull public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { if (positions.isEmpty()) { diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java index f6def4ed..4bb8e185 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java @@ -7,10 +7,12 @@ import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import io.github.sspanak.tt9.BuildConfig; +import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; public class SQLiteOpener extends SQLiteOpenHelper { + private static final String LOG_TAG = SQLiteOpener.class.getSimpleName(); private static final String DATABASE_NAME = "tt9.db"; private static final int DATABASE_VERSION = BuildConfig.VERSION_CODE; private static SQLiteOpener self; @@ -52,6 +54,19 @@ public class SQLiteOpener extends SQLiteOpenHelper { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { onCreate(db); + for (Migration migration : Migration.LIST) { + try { + db.execSQL(migration.query); + Logger.d(LOG_TAG, "Migration succeeded: '" + migration.query); + } catch (Exception e) { + if (migration.mayFail) { + Logger.e(LOG_TAG, "Ignoring migration: '" + migration.query + "'. "); + } else { + Logger.e(LOG_TAG, "Migration failed: '" + migration.query + "'. " + e.getMessage() + "\nAborting all subsequent migrations."); + break; + } + } + } } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java index cdeba401..6d8319f0 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/Tables.java @@ -103,7 +103,8 @@ public class Tables { private static String createLanguagesMeta() { return "CREATE TABLE IF NOT EXISTS " + LANGUAGES_META + " (" + "langId INTEGER UNIQUE NOT NULL, " + - "normalizationPending INT2 NOT NULL DEFAULT 0 " + + "normalizationPending INT2 NOT NULL DEFAULT 0," + + "fileHash TEXT NOT NULL DEFAULT 0 " + ")"; } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java b/app/src/main/java/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java deleted file mode 100644 index 7ebc2a58..00000000 --- a/app/src/main/java/io/github/sspanak/tt9/ime/EmptyDatabaseWarning.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.sspanak.tt9.ime; - -import android.content.Context; - -import java.util.HashMap; - -import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.WordStoreAsync; -import io.github.sspanak.tt9.languages.Language; -import io.github.sspanak.tt9.languages.LanguageCollection; -import io.github.sspanak.tt9.preferences.SettingsStore; -import io.github.sspanak.tt9.ui.UI; - -public class EmptyDatabaseWarning { - private static final HashMap warningDisplayedTime = new HashMap<>(); - - private Context context; - private Language language; - - public EmptyDatabaseWarning() { - for (Language lang : LanguageCollection.getAll(context)) { - if (!warningDisplayedTime.containsKey(lang.getId())) { - warningDisplayedTime.put(lang.getId(), 0L); - } - } - } - - public void emitOnce(Language language) { - context = context == null ? TraditionalT9.getMainContext() : context; - this.language = language; - - if (isItTimeAgain(TraditionalT9.getMainContext())) { - WordStoreAsync.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 > SettingsStore.DICTIONARY_MISSING_WARNING_INTERVAL; - } - - private void show(boolean areThereWords) { - if (areThereWords) { - return; - } - - warningDisplayedTime.put(language.getId(), System.currentTimeMillis()); - UI.toastLongFromAsync( - context, - context.getString(R.string.dictionary_missing_go_load_it, language.getName()) - ); - } -} diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java b/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java index 19956d0f..36534082 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -31,7 +31,7 @@ import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; import io.github.sspanak.tt9.preferences.SettingsStore; import io.github.sspanak.tt9.preferences.helpers.Hotkeys; -import io.github.sspanak.tt9.ui.AddWordAct; +import io.github.sspanak.tt9.ui.PopupDialogActivity; import io.github.sspanak.tt9.ui.UI; import io.github.sspanak.tt9.ui.main.MainView; import io.github.sspanak.tt9.ui.tray.StatusBar; @@ -139,7 +139,7 @@ public class TraditionalT9 extends KeyPadHandler { public int onStartCommand(Intent intent, int flags, int startId) { int result = super.onStartCommand(intent, flags, startId); - String message = intent != null ? intent.getStringExtra(AddWordAct.INTENT_FILTER) : null; + String message = intent != null ? intent.getStringExtra(PopupDialogActivity.DIALOG_CLOSED_INTENT) : null; if (message != null && !message.isEmpty()) { forceShowWindowIfHidden(); UI.toastLong(self, message); @@ -239,6 +239,8 @@ public class TraditionalT9 extends KeyPadHandler { clearSuggestions(); statusBar.setText("--"); + DictionaryLoader.autoLoad(this, mLanguage); + normalizationHandler.removeCallbacksAndMessages(null); normalizationHandler.postDelayed( () -> { if (!DictionaryLoader.getInstance(this).isRunning()) WordStoreAsync.normalizeNext(); }, @@ -483,6 +485,10 @@ public class TraditionalT9 extends KeyPadHandler { mainView.render(); forceShowWindowIfHidden(); + if (mInputMode instanceof ModePredictive) { + DictionaryLoader.autoLoad(this, mLanguage); + } + return true; } @@ -499,9 +505,8 @@ public class TraditionalT9 extends KeyPadHandler { scheduleAutoAccept(mInputMode.getAutoAcceptTimeout()); // restart the timer nextInputMode(); mainView.render(); - - forceShowWindowIfHidden(); + return true; } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java b/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java index 7e3fff3a..34e4ffaa 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/modes/helpers/Predictions.java @@ -3,12 +3,10 @@ package io.github.sspanak.tt9.ime.modes.helpers; import java.util.ArrayList; import io.github.sspanak.tt9.db.WordStoreAsync; -import io.github.sspanak.tt9.ime.EmptyDatabaseWarning; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.preferences.SettingsStore; public class Predictions { - private final EmptyDatabaseWarning emptyDbWarning; private Language language; private String digitSequence; @@ -25,7 +23,6 @@ public class Predictions { public Predictions() { - emptyDbWarning = new EmptyDatabaseWarning(); } @@ -155,10 +152,6 @@ public class Predictions { return; } - if (dbWords.isEmpty() && !digitSequence.isEmpty()) { - emptyDbWarning.emitOnce(language); - } - words.clear(); suggestStem(); suggestMissingWords(generatePossibleStemVariations(dbWords)); diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/SettingsStore.java index 412f591b..e0e8d2b3 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/SettingsStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/SettingsStore.java @@ -280,7 +280,7 @@ public class SettingsStore { public final static int DICTIONARY_IMPORT_BATCH_SIZE = 5000; // words public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms - public final static int DICTIONARY_MISSING_WARNING_INTERVAL = 30000; // ms + public final static int DICTIONARY_CONFIRM_UPDATE_COOLDOWN_TIME = 120000; // ms public final static byte SLOW_QUERY_TIME = 50; // ms public final static int SOFT_KEY_REPEAT_DELAY = 40; // ms public final static float SOFT_KEY_COMPLEX_LABEL_TITLE_SIZE = 0.55f; diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/AddWordAct.java b/app/src/main/java/io/github/sspanak/tt9/ui/AddWordAct.java deleted file mode 100644 index 26e10712..00000000 --- a/app/src/main/java/io/github/sspanak/tt9/ui/AddWordAct.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.github.sspanak.tt9.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; - -import io.github.sspanak.tt9.Logger; -import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.WordStoreAsync; -import io.github.sspanak.tt9.ime.TraditionalT9; -import io.github.sspanak.tt9.languages.Language; -import io.github.sspanak.tt9.languages.LanguageCollection; - -public class AddWordAct extends AppCompatActivity { - public static final int CODE_SUCCESS = 0; - public static final int CODE_BLANK_WORD = 1; - public static final int CODE_INVALID_LANGUAGE = 2; - public static final int CODE_WORD_EXISTS = 3; - public static final int CODE_GENERAL_ERROR = 666; - - public static final String INTENT_FILTER = "tt9.add_word"; - - private Language language; - private String word; - - @Override - protected void onCreate(Bundle savedData) { - super.onCreate(savedData); - readInput(); - render(getMessage()); - } - - - private void readInput() { - Intent i = getIntent(); - word = i.getStringExtra("io.github.sspanak.tt9.word"); - language = LanguageCollection.getLanguage(this, i.getIntExtra("io.github.sspanak.tt9.lang", -1)); - } - - private String getMessage() { - if (language == null) { - Logger.e("WordManager.confirmAddWord", "Cannot insert a word for NULL language"); - UI.toastLong(getApplicationContext(), R.string.add_word_invalid_language); - return null; - } - - return getString(R.string.add_word_confirm, word, language.getName()); - } - - private void render(String message) { - if (message == null || word == null || word.isEmpty()) { - finish(); - return; - } - - View main = View.inflate(this, R.layout.addwordview, null); - ((TextView) main.findViewById(R.id.add_word_dialog_text)).append(message); - setContentView(main); - } - - - private void onAddedWord(int statusCode) { - String message; - switch (statusCode) { - case CODE_SUCCESS: - message = getString(R.string.add_word_success, word); - break; - - case CODE_WORD_EXISTS: - message = getResources().getString(R.string.add_word_exist, word); - break; - - case CODE_BLANK_WORD: - message = getString(R.string.add_word_blank); - break; - - case CODE_INVALID_LANGUAGE: - message = getResources().getString(R.string.add_word_invalid_language); - break; - - default: - message = getString(R.string.error_unexpected); - break; - } - - finish(); - sendMessageToMain(message); - } - - - public void addWord(View v) { - WordStoreAsync.put(this::onAddedWord, language, word); - } - - - private void sendMessageToMain(String message) { - Intent intent = new Intent(this, TraditionalT9.class); - intent.putExtra(INTENT_FILTER, message); - startService(intent); - } - - - public void cancelAddingWord(View v) { - finish(); - } -} diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/PopupDialogActivity.java b/app/src/main/java/io/github/sspanak/tt9/ui/PopupDialogActivity.java new file mode 100644 index 00000000..8051633a --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ui/PopupDialogActivity.java @@ -0,0 +1,60 @@ +package io.github.sspanak.tt9.ui; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; + +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.ime.TraditionalT9; +import io.github.sspanak.tt9.ui.dialogs.AddWordDialog; +import io.github.sspanak.tt9.ui.dialogs.ConfirmDictionaryUpdateDialog; +import io.github.sspanak.tt9.ui.dialogs.PopupDialog; + +public class PopupDialogActivity extends AppCompatActivity { + private static final String LOG_TAG = PopupDialogActivity.class.getSimpleName(); + public static final String DIALOG_ADD_WORD_INTENT = "tt9.popup_dialog.add_word"; + public static final String DIALOG_CONFIRM_WORDS_UPDATE_INTENT = "tt9.popup_dialog.confirm_words_update"; + public static final String DIALOG_CLOSED_INTENT = "tt9.popup_dialog.closed"; + + + @Override + protected void onCreate(Bundle savedData) { + super.onCreate(savedData); + PopupDialog dialog = getDialog(); + if (dialog != null) { + dialog.render(); + } + } + + + private PopupDialog getDialog() { + Intent i = getIntent(); + + String popupType = i != null ? i.getStringExtra("popup_type") : ""; + popupType = popupType != null ? popupType : ""; + + switch (popupType) { + case DIALOG_ADD_WORD_INTENT: + return new AddWordDialog(this, i, this::onDialogClose); + case DIALOG_CONFIRM_WORDS_UPDATE_INTENT: + return new ConfirmDictionaryUpdateDialog(this, i, this::onDialogClose); + default: + Logger.w(LOG_TAG, "Unknown popup type: '" + popupType + "'. Not displaying anything."); + return null; + } + } + + private void onDialogClose(String message) { + finish(); + if (message != null && !message.isEmpty()) { + sendMessageToMain(message); + } + } + + private void sendMessageToMain(String message) { + Intent intent = new Intent(this, TraditionalT9.class); + intent.putExtra(DIALOG_CLOSED_INTENT, message); + startService(intent); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/UI.java b/app/src/main/java/io/github/sspanak/tt9/ui/UI.java index 4b645f34..6ad325aa 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/UI.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/UI.java @@ -12,12 +12,23 @@ import io.github.sspanak.tt9.preferences.PreferencesActivity; public class UI { public static void showAddWordDialog(TraditionalT9 tt9, int language, String currentWord) { - Intent awIntent = new Intent(tt9, AddWordAct.class); - awIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - awIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - awIntent.putExtra("io.github.sspanak.tt9.word", currentWord); - awIntent.putExtra("io.github.sspanak.tt9.lang", language); - tt9.startActivity(awIntent); + Intent intent = new Intent(tt9, PopupDialogActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.putExtra("word", currentWord); + intent.putExtra("lang", language); + intent.putExtra("popup_type", PopupDialogActivity.DIALOG_ADD_WORD_INTENT); + tt9.startActivity(intent); + } + + + public static void showConfirmDictionaryUpdateDialog(TraditionalT9 tt9, int language) { + Intent intent = new Intent(tt9, PopupDialogActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.putExtra("lang", language); + intent.putExtra("popup_type", PopupDialogActivity.DIALOG_CONFIRM_WORDS_UPDATE_INTENT); + tt9.startActivity(intent); } @@ -43,6 +54,16 @@ public class UI { .show(); } + public static void confirm(Context context, String title, String message, String OKLabel, Runnable onOk, Runnable onCancel) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(OKLabel, (dialog, whichButton) -> { if (onOk != null) onOk.run(); }) + .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> { if (onCancel != null) onCancel.run(); }) + .setOnCancelListener(dialog -> { if (onCancel != null) onCancel.run(); }) + .show(); + } + public static void toast(Context context, CharSequence msg) { Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } @@ -72,12 +93,4 @@ public class UI { public static void toastLong(Context context, CharSequence msg) { Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); } - - @Deprecated - public static void toastLongFromAsync(Context context, CharSequence msg) { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - toastLong(context, msg); - } } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java new file mode 100644 index 00000000..8a14dd80 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/AddWordDialog.java @@ -0,0 +1,79 @@ +package io.github.sspanak.tt9.ui.dialogs; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; + +import io.github.sspanak.tt9.ConsumerCompat; +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.WordStoreAsync; +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.LanguageCollection; + +public class AddWordDialog extends PopupDialog { + public static final int CODE_SUCCESS = 0; + public static final int CODE_BLANK_WORD = 1; + public static final int CODE_INVALID_LANGUAGE = 2; + public static final int CODE_WORD_EXISTS = 3; + public static final int CODE_GENERAL_ERROR = 666; + + private Language language; + private String word; + + + public AddWordDialog(@NonNull Context context, @NonNull Intent intent, ConsumerCompat activityFinisher) { + super(context, intent, activityFinisher); + + title = context.getResources().getString(R.string.add_word_title); + OKLabel = context.getResources().getString(R.string.add_word_add); + if (language == null) { + message = context.getString(R.string.add_word_invalid_language); + } else { + message = context.getString(R.string.add_word_confirm, word, language.getName()); + } + } + + protected void parseIntent(Context context, Intent intent) { + word = intent.getStringExtra("word"); + language = LanguageCollection.getLanguage(context, intent.getIntExtra("lang", -1)); + } + + + public void render() { + if (message == null || word == null || word.isEmpty()) { + if (activityFinisher != null) activityFinisher.accept(null); + return; + } + + Runnable OKAction = language == null ? null : () -> WordStoreAsync.put(this::onAddedWord, language, word); + super.render(OKAction); + } + + private void onAddedWord(int statusCode) { + String response; + switch (statusCode) { + case CODE_SUCCESS: + response = context.getString(R.string.add_word_success, word); + break; + + case CODE_WORD_EXISTS: + response = context.getResources().getString(R.string.add_word_exist, word); + break; + + case CODE_BLANK_WORD: + response = context.getString(R.string.add_word_blank); + break; + + case CODE_INVALID_LANGUAGE: + response = context.getResources().getString(R.string.add_word_invalid_language); + break; + + default: + response = context.getString(R.string.error_unexpected); + break; + } + + activityFinisher.accept(response); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/ConfirmDictionaryUpdateDialog.java b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/ConfirmDictionaryUpdateDialog.java new file mode 100644 index 00000000..31dd1218 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/ConfirmDictionaryUpdateDialog.java @@ -0,0 +1,45 @@ +package io.github.sspanak.tt9.ui.dialogs; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; + +import io.github.sspanak.tt9.ConsumerCompat; +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.DictionaryLoader; +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.LanguageCollection; +import io.github.sspanak.tt9.preferences.SettingsStore; + +public class ConfirmDictionaryUpdateDialog extends PopupDialog { + private static long lastDisplayTime = 0; + private Language language; + public ConfirmDictionaryUpdateDialog(@NonNull Context context, @NonNull Intent intent, ConsumerCompat activityFinisher) { + super(context, intent, activityFinisher); + + title = context.getString(R.string.dictionary_update_title); + OKLabel = context.getString(R.string.dictionary_update_update); + String langName = language != null ? language.getName() : ""; + message = context.getResources().getString(R.string.dictionary_update_message, langName); + } + + protected void parseIntent(Context context, Intent intent) { + language = LanguageCollection.getLanguage(context, intent.getIntExtra("lang", -1)); + } + + @Override + public void render() { + if (System.currentTimeMillis() - lastDisplayTime < SettingsStore.DICTIONARY_CONFIRM_UPDATE_COOLDOWN_TIME) { + activityFinisher.accept(null); + } else { + super.render(this::loadDictionary); + lastDisplayTime = System.currentTimeMillis(); + } + } + + private void loadDictionary() { + DictionaryLoader.load(context, language); + activityFinisher.accept(null); + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/PopupDialog.java b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/PopupDialog.java new file mode 100644 index 00000000..c9778c19 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ui/dialogs/PopupDialog.java @@ -0,0 +1,30 @@ +package io.github.sspanak.tt9.ui.dialogs; + +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; + +import io.github.sspanak.tt9.ConsumerCompat; +import io.github.sspanak.tt9.ui.UI; + +abstract public class PopupDialog { + protected final Context context; + protected final ConsumerCompat activityFinisher; + protected String title; + protected String message; + protected String OKLabel; + + public PopupDialog(@NonNull Context context, @NonNull Intent intent, ConsumerCompat activityFinisher) { + this.activityFinisher = activityFinisher; + this.context = context; + parseIntent(context, intent); + } + + abstract protected void parseIntent(Context context, Intent intent); + abstract public void render(); + + protected void render(Runnable OKAction) { + UI.confirm(context, title, message, OKLabel, OKAction, () -> activityFinisher.accept(null)); + } +} diff --git a/app/src/main/res/layout/addwordview.xml b/app/src/main/res/layout/addwordview.xml deleted file mode 100644 index d3083d41..00000000 --- a/app/src/main/res/layout/addwordview.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - -