1
0
Fork 0

db migrations

simplified the Add Word dialog

added a popup confirmation when there are new dictionary words
This commit is contained in:
sspanak 2024-02-19 14:29:13 +02:00 committed by Dimo Karaivanov
parent 0e8dfbe578
commit 4907671aa3
37 changed files with 497 additions and 313 deletions

View file

@ -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

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -31,8 +31,8 @@
<activity
android:excludeFromRecents="true"
android:label="@string/add_word_title"
android:name="io.github.sspanak.tt9.ui.AddWordAct"
android:theme="@style/Theme.AppCompat.DayNight.Dialog.MinWidth"/>
android:label=""
android:name="io.github.sspanak.tt9.ui.PopupDialogActivity"
android:theme="@style/alertDialog" />
</application>
</manifest>

View file

@ -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<Language>() {{ 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.");

View file

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

View file

@ -39,6 +39,10 @@ public class WordStoreAsync {
new Thread(() -> notification.accept(getStore().exists(language))).start();
}
public static void getLastLanguageUpdateTime(ConsumerCompat<String> notification, Language language) {
new Thread(() -> notification.accept(getStore().getLanguageFileHash(language))).start();
}
public static void deleteWords(Runnable notification, @NonNull ArrayList<Integer> languageIds) {
new Thread(() -> {

View file

@ -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 "";
}
}
}

View file

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

View file

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

View file

@ -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()) {

View file

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

View file

@ -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 " +
")";
}
}

View file

@ -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<Integer, Long> 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())
);
}
}

View file

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

View file

@ -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));

View file

@ -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;

View file

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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<String> 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);
}
}

View file

@ -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<String> 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);
}
}

View file

@ -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<String> activityFinisher;
protected String title;
protected String message;
protected String OKLabel;
public PopupDialog(@NonNull Context context, @NonNull Intent intent, ConsumerCompat<String> 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));
}
}

View file

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="6dp"
android:orientation="vertical" >
<TextView
android:id="@+id/add_word_dialog_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
</TextView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<Button
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:onClick="cancelAddingWord"
android:text="@android:string/cancel" />
<Button
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:onClick="addWord"
android:text="@string/add_word_add" />
</LinearLayout>
</LinearLayout>

View file

@ -34,6 +34,9 @@
<string name="pref_show_soft_function_keys">Бутони на екрана</string>
<string name="key_back">Назад</string>
<string name="key_call">Зелена слушалка</string>
<string name="dictionary_update_title">Налично е обновление на речника</string>
<string name="dictionary_update_message">Има нови думи за „%1$s“. Искате ли да ги заредите?</string>
<string name="dictionary_update_update">Зареди</string>
<string name="donate_title">Дарете</string>
<string name="donate_summary">Ако харесвате %1$s, подкрепете разработката му на: %2$s.</string>
<string name="function_add_word_key">Добавяне на нова дума</string>
@ -65,7 +68,6 @@
<string name="pref_auto_text_case">Автоматични главни букви</string>
<string name="pref_auto_text_case_summary">Започвай автоматично изреченията с главни букви.</string>
<string name="pref_category_predictive_mode">Подсказващ режим</string>
<string name="dictionary_missing_go_load_it">Няма речник за език „%1$s“. Заредете го в Настройки.</string>
<string name="pref_category_keypad">Клавиатура</string>
<string name="pref_double_zero_char">Символ при двойно натисната \"0\"</string>
<string name="char_newline">Нов ред</string>

View file

@ -31,6 +31,9 @@
<string name="pref_dark_theme_yes">Ja</string>
<string name="pref_dark_theme_no">Nein</string>
<string name="pref_dark_theme_auto">Automatisch</string>
<string name="dictionary_update_title">Wörterbuchupdate verfügbar</string>
<string name="dictionary_update_message">Es gibt neue Wörter für „%1$s“. Möchten Sie sie laden?</string>
<string name="dictionary_update_update">Laden</string>
<string name="donate_title">Spenden</string>
<string name="donate_summary">Wenn Ihnen %1$s gefällt, könnten Sie die Entwicklung auf %2$s unterstützen.</string>
<string name="pref_hack_fb_messenger">Mit \"OK\" in Facebook Messenger senden</string>

View file

@ -31,7 +31,6 @@
<string name="dictionary_loading_indeterminate">Cargando diccionario</string>
<string name="dictionary_loading">Cargando diccionario (%1$s)…</string>
<string name="pref_category_about">Acerca de esta aplicación</string>
<string name="dictionary_missing_go_load_it">No hay diccionario para el idioma \"%1$s\". Vaya a Configuración para cargarlo.</string>
<string name="dictionary_not_found">Falló al cargar. No se encontró el diccionario para \"%1$s\".</string>
<string name="dictionary_truncate_title">Borrar todos</string>
<string name="dictionary_truncate_unselected">Borrar no seleccionados</string>
@ -75,6 +74,9 @@
<string name="pref_dark_theme_yes"></string>
<string name="pref_dark_theme_no">No</string>
<string name="pref_dark_theme_auto">Automática</string>
<string name="dictionary_update_title">Actualización del Diccionario Disponible</string>
<string name="dictionary_update_message">Hay nuevas palabras para «%1$s». ¿Te gustaría cargarlas?</string>
<string name="dictionary_update_update">Cargar</string>
<string name="donate_title">Donar</string>
<string name="donate_summary">Si te gusta %1$s, podrías apoyar su desarrollo en: %2$s.</string>
<string name="pref_hack_fb_messenger">Enviar con «OK» en Facebook Messenger</string>

View file

@ -42,7 +42,6 @@
<string name="pref_auto_text_case">Majuscules automatiques</string>
<string name="pref_auto_space_summary">Ajouter automatiquement un espace après signes de ponctuation et mots.</string>
<string name="pref_auto_text_case_summary">Commencer automatiquement les phrases avec une majuscule.</string>
<string name="dictionary_missing_go_load_it">Pas de dictionnaire pour langue «%1$s». Veuillez le charger à l\'écran Paramètres.</string>
<string name="pref_category_keypad">Clavier</string>
<string name="char_space">Espace</string>
<string name="function_add_word_key">Ajouter un mot</string>
@ -75,6 +74,9 @@
<string name="pref_dark_theme_no">Non</string>
<string name="pref_dark_theme_auto">Automatique</string>
<string name="add_word_confirm">Ajouter mot « %1$s » à %2$s?</string>
<string name="dictionary_update_title">Mise à jour du dictionnaire disponible</string>
<string name="dictionary_update_message">Il y a de nouveaux mots pour «%1$s». Souhaitez-vous les charger ?</string>
<string name="dictionary_update_update">Charger</string>
<string name="donate_title">Donner</string>
<string name="donate_summary">Si vous aimez %1$s vous pouvez soutenir son développement à : %2$s</string>
<string name="pref_hack_key_pad_debounce_time">Protection multi-presse</string>

View file

@ -40,6 +40,9 @@
<string name="pref_dark_theme_yes">Si</string>
<string name="pref_dark_theme_no">No</string>
<string name="pref_dark_theme_auto">Automatica</string>
<string name="dictionary_update_title">Aggiornamento del dizionario disponibile</string>
<string name="dictionary_update_message">Ci sono nuove parole per \"%1$s\". Vuoi caricarle?</string>
<string name="dictionary_update_update">Carica</string>
<string name="donate_title">Donare</string>
<string name="donate_summary">Se ti piace %1$s, potresti supportarne lo sviluppo su: %2$s.</string>
<string name="pref_hack_fb_messenger">Inviare con \"OK\" su Facebook Messenger</string>

View file

@ -38,7 +38,6 @@
<string name="dictionary_load_title">טעינת מילון</string>
<string name="setup_keyboard_status">סטטוס</string>
<string name="setup_default_keyboard">בחר מקלדת ברירת מחדל</string>
<string name="dictionary_missing_go_load_it">אין מילון עבור \"%1$s\". טען את המילון דרך ההגדרות.</string>
<string name="dictionary_not_found">הטעינה נכשלה, לא נמצא מילון עבור \"%1$s\".</string>
<string name="dictionary_truncate_title">מחק הכל</string>
<string name="dictionary_truncate_unselected">מחק את הבלתי נבחר</string>
@ -68,6 +67,9 @@
<string name="pref_dark_theme_yes">כן</string>
<string name="pref_dark_theme_no">לא</string>
<string name="pref_dark_theme_auto">אוטומטי</string>
<string name="dictionary_update_title">עדכון מילון זמין</string>
<string name="dictionary_update_message">יש מילים חדשות עבור \"%1$s\". האם תרצה לטעון אותם?</string>
<string name="dictionary_update_update">טען</string>
<string name="donate_title">לִתְרוֹם</string>
<string name="donate_summary">אם אתה אוהב את %1$s, תוכל לתמוך בפיתוח שלו בכתובת: %2$s</string>
<string name="pref_hack_fb_messenger">שלח עם \"OK\" ב-Facebook Messenger.</string>

View file

@ -31,6 +31,9 @@
<string name="pref_dark_theme_yes">Ja</string>
<string name="pref_dark_theme_no">Nee</string>
<string name="pref_dark_theme_auto">Automatisch</string>
<string name="dictionary_update_title">Woordenboekupdate Beschikbaar</string>
<string name="dictionary_update_message">Er zijn nieuwe woorden voor \"%1$s\". Wil je ze laden?</string>
<string name="dictionary_update_update">Laden</string>
<string name="donate_title">Doneer</string>
<string name="donate_summary">Als je %1$s leuk vindt, zou je de ontwikkeling kunnen ondersteunen op: %2$s.</string>
<string name="pref_hack_fb_messenger">Verstuur met \"OK\" in Facebook Messenger</string>

View file

@ -37,7 +37,6 @@
<string name="dictionary_loading_indeterminate">Carregando dicionário…</string>
<string name="dictionary_loading_please_wait">Aguarde o carregamento do dicionário, por favor</string>
<string name="dictionary_load_title">Carregar Dicionário</string>
<string name="dictionary_missing_go_load_it">Não há um dicionário para o idioma \"%1$s\". É possível carregá-lo em "Configurações".</string>
<string name="dictionary_not_found">Falha no carregamento. Não foi encontrado um dicionário para o idioma \"%1$s\".</string>
<string name="dictionary_truncate_title">Limpar Dicionário</string>
<string name="dictionary_truncated">Dicionário apagado com sucesso.</string>
@ -64,6 +63,9 @@
<string name="pref_dark_theme_yes">Sim</string>
<string name="pref_dark_theme_no">Não</string>
<string name="pref_dark_theme_auto">Automático</string>
<string name="dictionary_update_title">Atualização do Dicionário Disponível</string>
<string name="dictionary_update_message">Há novas palavras para \"%1$s\". Você gostaria de carregá-las?</string>
<string name="dictionary_update_update">Carregar</string>
<string name="donate_title">Doar</string>
<string name="donate_summary">Se você gosta de %1$s, você poderia apoiar o seu desenvolvimento em: %2$s.</string>
<string name="pref_hack_fb_messenger">Enviar com \"OK\" no Facebook Messenger</string>

View file

@ -46,7 +46,6 @@
<string name="pref_hack_fb_messenger">Отправка с «ОК» в Messenger</string>
<string name="pref_show_soft_function_keys">Кнопки на экране</string>
<string name="dictionary_load_bad_char">Не удалось загрузить словарь. Проблема в слове «%1$s» в строке %2$d для языка «%3$s».</string>
<string name="dictionary_missing_go_load_it">Отсутствует словарь для языка «%1$s». Вы можете загрузить его в Настройках.</string>
<string name="function_add_word_key">Добавить новое слово</string>
<string name="function_backspace_key">Стереть</string>
<string name="function_change_keyboard_key">Выбор клавиатуры</string>
@ -80,6 +79,9 @@
<string name="pref_dark_theme_no">Нет</string>
<string name="pref_dark_theme_auto">Автоматически</string>
<string name="add_word_confirm">Добавить слово «%1$s» в %2$s?</string>
<string name="dictionary_update_title">Доступно обновление словаря</string>
<string name="dictionary_update_message">Для «%1$s» доступны новые слова. Хотите загрузить их?</string>
<string name="dictionary_update_update">Загрузить</string>
<string name="donate_title">Поддержать</string>
<string name="donate_summary">Если вам нравится %1$s, вы можете поддержать его разработку по: %2$s.</string>
<string name="pref_hack_google_chat">Отправка сообщения с «ОК» в Google Chat</string>

View file

@ -65,7 +65,6 @@
<string name="function_reset_keys_done">Налаштування кнопок за замовчуванням відновлено</string>
<string name="function_reset_keys_title">Повернути кнопки за замовчуванням</string>
<string name="dictionary_truncated">Словник успішно видалено.</string>
<string name="dictionary_missing_go_load_it">Немає словника для мови «%1$s». Перейдіть до Налаштувань, щоб завантажити його.</string>
<string name="dictionary_load_bad_char">Помилка завантаження. Недійсне слово «%1$s» в рядку %2$d мови «%3$s».</string>
<string name="pref_upside_down_keys">Зворотна клавіатура</string>
<string name="pref_upside_down_keys_summary">Використовуйте налаштування, якщо 789 у першому рядку замість 123.</string>
@ -80,6 +79,9 @@
<string name="pref_dark_theme_no">Ні</string>
<string name="pref_dark_theme_auto">Автоматично</string>
<string name="add_word_confirm">Додати слово «%1$s» до %2$s?</string>
<string name="dictionary_update_title">Доступне оновлення словника</string>
<string name="dictionary_update_message">Є нові слова для «%1$s». Бажаєте їх завантажити?</string>
<string name="dictionary_update_update">Завантажити</string>
<string name="donate_title">Підтримуйте</string>
<string name="donate_summary">Якщо вам подобається %1$s, ви можете підтримати його розробку за: %2$s.</string>
<string name="pref_hack_google_chat">Надсилати повідомлення з «ОК» до Google Chat</string>

View file

@ -65,13 +65,16 @@
<string name="dictionary_loading_indeterminate">Loading dictionary</string>
<string name="dictionary_loading_please_wait">Please wait for the dictionary to load.</string>
<string name="dictionary_load_title">Load Dictionary</string>
<string name="dictionary_missing_go_load_it">No dictionary for language \"%1$s\". Go to Settings to load it.</string>
<string name="dictionary_not_found">Loading failed. Dictionary for \"%1$s\" not found.</string>
<string name="dictionary_truncate_title">Delete All</string>
<string name="dictionary_truncate_unselected">Delete Unselected</string>
<string name="dictionary_truncated">Dictionary successfully cleared.</string>
<string name="dictionary_truncating">Deleting…</string>
<string name="dictionary_update_title">Dictionary Update Available</string>
<string name="dictionary_update_message">There are new words for \"%1$s\". Would you like to load them?</string>
<string name="dictionary_update_update">Load</string>
<string name="donate_title">Donate</string>
<string name="donate_summary">If you like %1$s, you could support its development at: %2$s.</string>
<string name="donate_url" translatable="false">https://www.buymeacoffee.com/sspanak</string>

View file

@ -19,4 +19,8 @@
<item name="android:layout_height">1dp</item>
<item name="android:layout_width">match_parent</item>
</style>
<style name="alertDialog" parent="Theme.AppCompat.DayNight.Dialog.Alert">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>

View file

@ -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