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:
parent
7fb1ca7b5b
commit
c02b4149e2
28 changed files with 693 additions and 691 deletions
|
|
@ -18,8 +18,8 @@ or get the APK from the [Releases Section](https://github.com/sspanak/tt9/releas
|
||||||
## System Requirements
|
## System Requirements
|
||||||
- Android 4.4 or higher. _(Tested and confirmed on Android 4.4.2, 10 and 11)_
|
- Android 4.4 or higher. _(Tested and confirmed on Android 4.4.2, 10 and 11)_
|
||||||
- Free space:
|
- Free space:
|
||||||
- Minimum 30 Mb when not using Predictive mode and no dictionaries are loaded.
|
- Minimum 40 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).
|
- 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.
|
- 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._
|
_If you own a phone with Android 2.2 up to 4.4, please refer to the original version of Traditional T9 from 2016._
|
||||||
|
|
|
||||||
13
build.gradle
13
build.gradle
|
|
@ -9,10 +9,13 @@ buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.0.2'
|
classpath 'com.android.tools.build:gradle:8.0.2'
|
||||||
classpath 'gradle.plugin.at.zierler:yaml-validator-plugin:1.5.0'
|
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: 'com.android.application'
|
||||||
apply plugin: 'at.zierler.yamlvalidator'
|
apply plugin: 'at.zierler.yamlvalidator'
|
||||||
|
apply plugin: "io.objectbox"
|
||||||
|
|
||||||
apply from: 'gradle/scripts/constants.gradle'
|
apply from: 'gradle/scripts/constants.gradle'
|
||||||
apply from: 'gradle/scripts/dictionary-tools.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'
|
apply from: 'gradle/scripts/version-tools.gradle'
|
||||||
|
|
||||||
configurations.configureEach {
|
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 {
|
yamlValidator {
|
||||||
searchPaths = ['languages/definitions']
|
searchPaths = ['languages/definitions']
|
||||||
}
|
}
|
||||||
|
|
@ -32,8 +31,10 @@ configurations.configureEach {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core:1.10.1'
|
implementation 'androidx.core:core:1.10.1'
|
||||||
implementation 'androidx.preference:preference:1.2.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 {
|
repositories {
|
||||||
|
|
|
||||||
78
objectbox-models/default.json
Normal file
78
objectbox-models/default.json
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
package io.github.sspanak.tt9.db;
|
package io.github.sspanak.tt9.db;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.sqlite.SQLiteConstraintException;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -13,24 +11,22 @@ import java.util.List;
|
||||||
import io.github.sspanak.tt9.ConsumerCompat;
|
import io.github.sspanak.tt9.ConsumerCompat;
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException;
|
import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException;
|
||||||
import io.github.sspanak.tt9.db.room.TT9Room;
|
import io.github.sspanak.tt9.db.objectbox.Word;
|
||||||
import io.github.sspanak.tt9.db.room.Word;
|
import io.github.sspanak.tt9.db.objectbox.WordList;
|
||||||
import io.github.sspanak.tt9.db.room.WordList;
|
import io.github.sspanak.tt9.db.objectbox.WordStore;
|
||||||
import io.github.sspanak.tt9.db.room.WordsDao;
|
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
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.languages.Language;
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
import io.objectbox.exception.UniqueViolationException;
|
||||||
|
|
||||||
public class DictionaryDb {
|
public class DictionaryDb {
|
||||||
private static TT9Room dbInstance;
|
private static WordStore store;
|
||||||
|
|
||||||
private static final Handler asyncHandler = new Handler();
|
private static final Handler asyncHandler = new Handler();
|
||||||
|
|
||||||
public static synchronized void init(Context context) {
|
public static synchronized void init(Context context) {
|
||||||
if (dbInstance == null) {
|
if (store == null) {
|
||||||
context = context == null ? TraditionalT9.getMainContext() : context;
|
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();
|
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()) {
|
if (!Logger.isDebugLevel()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder debugText = new StringBuilder(title);
|
StringBuilder debugText = new StringBuilder("===== Word Matches =====");
|
||||||
debugText
|
debugText
|
||||||
.append("\n")
|
.append("\n")
|
||||||
.append("Word Count: ").append(words.size())
|
.append("Word Count: ").append(words.size())
|
||||||
|
|
@ -62,12 +58,12 @@ public class DictionaryDb {
|
||||||
debugText.append(" Sequence: ").append(sequence);
|
debugText.append(" Sequence: ").append(sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.d(tag, debugText.toString());
|
Logger.d("loadWords", debugText.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void runInTransaction(Runnable r) {
|
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.
|
* This query will finish immediately, if there is nothing to do. It's safe to run it often.
|
||||||
*/
|
*/
|
||||||
public static void normalizeWordFrequencies(SettingsStore settings) {
|
public static void normalizeWordFrequencies(SettingsStore settings) {
|
||||||
|
final String LOG_TAG = "db.normalizeWordFrequencies";
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
long time = System.currentTimeMillis();
|
for (int langId : getStore().getLanguages()) {
|
||||||
|
getStore().runInTransactionAsync(() -> {
|
||||||
|
try {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
int affectedRows = getInstance().wordsDao().normalizeFrequencies(
|
if (getStore().getMaxFrequency(langId) < settings.getWordFrequencyMax()) {
|
||||||
settings.getWordFrequencyNormalizationDivider(),
|
return;
|
||||||
settings.getWordFrequencyMax()
|
}
|
||||||
);
|
|
||||||
|
|
||||||
Logger.d(
|
List<Word> words = getStore().getMany(langId);
|
||||||
"db.normalizeWordFrequencies",
|
if (words == null) {
|
||||||
"Normalized " + affectedRows + " words in: " + (System.currentTimeMillis() - time) + " ms"
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
for (Word w : words) {
|
||||||
|
w.frequency /= settings.getWordFrequencyNormalizationDivider();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStore().put(words);
|
||||||
|
|
||||||
|
Logger.d(
|
||||||
|
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();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void areThereWords(ConsumerCompat<Boolean> notification, Language language) {
|
public static void areThereWords(ConsumerCompat<Boolean> notification, Language language) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
int langId = language != null ? language.getId() : -1;
|
boolean areThere = getStore().count(language != null ? language.getId() : -1) > 0;
|
||||||
notification.accept(getInstance().wordsDao().count(langId) > 0);
|
getStore().closeThreadResources();
|
||||||
|
notification.accept(areThere);
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void deleteWords(Runnable notification) {
|
public static void deleteWords(Context context, Runnable notification) {
|
||||||
deleteWords(notification, null);
|
new Thread(() -> {
|
||||||
|
getStore().destroy();
|
||||||
|
store = null;
|
||||||
|
init(context);
|
||||||
|
notification.run();
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void deleteWords(Runnable notification, ArrayList<Integer> languageIds) {
|
public static void deleteWords(Runnable notification, @NonNull ArrayList<Integer> languageIds) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
if (languageIds == null) {
|
getStore().removeMany(languageIds).closeThreadResources();
|
||||||
getInstance().clearAllTables();
|
|
||||||
} else if (languageIds.size() > 0) {
|
|
||||||
getInstance().wordsDao().deleteByLanguage(languageIds);
|
|
||||||
}
|
|
||||||
notification.run();
|
notification.run();
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
@ -124,168 +144,128 @@ public class DictionaryDb {
|
||||||
throw new InsertBlankWordException();
|
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(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
if (getInstance().wordsDao().doesWordExist(dbWord.langId, dbWord.word) > 0) {
|
if (getStore().exists(language.getId(), word, language.getDigitSequenceForWord(word))) {
|
||||||
throw new SQLiteConstraintException("Word already exists.");
|
throw new UniqueViolationException("Word already exists");
|
||||||
}
|
}
|
||||||
|
getStore().put(Word.create(language, word, 1, true));
|
||||||
getInstance().wordsDao().insert(dbWord);
|
|
||||||
getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence);
|
|
||||||
statusHandler.accept(0);
|
statusHandler.accept(0);
|
||||||
} catch (SQLiteConstraintException e) {
|
} catch (UniqueViolationException e) {
|
||||||
String msg = "Constraint violation when inserting a word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId
|
String msg = "Skipping word: '" + word + "' for language: " + language.getId() + ", because it already exists.";
|
||||||
+ ". " + e.getMessage();
|
Logger.w("insertWord", msg);
|
||||||
Logger.e("insertWord", msg);
|
|
||||||
statusHandler.accept(1);
|
statusHandler.accept(1);
|
||||||
} catch (Exception e) {
|
} 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);
|
Logger.e("insertWord", msg);
|
||||||
statusHandler.accept(2);
|
statusHandler.accept(2);
|
||||||
|
} finally {
|
||||||
|
getStore().closeThreadResources();
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void upsertWordsSync(List<Word> words) {
|
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 {
|
public static void incrementWordFrequency(@NonNull Language language, @NonNull String word, @NonNull String sequence) {
|
||||||
Logger.d("incrementWordFrequency", "Incrementing priority of Word: " + word +" | Sequence: " + 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) {
|
||||||
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)) {
|
|
||||||
return;
|
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(() -> {
|
new Thread(() -> {
|
||||||
try {
|
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.
|
// In case the user has changed the text case, there would be no match.
|
||||||
// Try again with the lowercase equivalent.
|
// Try again with the lowercase equivalent.
|
||||||
if (affectedRows == 0) {
|
if (dbWord == null) {
|
||||||
String lowercaseWord = word.toLowerCase(language.getLocale());
|
dbWord = getStore().get(language.getId(), word.toLowerCase(language.getLocale()), sequence);
|
||||||
affectedRows = getInstance().wordsDao().incrementFrequency(language.getId(), lowercaseWord, sequence);
|
|
||||||
|
|
||||||
Logger.d("incrementWordFrequency", "Attempting to increment frequency for lowercase variant: " + lowercaseWord);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (Exception e) {
|
||||||
Logger.e(
|
Logger.e(
|
||||||
DictionaryDb.class.getName(),
|
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();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loadWordsExact
|
* loadWords
|
||||||
* Loads words that match exactly the "sequence" and the optional "filter".
|
* Loads words matching and similar to a given digit sequence
|
||||||
* For example: "7655" gets "roll".
|
* 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();
|
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();
|
return matches.toStringList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minimumWords, int maximumWords) {
|
||||||
* loadWordsFuzzy
|
final int minWords = Math.max(minimumWords, 0);
|
||||||
* Loads words that start with "sequence" and optionally match the "filter".
|
final int maxWords = Math.max(maximumWords, minWords);
|
||||||
* 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();
|
|
||||||
|
|
||||||
// fuzzy queries are heavy, so we must restrict the search range as much as possible
|
if (sequence == null || sequence.length() == 0) {
|
||||||
boolean noFilter = (filter == null || filter.equals(""));
|
Logger.w("db.getWords", "Attempting to get words for an empty sequence.");
|
||||||
int maxWordLength = noFilter && sequence.length() <= 2 ? 5 : 1000;
|
sendWords(dataHandler, new ArrayList<>());
|
||||||
String index = sequence.length() <= 2 ? WordsDao.indexShortWords : WordsDao.indexLongWords;
|
return;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printDebug("loadWordsFuzzy", "~=~=~=~ Fuzzy Word Matches ~=~=~=~", sequence, matches, start);
|
if (language == null) {
|
||||||
return matches.toStringList();
|
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) {
|
private static void sendWords(ConsumerCompat<ArrayList<String>> dataHandler, ArrayList<String> wordList) {
|
||||||
asyncHandler.post(() -> dataHandler.accept(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.DictionaryImportAbortedException;
|
||||||
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAlreadyRunningException;
|
import io.github.sspanak.tt9.db.exceptions.DictionaryImportAlreadyRunningException;
|
||||||
import io.github.sspanak.tt9.db.exceptions.DictionaryImportException;
|
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.InvalidLanguageCharactersException;
|
||||||
import io.github.sspanak.tt9.languages.InvalidLanguageException;
|
import io.github.sspanak.tt9.languages.InvalidLanguageException;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
@ -80,7 +80,7 @@ public class DictionaryLoader {
|
||||||
currentFile = 0;
|
currentFile = 0;
|
||||||
importStartTime = System.currentTimeMillis();
|
importStartTime = System.currentTimeMillis();
|
||||||
|
|
||||||
sendFileCount(languages.size());
|
sendStartMessage(languages.size());
|
||||||
|
|
||||||
// SQLite does not support parallel queries, so let's import them one by one
|
// SQLite does not support parallel queries, so let's import them one by one
|
||||||
for (Language lang : languages) {
|
for (Language lang : languages) {
|
||||||
|
|
@ -155,8 +155,8 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
Logger.e(
|
Logger.e(
|
||||||
logTag,
|
logTag,
|
||||||
"Failed loading dictionary: " + language.getDictionaryFile() +
|
"Failed loading dictionary: " + language.getDictionaryFile()
|
||||||
" for language '" + language.getName() + "'. "
|
+ " for language '" + language.getName() + "'. "
|
||||||
+ e.getMessage()
|
+ 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<>();
|
ArrayList<Word> letters = new ArrayList<>();
|
||||||
|
|
||||||
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
|
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
|
||||||
|
|
@ -172,15 +172,7 @@ public class DictionaryLoader {
|
||||||
for (int key = 2; key <= 9; key++) {
|
for (int key = 2; key <= 9; key++) {
|
||||||
for (String langChar : language.getKeyCharacters(key, false)) {
|
for (String langChar : language.getKeyCharacters(key, false)) {
|
||||||
langChar = (isEnglish && langChar.equals("i")) ? langChar.toUpperCase(Locale.ENGLISH) : langChar;
|
langChar = (isEnglish && langChar.equals("i")) ? langChar.toUpperCase(Locale.ENGLISH) : langChar;
|
||||||
|
letters.add(Word.create(language, langChar, 0));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +181,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
|
|
||||||
private void importWords(Language language, String dictionaryFile) throws Exception {
|
private void importWords(Language language, String dictionaryFile) throws Exception {
|
||||||
sendProgressMessage(language, 0, 0);
|
sendProgressMessage(language, 1, 0);
|
||||||
|
|
||||||
long currentLine = 0;
|
long currentLine = 0;
|
||||||
long totalLines = getFileSize(dictionaryFile);
|
long totalLines = getFileSize(dictionaryFile);
|
||||||
|
|
@ -200,7 +192,7 @@ public class DictionaryLoader {
|
||||||
for (String line; (line = br.readLine()) != null; currentLine++) {
|
for (String line; (line = br.readLine()) != null; currentLine++) {
|
||||||
if (loadThread.isInterrupted()) {
|
if (loadThread.isInterrupted()) {
|
||||||
br.close();
|
br.close();
|
||||||
sendProgressMessage(language, -1, 0);
|
sendProgressMessage(language, 0, 0);
|
||||||
throw new DictionaryImportAbortedException();
|
throw new DictionaryImportAbortedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +201,7 @@ public class DictionaryLoader {
|
||||||
int frequency = getFrequency(parts);
|
int frequency = getFrequency(parts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dbWords.add(stringToWord(language, word, frequency));
|
dbWords.add(Word.create(language, word, frequency));
|
||||||
} catch (InvalidLanguageCharactersException e) {
|
} catch (InvalidLanguageCharactersException e) {
|
||||||
br.close();
|
br.close();
|
||||||
throw new DictionaryImportException(word, currentLine);
|
throw new DictionaryImportException(word, currentLine);
|
||||||
|
|
@ -222,6 +214,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
if (totalLines > 0) {
|
if (totalLines > 0) {
|
||||||
int progress = (int) Math.floor(100.0 * currentLine / totalLines);
|
int progress = (int) Math.floor(100.0 * currentLine / totalLines);
|
||||||
|
progress = Math.max(1, progress);
|
||||||
sendProgressMessage(language, progress, settings.getDictionaryImportProgressUpdateInterval());
|
sendProgressMessage(language, progress, settings.getDictionaryImportProgressUpdateInterval());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,19 +262,7 @@ public class DictionaryLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Word stringToWord(Language language, String word, int frequency) throws InvalidLanguageCharactersException {
|
private void sendStartMessage(int fileCount) {
|
||||||
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) {
|
|
||||||
if (onStatusChange == null) {
|
if (onStatusChange == null) {
|
||||||
Logger.w(logTag, "Cannot send file count without a status Handler. Ignoring message.");
|
Logger.w(logTag, "Cannot send file count without a status Handler. Ignoring message.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -289,6 +270,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
Bundle progressMsg = new Bundle();
|
Bundle progressMsg = new Bundle();
|
||||||
progressMsg.putInt("fileCount", fileCount);
|
progressMsg.putInt("fileCount", fileCount);
|
||||||
|
progressMsg.putInt("progress", 1);
|
||||||
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
|
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
78
src/io/github/sspanak/tt9/db/SQLWords.java
Normal file
78
src/io/github/sspanak/tt9/db/SQLWords.java
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
@ -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");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
48
src/io/github/sspanak/tt9/db/objectbox/Word.java
Normal file
48
src/io/github/sspanak/tt9/db/objectbox/Word.java
Normal 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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
package io.github.sspanak.tt9.db.room;
|
package io.github.sspanak.tt9.db.objectbox;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
public class WordList extends ArrayList<Word> {
|
public class WordList extends ArrayList<Word> {
|
||||||
public WordList(List<Word> words) {
|
public WordList() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public WordList(@NonNull List<Word> words) {
|
||||||
addAll(words);
|
addAll(words);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|
@ -26,6 +33,19 @@ public class WordList extends ArrayList<Word> {
|
||||||
return sb.toString();
|
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
|
@NonNull
|
||||||
public ArrayList<String> toStringList() {
|
public ArrayList<String> toStringList() {
|
||||||
ArrayList<String> strings = new ArrayList<>();
|
ArrayList<String> strings = new ArrayList<>();
|
||||||
208
src/io/github/sspanak/tt9/db/objectbox/WordStore.java
Normal file
208
src/io/github/sspanak/tt9/db/objectbox/WordStore.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -355,7 +355,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
cancelAutoAccept();
|
cancelAutoAccept();
|
||||||
clearSuggestions();
|
clearSuggestions();
|
||||||
|
|
||||||
String word = textField.getSurroundingWord();
|
String word = textField.getSurroundingWord(mLanguage);
|
||||||
if (word.isEmpty()) {
|
if (word.isEmpty()) {
|
||||||
UI.toastLong(this, R.string.add_word_no_selection);
|
UI.toastLong(this, R.string.add_word_no_selection);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,16 @@ import java.util.regex.Pattern;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
import io.github.sspanak.tt9.ime.modes.InputMode;
|
import io.github.sspanak.tt9.ime.modes.InputMode;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
||||||
public class TextField {
|
public class TextField {
|
||||||
public static final int IME_ACTION_ENTER = EditorInfo.IME_MASK_ACTION + 1;
|
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 beforeCursorWordRegex = Pattern.compile("(\\w+)(?!\n)$");
|
||||||
private static final Pattern afterCursorWordRegex = Pattern.compile("^(?<!\n)(\\w+)");
|
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 InputConnection connection;
|
||||||
public final EditorInfo field;
|
public final EditorInfo field;
|
||||||
|
|
@ -177,9 +181,20 @@ public class TextField {
|
||||||
* getSurroundingWord
|
* getSurroundingWord
|
||||||
* Returns the word next or around the cursor. Scanning length is up to 50 chars in each direction.
|
* Returns the word next or around the cursor. Scanning length is up to 50 chars in each direction.
|
||||||
*/
|
*/
|
||||||
@NonNull public String getSurroundingWord() {
|
@NonNull public String getSurroundingWord(Language language) {
|
||||||
Matcher before = beforeCursorWordRegex.matcher(getTextBeforeCursor());
|
Matcher before;
|
||||||
Matcher after = afterCursorWordRegex.matcher(getTextAfterCursor());
|
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) : "");
|
return (before.find() ? before.group(1) : "") + (after.find() ? after.group(1) : "");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,11 @@ public class Language {
|
||||||
return letters.contains("α");
|
return letters.contains("α");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUkrainian() {
|
||||||
|
ArrayList<String> letters = getKeyCharacters(4, false);
|
||||||
|
return letters.contains("ї");
|
||||||
|
}
|
||||||
|
|
||||||
/* ************ utility ************ */
|
/* ************ utility ************ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import io.github.sspanak.tt9.Logger;
|
||||||
import io.github.sspanak.tt9.R;
|
import io.github.sspanak.tt9.R;
|
||||||
import io.github.sspanak.tt9.db.DictionaryDb;
|
import io.github.sspanak.tt9.db.DictionaryDb;
|
||||||
import io.github.sspanak.tt9.db.DictionaryLoader;
|
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.GlobalKeyboardSettings;
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
||||||
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
|
import io.github.sspanak.tt9.preferences.helpers.Hotkeys;
|
||||||
|
|
@ -41,6 +42,7 @@ public class PreferencesActivity extends AppCompatActivity implements Preference
|
||||||
applyTheme();
|
applyTheme();
|
||||||
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
|
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
|
||||||
|
|
||||||
|
new SQLWords(this).clear();
|
||||||
DictionaryDb.init(this);
|
DictionaryDb.init(this);
|
||||||
DictionaryDb.normalizeWordFrequencies(settings);
|
DictionaryDb.normalizeWordFrequencies(settings);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,20 @@ package io.github.sspanak.tt9.preferences.items;
|
||||||
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
|
||||||
abstract class ItemClickable {
|
abstract public class ItemClickable {
|
||||||
protected final int CLICK_DEBOUNCE_TIME = 250;
|
private final int CLICK_DEBOUNCE_TIME = 250;
|
||||||
private long lastClickTime = 0;
|
private long lastClickTime = 0;
|
||||||
|
|
||||||
protected final Preference item;
|
protected final Preference item;
|
||||||
|
private final ArrayList<ItemClickable> otherItems = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
ItemClickable(Preference item) {
|
public ItemClickable(Preference item) {
|
||||||
this.item = 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
|
* debounceClick
|
||||||
* Protection against faulty devices, that sometimes send two (or more) click events
|
* Protection against faulty devices, that sometimes send two (or more) click events
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,15 @@ public class ItemLoadDictionary extends ItemClickable {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
|
||||||
loader.setOnStatusChange(this::onLoadingStatusChange);
|
loader.setOnStatusChange(this::onLoadingStatusChange);
|
||||||
|
refreshStatus();
|
||||||
|
}
|
||||||
|
|
||||||
if (!progressBar.isCompleted() && !progressBar.isFailed()) {
|
|
||||||
changeToCancelButton();
|
public void refreshStatus() {
|
||||||
|
if (loader.isRunning()) {
|
||||||
|
setLoadingStatus();
|
||||||
} else {
|
} else {
|
||||||
changeToLoadButton();
|
setReadyStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,12 +53,12 @@ public class ItemLoadDictionary extends ItemClickable {
|
||||||
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
|
item.setSummary(progressBar.getTitle() + " " + progressBar.getMessage());
|
||||||
|
|
||||||
if (progressBar.isCancelled()) {
|
if (progressBar.isCancelled()) {
|
||||||
changeToLoadButton();
|
setReadyStatus();
|
||||||
} else if (progressBar.isFailed()) {
|
} else if (progressBar.isFailed()) {
|
||||||
changeToLoadButton();
|
setReadyStatus();
|
||||||
UI.toastFromAsync(context, progressBar.getMessage());
|
UI.toastFromAsync(context, progressBar.getMessage());
|
||||||
} else if (progressBar.isCompleted()) {
|
} else if (progressBar.isCompleted()) {
|
||||||
changeToLoadButton();
|
setReadyStatus();
|
||||||
UI.toastFromAsync(context, R.string.dictionary_loaded);
|
UI.toastFromAsync(context, R.string.dictionary_loaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,23 +69,25 @@ public class ItemLoadDictionary extends ItemClickable {
|
||||||
ArrayList<Language> languages = LanguageCollection.getAll(context, settings.getEnabledLanguageIds());
|
ArrayList<Language> languages = LanguageCollection.getAll(context, settings.getEnabledLanguageIds());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setLoadingStatus();
|
||||||
loader.load(languages);
|
loader.load(languages);
|
||||||
changeToCancelButton();
|
|
||||||
} catch (DictionaryImportAlreadyRunningException e) {
|
} catch (DictionaryImportAlreadyRunningException e) {
|
||||||
loader.stop();
|
loader.stop();
|
||||||
changeToLoadButton();
|
setReadyStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void changeToCancelButton() {
|
private void setLoadingStatus() {
|
||||||
|
disableOtherItems();
|
||||||
item.setTitle(context.getString(R.string.dictionary_cancel_load));
|
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.setTitle(context.getString(R.string.dictionary_load_title));
|
||||||
item.setSummary(progressBar.isFailed() || progressBar.isCancelled() ? progressBar.getMessage() : "");
|
item.setSummary(progressBar.isFailed() || progressBar.isCancelled() ? progressBar.getMessage() : "");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,42 +14,30 @@ public class ItemTruncateAll extends ItemClickable {
|
||||||
|
|
||||||
protected final PreferencesActivity activity;
|
protected final PreferencesActivity activity;
|
||||||
protected final DictionaryLoader loader;
|
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);
|
super(item);
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.loadItem = loadItem;
|
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ItemTruncateAll setOtherTruncateItem(ItemTruncateUnselected item) {
|
|
||||||
this.otherTruncateItem = item;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onClick(Preference p) {
|
protected boolean onClick(Preference p) {
|
||||||
if (loader != null && loader.isRunning()) {
|
if (loader != null && loader.isRunning()) {
|
||||||
loader.stop();
|
return false;
|
||||||
loadItem.changeToLoadButton();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStartDeleting();
|
onStartDeleting();
|
||||||
DictionaryDb.deleteWords(this::onFinishDeleting);
|
DictionaryDb.deleteWords(activity.getApplicationContext(), this::onFinishDeleting);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void onStartDeleting() {
|
protected void onStartDeleting() {
|
||||||
if (otherTruncateItem != null) {
|
disableOtherItems();
|
||||||
otherTruncateItem.disable();
|
|
||||||
}
|
|
||||||
loadItem.disable();
|
|
||||||
disable();
|
disable();
|
||||||
item.setSummary(R.string.dictionary_truncating);
|
item.setSummary(R.string.dictionary_truncating);
|
||||||
}
|
}
|
||||||
|
|
@ -57,10 +45,7 @@ public class ItemTruncateAll extends ItemClickable {
|
||||||
|
|
||||||
protected void onFinishDeleting() {
|
protected void onFinishDeleting() {
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
if (otherTruncateItem != null) {
|
enableOtherItems();
|
||||||
otherTruncateItem.enable();
|
|
||||||
}
|
|
||||||
loadItem.enable();
|
|
||||||
item.setSummary("");
|
item.setSummary("");
|
||||||
enable();
|
enable();
|
||||||
UI.toastFromAsync(activity, R.string.dictionary_truncated);
|
UI.toastFromAsync(activity, R.string.dictionary_truncated);
|
||||||
|
|
|
||||||
|
|
@ -18,23 +18,17 @@ public class ItemTruncateUnselected extends ItemTruncateAll {
|
||||||
private final SettingsStore settings;
|
private final SettingsStore settings;
|
||||||
|
|
||||||
|
|
||||||
public ItemTruncateUnselected(Preference item, ItemLoadDictionary loadItem, PreferencesActivity context, SettingsStore settings, DictionaryLoader loader) {
|
public ItemTruncateUnselected(Preference item, PreferencesActivity context, SettingsStore settings, DictionaryLoader loader) {
|
||||||
super(item, loadItem, context, loader);
|
super(item, context, loader);
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public ItemTruncateUnselected setOtherTruncateItem(ItemTruncateAll otherTruncateItem) {
|
|
||||||
this.otherTruncateItem = otherTruncateItem;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onClick(Preference p) {
|
protected boolean onClick(Preference p) {
|
||||||
if (loader != null && loader.isRunning()) {
|
if (loader != null && loader.isRunning()) {
|
||||||
loader.stop();
|
return false;
|
||||||
loadItem.changeToLoadButton();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ArrayList<Integer> unselectedLanguageIds = new ArrayList<>();
|
ArrayList<Integer> unselectedLanguageIds = new ArrayList<>();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package io.github.sspanak.tt9.preferences.screens;
|
package io.github.sspanak.tt9.preferences.screens;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.R;
|
import io.github.sspanak.tt9.R;
|
||||||
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
||||||
import io.github.sspanak.tt9.preferences.items.ItemLoadDictionary;
|
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;
|
import io.github.sspanak.tt9.preferences.items.ItemTruncateUnselected;
|
||||||
|
|
||||||
public class DictionariesScreen extends BaseScreenFragment {
|
public class DictionariesScreen extends BaseScreenFragment {
|
||||||
|
private ItemLoadDictionary loadItem;
|
||||||
|
private ItemTruncateUnselected deleteItem;
|
||||||
|
private ItemTruncateAll truncateItem;
|
||||||
|
|
||||||
public DictionariesScreen() { init(); }
|
public DictionariesScreen() { init(); }
|
||||||
public DictionariesScreen(PreferencesActivity activity) { init(activity); }
|
public DictionariesScreen(PreferencesActivity activity) { init(activity); }
|
||||||
|
|
||||||
|
|
@ -23,31 +29,35 @@ public class DictionariesScreen extends BaseScreenFragment {
|
||||||
);
|
);
|
||||||
multiSelect.populate().enableValidation();
|
multiSelect.populate().enableValidation();
|
||||||
|
|
||||||
ItemLoadDictionary loadItem = new ItemLoadDictionary(
|
loadItem = new ItemLoadDictionary(
|
||||||
findPreference(ItemLoadDictionary.NAME),
|
findPreference(ItemLoadDictionary.NAME),
|
||||||
activity,
|
activity,
|
||||||
activity.settings,
|
activity.settings,
|
||||||
activity.getDictionaryLoader(),
|
activity.getDictionaryLoader(),
|
||||||
activity.getDictionaryProgressBar()
|
activity.getDictionaryProgressBar()
|
||||||
);
|
);
|
||||||
loadItem.enableClickHandler();
|
|
||||||
|
|
||||||
ItemTruncateAll truncateItem = new ItemTruncateAll(
|
deleteItem = new ItemTruncateUnselected(
|
||||||
findPreference(ItemTruncateAll.NAME),
|
|
||||||
loadItem,
|
|
||||||
activity,
|
|
||||||
activity.getDictionaryLoader()
|
|
||||||
);
|
|
||||||
|
|
||||||
ItemTruncateUnselected truncateSelectedItem = new ItemTruncateUnselected(
|
|
||||||
findPreference(ItemTruncateUnselected.NAME),
|
findPreference(ItemTruncateUnselected.NAME),
|
||||||
loadItem,
|
|
||||||
activity,
|
activity,
|
||||||
activity.settings,
|
activity.settings,
|
||||||
activity.getDictionaryLoader()
|
activity.getDictionaryLoader()
|
||||||
);
|
);
|
||||||
|
|
||||||
truncateItem.setOtherTruncateItem(truncateSelectedItem).enableClickHandler();
|
truncateItem = new ItemTruncateAll(
|
||||||
truncateSelectedItem.setOtherTruncateItem(truncateItem).enableClickHandler();
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ public class DictionaryLoadingBar {
|
||||||
public void show(Context context, Bundle data) {
|
public void show(Context context, Bundle data) {
|
||||||
String error = data.getString("error", null);
|
String error = data.getString("error", null);
|
||||||
int fileCount = data.getInt("fileCount", -1);
|
int fileCount = data.getInt("fileCount", -1);
|
||||||
|
int progress = data.getInt("progress", -1);
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
hasFailed = true;
|
hasFailed = true;
|
||||||
|
|
@ -116,10 +117,12 @@ public class DictionaryLoadingBar {
|
||||||
data.getLong("fileLine", -1),
|
data.getLong("fileLine", -1),
|
||||||
data.getString("word", "")
|
data.getString("word", "")
|
||||||
);
|
);
|
||||||
} else if (fileCount > -1) {
|
} else if (progress >= 0) {
|
||||||
setFileCount(fileCount);
|
|
||||||
} else {
|
|
||||||
hasFailed = false;
|
hasFailed = false;
|
||||||
|
if (fileCount >= 0) {
|
||||||
|
setFileCount(fileCount);
|
||||||
|
}
|
||||||
|
|
||||||
showProgress(
|
showProgress(
|
||||||
context,
|
context,
|
||||||
data.getLong("time", 0),
|
data.getLong("time", 0),
|
||||||
|
|
@ -143,7 +146,7 @@ public class DictionaryLoadingBar {
|
||||||
|
|
||||||
|
|
||||||
private void showProgress(Context context, long time, int currentFile, int currentFileProgress, int languageId) {
|
private void showProgress(Context context, long time, int currentFile, int currentFileProgress, int languageId) {
|
||||||
if (currentFileProgress < 0) {
|
if (currentFileProgress <= 0) {
|
||||||
hide();
|
hide();
|
||||||
isStopped = true;
|
isStopped = true;
|
||||||
title = "";
|
title = "";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue