1
0
Fork 0

Improved db operations and db feedback (#279)

* prevented crashing when database indexes are missing, they are now permanent instead of being created on-the-fly when loading a dictionary

* code style fixes in DictionaryDb.java

* removed some unused code

* counting the dictionaries to be loaded is no longer responsibility of the Load Button, but of the DictionaryLoader

* delete dictionary buttons are now being locked while deleting is in progress; also, a status message is displayed for better UX

* updated translations and documentation
This commit is contained in:
Dimo Karaivanov 2023-06-07 11:08:00 +03:00 committed by GitHub
parent f92ad96827
commit c63d054422
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 202 additions and 180 deletions

View file

@ -70,23 +70,6 @@ public class DictionaryDb {
}
public static void createShortWordIndexSync() {
getInstance().wordsDao().rawQuery(TT9Room.createShortWordsIndexQuery());
}
public static void createLongWordIndexSync() {
getInstance().wordsDao().rawQuery(TT9Room.createLongWordsIndexQuery());
}
public static void dropShortWordIndexSync() {
getInstance().wordsDao().rawQuery(TT9Room.dropShortWordsIndexQuery());
}
public static void dropLongWordIndexSync() {
getInstance().wordsDao().rawQuery(TT9Room.dropLongWordsIndexQuery());
}
/**
* normalizeWordFrequencies
* Normalizes the word frequencies for all languages that have reached the maximum, as defined in
@ -95,28 +78,26 @@ public class DictionaryDb {
*/
public static void normalizeWordFrequencies(SettingsStore settings) {
new Thread(() -> {
long time = System.currentTimeMillis();
long time = System.currentTimeMillis();
int affectedRows = getInstance().wordsDao().normalizeFrequencies(
settings.getWordFrequencyNormalizationDivider(),
settings.getWordFrequencyMax()
);
int affectedRows = getInstance().wordsDao().normalizeFrequencies(
settings.getWordFrequencyNormalizationDivider(),
settings.getWordFrequencyMax()
);
Logger.d(
"db.normalizeWordFrequencies",
"Normalized " + affectedRows + " words in: " + (System.currentTimeMillis() - time) + " ms"
);
}
).start();
Logger.d(
"db.normalizeWordFrequencies",
"Normalized " + affectedRows + " words in: " + (System.currentTimeMillis() - time) + " ms"
);
}).start();
}
public static void areThereWords(ConsumerCompat<Boolean> notification, Language language) {
new Thread(() -> {
int langId = language != null ? language.getId() : -1;
notification.accept(getInstance().wordsDao().count(langId) > 0);
}
).start();
int langId = language != null ? language.getId() : -1;
notification.accept(getInstance().wordsDao().count(langId) > 0);
}).start();
}
@ -136,14 +117,13 @@ public class DictionaryDb {
public static void deleteWords(Runnable notification, ArrayList<Integer> languageIds) {
new Thread(() -> {
if (languageIds == null) {
getInstance().clearAllTables();
} else if (languageIds.size() > 0) {
getInstance().wordsDao().deleteByLanguage(languageIds);
}
notification.run();
if (languageIds == null) {
getInstance().clearAllTables();
} else if (languageIds.size() > 0) {
getInstance().wordsDao().deleteByLanguage(languageIds);
}
).start();
notification.run();
}).start();
}
@ -164,22 +144,21 @@ public class DictionaryDb {
dbWord.frequency = 1;
new Thread(() -> {
try {
getInstance().wordsDao().insert(dbWord);
getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence);
statusHandler.accept(0);
} catch (SQLiteConstraintException e) {
String msg = "Constraint violation when inserting a word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId
+ ". " + e.getMessage();
Logger.e("tt9/insertWord", msg);
statusHandler.accept(1);
} catch (Exception e) {
String msg = "Failed inserting word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId + ". " + e.getMessage();
Logger.e("tt9/insertWord", msg);
statusHandler.accept(2);
}
try {
getInstance().wordsDao().insert(dbWord);
getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence);
statusHandler.accept(0);
} catch (SQLiteConstraintException e) {
String msg = "Constraint violation when inserting a word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId
+ ". " + e.getMessage();
Logger.e("tt9/insertWord", msg);
statusHandler.accept(1);
} catch (Exception e) {
String msg = "Failed inserting word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId + ". " + e.getMessage();
Logger.e("tt9/insertWord", msg);
statusHandler.accept(2);
}
).start();
}).start();
}
@ -207,38 +186,37 @@ public class DictionaryDb {
}
new Thread(() -> {
try {
int affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), word, sequence);
try {
int affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), word, sequence);
// In case the user has changed the text case, there would be no match.
// Try again with the lowercase equivalent.
String lowercaseWord = "";
if (affectedRows == 0) {
lowercaseWord = word.toLowerCase(language.getLocale());
affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), lowercaseWord, sequence);
// In case the user has changed the text case, there would be no match.
// Try again with the lowercase equivalent.
String lowercaseWord = "";
if (affectedRows == 0) {
lowercaseWord = word.toLowerCase(language.getLocale());
affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), lowercaseWord, sequence);
Logger.d("incrementWordFrequency", "Attempting to increment frequency for lowercase variant: " + lowercaseWord);
}
// Some languages permit appending the punctuation to the end of the words, like so: "try,".
// But there are no such words in the dictionary, so try without the punctuation mark.
if (affectedRows == 0 && language.isPunctuationPartOfWords() && sequence.endsWith("1")) {
String truncatedWord = lowercaseWord.substring(0, word.length() - 1);
String truncatedSequence = sequence.substring(0, sequence.length() - 1);
affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), truncatedWord, truncatedSequence);
Logger.d("incrementWordFrequency", "Attempting to increment frequency with stripped punctuation: " + truncatedWord);
}
Logger.d("incrementWordFrequency", "Affected rows: " + affectedRows);
} catch (Exception e) {
Logger.e(
DictionaryDb.class.getName(),
"Failed incrementing word frequency. Word: " + word + " | Sequence: " + sequence + ". " + e.getMessage()
);
Logger.d("incrementWordFrequency", "Attempting to increment frequency for lowercase variant: " + lowercaseWord);
}
// Some languages permit appending the punctuation to the end of the words, like so: "try,".
// But there are no such words in the dictionary, so try without the punctuation mark.
if (affectedRows == 0 && language.isPunctuationPartOfWords() && sequence.endsWith("1")) {
String truncatedWord = lowercaseWord.substring(0, word.length() - 1);
String truncatedSequence = sequence.substring(0, sequence.length() - 1);
affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), truncatedWord, truncatedSequence);
Logger.d("incrementWordFrequency", "Attempting to increment frequency with stripped punctuation: " + truncatedWord);
}
Logger.d("incrementWordFrequency", "Affected rows: " + affectedRows);
} catch (Exception e) {
Logger.e(
DictionaryDb.class.getName(),
"Failed incrementing word frequency. Word: " + word + " | Sequence: " + sequence + ". " + e.getMessage()
);
}
).start();
}).start();
}
@ -320,14 +298,13 @@ public class DictionaryDb {
}
new Thread(() -> {
wordList.addAll(loadWordsExact(language, sequence, filter, maxWords));
wordList.addAll(loadWordsExact(language, sequence, filter, maxWords));
if (sequence.length() > 1 && wordList.size() < minWords) {
wordList.addAll(loadWordsFuzzy(language, sequence, filter, minWords - wordList.size()));
}
sendWords(dataHandler, wordList);
if (sequence.length() > 1 && wordList.size() < minWords) {
wordList.addAll(loadWordsFuzzy(language, sequence, filter, minWords - wordList.size()));
}
).start();
sendWords(dataHandler, wordList);
}).start();
}
}

View file

@ -47,7 +47,6 @@ public class DictionaryLoader {
}
public DictionaryLoader(Context context) {
assets = context.getAssets();
settings = new SettingsStore(context);
@ -80,7 +79,7 @@ public class DictionaryLoader {
currentFile = 0;
importStartTime = System.currentTimeMillis();
dropIndexes();
sendFileCount(languages.size());
// SQLite does not support parallel queries, so let's import them one by one
for (Language lang : languages) {
@ -90,8 +89,6 @@ public class DictionaryLoader {
importAll(lang);
currentFile++;
}
createIndexes();
}
};
@ -168,28 +165,6 @@ public class DictionaryLoader {
}
private void dropIndexes() {
long start = System.currentTimeMillis();
DictionaryDb.dropLongWordIndexSync();
Logger.d("dropIndexes", "Index 1: " + (System.currentTimeMillis() - start) + " ms");
start = System.currentTimeMillis();
DictionaryDb.dropShortWordIndexSync();
Logger.d("dropIndexes", "Index 2: " + (System.currentTimeMillis() - start) + " ms");
}
private void createIndexes() {
long start = System.currentTimeMillis();
DictionaryDb.createLongWordIndexSync();
Logger.d("createIndexes", "Index 1: " + (System.currentTimeMillis() - start) + " ms");
start = System.currentTimeMillis();
DictionaryDb.createShortWordIndexSync();
Logger.d("createIndexes", "Index 2: " + (System.currentTimeMillis() - start) + " ms");
}
private void importLetters(Language language) {
ArrayList<Word> letters = new ArrayList<>();
@ -315,6 +290,20 @@ public class DictionaryLoader {
}
private void sendFileCount(int fileCount) {
if (onStatusChange == null) {
Logger.w(
"tt9/DictionaryLoader.sendFileCount",
"Cannot send file count without a status Handler. Ignoring message.");
return;
}
Bundle progressMsg = new Bundle();
progressMsg.putInt("fileCount", fileCount);
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
}
private void sendProgressMessage(Language language, int progress, int progressUpdateInterval) {
if (onStatusChange == null) {
Logger.w(

View file

@ -0,0 +1,26 @@
package io.github.sspanak.tt9.db.migrations;
import androidx.annotation.NonNull;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.room.TT9Room;
public class DB10 {
public static final Migration MIGRATION = new Migration(9, 10) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
try {
database.beginTransaction();
database.execSQL(TT9Room.createShortWordsIndexQuery().getSql());
database.execSQL(TT9Room.createLongWordsIndexQuery().getSql());
database.setTransactionSuccessful();
} catch (Exception e) {
Logger.e("Migrate to DB10", "Migration failed. " + e.getMessage());
} finally {
database.endTransaction();
}
}
};
}

View file

@ -7,12 +7,13 @@ import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SimpleSQLiteQuery;
import io.github.sspanak.tt9.db.migrations.DB10;
import io.github.sspanak.tt9.db.migrations.DB6;
import io.github.sspanak.tt9.db.migrations.DB7;
import io.github.sspanak.tt9.db.migrations.DB8;
import io.github.sspanak.tt9.db.migrations.DB9;
@Database(version = 9, entities = Word.class, exportSchema = false)
@Database(version = 10, entities = Word.class, exportSchema = false)
public abstract class TT9Room extends RoomDatabase {
public abstract WordsDao wordsDao();
@ -23,7 +24,8 @@ public abstract class TT9Room extends RoomDatabase {
DB6.MIGRATION,
new DB7().getMigration(context),
DB8.MIGRATION,
DB9.MIGRATION
DB9.MIGRATION,
DB10.MIGRATION
)
.build();
}
@ -50,18 +52,10 @@ public abstract class TT9Room extends RoomDatabase {
}
public static SimpleSQLiteQuery createShortWordsIndexQuery() {
return new SimpleSQLiteQuery("CREATE INDEX " + WordsDao.indexShortWords + " ON words (lang ASC, len ASC, seq ASC)");
return new SimpleSQLiteQuery("CREATE INDEX IF NOT EXISTS " + WordsDao.indexShortWords + " ON words (lang ASC, len ASC, seq ASC)");
}
public static SimpleSQLiteQuery createLongWordsIndexQuery() {
return new SimpleSQLiteQuery("CREATE INDEX " + WordsDao.indexLongWords + " ON words (lang ASC, seq ASC, freq DESC)");
}
public static SimpleSQLiteQuery dropShortWordsIndexQuery() {
return new SimpleSQLiteQuery("DROP INDEX IF EXISTS " + WordsDao.indexShortWords);
}
public static SimpleSQLiteQuery dropLongWordsIndexQuery() {
return new SimpleSQLiteQuery("DROP INDEX IF EXISTS " + WordsDao.indexLongWords);
return new SimpleSQLiteQuery("CREATE INDEX IF NOT EXISTS " + WordsDao.indexLongWords + " ON words (lang ASC, seq ASC, freq DESC)");
}
}

View file

@ -6,10 +6,6 @@ import java.util.ArrayList;
import java.util.List;
public class WordList extends ArrayList<Word> {
public WordList() {
super();
}
public WordList(List<Word> words) {
addAll(words);
}

View file

@ -15,9 +15,6 @@ public interface WordsDao {
String indexLongWords = "index_words_lang_seq_freq";
String indexShortWords = "index_words_lang_len_seq";
@RawQuery()
Object rawQuery(SimpleSQLiteQuery sql);
@Query("SELECT COUNT(id) FROM words WHERE :langId < 0 OR lang = :langId")
int count(int langId);

View file

@ -16,6 +16,16 @@ abstract class ItemClickable {
}
public void disable() {
item.setEnabled(false);
}
public void enable() {
item.setEnabled(true);
}
public void enableClickHandler() {
item.setOnPreferenceClickListener(this::debounceClick);
}

View file

@ -63,7 +63,6 @@ public class ItemLoadDictionary extends ItemClickable {
@Override
protected boolean onClick(Preference p) {
ArrayList<Language> languages = LanguageCollection.getAll(settings.getEnabledLanguageIds());
progressBar.setFileCount(languages.size());
try {
loader.load(languages);

View file

@ -1,31 +1,36 @@
package io.github.sspanak.tt9.preferences.items;
import android.content.Context;
import androidx.preference.Preference;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryDb;
import io.github.sspanak.tt9.db.DictionaryLoader;
import io.github.sspanak.tt9.preferences.PreferencesActivity;
import io.github.sspanak.tt9.ui.UI;
public class ItemTruncateAll extends ItemClickable {
public static final String NAME = "dictionary_truncate";
private final Context context;
private final DictionaryLoader loader;
private final ItemLoadDictionary loadItem;
protected final PreferencesActivity activity;
protected final DictionaryLoader loader;
protected final ItemLoadDictionary loadItem;
protected ItemClickable otherTruncateItem;
public ItemTruncateAll(Preference item, ItemLoadDictionary loadItem, Context context, DictionaryLoader loader) {
public ItemTruncateAll(Preference item, ItemLoadDictionary loadItem, PreferencesActivity activity, DictionaryLoader loader) {
super(item);
this.context = context;
this.activity = activity;
this.loadItem = loadItem;
this.loader = loader;
}
public ItemTruncateAll setOtherTruncateItem(ItemTruncateUnselected item) {
this.otherTruncateItem = item;
return this;
}
@Override
protected boolean onClick(Preference p) {
if (loader != null && loader.isRunning()) {
@ -33,8 +38,32 @@ public class ItemTruncateAll extends ItemClickable {
loadItem.changeToLoadButton();
}
DictionaryDb.deleteWords(() -> UI.toastFromAsync(context, R.string.dictionary_truncated));
onStartDeleting();
DictionaryDb.deleteWords(this::onFinishDeleting);
return true;
}
protected void onStartDeleting() {
if (otherTruncateItem != null) {
otherTruncateItem.disable();
}
loadItem.disable();
disable();
item.setSummary(R.string.dictionary_truncating);
}
protected void onFinishDeleting() {
activity.runOnUiThread(() -> {
if (otherTruncateItem != null) {
otherTruncateItem.enable();
}
loadItem.enable();
item.setSummary("");
enable();
UI.toastFromAsync(activity, R.string.dictionary_truncated);
});
}
}

View file

@ -1,35 +1,32 @@
package io.github.sspanak.tt9.preferences.items;
import android.content.Context;
import androidx.preference.Preference;
import java.util.ArrayList;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.db.DictionaryDb;
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.PreferencesActivity;
import io.github.sspanak.tt9.preferences.SettingsStore;
import io.github.sspanak.tt9.ui.UI;
public class ItemTruncateUnselected extends ItemClickable {
public class ItemTruncateUnselected extends ItemTruncateAll {
public static final String NAME = "dictionary_truncate_unselected";
private final Context context;
private final DictionaryLoader loader;
private final ItemLoadDictionary loadItem;
private final SettingsStore settings;
public ItemTruncateUnselected(Preference item, ItemLoadDictionary loadItem, Context context, SettingsStore settings, DictionaryLoader loader) {
super(item);
this.context = context;
this.loadItem = loadItem;
public ItemTruncateUnselected(Preference item, ItemLoadDictionary loadItem, PreferencesActivity context, SettingsStore settings, DictionaryLoader loader) {
super(item, loadItem, context, loader);
this.settings = settings;
this.loader = loader;
}
public ItemTruncateUnselected setOtherTruncateItem(ItemTruncateAll otherTruncateItem) {
this.otherTruncateItem = otherTruncateItem;
return this;
}
@ -48,10 +45,8 @@ public class ItemTruncateUnselected extends ItemClickable {
}
}
DictionaryDb.deleteWords(
() -> UI.toastFromAsync(context, R.string.dictionary_truncated),
unselectedLanguageIds
);
onStartDeleting();
DictionaryDb.deleteWords(this::onFinishDeleting, unselectedLanguageIds);
return true;
}

View file

@ -37,7 +37,6 @@ public class DictionariesScreen extends BaseScreenFragment {
activity,
activity.getDictionaryLoader()
);
truncateItem.enableClickHandler();
ItemTruncateUnselected truncateSelectedItem = new ItemTruncateUnselected(
findPreference(ItemTruncateUnselected.NAME),
@ -46,6 +45,8 @@ public class DictionariesScreen extends BaseScreenFragment {
activity.settings,
activity.getDictionaryLoader()
);
truncateSelectedItem.enableClickHandler();
truncateItem.setOtherTruncateItem(truncateSelectedItem).enableClickHandler();
truncateSelectedItem.setOtherTruncateItem(truncateItem).enableClickHandler();
}
}

View file

@ -105,6 +105,7 @@ public class DictionaryLoadingBar {
public void show(Bundle data) {
String error = data.getString("error", null);
int fileCount = data.getInt("fileCount", -1);
if (error != null) {
hasFailed = true;
@ -114,7 +115,9 @@ public class DictionaryLoadingBar {
data.getLong("fileLine", -1),
data.getString("word", "")
);
} else {
} else if (fileCount > -1) {
setFileCount(fileCount);
} else {
hasFailed = false;
showProgress(
data.getLong("time", 0),