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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index d69975a9..85dd76ee 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -34,6 +34,9 @@
Бутони на екрана
Назад
Зелена слушалка
+ Налично е обновление на речника
+ Има нови думи за „%1$s“. Искате ли да ги заредите?
+ Зареди
Дарете
Ако харесвате %1$s, подкрепете разработката му на: %2$s.
Добавяне на нова дума
@@ -65,7 +68,6 @@
Автоматични главни букви
Започвай автоматично изреченията с главни букви.
Подсказващ режим
- Няма речник за език „%1$s“. Заредете го в Настройки.
Клавиатура
Символ при двойно натисната \"0\"
Нов ред
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 307e1d87..f29870c6 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -31,6 +31,9 @@
Ja
Nein
Automatisch
+ Wörterbuchupdate verfügbar
+ Es gibt neue Wörter für „%1$s“. Möchten Sie sie laden?
+ Laden
Spenden
Wenn Ihnen %1$s gefällt, könnten Sie die Entwicklung auf %2$s unterstützen.
Mit \"OK\" in Facebook Messenger senden
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index cdc911dc..fb0eaf2f 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -31,7 +31,6 @@
Cargando diccionario
Cargando diccionario (%1$s)…
Acerca de esta aplicación
- No hay diccionario para el idioma \"%1$s\". Vaya a Configuración para cargarlo.
Falló al cargar. No se encontró el diccionario para \"%1$s\".
Borrar todos
Borrar no seleccionados
@@ -75,6 +74,9 @@
Sí
No
Automática
+ Actualización del Diccionario Disponible
+ Hay nuevas palabras para «%1$s». ¿Te gustaría cargarlas?
+ Cargar
Donar
Si te gusta %1$s, podrías apoyar su desarrollo en: %2$s.
Enviar con «OK» en Facebook Messenger
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 43c6d1b9..2b33d480 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -42,7 +42,6 @@
Majuscules automatiques
Ajouter automatiquement un espace après signes de ponctuation et mots.
Commencer automatiquement les phrases avec une majuscule.
- Pas de dictionnaire pour langue «%1$s». Veuillez le charger à l\'écran Paramètres.
Clavier
Espace
Ajouter un mot
@@ -75,6 +74,9 @@
Non
Automatique
Ajouter mot « %1$s » à %2$s?
+ Mise à jour du dictionnaire disponible
+ Il y a de nouveaux mots pour «%1$s». Souhaitez-vous les charger ?
+ Charger
Donner
Si vous aimez %1$s vous pouvez soutenir son développement à : %2$s
Protection multi-presse
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index b5439739..3798033d 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -40,6 +40,9 @@
Si
No
Automatica
+ Aggiornamento del dizionario disponibile
+ Ci sono nuove parole per \"%1$s\". Vuoi caricarle?
+ Carica
Donare
Se ti piace %1$s, potresti supportarne lo sviluppo su: %2$s.
Inviare con \"OK\" su Facebook Messenger
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 97c87a8f..1676416d 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -38,7 +38,6 @@
טעינת מילון
סטטוס
בחר מקלדת ברירת מחדל
- אין מילון עבור \"%1$s\". טען את המילון דרך ההגדרות.
הטעינה נכשלה, לא נמצא מילון עבור \"%1$s\".
מחק הכל
מחק את הבלתי נבחר
@@ -68,6 +67,9 @@
כן
לא
אוטומטי
+ עדכון מילון זמין
+ יש מילים חדשות עבור \"%1$s\". האם תרצה לטעון אותם?
+ טען
לִתְרוֹם
אם אתה אוהב את %1$s, תוכל לתמוך בפיתוח שלו בכתובת: %2$s
שלח עם \"OK\" ב-Facebook Messenger.
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 7f429207..fdf4a38e 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -31,6 +31,9 @@
Ja
Nee
Automatisch
+ Woordenboekupdate Beschikbaar
+ Er zijn nieuwe woorden voor \"%1$s\". Wil je ze laden?
+ Laden
Doneer
Als je %1$s leuk vindt, zou je de ontwikkeling kunnen ondersteunen op: %2$s.
Verstuur met \"OK\" in Facebook Messenger
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 14426af9..1dfd200a 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -37,7 +37,6 @@
Carregando dicionário…
Aguarde o carregamento do dicionário, por favor
Carregar Dicionário
- Não há um dicionário para o idioma \"%1$s\". É possível carregá-lo em "Configurações".
Falha no carregamento. Não foi encontrado um dicionário para o idioma \"%1$s\".
Limpar Dicionário
Dicionário apagado com sucesso.
@@ -64,6 +63,9 @@
Sim
Não
Automático
+ Atualização do Dicionário Disponível
+ Há novas palavras para \"%1$s\". Você gostaria de carregá-las?
+ Carregar
Doar
Se você gosta de %1$s, você poderia apoiar o seu desenvolvimento em: %2$s.
Enviar com \"OK\" no Facebook Messenger
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index ec0fbd77..38307c41 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -46,7 +46,6 @@
Отправка с «ОК» в Messenger
Кнопки на экране
Не удалось загрузить словарь. Проблема в слове «%1$s» в строке %2$d для языка «%3$s».
- Отсутствует словарь для языка «%1$s». Вы можете загрузить его в Настройках.
Добавить новое слово
Стереть
Выбор клавиатуры
@@ -80,6 +79,9 @@
Нет
Автоматически
Добавить слово «%1$s» в %2$s?
+ Доступно обновление словаря
+ Для «%1$s» доступны новые слова. Хотите загрузить их?
+ Загрузить
Поддержать
Если вам нравится %1$s, вы можете поддержать его разработку по: %2$s.
Отправка сообщения с «ОК» в Google Chat
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index a765a00c..34802413 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -65,7 +65,6 @@
Налаштування кнопок за замовчуванням відновлено
Повернути кнопки за замовчуванням
Словник успішно видалено.
- Немає словника для мови «%1$s». Перейдіть до Налаштувань, щоб завантажити його.
Помилка завантаження. Недійсне слово «%1$s» в рядку %2$d мови «%3$s».
Зворотна клавіатура
Використовуйте налаштування, якщо 7–8–9 у першому рядку замість 1–2–3.
@@ -80,6 +79,9 @@
Ні
Автоматично
Додати слово «%1$s» до %2$s?
+ Доступне оновлення словника
+ Є нові слова для «%1$s». Бажаєте їх завантажити?
+ Завантажити
Підтримуйте
Якщо вам подобається %1$s, ви можете підтримати його розробку за: %2$s.
Надсилати повідомлення з «ОК» до Google Chat
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 05b83ffd..7b80cb04 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -65,13 +65,16 @@
Loading dictionary
Please wait for the dictionary to load.
Load Dictionary
- No dictionary for language \"%1$s\". Go to Settings to load it.
Loading failed. Dictionary for \"%1$s\" not found.
Delete All
Delete Unselected
Dictionary successfully cleared.
Deleting…
+ Dictionary Update Available
+ There are new words for \"%1$s\". Would you like to load them?
+ Load
+
Donate
If you like %1$s, you could support its development at: %2$s.
https://www.buymeacoffee.com/sspanak
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index f4d21dae..ce15d161 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -19,4 +19,8 @@
- 1dp
- match_parent
+
+
diff --git a/docs/user-manual.md b/docs/user-manual.md
index 48a77d84..570761ff 100644
--- a/docs/user-manual.md
+++ b/docs/user-manual.md
@@ -10,13 +10,11 @@ _If you don't see the icon right after installing, restart your phone and it sho
If your phone does not have a hardware keypad, check out the [On-screen Keypad section](#on-screen-keypad).
### Enabling Predictive Mode
-With the default settings, it is only possible to type in 123 and ABC modes. In order to enable the Predictive mode, there are additional steps:
+Predictive Mode requires a language dictionary to be loaded in order provide word suggestions. By default, when Traditional T9 activates, it will automatically load the dictionary for the current language. If you have enabled more than one language, you can cycle them, until all the dictionaries are loaded.
-- Open the [Settings screen](#settings-screen).
-- Select the desired languages.
-- Load the dictionaries.
+Alternatively, you can load them all at once, manually, from: [Settings screen](#settings-screen) → Languages → Load.
-_If you don't do the above, there will be no suggestions when typing in Predictive mode._
+_If you [delete one or more dictionaries](#deleting-a-dictionary), they will NOT reload automatically, when you go back to typing. You will have to do so manually. Only dictionaries for newly enabled languages will load automatically._
### Dictionary Tips