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

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