1
0
Fork 0

Objectbox (#400)

* replaced SQLite/RoomDb with Objectbox for improved dictionary performance

* SQLite words are automatically cleaned up when opening the Preferences

* added protection against deleting dictionaries while loading other dictionaries

* enabled adding words with apostrophes in Ukrainian

* updated system requirements
This commit is contained in:
Dimo Karaivanov 2023-12-21 14:30:27 +02:00 committed by GitHub
parent 7fb1ca7b5b
commit c02b4149e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 693 additions and 691 deletions

View file

@ -18,8 +18,8 @@ or get the APK from the [Releases Section](https://github.com/sspanak/tt9/releas
## System Requirements
- Android 4.4 or higher. _(Tested and confirmed on Android 4.4.2, 10 and 11)_
- Free space:
- Minimum 30 Mb when not using Predictive mode and no dictionaries are loaded.
- Plenty of space per each enabled language in Predictive mode (25-100 Mb, depending on the word count).
- Minimum 40 Mb when not using Predictive mode and no dictionaries are loaded.
- Plenty of space per each enabled language in Predictive mode (50-400 Mb, depending on the word count).
- A hardware keypad or a keyboard. For touchscreen-only devices, an on-screen keypad can be enabled in the Settings.
_If you own a phone with Android 2.2 up to 4.4, please refer to the original version of Traditional T9 from 2016._

View file

@ -9,10 +9,13 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.0.2'
classpath 'gradle.plugin.at.zierler:yaml-validator-plugin:1.5.0'
classpath("io.objectbox:objectbox-gradle-plugin:3.7.1")
}
}
apply plugin: 'com.android.application'
apply plugin: 'at.zierler.yamlvalidator'
apply plugin: "io.objectbox"
apply from: 'gradle/scripts/constants.gradle'
apply from: 'gradle/scripts/dictionary-tools.gradle'
@ -20,10 +23,6 @@ apply from: 'gradle/scripts/validate-languages.gradle'
apply from: 'gradle/scripts/version-tools.gradle'
configurations.configureEach {
// fixes 'duplicate class error', when using these combine: androidx.core:1.10.1, androidx.preference:1.2.0 and androidx.room:2.5.1
// see: https://stackoverflow.com/questions/75274720/a-failure-occurred-while-executing-appcheckdebugduplicateclasses/75315276#75315276
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
yamlValidator {
searchPaths = ['languages/definitions']
}
@ -32,8 +31,10 @@ configurations.configureEach {
dependencies {
implementation 'androidx.core:core:1.10.1'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.room:room-runtime:2.5.2'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
// fixes 'duplicate class error' when using "androidx.core" > 1.9.0
// see: https://stackoverflow.com/a/77323424
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
}
repositories {

View file

@ -0,0 +1,78 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:8686759156275895115",
"lastPropertyId": "9:148562041880145406",
"name": "Word",
"properties": [
{
"id": "1:7053654361616810150",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:1100881396300803213",
"name": "frequency",
"type": 5
},
{
"id": "3:3482550679443532896",
"name": "isCustom",
"type": 1
},
{
"id": "4:7366572918924354162",
"name": "langId",
"type": 5
},
{
"id": "5:2610279871343053806",
"name": "length",
"type": 5
},
{
"id": "6:5269773217039117329",
"name": "sequence",
"indexId": "1:2971223841434624317",
"type": 9,
"flags": 2048
},
{
"id": "7:3922044271904033267",
"name": "sequenceShort",
"indexId": "2:2641086768976362614",
"type": 2,
"flags": 8
},
{
"id": "8:1684236207225806285",
"name": "uniqueId",
"indexId": "3:5820769207826940948",
"type": 9,
"flags": 34848
},
{
"id": "9:148562041880145406",
"name": "word",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "1:8686759156275895115",
"lastIndexId": "3:5820769207826940948",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}

View file

@ -1,11 +1,9 @@
package io.github.sspanak.tt9.db;
import android.content.Context;
import android.database.sqlite.SQLiteConstraintException;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SimpleSQLiteQuery;
import java.util.ArrayList;
import java.util.List;
@ -13,24 +11,22 @@ import java.util.List;
import io.github.sspanak.tt9.ConsumerCompat;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException;
import io.github.sspanak.tt9.db.room.TT9Room;
import io.github.sspanak.tt9.db.room.Word;
import io.github.sspanak.tt9.db.room.WordList;
import io.github.sspanak.tt9.db.room.WordsDao;
import io.github.sspanak.tt9.db.objectbox.Word;
import io.github.sspanak.tt9.db.objectbox.WordList;
import io.github.sspanak.tt9.db.objectbox.WordStore;
import io.github.sspanak.tt9.ime.TraditionalT9;
import io.github.sspanak.tt9.languages.InvalidLanguageException;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.SettingsStore;
import io.objectbox.exception.UniqueViolationException;
public class DictionaryDb {
private static TT9Room dbInstance;
private static WordStore store;
private static final Handler asyncHandler = new Handler();
public static synchronized void init(Context context) {
if (dbInstance == null) {
if (store == null) {
context = context == null ? TraditionalT9.getMainContext() : context;
dbInstance = TT9Room.getInstance(context);
store = new WordStore(context);
}
}
@ -40,18 +36,18 @@ public class DictionaryDb {
}
private static TT9Room getInstance() {
private static WordStore getStore() {
init();
return dbInstance;
return store;
}
private static void printDebug(String tag, String title, String sequence, WordList words, long startTime) {
private static void printLoadDebug(String sequence, WordList words, long startTime) {
if (!Logger.isDebugLevel()) {
return;
}
StringBuilder debugText = new StringBuilder(title);
StringBuilder debugText = new StringBuilder("===== Word Matches =====");
debugText
.append("\n")
.append("Word Count: ").append(words.size())
@ -62,12 +58,12 @@ public class DictionaryDb {
debugText.append(" Sequence: ").append(sequence);
}
Logger.d(tag, debugText.toString());
Logger.d("loadWords", debugText.toString());
}
public static void runInTransaction(Runnable r) {
getInstance().runInTransaction(r);
getStore().runInTransaction(r);
}
@ -78,42 +74,66 @@ public class DictionaryDb {
* This query will finish immediately, if there is nothing to do. It's safe to run it often.
*/
public static void normalizeWordFrequencies(SettingsStore settings) {
new Thread(() -> {
long time = System.currentTimeMillis();
final String LOG_TAG = "db.normalizeWordFrequencies";
int affectedRows = getInstance().wordsDao().normalizeFrequencies(
settings.getWordFrequencyNormalizationDivider(),
settings.getWordFrequencyMax()
);
new Thread(() -> {
for (int langId : getStore().getLanguages()) {
getStore().runInTransactionAsync(() -> {
try {
long start = System.currentTimeMillis();
if (getStore().getMaxFrequency(langId) < settings.getWordFrequencyMax()) {
return;
}
List<Word> words = getStore().getMany(langId);
if (words == null) {
return;
}
for (Word w : words) {
w.frequency /= settings.getWordFrequencyNormalizationDivider();
}
getStore().put(words);
Logger.d(
"db.normalizeWordFrequencies",
"Normalized " + affectedRows + " words in: " + (System.currentTimeMillis() - time) + " ms"
LOG_TAG,
"Normalized language: " + langId + ", " + words.size() + " words in: " + (System.currentTimeMillis() - start) + " ms"
);
} catch (Exception e) {
Logger.e(LOG_TAG, "Word normalization failed. " + e.getMessage());
} finally {
getStore().closeThreadResources();
}
});
}
}).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);
boolean areThere = getStore().count(language != null ? language.getId() : -1) > 0;
getStore().closeThreadResources();
notification.accept(areThere);
}).start();
}
public static void deleteWords(Runnable notification) {
deleteWords(notification, null);
}
public static void deleteWords(Runnable notification, ArrayList<Integer> languageIds) {
public static void deleteWords(Context context, Runnable notification) {
new Thread(() -> {
if (languageIds == null) {
getInstance().clearAllTables();
} else if (languageIds.size() > 0) {
getInstance().wordsDao().deleteByLanguage(languageIds);
getStore().destroy();
store = null;
init(context);
notification.run();
}).start();
}
public static void deleteWords(Runnable notification, @NonNull ArrayList<Integer> languageIds) {
new Thread(() -> {
getStore().removeMany(languageIds).closeThreadResources();
notification.run();
}).start();
}
@ -124,168 +144,128 @@ public class DictionaryDb {
throw new InsertBlankWordException();
}
Word dbWord = new Word();
dbWord.langId = language.getId();
dbWord.sequence = language.getDigitSequenceForWord(word);
dbWord.word = word;
dbWord.length = word.length();
dbWord.frequency = 1;
new Thread(() -> {
try {
if (getInstance().wordsDao().doesWordExist(dbWord.langId, dbWord.word) > 0) {
throw new SQLiteConstraintException("Word already exists.");
if (getStore().exists(language.getId(), word, language.getDigitSequenceForWord(word))) {
throw new UniqueViolationException("Word already exists");
}
getInstance().wordsDao().insert(dbWord);
getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence);
getStore().put(Word.create(language, word, 1, true));
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("insertWord", msg);
} catch (UniqueViolationException e) {
String msg = "Skipping word: '" + word + "' for language: " + language.getId() + ", because it already exists.";
Logger.w("insertWord", msg);
statusHandler.accept(1);
} catch (Exception e) {
String msg = "Failed inserting word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId + ". " + e.getMessage();
String msg = "Failed inserting word: '" + word + "' for language: " + language.getId() + ". " + e.getMessage();
Logger.e("insertWord", msg);
statusHandler.accept(2);
} finally {
getStore().closeThreadResources();
}
}).start();
}
public static void upsertWordsSync(List<Word> words) {
getInstance().wordsDao().upsertMany(words);
getStore().put(words);
getStore().closeThreadResources();
}
public static void incrementWordFrequency(Language language, String word, String sequence) throws Exception {
Logger.d("incrementWordFrequency", "Incrementing priority of Word: " + word +" | Sequence: " + sequence);
if (language == null) {
throw new InvalidLanguageException();
}
// If both are empty, it is the same as changing the frequency of: "", which is simply a no-op.
if ((word == null || word.length() == 0) && (sequence == null || sequence.length() == 0)) {
public static void incrementWordFrequency(@NonNull Language language, @NonNull String word, @NonNull String sequence) {
// If any of these is empty, it is the same as changing the frequency of: "", which is simply a no-op.
if (word.length() == 0 || sequence.length() == 0) {
return;
}
// If one of them is empty, then this is an invalid operation,
// because a digit sequence exist for every word.
if (word == null || word.length() == 0 || sequence == null || sequence.length() == 0) {
throw new Exception("Cannot increment word frequency. Word: " + word + " | Sequence: " + sequence);
}
new Thread(() -> {
try {
int affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), word, sequence);
long start = System.currentTimeMillis();
Word dbWord = getStore().get(language.getId(), word, sequence);
// In case the user has changed the text case, there would be no match.
// Try again with the lowercase equivalent.
if (affectedRows == 0) {
String lowercaseWord = word.toLowerCase(language.getLocale());
affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), lowercaseWord, sequence);
Logger.d("incrementWordFrequency", "Attempting to increment frequency for lowercase variant: " + lowercaseWord);
if (dbWord == null) {
dbWord = getStore().get(language.getId(), word.toLowerCase(language.getLocale()), sequence);
}
Logger.d("incrementWordFrequency", "Affected rows: " + affectedRows);
if (dbWord == null) {
throw new Exception("No such word");
}
int max = getStore().getMaxFrequency(dbWord.langId, dbWord.sequence, dbWord.word);
if (dbWord.frequency <= max) {
dbWord.frequency = max + 1;
getStore().put(dbWord);
long time = System.currentTimeMillis() - start;
Logger.d(
"incrementWordFrequency",
"Incremented frequency of '" + dbWord.word + "' to: " + dbWord.frequency + ". Time: " + time + " ms"
);
} else {
long time = System.currentTimeMillis() - start;
Logger.d(
"incrementWordFrequency",
"'" + dbWord.word + "' is already the top word. Keeping frequency: " + dbWord.frequency + ". Time: " + time + " ms"
);
}
} catch (Exception e) {
Logger.e(
DictionaryDb.class.getName(),
"Failed incrementing word frequency. Word: " + word + " | Sequence: " + sequence + ". " + e.getMessage()
"Failed incrementing word frequency. Word: " + word + ". " + e.getMessage()
);
} finally {
getStore().closeThreadResources();
}
}).start();
}
/**
* loadWordsExact
* Loads words that match exactly the "sequence" and the optional "filter".
* For example: "7655" gets "roll".
* loadWords
* Loads words matching and similar to a given digit sequence
* For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ...
* and other similar.
*/
private static ArrayList<String> loadWordsExact(Language language, String sequence, String filter, int maximumWords) {
private static ArrayList<String> loadWords(Language language, String sequence, String filter, int minimumWords, int maximumWords) {
long start = System.currentTimeMillis();
WordList matches = new WordList(getInstance().wordsDao().getMany(
language.getId(),
maximumWords,
sequence,
filter == null || filter.equals("") ? null : filter
));
printDebug("loadWordsExact", "===== Exact Word Matches =====", sequence, matches, start);
WordList matches = getStore()
.getMany(language, sequence, filter, maximumWords)
.filter(sequence.length(), minimumWords);
getStore().closeThreadResources();
printLoadDebug(sequence, matches, start);
return matches.toStringList();
}
/**
* loadWordsFuzzy
* Loads words that start with "sequence" and optionally match the "filter".
* For example: "7655" -> "roll", but also: "rolled", "roller", "rolling", ...
*/
private static ArrayList<String> loadWordsFuzzy(Language language, String sequence, String filter, int maximumWords) {
long start = System.currentTimeMillis();
public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minimumWords, int maximumWords) {
final int minWords = Math.max(minimumWords, 0);
final int maxWords = Math.max(maximumWords, minWords);
// fuzzy queries are heavy, so we must restrict the search range as much as possible
boolean noFilter = (filter == null || filter.equals(""));
int maxWordLength = noFilter && sequence.length() <= 2 ? 5 : 1000;
String index = sequence.length() <= 2 ? WordsDao.indexShortWords : WordsDao.indexLongWords;
SimpleSQLiteQuery sql = TT9Room.getFuzzyQuery(index, language.getId(), maximumWords, sequence, sequence.length(), maxWordLength, filter);
WordList matches = new WordList(getInstance().wordsDao().getCustom(sql));
// In some cases, searching for words starting with "digitSequence" and limited to "maxWordLength" of 5,
// may yield too few results. If so, we expand the search range a bit.
if (noFilter && matches.size() < maximumWords) {
sql = TT9Room.getFuzzyQuery(
WordsDao.indexLongWords,
language.getId(),
maximumWords - matches.size(),
sequence,
5,
1000
);
matches.addAll(getInstance().wordsDao().getCustom(sql));
if (sequence == null || sequence.length() == 0) {
Logger.w("db.getWords", "Attempting to get words for an empty sequence.");
sendWords(dataHandler, new ArrayList<>());
return;
}
printDebug("loadWordsFuzzy", "~=~=~=~ Fuzzy Word Matches ~=~=~=~", sequence, matches, start);
return matches.toStringList();
if (language == null) {
Logger.w("db.getWords", "Attempting to get words for NULL language.");
sendWords(dataHandler, new ArrayList<>());
return;
}
new Thread(() -> sendWords(
dataHandler,
loadWords(language, sequence, filter, minWords, maxWords))
).start();
}
private static void sendWords(ConsumerCompat<ArrayList<String>> dataHandler, ArrayList<String> wordList) {
asyncHandler.post(() -> dataHandler.accept(wordList));
}
public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minimumWords, int maximumWords) {
final int minWords = Math.max(minimumWords, 0);
final int maxWords = Math.max(maximumWords, minimumWords);
ArrayList<String> wordList = new ArrayList<>(maxWords);
if (sequence == null || sequence.length() == 0) {
Logger.w("db.getWords", "Attempting to get words for an empty sequence.");
sendWords(dataHandler, wordList);
return;
}
if (language == null) {
Logger.w("db.getWords", "Attempting to get words for NULL language.");
sendWords(dataHandler, wordList);
return;
}
new Thread(() -> {
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);
}).start();
}
}

View file

@ -16,7 +16,7 @@ import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAbortedException;
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAlreadyRunningException;
import io.github.sspanak.tt9.db.exceptions.DictionaryImportException;
import io.github.sspanak.tt9.db.room.Word;
import io.github.sspanak.tt9.db.objectbox.Word;
import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.languages.InvalidLanguageException;
import io.github.sspanak.tt9.languages.Language;
@ -80,7 +80,7 @@ public class DictionaryLoader {
currentFile = 0;
importStartTime = System.currentTimeMillis();
sendFileCount(languages.size());
sendStartMessage(languages.size());
// SQLite does not support parallel queries, so let's import them one by one
for (Language lang : languages) {
@ -155,8 +155,8 @@ public class DictionaryLoader {
Logger.e(
logTag,
"Failed loading dictionary: " + language.getDictionaryFile() +
" for language '" + language.getName() + "'. "
"Failed loading dictionary: " + language.getDictionaryFile()
+ " for language '" + language.getName() + "'. "
+ e.getMessage()
);
}
@ -164,7 +164,7 @@ public class DictionaryLoader {
}
private void importLetters(Language language) {
private void importLetters(Language language) throws InvalidLanguageCharactersException {
ArrayList<Word> letters = new ArrayList<>();
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
@ -172,15 +172,7 @@ public class DictionaryLoader {
for (int key = 2; key <= 9; key++) {
for (String langChar : language.getKeyCharacters(key, false)) {
langChar = (isEnglish && langChar.equals("i")) ? langChar.toUpperCase(Locale.ENGLISH) : langChar;
Word word = new Word();
word.langId = language.getId();
word.frequency = 0;
word.length = 1;
word.sequence = String.valueOf(key);
word.word = langChar;
letters.add(word);
letters.add(Word.create(language, langChar, 0));
}
}
@ -189,7 +181,7 @@ public class DictionaryLoader {
private void importWords(Language language, String dictionaryFile) throws Exception {
sendProgressMessage(language, 0, 0);
sendProgressMessage(language, 1, 0);
long currentLine = 0;
long totalLines = getFileSize(dictionaryFile);
@ -200,7 +192,7 @@ public class DictionaryLoader {
for (String line; (line = br.readLine()) != null; currentLine++) {
if (loadThread.isInterrupted()) {
br.close();
sendProgressMessage(language, -1, 0);
sendProgressMessage(language, 0, 0);
throw new DictionaryImportAbortedException();
}
@ -209,7 +201,7 @@ public class DictionaryLoader {
int frequency = getFrequency(parts);
try {
dbWords.add(stringToWord(language, word, frequency));
dbWords.add(Word.create(language, word, frequency));
} catch (InvalidLanguageCharactersException e) {
br.close();
throw new DictionaryImportException(word, currentLine);
@ -222,6 +214,7 @@ public class DictionaryLoader {
if (totalLines > 0) {
int progress = (int) Math.floor(100.0 * currentLine / totalLines);
progress = Math.max(1, progress);
sendProgressMessage(language, progress, settings.getDictionaryImportProgressUpdateInterval());
}
}
@ -269,19 +262,7 @@ public class DictionaryLoader {
}
private Word stringToWord(Language language, String word, int frequency) throws InvalidLanguageCharactersException {
Word dbWord = new Word();
dbWord.langId = language.getId();
dbWord.frequency = frequency;
dbWord.length = word.length();
dbWord.sequence = language.getDigitSequenceForWord(word);
dbWord.word = word;
return dbWord;
}
private void sendFileCount(int fileCount) {
private void sendStartMessage(int fileCount) {
if (onStatusChange == null) {
Logger.w(logTag, "Cannot send file count without a status Handler. Ignoring message.");
return;
@ -289,6 +270,7 @@ public class DictionaryLoader {
Bundle progressMsg = new Bundle();
progressMsg.putInt("fileCount", fileCount);
progressMsg.putInt("progress", 1);
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
}

View file

@ -0,0 +1,78 @@
package io.github.sspanak.tt9.db;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.io.File;
import io.github.sspanak.tt9.Logger;
public class SQLWords {
private static final String DB_NAME = "t9dict.db";
private static final String TABLE_NAME = "words";
private static boolean isCompleted = false;
private final String LOG_TAG;
private final Activity activity;
private SQLiteDatabase db;
public SQLWords(Activity activity) {
this.activity = activity;
LOG_TAG = getClass().getSimpleName();
}
public void clear() {
if (isCompleted) {
return;
}
new Thread(() -> {
openDb();
if (areThereWords()) {
deleteAll();
}
closeDb();
isCompleted = true;
}).start();
}
private void openDb() {
try {
db = null;
File dbFile = activity.getDatabasePath(DB_NAME);
if (dbFile.exists()) {
db = SQLiteDatabase.openDatabase(dbFile.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE);
}
} catch (Exception e) {
Logger.d(LOG_TAG, "Assuming no SQL database, because of error while opening. " + e.getMessage());
db = null;
}
}
private void closeDb() {
if (db != null) {
db.close();
}
}
private boolean areThereWords() {
String sql = "SELECT COUNT(*) FROM (SELECT id FROM " + TABLE_NAME + " LIMIT 1)";
try (Cursor cursor = db.rawQuery(sql, null)) {
return cursor.moveToFirst() && cursor.getInt(0) > 0;
} catch (Exception e) {
Logger.d(LOG_TAG, "Assuming no words, because of query error. " + e.getMessage());
return false;
}
}
private void deleteAll() {
db.execSQL("DROP TABLE words");
Logger.d(LOG_TAG, "SQL Words cleaned successfully.");
}
}

View file

@ -1,26 +0,0 @@
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();
}
}
};
}

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
package io.github.sspanak.tt9.db.migrations;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
public class DB6 {
public static final Migration MIGRATION = new Migration(5, 6) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("DROP TRIGGER IF EXISTS normalize_freq");
}
};
}

View file

@ -1,90 +0,0 @@
package io.github.sspanak.tt9.db.migrations;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.ArrayList;
import java.util.Locale;
import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.languages.LanguageCollection;
import io.github.sspanak.tt9.preferences.SettingsStore;
public class DB7 {
private Context ctx;
public Migration getMigration(Context context) {
ctx = context;
return migration;
}
private int getNewLanguageId(int oldId) {
Language language;
switch (oldId) {
default:
return oldId;
case 1:
language = LanguageCollection.getByLocale(ctx, Locale.ENGLISH.toString());
break;
case 2:
language = LanguageCollection.getByLocale(ctx, "ru_RU");
break;
case 3:
language = LanguageCollection.getByLocale(ctx, Locale.GERMAN.toString());
break;
case 4:
language = LanguageCollection.getByLocale(ctx, Locale.FRENCH.toString());
break;
case 5:
language = LanguageCollection.getByLocale(ctx, Locale.ITALIAN.toString());
break;
case 6:
language = LanguageCollection.getByLocale(ctx, "uk_UA");
break;
case 7:
language = LanguageCollection.getByLocale(ctx, "bg_BG");
break;
case 8:
language = LanguageCollection.getByLocale(ctx, "nl_NL");
break;
case 9:
language = LanguageCollection.getByLocale(ctx, "es_ES");
break;
}
return language != null ? language.getId() : -1;
}
private final Migration migration = new Migration(6, 7) {
private void migrateSQL(SupportSQLiteDatabase database) {
for (int oldLangId = 1; oldLangId <= 9; oldLangId++) {
database.execSQL(
"UPDATE words " +
" SET lang = " + getNewLanguageId(oldLangId) +
" WHERE lang = " + oldLangId
);
}
}
private void migrateSettings() {
SettingsStore settings = new SettingsStore(ctx);
ArrayList<Integer> newLangIds = new ArrayList<>();
for (int langId : settings.getEnabledLanguageIds()) {
newLangIds.add(getNewLanguageId(langId));
}
settings.saveEnabledLanguageIds(newLangIds);
}
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
migrateSQL(database);
migrateSettings();
}
};
}

View file

@ -1,27 +0,0 @@
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 DB8 {
public static final Migration MIGRATION = new Migration(7, 8) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
try {
database.beginTransaction();
database.execSQL("ALTER TABLE words ADD COLUMN len INTEGER NOT NULL DEFAULT 0");
database.execSQL("UPDATE words SET len = LENGTH(seq)");
database.execSQL(TT9Room.createShortWordsIndexQuery().getSql());
database.setTransactionSuccessful();
} catch (Exception e) {
Logger.e("Migrate to DB8", "Migration failed. " + e.getMessage());
} finally {
database.endTransaction();
}
}
};
}

View file

@ -1,27 +0,0 @@
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;
import io.github.sspanak.tt9.db.room.WordsDao;
public class DB9 {
public static final Migration MIGRATION = new Migration(8, 9) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
try {
database.beginTransaction();
database.execSQL("DROP INDEX " + WordsDao.indexLongWords);
database.execSQL(TT9Room.createLongWordsIndexQuery().getSql());
database.setTransactionSuccessful();
} catch (Exception e) {
Logger.e("Migrate to DB9", "Migration failed. " + e.getMessage());
} finally {
database.endTransaction();
}
}
};
}

View file

@ -0,0 +1,48 @@
package io.github.sspanak.tt9.db.objectbox;
import androidx.annotation.NonNull;
import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException;
import io.github.sspanak.tt9.languages.Language;
import io.objectbox.annotation.ConflictStrategy;
import io.objectbox.annotation.Entity;
import io.objectbox.annotation.Id;
import io.objectbox.annotation.Index;
import io.objectbox.annotation.Unique;
@Entity
public class Word {
@Id public long id;
public int frequency;
public boolean isCustom;
public int langId;
public int length;
@Index public String sequence;
@Index public byte sequenceShort; // up to 2 digits
@Unique(onConflict = ConflictStrategy.REPLACE) public String uniqueId;
public String word;
public static Word create(@NonNull Language language, @NonNull String word, int frequency) throws InvalidLanguageCharactersException {
Word w = new Word();
w.frequency = frequency;
w.isCustom = false;
w.langId = language.getId();
w.length = word.length();
w.sequence = language.getDigitSequenceForWord(word);
w.sequenceShort = shrinkSequence(w.sequence);
w.uniqueId = (language.getId() + "-" + word);
w.word = word;
return w;
}
public static Word create(@NonNull Language language, @NonNull String word, int frequency, boolean isCustom) throws InvalidLanguageCharactersException {
Word w = create(language, word, frequency);
w.isCustom = isCustom;
return w;
}
public static Byte shrinkSequence(@NonNull String sequence) {
return Byte.parseByte(sequence.substring(0, Math.min(2, sequence.length())));
}
}

View file

@ -1,15 +1,22 @@
package io.github.sspanak.tt9.db.room;
package io.github.sspanak.tt9.db.objectbox;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
public class WordList extends ArrayList<Word> {
public WordList(List<Word> words) {
public WordList() {
super();
}
public WordList(@NonNull List<Word> words) {
addAll(words);
}
@NonNull
@Override
public String toString() {
@ -26,6 +33,19 @@ public class WordList extends ArrayList<Word> {
return sb.toString();
}
@NonNull
public WordList filter(int minLength, int minWords) {
WordList filtered = new WordList();
for (int i = 0; i < size(); i++) {
if (get(i).length == minLength || filtered.size() < minWords) {
filtered.add(get(i));
}
}
return filtered;
}
@NonNull
public ArrayList<String> toStringList() {
ArrayList<String> strings = new ArrayList<>();

View file

@ -0,0 +1,208 @@
package io.github.sspanak.tt9.db.objectbox;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.languages.Language;
import io.objectbox.Box;
import io.objectbox.BoxStore;
import io.objectbox.config.DebugFlags;
import io.objectbox.query.Query;
import io.objectbox.query.QueryBuilder;
import io.objectbox.query.QueryCondition;
public class WordStore {
private BoxStore boxStore;
private Box<Word> wordBox;
private Query<Word> longWordQuery;
private Query<Word> singleLetterQuery;
public WordStore(Context context) {
init(context);
}
private void init(Context context) {
boxStore = MyObjectBox.builder()
.androidContext(context.getApplicationContext())
.debugFlags(Logger.isDebugLevel() ? DebugFlags.LOG_QUERY_PARAMETERS : 0)
.build();
wordBox = boxStore.boxFor(Word.class);
longWordQuery = getLongWordQuery();
singleLetterQuery = getSingleLetterQuery();
}
private Query<Word> getLongWordQuery() {
return boxStore.boxFor(Word.class)
.query()
.equal(Word_.langId, 0)
.startsWith(Word_.sequence, "", QueryBuilder.StringOrder.CASE_SENSITIVE).parameterAlias("seq_start")
.lessOrEqual(Word_.sequence, "", QueryBuilder.StringOrder.CASE_SENSITIVE).parameterAlias("seq_end")
.equal(Word_.sequenceShort, 0)
.startsWith(Word_.word, "", QueryBuilder.StringOrder.CASE_SENSITIVE)
.order(Word_.length)
.orderDesc(Word_.frequency)
.build();
}
private Query<Word> getSingleLetterQuery() {
return wordBox
.query()
.equal(Word_.langId, 0)
.equal(Word_.sequenceShort, 0)
.startsWith(Word_.word, "", QueryBuilder.StringOrder.CASE_SENSITIVE)
.orderDesc(Word_.frequency)
.build();
}
public long count(int langId) {
try (Query<Word> query = wordBox.query(Word_.langId.equal(langId)).build()) {
return query.count();
} catch (Exception e) {
return 0;
}
}
public boolean exists(int langId, @NonNull String word, @NonNull String sequence) {
return get(langId, word, sequence) != null;
}
@Nullable
public Word get(int langId, @NonNull String word, @NonNull String sequence) {
QueryCondition<Word> where = Word_.langId.equal(langId)
.and(Word_.sequenceShort.equal(Word.shrinkSequence(sequence)))
.and(Word_.word.equal(word, QueryBuilder.StringOrder.CASE_SENSITIVE));
try (Query<Word> query = wordBox.query(where).build()) {
return query.findFirst();
}
}
@Nullable
public List<Word> getMany(int langId) {
try (Query<Word> query = wordBox.query(Word_.langId.equal(langId)).build()) {
return query.find();
}
}
@NonNull
public WordList getMany(Language language, @NonNull String sequence, @Nullable String filter, int maxWords) {
Query<Word> query;
if (sequence.length() < 2) {
singleLetterQuery.setParameter(Word_.sequenceShort, Byte.parseByte(sequence));
query = singleLetterQuery;
} else {
longWordQuery.setParameter(Word_.sequenceShort, Word.shrinkSequence(sequence));
longWordQuery.setParameter("seq_start", sequence);
longWordQuery.setParameter("seq_end", sequence + "99");
query = longWordQuery;
}
query.setParameter(Word_.langId, language.getId());
if (filter != null && !filter.equals("")) {
query.setParameter(Word_.word, filter);
} else {
query.setParameter(Word_.word, "");
}
return new WordList(query.find(0, maxWords));
}
public int[] getLanguages() {
try (Query<Word> query = wordBox.query().build()) {
return query.property(Word_.langId).distinct().findInts();
}
}
public int getMaxFrequency(int langId) {
return getMaxFrequency(langId, null, null);
}
public int getMaxFrequency(int langId, String sequence, String word) {
QueryCondition<Word> where = Word_.langId.equal(langId);
if (sequence != null && word != null) {
where = where.and(Word_.sequenceShort.equal(Word.shrinkSequence(sequence)))
.and(Word_.sequence.equal(sequence))
.and(Word_.word.notEqual(word));
}
try (Query<Word> query = wordBox.query(where).build()) {
long max = query.property(Word_.frequency).max();
return max == Long.MIN_VALUE ? 0 : (int)max;
}
}
public void put(@NonNull List<Word> words) {
wordBox.put(words);
}
public void put(@NonNull Word word) {
wordBox.put(word);
}
public WordStore removeMany(@NonNull ArrayList<Integer> languageIds) {
if (languageIds.size() > 0) {
try (Query<Word> query = wordBox.query(Word_.langId.oneOf(IntegerListToIntArray(languageIds))).build()) {
query.remove();
}
}
return this;
}
public void destroy() {
boxStore.closeThreadResources();
boxStore.close();
boxStore.deleteAllFiles();
}
public void runInTransaction(Runnable r) {
boxStore.runInTx(r);
}
public void runInTransactionAsync(Runnable action) {
boxStore.runInTxAsync(action, null);
}
public void closeThreadResources() {
boxStore.closeThreadResources();
}
private int[] IntegerListToIntArray(ArrayList<Integer> in) {
int[] out = new int[in.size()];
Iterator<Integer> iterator = in.iterator();
for (int i = 0; i < out.length; i++) {
out[i] = iterator.next();
}
return out;
}
}

View file

@ -1,63 +0,0 @@
package io.github.sspanak.tt9.db.room;
import android.content.Context;
import androidx.room.Database;
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.DB11;
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 = 11, entities = Word.class, exportSchema = false)
public abstract class TT9Room extends RoomDatabase {
public abstract WordsDao wordsDao();
public static synchronized TT9Room getInstance(Context context) {
return Room
.databaseBuilder(context, TT9Room.class, "t9dict.db")
.addMigrations(
DB6.MIGRATION,
new DB7().getMigration(context),
DB8.MIGRATION,
DB9.MIGRATION,
DB10.MIGRATION,
new DB11().getMigration(context)
)
.build();
}
public static SimpleSQLiteQuery getFuzzyQuery(String index, int langId, int limit, String sequence, int minWordLength, int maxWordLength, String word) {
String sql = "SELECT *" +
" FROM words INDEXED BY " + index + " " +
" WHERE 1" +
" AND lang = " + langId +
" AND len BETWEEN " + minWordLength + " AND " + maxWordLength +
" AND seq > " + sequence + " AND seq <= " + sequence + "99 " +
" ORDER BY len ASC, freq DESC " +
" LIMIT " + limit;
if (word != null) {
sql = sql.replace("WHERE 1", "WHERE 1 AND word LIKE '" + word.replace("'", "''") + "%'");
}
return new SimpleSQLiteQuery(sql);
}
public static SimpleSQLiteQuery getFuzzyQuery(String index, int langId, int limit, String sequence, int minWordLength, int maxWordLength) {
return getFuzzyQuery(index, langId, limit, sequence, minWordLength, maxWordLength, null);
}
public static SimpleSQLiteQuery createShortWordsIndexQuery() {
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 IF NOT EXISTS " + WordsDao.indexLongWords + " ON words (lang ASC, seq ASC, freq DESC)");
}
}

View file

@ -1,33 +0,0 @@
package io.github.sspanak.tt9.db.room;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Index;
import androidx.room.PrimaryKey;
@Entity(
indices = {
@Index(value = {"lang", "word"}, unique = true),
@Index(value = {"lang", "seq", "freq"}, orders = {Index.Order.ASC, Index.Order.ASC, Index.Order.DESC}),
@Index(value = {"lang", "len", "seq"}, orders = {Index.Order.ASC, Index.Order.ASC, Index.Order.ASC})
},
tableName = "words"
)
public class Word {
@PrimaryKey(autoGenerate = true)
public int id;
@ColumnInfo(name = "lang")
public int langId;
public String word;
@ColumnInfo(name = "seq")
public String sequence;
@ColumnInfo(name = "freq")
public int frequency;
@ColumnInfo(name = "len")
public int length;
}

View file

@ -1,66 +0,0 @@
package io.github.sspanak.tt9.db.room;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.RawQuery;
import androidx.sqlite.db.SimpleSQLiteQuery;
import java.util.ArrayList;
import java.util.List;
@Dao
public interface WordsDao {
String indexLongWords = "index_words_lang_seq_freq";
String indexShortWords = "index_words_lang_len_seq";
@Query("SELECT COUNT(id) FROM words WHERE :langId < 0 OR lang = :langId")
int count(int langId);
@Query("DELETE FROM words WHERE lang IN(:langIds)")
void deleteByLanguage(ArrayList<Integer> langIds);
@Query("SELECT COUNT(id) FROM words WHERE lang = :langId AND word LIKE :word")
int doesWordExist(int langId, String word);
@Query(
"SELECT * " +
"FROM words " +
"WHERE " +
"lang = :langId " +
"AND seq = :sequence " +
"AND (:word IS NULL OR word LIKE :word || '%') " +
"ORDER BY freq DESC " +
"LIMIT :limit"
)
List<Word> getMany(int langId, int limit, String sequence, String word);
@RawQuery(observedEntities = Word.class)
List<Word> getCustom(SimpleSQLiteQuery query);
@Insert
void insert(Word word);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void upsertMany(List<Word> words);
@Query(
"UPDATE words " +
"SET freq = (SELECT IFNULL(MAX(freq), 0) FROM words WHERE lang = :langId AND seq = :sequence AND word <> :word) + 1 " +
"WHERE lang = :langId AND word = :word AND seq = :sequence"
)
int incrementFrequency(int langId, String word, String sequence);
@Query(
"UPDATE words " +
"SET freq = freq / :normalizationDivider " +
"WHERE lang IN ( " +
"SELECT lang " +
"FROM words " +
"WHERE freq >= :maxFrequency " +
"GROUP BY lang" +
")"
)
int normalizeFrequencies(int normalizationDivider, int maxFrequency);
}

View file

@ -355,7 +355,7 @@ public class TraditionalT9 extends KeyPadHandler {
cancelAutoAccept();
clearSuggestions();
String word = textField.getSurroundingWord();
String word = textField.getSurroundingWord(mLanguage);
if (word.isEmpty()) {
UI.toastLong(this, R.string.add_word_no_selection);
} else {

View file

@ -18,12 +18,16 @@ import java.util.regex.Pattern;
import io.github.sspanak.tt9.Logger;
import io.github.sspanak.tt9.ime.modes.InputMode;
import io.github.sspanak.tt9.languages.Language;
public class TextField {
public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1;
private static final Pattern beforeCursorWordRegex = Pattern.compile("(\\w+)(?!\n)$");
private static final Pattern afterCursorWordRegex = Pattern.compile("^(?<!\n)(\\w+)");
private static final Pattern beforeCursorUkrainianRegex = Pattern.compile("([\\w']+)(?!\n)$");
private static final Pattern afterCursorUkrainianRegex = Pattern.compile("^(?<!\n)([\\w']+)");
public final InputConnection connection;
public final EditorInfo field;
@ -177,9 +181,20 @@ public class TextField {
* getSurroundingWord
* Returns the word next or around the cursor. Scanning length is up to 50 chars in each direction.
*/
@NonNull public String getSurroundingWord() {
Matcher before = beforeCursorWordRegex.matcher(getTextBeforeCursor());
Matcher after = afterCursorWordRegex.matcher(getTextAfterCursor());
@NonNull public String getSurroundingWord(Language language) {
Matcher before;
Matcher after;
if (language != null && language.isUkrainian()) {
// Ukrainian uses apostrophes as letters
before = beforeCursorUkrainianRegex.matcher(getTextBeforeCursor());
after = afterCursorUkrainianRegex.matcher(getTextAfterCursor());
} else {
// In other languages, special characters in words will cause automatic word break to fail,
// resulting in unexpected suggestions. Therefore, they are not allowed.
before = beforeCursorWordRegex.matcher(getTextBeforeCursor());
after = afterCursorWordRegex.matcher(getTextAfterCursor());
}
return (before.find() ? before.group(1) : "") + (after.find() ? after.group(1) : "");
}

View file

@ -167,6 +167,11 @@ public class Language {
return letters.contains("α");
}
public boolean isUkrainian() {
ArrayList<String> letters = getKeyCharacters(4, false);
return letters.contains("ї");
}
/* ************ utility ************ */
/**

View file

@ -17,6 +17,7 @@ import io.github.sspanak.tt9.Logger;
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.db.SQLWords;
import io.github.sspanak.tt9.ime.helpers.GlobalKeyboardSettings;
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
@ -41,6 +42,7 @@ public class PreferencesActivity extends AppCompatActivity implements Preference
applyTheme();
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
new SQLWords(this).clear();
DictionaryDb.init(this);
DictionaryDb.normalizeWordFrequencies(settings);

View file

@ -2,16 +2,20 @@ package io.github.sspanak.tt9.preferences.items;
import androidx.preference.Preference;
import java.util.ArrayList;
import java.util.List;
import io.github.sspanak.tt9.Logger;
abstract class ItemClickable {
protected final int CLICK_DEBOUNCE_TIME = 250;
abstract public class ItemClickable {
private final int CLICK_DEBOUNCE_TIME = 250;
private long lastClickTime = 0;
protected final Preference item;
private final ArrayList<ItemClickable> otherItems = new ArrayList<>();
ItemClickable(Preference item) {
public ItemClickable(Preference item) {
this.item = item;
}
@ -31,6 +35,29 @@ abstract class ItemClickable {
}
public ItemClickable setOtherItems(List<ItemClickable> others) {
otherItems.clear();
otherItems.addAll(others);
return this;
}
protected void disableOtherItems() {
for (ItemClickable i : otherItems) {
i.disable();
}
}
protected void enableOtherItems() {
for (ItemClickable i : otherItems) {
i.enable();
}
}
/**
* debounceClick
* Protection against faulty devices, that sometimes send two (or more) click events

View file

@ -35,11 +35,15 @@ public class ItemLoadDictionary extends ItemClickable {
this.settings = settings;
loader.setOnStatusChange(this::onLoadingStatusChange);
refreshStatus();
}
if (!progressBar.isCompleted() && !progressBar.isFailed()) {
changeToCancelButton();
public void refreshStatus() {
if (loader.isRunning()) {
setLoadingStatus();
} else {
changeToLoadButton();
setReadyStatus();
}
}
@ -49,12 +53,12 @@ public class ItemLoadDictionary extends ItemClickable {
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
if (progressBar.isCancelled()) {
changeToLoadButton();
setReadyStatus();
} else if (progressBar.isFailed()) {
changeToLoadButton();
setReadyStatus();
UI.toastFromAsync(context, progressBar.getMessage());
} else if (progressBar.isCompleted()) {
changeToLoadButton();
setReadyStatus();
UI.toastFromAsync(context, R.string.dictionary_loaded);
}
}
@ -65,23 +69,25 @@ public class ItemLoadDictionary extends ItemClickable {
ArrayList<Language> languages = LanguageCollection.getAll(context, settings.getEnabledLanguageIds());
try {
setLoadingStatus();
loader.load(languages);
changeToCancelButton();
} catch (DictionaryImportAlreadyRunningException e) {
loader.stop();
changeToLoadButton();
setReadyStatus();
}
return true;
}
public void changeToCancelButton() {
private void setLoadingStatus() {
disableOtherItems();
item.setTitle(context.getString(R.string.dictionary_cancel_load));
}
public void changeToLoadButton() {
private void setReadyStatus() {
enableOtherItems();
item.setTitle(context.getString(R.string.dictionary_load_title));
item.setSummary(progressBar.isFailed() || progressBar.isCancelled() ? progressBar.getMessage() : "");
}

View file

@ -14,42 +14,30 @@ public class ItemTruncateAll extends ItemClickable {
protected final PreferencesActivity activity;
protected final DictionaryLoader loader;
protected final ItemLoadDictionary loadItem;
protected ItemClickable otherTruncateItem;
public ItemTruncateAll(Preference item, ItemLoadDictionary loadItem, PreferencesActivity activity, DictionaryLoader loader) {
public ItemTruncateAll(Preference item, PreferencesActivity activity, DictionaryLoader loader) {
super(item);
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()) {
loader.stop();
loadItem.changeToLoadButton();
return false;
}
onStartDeleting();
DictionaryDb.deleteWords(this::onFinishDeleting);
DictionaryDb.deleteWords(activity.getApplicationContext(), this::onFinishDeleting);
return true;
}
protected void onStartDeleting() {
if (otherTruncateItem != null) {
otherTruncateItem.disable();
}
loadItem.disable();
disableOtherItems();
disable();
item.setSummary(R.string.dictionary_truncating);
}
@ -57,10 +45,7 @@ public class ItemTruncateAll extends ItemClickable {
protected void onFinishDeleting() {
activity.runOnUiThread(() -> {
if (otherTruncateItem != null) {
otherTruncateItem.enable();
}
loadItem.enable();
enableOtherItems();
item.setSummary("");
enable();
UI.toastFromAsync(activity, R.string.dictionary_truncated);

View file

@ -18,23 +18,17 @@ public class ItemTruncateUnselected extends ItemTruncateAll {
private final SettingsStore settings;
public ItemTruncateUnselected(Preference item, ItemLoadDictionary loadItem, PreferencesActivity context, SettingsStore settings, DictionaryLoader loader) {
super(item, loadItem, context, loader);
public ItemTruncateUnselected(Preference item, PreferencesActivity context, SettingsStore settings, DictionaryLoader loader) {
super(item, context, loader);
this.settings = settings;
}
public ItemTruncateUnselected setOtherTruncateItem(ItemTruncateAll otherTruncateItem) {
this.otherTruncateItem = otherTruncateItem;
return this;
}
@Override
protected boolean onClick(Preference p) {
if (loader != null && loader.isRunning()) {
loader.stop();
loadItem.changeToLoadButton();
return false;
}
ArrayList<Integer> unselectedLanguageIds = new ArrayList<>();

View file

@ -1,5 +1,7 @@
package io.github.sspanak.tt9.preferences.screens;
import java.util.Arrays;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.preferences.PreferencesActivity;
import io.github.sspanak.tt9.preferences.items.ItemLoadDictionary;
@ -8,6 +10,10 @@ import io.github.sspanak.tt9.preferences.items.ItemTruncateAll;
import io.github.sspanak.tt9.preferences.items.ItemTruncateUnselected;
public class DictionariesScreen extends BaseScreenFragment {
private ItemLoadDictionary loadItem;
private ItemTruncateUnselected deleteItem;
private ItemTruncateAll truncateItem;
public DictionariesScreen() { init(); }
public DictionariesScreen(PreferencesActivity activity) { init(activity); }
@ -23,31 +29,35 @@ public class DictionariesScreen extends BaseScreenFragment {
);
multiSelect.populate().enableValidation();
ItemLoadDictionary loadItem = new ItemLoadDictionary(
loadItem = new ItemLoadDictionary(
findPreference(ItemLoadDictionary.NAME),
activity,
activity.settings,
activity.getDictionaryLoader(),
activity.getDictionaryProgressBar()
);
loadItem.enableClickHandler();
ItemTruncateAll truncateItem = new ItemTruncateAll(
findPreference(ItemTruncateAll.NAME),
loadItem,
activity,
activity.getDictionaryLoader()
);
ItemTruncateUnselected truncateSelectedItem = new ItemTruncateUnselected(
deleteItem = new ItemTruncateUnselected(
findPreference(ItemTruncateUnselected.NAME),
loadItem,
activity,
activity.settings,
activity.getDictionaryLoader()
);
truncateItem.setOtherTruncateItem(truncateSelectedItem).enableClickHandler();
truncateSelectedItem.setOtherTruncateItem(truncateItem).enableClickHandler();
truncateItem = new ItemTruncateAll(
findPreference(ItemTruncateAll.NAME),
activity,
activity.getDictionaryLoader()
);
loadItem.setOtherItems(Arrays.asList(truncateItem, deleteItem)).enableClickHandler();
deleteItem.setOtherItems(Arrays.asList(truncateItem, loadItem)).enableClickHandler();
truncateItem.setOtherItems(Arrays.asList(deleteItem, loadItem)).enableClickHandler();
}
@Override
public void onResume() {
super.onResume();
loadItem.refreshStatus();
}
}

View file

@ -106,6 +106,7 @@ public class DictionaryLoadingBar {
public void show(Context context, Bundle data) {
String error = data.getString("error", null);
int fileCount = data.getInt("fileCount", -1);
int progress = data.getInt("progress", -1);
if (error != null) {
hasFailed = true;
@ -116,10 +117,12 @@ public class DictionaryLoadingBar {
data.getLong("fileLine", -1),
data.getString("word", "")
);
} else if (fileCount > -1) {
setFileCount(fileCount);
} else {
} else if (progress >= 0) {
hasFailed = false;
if (fileCount >= 0) {
setFileCount(fileCount);
}
showProgress(
context,
data.getLong("time", 0),
@ -143,7 +146,7 @@ public class DictionaryLoadingBar {
private void showProgress(Context context, long time, int currentFile, int currentFileProgress, int languageId) {
if (currentFileProgress < 0) {
if (currentFileProgress <= 0) {
hide();
isStopped = true;
title = "";