Back to SQLite (#420)
* Deleted Objectbox and went back to SQLite. The database structure is entirely new and optimized for fast performance * Added slow query stats + cache for even faster performance * automatic language sorting script * legacy database management using SQLiteOpener * simplified access to the constant settings
This commit is contained in:
parent
e1574c38e5
commit
f1657a0285
51 changed files with 1763 additions and 946 deletions
|
|
@ -9,13 +9,11 @@ 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'
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
{
|
|
||||||
"_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
|
|
||||||
}
|
|
||||||
|
|
@ -25,11 +25,11 @@
|
||||||
<string name="pref_category_hacks">Compatibility</string>
|
<string name="pref_category_hacks">Compatibility</string>
|
||||||
<string name="pref_category_appearance">Appearance</string>
|
<string name="pref_category_appearance">Appearance</string>
|
||||||
<string name="pref_category_debug_options" translatable="false">Debug Options</string>
|
<string name="pref_category_debug_options" translatable="false">Debug Options</string>
|
||||||
<string name="pref_category_log_messages" translatable="false">Recent Log Messages</string>
|
|
||||||
<string name="pref_category_predictive_mode">Predictive Mode</string>
|
<string name="pref_category_predictive_mode">Predictive Mode</string>
|
||||||
<string name="pref_category_function_keys">Select Hotkeys</string>
|
<string name="pref_category_function_keys">Select Hotkeys</string>
|
||||||
<string name="pref_category_keypad">Keypad</string>
|
<string name="pref_category_keypad">Keypad</string>
|
||||||
<string name="pref_category_setup">Initial Setup</string>
|
<string name="pref_category_setup">Initial Setup</string>
|
||||||
|
<string name="pref_category_usage_stats" translatable="false">Usage Stats</string>
|
||||||
|
|
||||||
<string name="pref_abc_auto_accept">Automatic Letter Select</string>
|
<string name="pref_abc_auto_accept">Automatic Letter Select</string>
|
||||||
<string name="pref_abc_auto_accept_summary">Automatically type the selected letter after a short delay.</string>
|
<string name="pref_abc_auto_accept_summary">Automatically type the selected letter after a short delay.</string>
|
||||||
|
|
@ -44,8 +44,6 @@
|
||||||
<string name="pref_dark_theme_yes">Yes</string>
|
<string name="pref_dark_theme_yes">Yes</string>
|
||||||
<string name="pref_dark_theme_no">No</string>
|
<string name="pref_dark_theme_no">No</string>
|
||||||
<string name="pref_dark_theme_auto">Auto</string>
|
<string name="pref_dark_theme_auto">Auto</string>
|
||||||
<string name="pref_debug_logs" translatable="false">Detailed Debug Logs</string>
|
|
||||||
<string name="pref_system_logs" translatable="false">System Logs</string>
|
|
||||||
<string name="pref_double_zero_char">Character for Double 0-key Press</string>
|
<string name="pref_double_zero_char">Character for Double 0-key Press</string>
|
||||||
<string name="pref_hack_fb_messenger">Send messages with OK in Messenger</string>
|
<string name="pref_hack_fb_messenger">Send messages with OK in Messenger</string>
|
||||||
<string name="pref_hack_fb_messenger_summary">Allows sending messages with the OK key in Facebook Messenger.</string>
|
<string name="pref_hack_fb_messenger_summary">Allows sending messages with the OK key in Facebook Messenger.</string>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto" app:orderingFromXml="true">
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto" app:orderingFromXml="true">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:fragment="io.github.sspanak.tt9.preferences.SlowQueriesScreen"
|
||||||
|
app:key="pref_slow_queries"
|
||||||
|
app:layout="@layout/pref_text"
|
||||||
|
app:title="@string/pref_category_usage_stats" />
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:defaultValue="false"
|
app:defaultValue="false"
|
||||||
app:key="pref_enable_debug_logs"
|
app:key="pref_enable_debug_logs"
|
||||||
app:layout="@layout/pref_switch"
|
app:layout="@layout/pref_switch"
|
||||||
app:title="@string/pref_debug_logs" />
|
app:title="Debug Logs" />
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:defaultValue="false"
|
app:defaultValue="false"
|
||||||
app:key="pref_enable_system_logs"
|
app:key="pref_enable_system_logs"
|
||||||
app:layout="@layout/pref_switch"
|
app:layout="@layout/pref_switch"
|
||||||
app:title="@string/pref_system_logs" />
|
app:title="System Logs" />
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
app:title="@string/pref_category_log_messages"
|
app:title="Recent Log Messages"
|
||||||
app:layout="@layout/pref_category"
|
app:layout="@layout/pref_category"
|
||||||
app:singleLineTitle="true">
|
app:singleLineTitle="true">
|
||||||
<Preference
|
<Preference
|
||||||
|
|
|
||||||
31
res/xml/prefs_screen_usage_stats.xml
Normal file
31
res/xml/prefs_screen_usage_stats.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto" app:orderingFromXml="true">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="pref_slow_queries_reset_stats"
|
||||||
|
app:layout="@layout/pref_text"
|
||||||
|
app:title="Clear DB Cache" />
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:title="Summary"
|
||||||
|
app:layout="@layout/pref_category"
|
||||||
|
app:singleLineTitle="true">
|
||||||
|
<Preference
|
||||||
|
app:key="summary_container"
|
||||||
|
app:summary="--"
|
||||||
|
app:layout="@layout/pref_plain_text">
|
||||||
|
</Preference>
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:title="Slow Queries"
|
||||||
|
app:layout="@layout/pref_category"
|
||||||
|
app:singleLineTitle="true">
|
||||||
|
<Preference
|
||||||
|
app:key="query_list_container"
|
||||||
|
app:summary="--"
|
||||||
|
app:layout="@layout/pref_plain_text">
|
||||||
|
</Preference>
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
123
scripts/sort-dictionary.js
Normal file
123
scripts/sort-dictionary.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
const { basename } = require('path');
|
||||||
|
const { createReadStream, existsSync } = require('fs');
|
||||||
|
const { createInterface } = require('readline');
|
||||||
|
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage ${basename(process.argv[1])} LOCALE WORD-LIST.txt LANGUAGE-DEFINITION.yml`);
|
||||||
|
console.log('Sorts a dictionary for optimum search speed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function validateInput() {
|
||||||
|
if (process.argv.length < 4) {
|
||||||
|
printHelp();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(process.argv[3])) {
|
||||||
|
console.error(`Failure! Could not find word list file "${process.argv[3]}."`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(process.argv[4])) {
|
||||||
|
console.error(`Failure! Could not find language definition file "${process.argv[3]}."`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
definitionFile: process.argv[4],
|
||||||
|
wordsFile: process.argv[3],
|
||||||
|
locale: process.argv[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function printWords(wordList) {
|
||||||
|
if (Array.isArray(wordList)) {
|
||||||
|
wordList.forEach(w => console.log(`${w.word}${w.frequency ? '\t' + w.frequency : ''}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function readWords(fileName) {
|
||||||
|
const words = [];
|
||||||
|
|
||||||
|
if (!fileName) {
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const line of createInterface({ input: createReadStream(fileName) })) {
|
||||||
|
const [word, frequency] = line.split("\t");
|
||||||
|
words.push({
|
||||||
|
word,
|
||||||
|
frequency: Number.isNaN(Number.parseInt(frequency)) ? 0 : Number.parseInt(frequency)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function readDefinition(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lettersPattern = /^\s+-\s*\[([^\]]+)/;
|
||||||
|
let letterWeights = new Map([["'", 1], ['-', 1], ['"', 1]]);
|
||||||
|
|
||||||
|
let key = 2;
|
||||||
|
for await (const line of createInterface({ input: createReadStream(fileName) })) {
|
||||||
|
if (line.includes('SPECIAL') || line.includes('PUNCTUATION')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = line.match(lettersPattern);
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
const letters = matches[1].replace(/\s/g, '').split(',');
|
||||||
|
letters.forEach(l => letterWeights.set(l, key));
|
||||||
|
key++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return letterWeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dictionarySort(a, b, letterWeights, locale) {
|
||||||
|
if (a.word.length !== b.word.length) {
|
||||||
|
return a.word.length - b.word.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, end = a.word.length; i < end; i++) {
|
||||||
|
const charA = a.word.toLocaleLowerCase(locale).charAt(i);
|
||||||
|
const charB = b.word.toLocaleLowerCase(locale).charAt(i);
|
||||||
|
const distance = letterWeights.get(charA) - letterWeights.get(charB);
|
||||||
|
|
||||||
|
|
||||||
|
if (distance !== 0) {
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function work({ definitionFile, wordsFile, locale }) {
|
||||||
|
return Promise.all([
|
||||||
|
readWords(wordsFile),
|
||||||
|
readDefinition(definitionFile)
|
||||||
|
]).then(([words, letterWeights]) =>
|
||||||
|
words.sort((a, b) => dictionarySort(a, b, letterWeights, locale))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** main **/
|
||||||
|
work(validateInput())
|
||||||
|
.then(words => printWords(words))
|
||||||
|
.catch(e => console.error(e));
|
||||||
|
|
@ -7,7 +7,7 @@ public class Logger {
|
||||||
public static int LEVEL = BuildConfig.DEBUG ? Log.DEBUG : Log.ERROR;
|
public static int LEVEL = BuildConfig.DEBUG ? Log.DEBUG : Log.ERROR;
|
||||||
|
|
||||||
public static boolean isDebugLevel() {
|
public static boolean isDebugLevel() {
|
||||||
return LEVEL == Log.DEBUG;
|
return LEVEL <= Log.DEBUG;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void enableDebugLevel(boolean yes) {
|
public static void enableDebugLevel(boolean yes) {
|
||||||
|
|
@ -16,7 +16,7 @@ public class Logger {
|
||||||
|
|
||||||
static public void v(String tag, String msg) {
|
static public void v(String tag, String msg) {
|
||||||
if (LEVEL <= Log.VERBOSE) {
|
if (LEVEL <= Log.VERBOSE) {
|
||||||
Log.v(TAG_PREFIX + tag, msg);
|
Log.d(TAG_PREFIX + tag, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package io.github.sspanak.tt9;
|
package io.github.sspanak.tt9;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class TextTools {
|
public class TextTools {
|
||||||
|
|
@ -37,7 +41,14 @@ public class TextTools {
|
||||||
return str != null && !str.isEmpty() && (str.charAt(0) >= '0' && str.charAt(0) <= '9');
|
return str != null && !str.isEmpty() && (str.charAt(0) >= '0' && str.charAt(0) <= '9');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String removeNonLetters(String str) {
|
public static String unixTimestampToISODate(long timestamp) {
|
||||||
return str != null ? str.replaceAll("\\P{L}", "") : null;
|
if (timestamp < 0) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
|
||||||
|
sdf.setTimeZone(TimeZone.getDefault());
|
||||||
|
|
||||||
|
return sdf.format(new Date(timestamp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,271 +0,0 @@
|
||||||
package io.github.sspanak.tt9.db;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Handler;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
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.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.Language;
|
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
|
||||||
import io.objectbox.exception.UniqueViolationException;
|
|
||||||
|
|
||||||
public class DictionaryDb {
|
|
||||||
private static WordStore store;
|
|
||||||
private static final Handler asyncHandler = new Handler();
|
|
||||||
|
|
||||||
public static synchronized void init(Context context) {
|
|
||||||
if (store == null) {
|
|
||||||
context = context == null ? TraditionalT9.getMainContext() : context;
|
|
||||||
store = new WordStore(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static synchronized void init() {
|
|
||||||
init(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static WordStore getStore() {
|
|
||||||
init();
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void printLoadDebug(String sequence, WordList words, long startTime) {
|
|
||||||
if (!Logger.isDebugLevel()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder debugText = new StringBuilder("===== Word Matches =====");
|
|
||||||
debugText
|
|
||||||
.append("\n")
|
|
||||||
.append("Word Count: ").append(words.size())
|
|
||||||
.append(". Time: ").append(System.currentTimeMillis() - startTime).append(" ms.");
|
|
||||||
if (words.size() > 0) {
|
|
||||||
debugText.append("\n").append(words);
|
|
||||||
} else {
|
|
||||||
debugText.append(" Sequence: ").append(sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d("loadWords", debugText.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void runInTransaction(Runnable r) {
|
|
||||||
getStore().runInTransaction(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* normalizeWordFrequencies
|
|
||||||
* Normalizes the word frequencies for all languages that have reached the maximum, as defined in
|
|
||||||
* the settings.
|
|
||||||
* This query will finish immediately, if there is nothing to do. It's safe to run it often.
|
|
||||||
*/
|
|
||||||
public static void normalizeWordFrequencies(SettingsStore settings) {
|
|
||||||
final String LOG_TAG = "db.normalizeWordFrequencies";
|
|
||||||
|
|
||||||
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(
|
|
||||||
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(() -> {
|
|
||||||
boolean areThere = getStore().count(language != null ? language.getId() : -1) > 0;
|
|
||||||
getStore().closeThreadResources();
|
|
||||||
notification.accept(areThere);
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void deleteWords(Context context, Runnable notification) {
|
|
||||||
new Thread(() -> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void insertWord(ConsumerCompat<Integer> statusHandler, @NonNull Language language, String word) throws Exception {
|
|
||||||
if (word == null || word.length() == 0) {
|
|
||||||
throw new InsertBlankWordException();
|
|
||||||
}
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
if (getStore().exists(language.getId(), word, language.getDigitSequenceForWord(word))) {
|
|
||||||
throw new UniqueViolationException("Word already exists");
|
|
||||||
}
|
|
||||||
getStore().put(Word.create(language, word, 1, true));
|
|
||||||
statusHandler.accept(0);
|
|
||||||
} 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: '" + 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) {
|
|
||||||
getStore().put(words);
|
|
||||||
getStore().closeThreadResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
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 (dbWord == null) {
|
|
||||||
dbWord = getStore().get(language.getId(), word.toLowerCase(language.getLocale()), sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + ". " + e.getMessage()
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
getStore().closeThreadResources();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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> loadWords(Language language, String sequence, String filter, int minimumWords, int maximumWords) {
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
|
|
||||||
WordList matches = getStore()
|
|
||||||
.getMany(language, sequence, filter, maximumWords)
|
|
||||||
.filter(sequence.length(), minimumWords);
|
|
||||||
|
|
||||||
getStore().closeThreadResources();
|
|
||||||
printLoadDebug(sequence, matches, start);
|
|
||||||
return matches.toStringList();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (sequence == null || sequence.length() == 0) {
|
|
||||||
Logger.w("db.getWords", "Attempting to get words for an empty sequence.");
|
|
||||||
sendWords(dataHandler, new ArrayList<>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,21 +13,25 @@ import java.util.Locale;
|
||||||
|
|
||||||
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.entities.WordBatch;
|
||||||
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.objectbox.Word;
|
import io.github.sspanak.tt9.db.sqlite.DeleteOps;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.InsertOps;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.Tables;
|
||||||
import io.github.sspanak.tt9.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;
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
public class DictionaryLoader {
|
public class DictionaryLoader {
|
||||||
|
private static final String LOG_TAG = "DictionaryLoader";
|
||||||
private static DictionaryLoader self;
|
private static DictionaryLoader self;
|
||||||
private final String logTag = "DictionaryLoader";
|
|
||||||
|
|
||||||
private final AssetManager assets;
|
private final AssetManager assets;
|
||||||
private final SettingsStore settings;
|
private final SQLiteOpener sqlite;
|
||||||
|
|
||||||
private static final Handler asyncHandler = new Handler();
|
private static final Handler asyncHandler = new Handler();
|
||||||
private ConsumerCompat<Bundle> onStatusChange;
|
private ConsumerCompat<Bundle> onStatusChange;
|
||||||
|
|
@ -50,7 +54,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
public DictionaryLoader(Context context) {
|
public DictionaryLoader(Context context) {
|
||||||
assets = context.getAssets();
|
assets = context.getAssets();
|
||||||
settings = new SettingsStore(context);
|
sqlite = SQLiteOpener.getInstance(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,7 +74,7 @@ public class DictionaryLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (languages.size() == 0) {
|
if (languages.size() == 0) {
|
||||||
Logger.d(logTag, "Nothing to do");
|
Logger.d(LOG_TAG, "Nothing to do");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,40 +113,69 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
private void importAll(Language language) {
|
private void importAll(Language language) {
|
||||||
if (language == null) {
|
if (language == null) {
|
||||||
Logger.e(logTag, "Failed loading a dictionary for NULL language.");
|
Logger.e(LOG_TAG, "Failed loading a dictionary for NULL language.");
|
||||||
sendError(InvalidLanguageException.class.getSimpleName(), -1);
|
sendError(InvalidLanguageException.class.getSimpleName(), -1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DictionaryDb.runInTransaction(() -> {
|
|
||||||
try {
|
try {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
importWords(language, language.getDictionaryFile());
|
float progress = 1;
|
||||||
Logger.i(
|
final float dictionaryMaxProgress = 90f;
|
||||||
logTag,
|
|
||||||
"Dictionary: '" + language.getDictionaryFile() + "'" +
|
sqlite.beginTransaction();
|
||||||
" processing time: " + (System.currentTimeMillis() - start) + " ms"
|
|
||||||
);
|
Tables.dropIndexes(sqlite.getDb(), language);
|
||||||
|
sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
logLoadingStep("Indexes dropped", language, start);
|
||||||
|
|
||||||
start = System.currentTimeMillis();
|
start = System.currentTimeMillis();
|
||||||
importLetters(language);
|
DeleteOps.delete(sqlite, language.getId());
|
||||||
Logger.i(
|
sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
logTag,
|
logLoadingStep("Storage cleared", language, start);
|
||||||
"Loaded letters for '" + language.getName() + "' language in: " + (System.currentTimeMillis() - start) + " ms"
|
|
||||||
);
|
|
||||||
} catch (DictionaryImportAbortedException e) {
|
|
||||||
stop();
|
|
||||||
|
|
||||||
Logger.i(
|
start = System.currentTimeMillis();
|
||||||
logTag,
|
int lettersCount = importLetters(language);
|
||||||
e.getMessage() + ". File '" + language.getDictionaryFile() + "' not imported."
|
sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
);
|
logLoadingStep("Letters imported", language, start);
|
||||||
|
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
InsertOps.restoreCustomWords(sqlite.getDb(), language);
|
||||||
|
sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
logLoadingStep("Custom words restored", language, start);
|
||||||
|
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
WordBatch words = readWordsFile(language, lettersCount, progress, progress + 25f);
|
||||||
|
progress += 25;
|
||||||
|
sendProgressMessage(language, progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
logLoadingStep("Dictionary file loaded in memory", language, start);
|
||||||
|
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
saveWordBatch(words, progress, dictionaryMaxProgress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
progress = dictionaryMaxProgress;
|
||||||
|
sendProgressMessage(language, progress, 0);
|
||||||
|
logLoadingStep("Dictionary words saved in database", language, start);
|
||||||
|
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
Tables.createPositionIndex(sqlite.getDb(), language);
|
||||||
|
sendProgressMessage(language, progress + (100f - progress) / 2f, 0);
|
||||||
|
Tables.createWordIndex(sqlite.getDb(), language);
|
||||||
|
sendProgressMessage(language, 100, 0);
|
||||||
|
logLoadingStep("Indexes restored", language, start);
|
||||||
|
|
||||||
|
sqlite.finishTransaction();
|
||||||
|
SlowQueryStats.clear();
|
||||||
|
} catch (DictionaryImportAbortedException e) {
|
||||||
|
sqlite.failTransaction();
|
||||||
|
stop();
|
||||||
|
Logger.i(LOG_TAG, e.getMessage() + ". File '" + language.getDictionaryFile() + "' not imported.");
|
||||||
} catch (DictionaryImportException e) {
|
} catch (DictionaryImportException e) {
|
||||||
stop();
|
stop();
|
||||||
|
sqlite.failTransaction();
|
||||||
sendImportError(DictionaryImportException.class.getSimpleName(), language.getId(), e.line, e.word);
|
sendImportError(DictionaryImportException.class.getSimpleName(), language.getId(), e.line, e.word);
|
||||||
|
|
||||||
Logger.e(
|
Logger.e(
|
||||||
logTag,
|
LOG_TAG,
|
||||||
" Invalid word: '" + e.word
|
" Invalid word: '" + e.word
|
||||||
+ "' in dictionary: '" + language.getDictionaryFile() + "'"
|
+ "' in dictionary: '" + language.getDictionaryFile() + "'"
|
||||||
+ " on line " + e.line
|
+ " on line " + e.line
|
||||||
|
|
@ -151,76 +184,126 @@ public class DictionaryLoader {
|
||||||
);
|
);
|
||||||
} catch (Exception | Error e) {
|
} catch (Exception | Error e) {
|
||||||
stop();
|
stop();
|
||||||
|
sqlite.failTransaction();
|
||||||
sendError(e.getClass().getSimpleName(), language.getId());
|
sendError(e.getClass().getSimpleName(), language.getId());
|
||||||
|
|
||||||
Logger.e(
|
Logger.e(
|
||||||
logTag,
|
LOG_TAG,
|
||||||
"Failed loading dictionary: " + language.getDictionaryFile()
|
"Failed loading dictionary: " + language.getDictionaryFile()
|
||||||
+ " for language '" + language.getName() + "'. "
|
+ " for language '" + language.getName() + "'. "
|
||||||
+ e.getMessage()
|
+ e.getMessage()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void importLetters(Language language) throws InvalidLanguageCharactersException {
|
private int importLetters(Language language) throws InvalidLanguageCharactersException, DictionaryImportAbortedException {
|
||||||
ArrayList<Word> letters = new ArrayList<>();
|
int lettersCount = 0;
|
||||||
|
|
||||||
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
|
boolean isEnglish = language.getLocale().equals(Locale.ENGLISH);
|
||||||
|
WordBatch letters = new WordBatch(language);
|
||||||
|
|
||||||
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));
|
letters.add(langChar, 0, key);
|
||||||
|
lettersCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DictionaryDb.upsertWordsSync(letters);
|
saveWordBatch(letters, -1, -1, -1);
|
||||||
|
|
||||||
|
return lettersCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void importWords(Language language, String dictionaryFile) throws Exception {
|
private WordBatch readWordsFile(Language language, int positionShift, float minProgress, float maxProgress) throws Exception {
|
||||||
sendProgressMessage(language, 1, 0);
|
int currentLine = 1;
|
||||||
|
int totalLines = getFileSize(language.getDictionaryFile());
|
||||||
|
float progressRatio = (maxProgress - minProgress) / totalLines;
|
||||||
|
|
||||||
long currentLine = 0;
|
WordBatch batch = new WordBatch(language, totalLines);
|
||||||
long totalLines = getFileSize(dictionaryFile);
|
|
||||||
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(assets.open(dictionaryFile), StandardCharsets.UTF_8));
|
|
||||||
ArrayList<Word> dbWords = new ArrayList<>();
|
|
||||||
|
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(assets.open(language.getDictionaryFile()), StandardCharsets.UTF_8))) {
|
||||||
for (String line; (line = br.readLine()) != null; currentLine++) {
|
for (String line; (line = br.readLine()) != null; currentLine++) {
|
||||||
if (loadThread.isInterrupted()) {
|
if (loadThread.isInterrupted()) {
|
||||||
br.close();
|
|
||||||
sendProgressMessage(language, 0, 0);
|
sendProgressMessage(language, 0, 0);
|
||||||
throw new DictionaryImportAbortedException();
|
throw new DictionaryImportAbortedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] parts = splitLine(line);
|
String[] parts = splitLine(line);
|
||||||
String word = parts[0];
|
String word = parts[0];
|
||||||
int frequency = getFrequency(parts);
|
short frequency = getFrequency(parts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dbWords.add(Word.create(language, word, frequency));
|
batch.add(word, frequency, currentLine + positionShift);
|
||||||
} catch (InvalidLanguageCharactersException e) {
|
} catch (InvalidLanguageCharactersException e) {
|
||||||
br.close();
|
|
||||||
throw new DictionaryImportException(word, currentLine);
|
throw new DictionaryImportException(word, currentLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbWords.size() >= settings.getDictionaryImportWordChunkSize() || currentLine >= totalLines - 1) {
|
if (totalLines > 0 && currentLine % SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_BATCH_SIZE == 0) {
|
||||||
DictionaryDb.upsertWordsSync(dbWords);
|
sendProgressMessage(language, minProgress + progressRatio * currentLine, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
dbWords.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalLines > 0) {
|
|
||||||
int progress = (int) Math.floor(100.0 * currentLine / totalLines);
|
|
||||||
progress = Math.max(1, progress);
|
|
||||||
sendProgressMessage(language, progress, settings.getDictionaryImportProgressUpdateInterval());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
br.close();
|
return batch;
|
||||||
sendProgressMessage(language, 100, 0);
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void saveWordBatch(WordBatch batch, float minProgress, float maxProgress, int sizeUpdateInterval) throws DictionaryImportAbortedException {
|
||||||
|
float middleProgress = minProgress + (maxProgress - minProgress) / 2;
|
||||||
|
|
||||||
|
InsertOps insertOps = new InsertOps(sqlite.getDb(), batch.getLanguage());
|
||||||
|
|
||||||
|
insertWordsBatch(insertOps, batch, minProgress, middleProgress - 2, sizeUpdateInterval);
|
||||||
|
insertWordPositionsBatch(insertOps, batch, middleProgress - 2, maxProgress - 2, sizeUpdateInterval);
|
||||||
|
InsertOps.insertLanguageMeta(sqlite.getDb(), batch.getLanguage().getId());
|
||||||
|
|
||||||
|
if (sizeUpdateInterval > 0) {
|
||||||
|
sendProgressMessage(batch.getLanguage(), maxProgress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void insertWordsBatch(InsertOps insertOps, WordBatch batch, float minProgress, float maxProgress, int sizeUpdateInterval) throws DictionaryImportAbortedException {
|
||||||
|
if (batch.getWords().size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float progressRatio = (maxProgress - minProgress) / batch.getWords().size();
|
||||||
|
|
||||||
|
for (int progress = 0, end = batch.getWords().size(); progress < end; progress++) {
|
||||||
|
if (loadThread.isInterrupted()) {
|
||||||
|
sendProgressMessage(batch.getLanguage(), 0, 0);
|
||||||
|
throw new DictionaryImportAbortedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
insertOps.insertWord(batch.getWords().get(progress));
|
||||||
|
if (sizeUpdateInterval > 0 && progress % sizeUpdateInterval == 0) {
|
||||||
|
sendProgressMessage(batch.getLanguage(), minProgress + progress * progressRatio, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void insertWordPositionsBatch(InsertOps insertOps, WordBatch batch, float minProgress, float maxProgress, int sizeUpdateInterval) throws DictionaryImportAbortedException {
|
||||||
|
if (batch.getPositions().size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float progressRatio = (maxProgress - minProgress) / batch.getPositions().size();
|
||||||
|
|
||||||
|
for (int progress = 0, end = batch.getPositions().size(); progress < end; progress++) {
|
||||||
|
if (loadThread.isInterrupted()) {
|
||||||
|
sendProgressMessage(batch.getLanguage(), 0, 0);
|
||||||
|
throw new DictionaryImportAbortedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
insertOps.insertWordPosition(batch.getPositions().get(progress));
|
||||||
|
if (sizeUpdateInterval > 0 && progress % sizeUpdateInterval == 0) {
|
||||||
|
sendProgressMessage(batch.getLanguage(), minProgress + progress * progressRatio, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -241,21 +324,21 @@ public class DictionaryLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private long getFileSize(String filename) {
|
private int getFileSize(String filename) {
|
||||||
String sizeFilename = filename + ".size";
|
String sizeFilename = filename + ".size";
|
||||||
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(sizeFilename), StandardCharsets.UTF_8))) {
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(sizeFilename), StandardCharsets.UTF_8))) {
|
||||||
return Integer.parseInt(reader.readLine());
|
return Integer.parseInt(reader.readLine());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.w(logTag, "Could not read the size of: " + filename + " from: " + sizeFilename + ". " + e.getMessage());
|
Logger.w(LOG_TAG, "Could not read the size of: " + filename + " from: " + sizeFilename + ". " + e.getMessage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private int getFrequency(String[] lineParts) {
|
private short getFrequency(String[] lineParts) {
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(lineParts[1]);
|
return Short.parseShort(lineParts[1]);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +347,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
private void sendStartMessage(int fileCount) {
|
private void sendStartMessage(int fileCount) {
|
||||||
if (onStatusChange == null) {
|
if (onStatusChange == null) {
|
||||||
Logger.w(logTag, "Cannot send file count without a status Handler. Ignoring message.");
|
Logger.w(LOG_TAG, "Cannot send file count without a status Handler. Ignoring message.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,9 +358,9 @@ public class DictionaryLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sendProgressMessage(Language language, int progress, int progressUpdateInterval) {
|
private void sendProgressMessage(Language language, float progress, int progressUpdateInterval) {
|
||||||
if (onStatusChange == null) {
|
if (onStatusChange == null) {
|
||||||
Logger.w(logTag, "Cannot send progress without a status Handler. Ignoring message.");
|
Logger.w(LOG_TAG, "Cannot send progress without a status Handler. Ignoring message.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +374,7 @@ public class DictionaryLoader {
|
||||||
Bundle progressMsg = new Bundle();
|
Bundle progressMsg = new Bundle();
|
||||||
progressMsg.putInt("languageId", language.getId());
|
progressMsg.putInt("languageId", language.getId());
|
||||||
progressMsg.putLong("time", getImportTime());
|
progressMsg.putLong("time", getImportTime());
|
||||||
progressMsg.putInt("progress", progress);
|
progressMsg.putInt("progress", (int) Math.round(progress));
|
||||||
progressMsg.putInt("currentFile", currentFile);
|
progressMsg.putInt("currentFile", currentFile);
|
||||||
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
|
asyncHandler.post(() -> onStatusChange.accept(progressMsg));
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +382,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
private void sendError(String message, int langId) {
|
private void sendError(String message, int langId) {
|
||||||
if (onStatusChange == null) {
|
if (onStatusChange == null) {
|
||||||
Logger.w(logTag, "Cannot send an error without a status Handler. Ignoring message.");
|
Logger.w(LOG_TAG, "Cannot send an error without a status Handler. Ignoring message.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,7 +395,7 @@ public class DictionaryLoader {
|
||||||
|
|
||||||
private void sendImportError(String message, int langId, long fileLine, String word) {
|
private void sendImportError(String message, int langId, long fileLine, String word) {
|
||||||
if (onStatusChange == null) {
|
if (onStatusChange == null) {
|
||||||
Logger.w(logTag, "Cannot send an import error without a status Handler. Ignoring message.");
|
Logger.w(LOG_TAG, "Cannot send an import error without a status Handler. Ignoring message.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,4 +406,11 @@ public class DictionaryLoader {
|
||||||
errorMsg.putString("word", word);
|
errorMsg.putString("word", word);
|
||||||
asyncHandler.post(() -> onStatusChange.accept(errorMsg));
|
asyncHandler.post(() -> onStatusChange.accept(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void logLoadingStep(String message, Language language, long time) {
|
||||||
|
if (Logger.isDebugLevel()) {
|
||||||
|
Logger.d(LOG_TAG, message + " for language '" + language.getName() + "' in: " + (System.currentTimeMillis() - time) + " ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
src/io/github/sspanak/tt9/db/LegacyDb.java
Normal file
39
src/io/github/sspanak/tt9/db/LegacyDb.java
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package io.github.sspanak.tt9.db;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
|
||||||
|
public class LegacyDb extends SQLiteOpenHelper {
|
||||||
|
private final String LOG_TAG = getClass().getSimpleName();
|
||||||
|
private static final String DB_NAME = "t9dict.db";
|
||||||
|
private static final String TABLE_NAME = "words";
|
||||||
|
|
||||||
|
private static boolean isCompleted = false;
|
||||||
|
|
||||||
|
public LegacyDb(Activity activity) {
|
||||||
|
super(activity.getApplicationContext(), DB_NAME, null, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
if (isCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try (SQLiteDatabase db = getWritableDatabase()) {
|
||||||
|
db.compileStatement("DROP TABLE " + TABLE_NAME).execute();
|
||||||
|
Logger.d(LOG_TAG, "SQL Words cleaned successfully.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.d(LOG_TAG, "Assuming no words, because of query error. " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
isCompleted = true;
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onCreate(SQLiteDatabase db) {}
|
||||||
|
@Override public void onUpgrade(SQLiteDatabase db, int i, int ii) {}
|
||||||
|
}
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
src/io/github/sspanak/tt9/db/SlowQueryStats.java
Normal file
87
src/io/github/sspanak/tt9/db/SlowQueryStats.java
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package io.github.sspanak.tt9.db;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
import io.github.sspanak.tt9.TextTools;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
|
public class SlowQueryStats {
|
||||||
|
private static final String LOG_TAG = SlowQueryStats.class.getSimpleName();
|
||||||
|
private static long firstQueryTime = -1;
|
||||||
|
private static long maxQueryTime = 0;
|
||||||
|
private static long totalQueries = 0;
|
||||||
|
private static long totalQueryTime = 0;
|
||||||
|
private static final HashMap<String, Integer> slowQueries = new HashMap<>();
|
||||||
|
private static final HashMap<String, String> resultCache = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
public static String generateKey(Language language, String sequence, String wordFilter, int minimumWords) {
|
||||||
|
return language.getId() + "_" + sequence + "_" + wordFilter + "_" + minimumWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void add(String key, int time, String positionsList) {
|
||||||
|
if (firstQueryTime == -1) {
|
||||||
|
firstQueryTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
maxQueryTime = Math.max(maxQueryTime, time);
|
||||||
|
totalQueries++;
|
||||||
|
totalQueryTime += time;
|
||||||
|
if (time < SettingsStore.SLOW_QUERY_TIME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slowQueries.put(key, time);
|
||||||
|
if (!resultCache.containsKey(key)) {
|
||||||
|
resultCache.put(key, positionsList.replaceAll("-\\d+,", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCachedIfSlow(String key) {
|
||||||
|
Integer queryTime = slowQueries.get(key);
|
||||||
|
boolean isSlow = queryTime != null && queryTime >= SettingsStore.SLOW_QUERY_TIME;
|
||||||
|
|
||||||
|
if (isSlow) {
|
||||||
|
Logger.d(LOG_TAG, "Loading cached positions for query: " + key);
|
||||||
|
return resultCache.get(key);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSummary() {
|
||||||
|
long slowQueryTotalTime = 0;
|
||||||
|
for (int time : slowQueries.values()) {
|
||||||
|
slowQueryTotalTime += time;
|
||||||
|
}
|
||||||
|
|
||||||
|
long averageTime = totalQueries == 0 ? 0 : totalQueryTime / totalQueries;
|
||||||
|
long slowAverageTime = slowQueries.size() == 0 ? 0 : slowQueryTotalTime / slowQueries.size();
|
||||||
|
|
||||||
|
return
|
||||||
|
"Queries: " + totalQueries + ". Average time: " + averageTime + " ms." +
|
||||||
|
"\nSlow: " + slowQueries.size() + ". Average time: " + slowAverageTime + " ms." +
|
||||||
|
"\nSlowest: " + maxQueryTime + " ms." +
|
||||||
|
"\nFirst: " + TextTools.unixTimestampToISODate(firstQueryTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getList() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String key : slowQueries.keySet()) {
|
||||||
|
sb.append(key).append(": ").append(slowQueries.get(key)).append(" ms\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
firstQueryTime = -1;
|
||||||
|
maxQueryTime = 0;
|
||||||
|
totalQueries = 0;
|
||||||
|
totalQueryTime = 0;
|
||||||
|
slowQueries.clear();
|
||||||
|
resultCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/io/github/sspanak/tt9/db/WordStore.java
Normal file
259
src/io/github/sspanak/tt9/db/WordStore.java
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
package io.github.sspanak.tt9.db;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
import io.github.sspanak.tt9.db.entities.Word;
|
||||||
|
import io.github.sspanak.tt9.db.entities.WordList;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.DeleteOps;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.InsertOps;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.ReadOps;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.SQLiteOpener;
|
||||||
|
import io.github.sspanak.tt9.db.sqlite.UpdateOps;
|
||||||
|
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
import io.github.sspanak.tt9.ui.AddWordAct;
|
||||||
|
|
||||||
|
|
||||||
|
public class WordStore {
|
||||||
|
private final String LOG_TAG = "sqlite.WordStore";
|
||||||
|
private static WordStore self;
|
||||||
|
|
||||||
|
private SQLiteOpener sqlite = null;
|
||||||
|
private ReadOps readOps = null;
|
||||||
|
|
||||||
|
|
||||||
|
public WordStore(@NonNull Context context) {
|
||||||
|
try {
|
||||||
|
sqlite = SQLiteOpener.getInstance(context);
|
||||||
|
sqlite.getDb();
|
||||||
|
readOps = new ReadOps();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.w(LOG_TAG, "Database connection failure. All operations will return empty results. " + e.getMessage());
|
||||||
|
}
|
||||||
|
self = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static synchronized WordStore getInstance(Context context) {
|
||||||
|
if (self == null) {
|
||||||
|
context = context == null ? TraditionalT9.getMainContext() : context;
|
||||||
|
self = new WordStore(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads words matching and similar to a given digit sequence
|
||||||
|
* For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ...
|
||||||
|
* and other similar.
|
||||||
|
*/
|
||||||
|
public ArrayList<String> getSimilar(Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) {
|
||||||
|
if (!checkOrNotify()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequence == null || sequence.length() == 0) {
|
||||||
|
Logger.w(LOG_TAG, "Attempting to get words for an empty sequence.");
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (language == null) {
|
||||||
|
Logger.w(LOG_TAG, "Attempting to get words for NULL language.");
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int minWords = Math.max(minimumWords, 0);
|
||||||
|
final int maxWords = Math.max(maximumWords, minWords);
|
||||||
|
final String filter = wordFilter == null ? "" : wordFilter;
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String positions = readOps.getSimilarWordPositions(sqlite.getDb(), language, sequence, filter, minWords);
|
||||||
|
long positionsTime = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
ArrayList<String> words = readOps.getWords(sqlite.getDb(), language, positions, filter, maxWords, false).toStringList();
|
||||||
|
long wordsTime = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
printLoadingSummary(sequence, words, positionsTime, wordsTime);
|
||||||
|
SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions);
|
||||||
|
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean exists(Language language) {
|
||||||
|
return language != null && checkOrNotify() && readOps.exists(sqlite.getDb(), language.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void remove(ArrayList<Integer> languageIds) {
|
||||||
|
if (!checkOrNotify()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
sqlite.beginTransaction();
|
||||||
|
for (int langId : languageIds) {
|
||||||
|
if (readOps.exists(sqlite.getDb(), langId)) {
|
||||||
|
DeleteOps.delete(sqlite, langId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite.finishTransaction();
|
||||||
|
|
||||||
|
Logger.d(LOG_TAG, "Deleted " + languageIds.size() + " languages. Time: " + (System.currentTimeMillis() - start) + " ms");
|
||||||
|
} catch (Exception e) {
|
||||||
|
sqlite.failTransaction();
|
||||||
|
Logger.e(LOG_TAG, "Failed deleting languages. " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int put(Language language, String word) {
|
||||||
|
if (word == null || word.isEmpty()) {
|
||||||
|
return AddWordAct.CODE_BLANK_WORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (language == null) {
|
||||||
|
return AddWordAct.CODE_INVALID_LANGUAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkOrNotify()) {
|
||||||
|
return AddWordAct.CODE_GENERAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (readOps.exists(sqlite.getDb(), language, word)) {
|
||||||
|
return AddWordAct.CODE_WORD_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String sequence = language.getDigitSequenceForWord(word);
|
||||||
|
|
||||||
|
if (InsertOps.insertCustomWord(sqlite.getDb(), language, sequence, word)) {
|
||||||
|
makeTopWord(language, word, sequence);
|
||||||
|
} else {
|
||||||
|
throw new Exception("SQLite INSERT failure.");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = "Failed inserting word: '" + word + "' for language: " + language.getId() + ". " + e.getMessage();
|
||||||
|
Logger.e("insertWord", msg);
|
||||||
|
return AddWordAct.CODE_GENERAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddWordAct.CODE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean checkOrNotify() {
|
||||||
|
if (sqlite == null || sqlite.getDb() == null) {
|
||||||
|
Logger.e(LOG_TAG, "No database connection. Cannot query any data.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void makeTopWord(@NonNull Language language, @NonNull String word, @NonNull String sequence) {
|
||||||
|
if (!checkOrNotify() || word.isEmpty() || sequence.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String topWordPositions = readOps.getWordPositions(sqlite.getDb(), language, sequence, 0, 0, "");
|
||||||
|
WordList topWords = readOps.getWords(sqlite.getDb(), language, topWordPositions, "", 9999, true);
|
||||||
|
if (topWords.isEmpty()) {
|
||||||
|
throw new Exception("No such word");
|
||||||
|
}
|
||||||
|
|
||||||
|
Word topWord = topWords.get(0);
|
||||||
|
if (topWord.word.toUpperCase(language.getLocale()).equals(word.toUpperCase(language.getLocale()))) {
|
||||||
|
Logger.d(LOG_TAG, "Word '" + word + "' is already the top word. Time: " + (System.currentTimeMillis() - start) + " ms");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wordPosition = 0;
|
||||||
|
for (Word tw : topWords) {
|
||||||
|
if (tw.word.toUpperCase(language.getLocale()).equals(word.toUpperCase(language.getLocale()))) {
|
||||||
|
wordPosition = tw.position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int newTopFrequency = topWord.frequency + 1;
|
||||||
|
String wordFilter = word.length() == 1 ? word.toLowerCase(language.getLocale()) : null;
|
||||||
|
if (!UpdateOps.changeFrequency(sqlite.getDb(), language, wordFilter, wordPosition, newTopFrequency)) {
|
||||||
|
throw new Exception("No such word");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTopFrequency > SettingsStore.WORD_FREQUENCY_MAX) {
|
||||||
|
scheduleNormalization(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d(LOG_TAG, "Changed frequency of '" + word + "' to: " + newTopFrequency + ". Time: " + (System.currentTimeMillis() - start) + " ms");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.e(LOG_TAG,"Frequency change failed. Word: '" + word + "'. " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void normalizeNext() {
|
||||||
|
if (!checkOrNotify()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
sqlite.beginTransaction();
|
||||||
|
int nextLangId = readOps.getNextInNormalizationQueue(sqlite.getDb());
|
||||||
|
UpdateOps.normalize(sqlite.getDb(), nextLangId);
|
||||||
|
sqlite.finishTransaction();
|
||||||
|
|
||||||
|
String message = nextLangId > 0 ? "Normalized language: " + nextLangId : "No languages to normalize";
|
||||||
|
Logger.d(LOG_TAG, message + ". Time: " + (System.currentTimeMillis() - start) + " ms");
|
||||||
|
} catch (Exception e) {
|
||||||
|
sqlite.failTransaction();
|
||||||
|
Logger.e(LOG_TAG, "Normalization failed. " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void scheduleNormalization(Language language) {
|
||||||
|
if (language != null && checkOrNotify()) {
|
||||||
|
UpdateOps.scheduleNormalization(sqlite.getDb(), language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void printLoadingSummary(String sequence, ArrayList<String> words, long positionIndexTime, long wordsTime) {
|
||||||
|
if (!Logger.isDebugLevel()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder debugText = new StringBuilder("===== Word Loading Summary =====");
|
||||||
|
debugText
|
||||||
|
.append("\nWord Count: ").append(words.size())
|
||||||
|
.append(".\nTime: ").append(positionIndexTime + wordsTime)
|
||||||
|
.append(" ms (positions: ").append(positionIndexTime)
|
||||||
|
.append(" ms, words: ").append(wordsTime).append(" ms).");
|
||||||
|
|
||||||
|
if (words.isEmpty()) {
|
||||||
|
debugText.append(" Sequence: ").append(sequence);
|
||||||
|
} else {
|
||||||
|
debugText.append("\n").append(words);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d(LOG_TAG, debugText.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/io/github/sspanak/tt9/db/WordStoreAsync.java
Normal file
66
src/io/github/sspanak/tt9/db/WordStoreAsync.java
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package io.github.sspanak.tt9.db;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.ConsumerCompat;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
||||||
|
public class WordStoreAsync {
|
||||||
|
private static WordStore store;
|
||||||
|
private static final Handler asyncHandler = new Handler();
|
||||||
|
|
||||||
|
public static synchronized void init(Context context) {
|
||||||
|
store = WordStore.getInstance(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static synchronized void init() {
|
||||||
|
init(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static WordStore getStore() {
|
||||||
|
init();
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void normalizeNext() {
|
||||||
|
new Thread(() -> getStore().normalizeNext()).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void areThereWords(ConsumerCompat<Boolean> notification, Language language) {
|
||||||
|
new Thread(() -> notification.accept(getStore().exists(language))).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void deleteWords(Runnable notification, @NonNull ArrayList<Integer> languageIds) {
|
||||||
|
new Thread(() -> {
|
||||||
|
getStore().remove(languageIds);
|
||||||
|
notification.run();
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void put(ConsumerCompat<Integer> statusHandler, Language language, String word) {
|
||||||
|
new Thread(() -> statusHandler.accept(getStore().put(language, word))).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void makeTopWord(@NonNull Language language, @NonNull String word, @NonNull String sequence) {
|
||||||
|
new Thread(() -> getStore().makeTopWord(language, word, sequence)).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) {
|
||||||
|
new Thread(() -> asyncHandler.post(() -> dataHandler.accept(
|
||||||
|
getStore().getSimilar(language, sequence, filter, minWords, maxWords)))
|
||||||
|
).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/io/github/sspanak/tt9/db/entities/Word.java
Normal file
18
src/io/github/sspanak/tt9/db/entities/Word.java
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package io.github.sspanak.tt9.db.entities;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class Word {
|
||||||
|
public int frequency;
|
||||||
|
public int position;
|
||||||
|
public String word;
|
||||||
|
|
||||||
|
public static Word create(@NonNull String word, int frequency, int position) {
|
||||||
|
Word w = new Word();
|
||||||
|
w.frequency = frequency;
|
||||||
|
w.position = position;
|
||||||
|
w.word = word;
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/io/github/sspanak/tt9/db/entities/WordBatch.java
Normal file
61
src/io/github/sspanak/tt9/db/entities/WordBatch.java
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package io.github.sspanak.tt9.db.entities;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.languages.InvalidLanguageCharactersException;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
||||||
|
public class WordBatch {
|
||||||
|
@NonNull private final Language language;
|
||||||
|
@NonNull private final ArrayList<Word> words;
|
||||||
|
@NonNull private final ArrayList<WordPosition> positions;
|
||||||
|
|
||||||
|
private WordPosition lastWordPosition;
|
||||||
|
|
||||||
|
public WordBatch(@NonNull Language language, int size) {
|
||||||
|
this.language = language;
|
||||||
|
words = size > 0 ? new ArrayList<>(size) : new ArrayList<>();
|
||||||
|
positions = size > 0 ? new ArrayList<>(size) : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WordBatch(@NonNull Language language) {
|
||||||
|
this(language, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(@NonNull String word, int frequency, int position) throws InvalidLanguageCharactersException {
|
||||||
|
words.add(Word.create(word, frequency, position));
|
||||||
|
|
||||||
|
if (position == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String sequence = language.getDigitSequenceForWord(word);
|
||||||
|
|
||||||
|
if (position == 1 || lastWordPosition == null) {
|
||||||
|
lastWordPosition = WordPosition.create(sequence, position);
|
||||||
|
} else {
|
||||||
|
lastWordPosition.end = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sequence.equals(lastWordPosition.sequence)) {
|
||||||
|
lastWordPosition.end--;
|
||||||
|
positions.add(lastWordPosition);
|
||||||
|
|
||||||
|
lastWordPosition = WordPosition.create(sequence, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public Language getLanguage() {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public ArrayList<Word> getWords() {
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull public ArrayList<WordPosition> getPositions() {
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/io/github/sspanak/tt9/db/entities/WordList.java
Normal file
17
src/io/github/sspanak/tt9/db/entities/WordList.java
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.github.sspanak.tt9.db.entities;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class WordList extends ArrayList<Word> {
|
||||||
|
public void add(String word, int frequency, int position) {
|
||||||
|
add(Word.create(word, frequency, position));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<String> toStringList() {
|
||||||
|
ArrayList<String> list = new ArrayList<>(size());
|
||||||
|
for (Word word : this) {
|
||||||
|
list.add(word.word);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/io/github/sspanak/tt9/db/entities/WordPosition.java
Normal file
17
src/io/github/sspanak/tt9/db/entities/WordPosition.java
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.github.sspanak.tt9.db.entities;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class WordPosition {
|
||||||
|
public String sequence;
|
||||||
|
public int start;
|
||||||
|
public int end;
|
||||||
|
|
||||||
|
public static WordPosition create(@NonNull String sequence, int start) {
|
||||||
|
WordPosition position = new WordPosition();
|
||||||
|
position.sequence = sequence;
|
||||||
|
position.start = start;
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package io.github.sspanak.tt9.db.entities;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
|
||||||
|
public class WordPositionsStringBuilder {
|
||||||
|
public int size = 0;
|
||||||
|
private final StringBuilder positions = new StringBuilder();
|
||||||
|
|
||||||
|
public WordPositionsStringBuilder appendFromDbRanges(Cursor cursor) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
append(cursor.getInt(0), cursor.getInt(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void append(int start, int end) {
|
||||||
|
if (size > 0) {
|
||||||
|
positions.append(",");
|
||||||
|
}
|
||||||
|
positions.append(start);
|
||||||
|
size++;
|
||||||
|
|
||||||
|
for (int position = start + 1; position <= end; position++) {
|
||||||
|
positions.append(",").append(position);
|
||||||
|
size++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return positions.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package io.github.sspanak.tt9.db.exceptions;
|
|
||||||
|
|
||||||
public class InsertBlankWordException extends Exception {
|
|
||||||
public InsertBlankWordException() {
|
|
||||||
super("Cannot insert a blank word.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
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,57 +0,0 @@
|
||||||
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() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public WordList(@NonNull List<Word> words) {
|
|
||||||
addAll(words);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
for (int i = 0; i < size(); i++) {
|
|
||||||
sb
|
|
||||||
.append("word: ").append(get(i).word)
|
|
||||||
.append(" | sequence: ").append(get(i).sequence)
|
|
||||||
.append(" | priority: ").append(get(i).frequency)
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
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<>();
|
|
||||||
for (int i = 0; i < size(); i++) {
|
|
||||||
strings.add(get(i).word);
|
|
||||||
}
|
|
||||||
return strings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
src/io/github/sspanak/tt9/db/sqlite/CompiledQueryCache.java
Normal file
63
src/io/github/sspanak/tt9/db/sqlite/CompiledQueryCache.java
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteDoneException;
|
||||||
|
import android.database.sqlite.SQLiteStatement;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
class CompiledQueryCache {
|
||||||
|
private static CompiledQueryCache self;
|
||||||
|
private final SQLiteDatabase db;
|
||||||
|
private final HashMap<Integer, SQLiteStatement> statements = new HashMap<>();
|
||||||
|
|
||||||
|
private CompiledQueryCache(@NonNull SQLiteDatabase db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompiledQueryCache execute(String sql) {
|
||||||
|
get(sql).execute();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLiteStatement get(@NonNull String sql) {
|
||||||
|
SQLiteStatement statement = statements.get(sql.hashCode());
|
||||||
|
if (statement == null) {
|
||||||
|
statement = db.compileStatement(sql);
|
||||||
|
statements.put(sql.hashCode(), statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
long simpleQueryForLong(String sql, long defaultValue) {
|
||||||
|
try {
|
||||||
|
return get(sql).simpleQueryForLong();
|
||||||
|
} catch (SQLiteDoneException e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static CompiledQueryCache getInstance(SQLiteDatabase db) {
|
||||||
|
if (self == null) {
|
||||||
|
self = new CompiledQueryCache(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompiledQueryCache execute(SQLiteDatabase db, String sql) {
|
||||||
|
return getInstance(db).execute(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SQLiteStatement get(SQLiteDatabase db, String sql) {
|
||||||
|
return getInstance(db).get(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
static long simpleQueryForLong(SQLiteDatabase db, String sql, long defaultValue) {
|
||||||
|
return getInstance(db).simpleQueryForLong(sql, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/io/github/sspanak/tt9/db/sqlite/DeleteOps.java
Normal file
10
src/io/github/sspanak/tt9/db/sqlite/DeleteOps.java
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class DeleteOps {
|
||||||
|
public static void delete(@NonNull SQLiteOpener sqlite, int languageId) {
|
||||||
|
sqlite.getDb().delete(Tables.getWords(languageId), null, null);
|
||||||
|
sqlite.getDb().delete(Tables.getWordPositions(languageId), null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/io/github/sspanak/tt9/db/sqlite/InsertOps.java
Normal file
79
src/io/github/sspanak/tt9/db/sqlite/InsertOps.java
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteStatement;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.db.entities.Word;
|
||||||
|
import io.github.sspanak.tt9.db.entities.WordPosition;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
||||||
|
|
||||||
|
public class InsertOps {
|
||||||
|
private final SQLiteStatement insertWordsQuery;
|
||||||
|
private final SQLiteStatement insertPositionsQuery;
|
||||||
|
|
||||||
|
|
||||||
|
public InsertOps(SQLiteDatabase db, @NonNull Language language) {
|
||||||
|
// super cache to avoid String concatenation in the dictionary loading loop
|
||||||
|
insertWordsQuery = CompiledQueryCache.get(db, "INSERT INTO " + Tables.getWords(language.getId()) + " (frequency, position, word) VALUES (?, ?, ?)");
|
||||||
|
insertPositionsQuery = CompiledQueryCache.get(db, "INSERT INTO " + Tables.getWordPositions(language.getId()) + " (sequence, `start`, `end`) VALUES (?, ?, ?)");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void insertWord(Word word) {
|
||||||
|
insertWordsQuery.bindLong(1, word.frequency);
|
||||||
|
insertWordsQuery.bindLong(2, word.position);
|
||||||
|
insertWordsQuery.bindString(3, word.word);
|
||||||
|
insertWordsQuery.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void insertWordPosition(WordPosition position) {
|
||||||
|
insertPositionsQuery.bindString(1, position.sequence);
|
||||||
|
insertPositionsQuery.bindLong(2, position.start);
|
||||||
|
insertPositionsQuery.bindLong(3, position.end);
|
||||||
|
insertPositionsQuery.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void insertLanguageMeta(@NonNull SQLiteDatabase db, int langId) {
|
||||||
|
SQLiteStatement query = CompiledQueryCache.get(db, "REPLACE INTO " + Tables.LANGUAGES_META + " (langId) VALUES (?)");
|
||||||
|
query.bindLong(1, langId);
|
||||||
|
query.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean insertCustomWord(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, @NonNull String word) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put("langId", language.getId());
|
||||||
|
values.put("sequence", sequence);
|
||||||
|
values.put("word", word);
|
||||||
|
|
||||||
|
long insertId = db.insert(Tables.CUSTOM_WORDS, null, values);
|
||||||
|
if (insertId == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user inserts more than 2^31 custom words, the "position" will overflow and will mess up
|
||||||
|
// the words table, but realistically it will never happen, so we don't bother preventing it.
|
||||||
|
|
||||||
|
values = new ContentValues();
|
||||||
|
values.put("position", (int)-insertId);
|
||||||
|
values.put("word", word);
|
||||||
|
insertId = db.insert(Tables.getWords(language.getId()), null, values);
|
||||||
|
|
||||||
|
return insertId != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void restoreCustomWords(@NonNull SQLiteDatabase db, @NonNull Language language) {
|
||||||
|
CompiledQueryCache.execute(
|
||||||
|
db,
|
||||||
|
"INSERT INTO " + Tables.getWords(language.getId()) + " (position, word) " +
|
||||||
|
"SELECT -id, word FROM " + Tables.CUSTOM_WORDS + " WHERE langId = " + language.getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/io/github/sspanak/tt9/db/sqlite/ReadOps.java
Normal file
223
src/io/github/sspanak/tt9/db/sqlite/ReadOps.java
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteDoneException;
|
||||||
|
import android.database.sqlite.SQLiteStatement;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
import io.github.sspanak.tt9.db.SlowQueryStats;
|
||||||
|
import io.github.sspanak.tt9.db.entities.WordList;
|
||||||
|
import io.github.sspanak.tt9.db.entities.WordPositionsStringBuilder;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
||||||
|
public class ReadOps {
|
||||||
|
private final String LOG_TAG = "ReadOperations";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a word exists in the database for the given language (case-insensitive).
|
||||||
|
*/
|
||||||
|
public boolean exists(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String word) {
|
||||||
|
String lowercaseWord = word.toLowerCase(language.getLocale());
|
||||||
|
String uppercaseWord = word.toUpperCase(language.getLocale());
|
||||||
|
|
||||||
|
SQLiteStatement query = CompiledQueryCache.get(db, "SELECT COUNT(*) FROM " + Tables.getWords(language.getId()) + " WHERE word IN(?, ?, ?)");
|
||||||
|
query.bindString(1, word);
|
||||||
|
query.bindString(2, lowercaseWord);
|
||||||
|
query.bindString(3, uppercaseWord);
|
||||||
|
try {
|
||||||
|
return query.simpleQueryForLong() > 0;
|
||||||
|
} catch (SQLiteDoneException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if language exists (has words) in the database.
|
||||||
|
*/
|
||||||
|
public boolean exists(@NonNull SQLiteDatabase db, int langId) {
|
||||||
|
return CompiledQueryCache.simpleQueryForLong(
|
||||||
|
db,
|
||||||
|
"SELECT COUNT(*) FROM " + Tables.getWords(langId),
|
||||||
|
0
|
||||||
|
) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) {
|
||||||
|
if (positions.isEmpty()) {
|
||||||
|
Logger.d(LOG_TAG, "No word positions. Not searching words.");
|
||||||
|
return new WordList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput);
|
||||||
|
if (wordsQuery.isEmpty()) {
|
||||||
|
return new WordList();
|
||||||
|
}
|
||||||
|
|
||||||
|
WordList words = new WordList();
|
||||||
|
try (Cursor cursor = db.rawQuery(wordsQuery, null)) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
words.add(
|
||||||
|
cursor.getString(0),
|
||||||
|
fullOutput ? cursor.getInt(1) : 0,
|
||||||
|
fullOutput ? cursor.getInt(2) : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) {
|
||||||
|
int generations;
|
||||||
|
|
||||||
|
switch (sequence.length()) {
|
||||||
|
case 2:
|
||||||
|
generations = wordFilter.isEmpty() ? 1 : Integer.MAX_VALUE;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
generations = wordFilter.isEmpty() ? 2 : Integer.MAX_VALUE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
generations = Integer.MAX_VALUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getWordPositions(db, language, sequence, generations, minPositions, wordFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String getWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) {
|
||||||
|
if (sequence.length() == 1) {
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
WordPositionsStringBuilder positions = new WordPositionsStringBuilder();
|
||||||
|
|
||||||
|
String cachedFactoryPositions = SlowQueryStats.getCachedIfSlow(SlowQueryStats.generateKey(language, sequence, wordFilter, minPositions));
|
||||||
|
if (cachedFactoryPositions != null) {
|
||||||
|
String customWordPositions = getCustomWordPositions(db, language, sequence, generations);
|
||||||
|
return customWordPositions.isEmpty() ? cachedFactoryPositions : customWordPositions + "," + cachedFactoryPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null)) {
|
||||||
|
positions.appendFromDbRanges(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positions.size < minPositions) {
|
||||||
|
Logger.d(LOG_TAG, "Not enough positions: " + positions.size + " < " + minPositions + ". Searching for more.");
|
||||||
|
try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null)) {
|
||||||
|
positions.appendFromDbRanges(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, Language language, String sequence, int generations) {
|
||||||
|
try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null)) {
|
||||||
|
return new WordPositionsStringBuilder().appendFromDbRanges(cursor).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String getPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||||
|
return
|
||||||
|
"SELECT `start`, `end` FROM ( " +
|
||||||
|
getFactoryWordPositionsQuery(language, sequence, generations) +
|
||||||
|
") UNION " +
|
||||||
|
getCustomWordPositionsQuery(language, sequence, generations);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull private String getFactoryWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||||
|
StringBuilder sql = new StringBuilder("SELECT `start`, `end` FROM ")
|
||||||
|
.append(Tables.getWordPositions(language.getId()))
|
||||||
|
.append(" WHERE ");
|
||||||
|
|
||||||
|
if (generations >= 0 && generations < 10) {
|
||||||
|
sql.append(" sequence IN(").append(sequence);
|
||||||
|
|
||||||
|
int lastChild = (int)Math.pow(10, generations) - 1;
|
||||||
|
|
||||||
|
for (int seqEnd = 1; seqEnd <= lastChild; seqEnd++) {
|
||||||
|
if (seqEnd % 10 != 0) {
|
||||||
|
sql.append(",").append(sequence).append(seqEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(")");
|
||||||
|
} else {
|
||||||
|
sql.append(" sequence = ").append(sequence).append(" OR sequence BETWEEN ").append(sequence).append("1 AND ").append(sequence).append("9");
|
||||||
|
sql.append(" ORDER BY `start` ");
|
||||||
|
sql.append(" LIMIT 100");
|
||||||
|
}
|
||||||
|
|
||||||
|
String positionsSql = sql.toString();
|
||||||
|
Logger.v(LOG_TAG, "Index SQL: " + positionsSql);
|
||||||
|
return positionsSql;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull private String getCustomWordPositionsQuery(@NonNull Language language, @NonNull String sequence, int generations) {
|
||||||
|
String sql = "SELECT -id as `start`, -id as `end` FROM " + Tables.CUSTOM_WORDS +
|
||||||
|
" WHERE langId = " + language.getId() +
|
||||||
|
" AND (sequence = " + sequence;
|
||||||
|
|
||||||
|
if (generations > 0) {
|
||||||
|
sql += " OR sequence BETWEEN " + sequence + "1 AND " + sequence + "9)";
|
||||||
|
} else {
|
||||||
|
sql += ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.v(LOG_TAG, "Custom words SQL: " + sql);
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull private String getWordsQuery(@NonNull Language language, @NonNull String positions, @NonNull String filter, int maxWords, boolean fullOutput) {
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql
|
||||||
|
.append("SELECT word");
|
||||||
|
if (fullOutput) {
|
||||||
|
sql.append(",frequency,position");
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(" FROM ").append(Tables.getWords(language.getId()))
|
||||||
|
.append(" WHERE position IN(").append(positions).append(")");
|
||||||
|
|
||||||
|
if (!filter.isEmpty()) {
|
||||||
|
sql.append(" AND word LIKE '").append(filter.replaceAll("'", "''")).append("%'");
|
||||||
|
}
|
||||||
|
|
||||||
|
sql
|
||||||
|
.append(" ORDER BY LENGTH(word), frequency DESC")
|
||||||
|
.append(" LIMIT ").append(maxWords);
|
||||||
|
|
||||||
|
String wordsSql = sql.toString();
|
||||||
|
Logger.v(LOG_TAG, "Words SQL: " + wordsSql);
|
||||||
|
return wordsSql;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getNextInNormalizationQueue(@NonNull SQLiteDatabase db) {
|
||||||
|
return (int) CompiledQueryCache.simpleQueryForLong(
|
||||||
|
db,
|
||||||
|
"SELECT langId FROM " + Tables.LANGUAGES_META + " WHERE normalizationPending = 1 LIMIT 1",
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java
Normal file
85
src/io/github/sspanak/tt9/db/sqlite/SQLiteOpener.java
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
|
|
||||||
|
public class SQLiteOpener extends SQLiteOpenHelper {
|
||||||
|
private static final String DATABASE_NAME = "tt9.db";
|
||||||
|
private static final int DATABASE_VERSION = 1;
|
||||||
|
private static SQLiteOpener self;
|
||||||
|
|
||||||
|
private final ArrayList<Language> allLanguages;
|
||||||
|
private SQLiteDatabase db;
|
||||||
|
|
||||||
|
|
||||||
|
public SQLiteOpener(Context context) {
|
||||||
|
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||||
|
allLanguages = LanguageCollection.getAll(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static SQLiteOpener getInstance(Context context) {
|
||||||
|
if (self == null) {
|
||||||
|
self = new SQLiteOpener(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
for (String query : Tables.getCreateQueries(allLanguages)) {
|
||||||
|
db.execSQL(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigure(SQLiteDatabase db) {
|
||||||
|
super.onConfigure(db);
|
||||||
|
setWriteAheadLoggingEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
// No migrations as of now
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SQLiteDatabase getDb() {
|
||||||
|
if (db == null) {
|
||||||
|
db = getWritableDatabase();
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void beginTransaction() {
|
||||||
|
if (db != null) {
|
||||||
|
db.beginTransactionNonExclusive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void failTransaction() {
|
||||||
|
if (db != null) {
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void finishTransaction() {
|
||||||
|
if (db != null) {
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/io/github/sspanak/tt9/db/sqlite/Tables.java
Normal file
111
src/io/github/sspanak/tt9/db/sqlite/Tables.java
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
||||||
|
public class Tables {
|
||||||
|
|
||||||
|
static final String LANGUAGES_META = "languages_meta";
|
||||||
|
static final String CUSTOM_WORDS = "custom_words";
|
||||||
|
private static final String POSITIONS_TABLE_BASE_NAME = "word_positions_";
|
||||||
|
private static final String WORDS_TABLE_BASE_NAME = "words_";
|
||||||
|
|
||||||
|
static String getWords(int langId) { return WORDS_TABLE_BASE_NAME + langId; }
|
||||||
|
static String getWordPositions(int langId) { return POSITIONS_TABLE_BASE_NAME + langId; }
|
||||||
|
|
||||||
|
|
||||||
|
static String[] getCreateQueries(ArrayList<Language> languages) {
|
||||||
|
int languageCount = languages.size();
|
||||||
|
String[] queries = new String[languageCount * 4 + 3];
|
||||||
|
|
||||||
|
queries[0] = createCustomWords();
|
||||||
|
queries[1] = createCustomWordsIndex();
|
||||||
|
queries[2] = createLanguagesMeta();
|
||||||
|
|
||||||
|
int queryId = 3;
|
||||||
|
for (Language language : languages) {
|
||||||
|
queries[queryId++] = createWordsTable(language.getId());
|
||||||
|
queries[queryId++] = createWordsIndex(language.getId());
|
||||||
|
queries[queryId++] = createWordPositions(language.getId());
|
||||||
|
queries[queryId++] = createWordsPositionsIndex(language.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void createWordIndex(@NonNull SQLiteDatabase db, @NonNull Language language) {
|
||||||
|
CompiledQueryCache.execute(db, createWordsIndex(language.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createPositionIndex(@NonNull SQLiteDatabase db, @NonNull Language language) {
|
||||||
|
CompiledQueryCache.execute(db, createWordsPositionsIndex(language.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void dropIndexes(@NonNull SQLiteDatabase db, @NonNull Language language) {
|
||||||
|
CompiledQueryCache
|
||||||
|
.execute(db, dropWordsIndex(language.getId()))
|
||||||
|
.execute(dropWordPositionsIndex(language.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static String createWordsTable(int langId) {
|
||||||
|
return
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + getWords(langId) + " (" +
|
||||||
|
"frequency INTEGER NOT NULL DEFAULT 0, " +
|
||||||
|
"position INTEGER NOT NULL, " +
|
||||||
|
"word TEXT NOT NULL" +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createWordsIndex(int langId) {
|
||||||
|
return "CREATE INDEX IF NOT EXISTS idx_position_" + langId + " ON " + getWords(langId) + " (position, word)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String dropWordsIndex(int langId) {
|
||||||
|
return "DROP INDEX IF EXISTS idx_position_" + langId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createWordPositions(int langId) {
|
||||||
|
return
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + getWordPositions(langId) + " (" +
|
||||||
|
"sequence TEXT NOT NULL, " +
|
||||||
|
"start INTEGER NOT NULL, " +
|
||||||
|
"end INTEGER NOT NULL" +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createWordsPositionsIndex(int langId) {
|
||||||
|
return "CREATE INDEX IF NOT EXISTS idx_sequence_start_" + langId + " ON " + getWordPositions(langId) + " (sequence, `start`)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String dropWordPositionsIndex(int langId) {
|
||||||
|
return "DROP INDEX IF EXISTS idx_sequence_start_" + langId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createCustomWords() {
|
||||||
|
return "CREATE TABLE IF NOT EXISTS " + CUSTOM_WORDS + " (" +
|
||||||
|
"id INTEGER PRIMARY KEY, " +
|
||||||
|
"langId INTEGER NOT NULL, " +
|
||||||
|
"sequence TEXT NOT NULL, " +
|
||||||
|
"word INTEGER NOT NULL " +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createCustomWordsIndex() {
|
||||||
|
return "CREATE INDEX IF NOT EXISTS idx_langId_sequence ON " + CUSTOM_WORDS + " (langId, sequence)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createLanguagesMeta() {
|
||||||
|
return "CREATE TABLE IF NOT EXISTS " + LANGUAGES_META + " (" +
|
||||||
|
"langId INTEGER UNIQUE NOT NULL, " +
|
||||||
|
"normalizationPending INT2 NOT NULL DEFAULT 0 " +
|
||||||
|
")";
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/io/github/sspanak/tt9/db/sqlite/UpdateOps.java
Normal file
59
src/io/github/sspanak/tt9/db/sqlite/UpdateOps.java
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package io.github.sspanak.tt9.db.sqlite;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteStatement;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
|
|
||||||
|
public class UpdateOps {
|
||||||
|
private static final String LOG_TAG = UpdateOps.class.getSimpleName();
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean changeFrequency(@NonNull SQLiteDatabase db, @NonNull Language language, String word, int position, int frequency) {
|
||||||
|
String sql = "UPDATE " + Tables.getWords(language.getId()) + " SET frequency = ? WHERE position = ?";
|
||||||
|
|
||||||
|
if (word != null && !word.isEmpty()) {
|
||||||
|
sql += " AND word = ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLiteStatement query = CompiledQueryCache.get(db, sql);
|
||||||
|
query.bindLong(1, frequency);
|
||||||
|
query.bindLong(2, position);
|
||||||
|
if (word != null && !word.isEmpty()) {
|
||||||
|
query.bindString(3, word);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.v(LOG_TAG, "Change frequency SQL: " + query + "; (" + frequency + ", " + position + ", " + word + ")");
|
||||||
|
|
||||||
|
return query.executeUpdateDelete() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void normalize(@NonNull SQLiteDatabase db, int langId) {
|
||||||
|
if (langId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLiteStatement query = CompiledQueryCache.get(db, "UPDATE " + Tables.getWords(langId) + " SET frequency = frequency / ?");
|
||||||
|
query.bindLong(1, SettingsStore.WORD_FREQUENCY_NORMALIZATION_DIVIDER);
|
||||||
|
query.execute();
|
||||||
|
|
||||||
|
query = CompiledQueryCache.get(db, "UPDATE " + Tables.LANGUAGES_META + " SET normalizationPending = ? WHERE langId = ?");
|
||||||
|
query.bindLong(1, 0);
|
||||||
|
query.bindLong(2, langId);
|
||||||
|
query.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void scheduleNormalization(@NonNull SQLiteDatabase db, @NonNull Language language) {
|
||||||
|
SQLiteStatement query = CompiledQueryCache.get(db, "UPDATE " + Tables.LANGUAGES_META + " SET normalizationPending = ? WHERE langId = ?");
|
||||||
|
query.bindLong(1, 1);
|
||||||
|
query.bindLong(2, language.getId());
|
||||||
|
query.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,22 +5,19 @@ import android.content.Context;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
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.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
import io.github.sspanak.tt9.preferences.SettingsStore;
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
import io.github.sspanak.tt9.ui.UI;
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
|
|
||||||
public class EmptyDatabaseWarning {
|
public class EmptyDatabaseWarning {
|
||||||
final int WARNING_INTERVAL;
|
|
||||||
private static final HashMap<Integer, Long> warningDisplayedTime = new HashMap<>();
|
private static final HashMap<Integer, Long> warningDisplayedTime = new HashMap<>();
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private Language language;
|
private Language language;
|
||||||
|
|
||||||
public EmptyDatabaseWarning(SettingsStore settings) {
|
public EmptyDatabaseWarning() {
|
||||||
WARNING_INTERVAL = settings.getDictionaryMissingWarningInterval();
|
|
||||||
|
|
||||||
for (Language lang : LanguageCollection.getAll(context)) {
|
for (Language lang : LanguageCollection.getAll(context)) {
|
||||||
if (!warningDisplayedTime.containsKey(lang.getId())) {
|
if (!warningDisplayedTime.containsKey(lang.getId())) {
|
||||||
warningDisplayedTime.put(lang.getId(), 0L);
|
warningDisplayedTime.put(lang.getId(), 0L);
|
||||||
|
|
@ -33,7 +30,7 @@ public class EmptyDatabaseWarning {
|
||||||
this.language = language;
|
this.language = language;
|
||||||
|
|
||||||
if (isItTimeAgain(TraditionalT9.getMainContext())) {
|
if (isItTimeAgain(TraditionalT9.getMainContext())) {
|
||||||
DictionaryDb.areThereWords(this::show, language);
|
WordStoreAsync.areThereWords(this::show, language);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +41,7 @@ public class EmptyDatabaseWarning {
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
Long lastWarningTime = warningDisplayedTime.get(language.getId());
|
Long lastWarningTime = warningDisplayedTime.get(language.getId());
|
||||||
return lastWarningTime != null && now - lastWarningTime > WARNING_INTERVAL;
|
return lastWarningTime != null && now - lastWarningTime > SettingsStore.DICTIONARY_MISSING_WARNING_INTERVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void show(boolean areThereWords) {
|
private void show(boolean areThereWords) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import java.util.List;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
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.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.ime.helpers.AppHacks;
|
import io.github.sspanak.tt9.ime.helpers.AppHacks;
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
import io.github.sspanak.tt9.ime.helpers.InputModeValidator;
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputType;
|
import io.github.sspanak.tt9.ime.helpers.InputType;
|
||||||
|
|
@ -41,6 +41,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
@NonNull private TextField textField = new TextField(null, null);
|
@NonNull private TextField textField = new TextField(null, null);
|
||||||
@NonNull private InputType inputType = new InputType(null, null);
|
@NonNull private InputType inputType = new InputType(null, null);
|
||||||
@NonNull private final Handler autoAcceptHandler = new Handler(Looper.getMainLooper());
|
@NonNull private final Handler autoAcceptHandler = new Handler(Looper.getMainLooper());
|
||||||
|
@NonNull private final Handler normalizationHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
// input mode
|
// input mode
|
||||||
private ArrayList<Integer> allowedInputModes = new ArrayList<>();
|
private ArrayList<Integer> allowedInputModes = new ArrayList<>();
|
||||||
|
|
@ -149,8 +150,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
self = this;
|
self = this;
|
||||||
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
|
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
|
||||||
|
|
||||||
DictionaryDb.init(this);
|
WordStoreAsync.init(this);
|
||||||
DictionaryDb.normalizeWordFrequencies(settings);
|
|
||||||
|
|
||||||
if (mainView == null) {
|
if (mainView == null) {
|
||||||
mainView = new MainView(this);
|
mainView = new MainView(this);
|
||||||
|
|
@ -219,6 +219,7 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizationHandler.removeCallbacksAndMessages(null);
|
||||||
initUi();
|
initUi();
|
||||||
updateInputViewShown();
|
updateInputViewShown();
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +235,9 @@ public class TraditionalT9 extends KeyPadHandler {
|
||||||
onFinishTyping();
|
onFinishTyping();
|
||||||
clearSuggestions();
|
clearSuggestions();
|
||||||
statusBar.setText("--");
|
statusBar.setText("--");
|
||||||
|
|
||||||
|
normalizationHandler.removeCallbacksAndMessages(null);
|
||||||
|
normalizationHandler.postDelayed(WordStoreAsync::normalizeNext, SettingsStore.WORD_NORMALIZATION_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import java.util.ArrayList;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
import io.github.sspanak.tt9.TextTools;
|
import io.github.sspanak.tt9.TextTools;
|
||||||
import io.github.sspanak.tt9.db.DictionaryDb;
|
import io.github.sspanak.tt9.db.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.ime.helpers.InputType;
|
import io.github.sspanak.tt9.ime.helpers.InputType;
|
||||||
import io.github.sspanak.tt9.ime.helpers.TextField;
|
import io.github.sspanak.tt9.ime.helpers.TextField;
|
||||||
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
|
import io.github.sspanak.tt9.ime.modes.helpers.AutoSpace;
|
||||||
|
|
@ -271,7 +271,7 @@ public class ModePredictive extends InputMode {
|
||||||
// emoji and punctuation are not in the database, so there is no point in
|
// emoji and punctuation are not in the database, so there is no point in
|
||||||
// running queries that would update nothing
|
// running queries that would update nothing
|
||||||
if (!sequence.startsWith("11") && !sequence.equals("1") && !sequence.startsWith("0")) {
|
if (!sequence.startsWith("11") && !sequence.equals("1") && !sequence.startsWith("0")) {
|
||||||
DictionaryDb.incrementWordFrequency(language, currentWord, sequence);
|
WordStoreAsync.makeTopWord(language, currentWord, sequence);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.e(LOG_TAG, "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage());
|
Logger.e(LOG_TAG, "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage());
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package io.github.sspanak.tt9.ime.modes.helpers;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.db.DictionaryDb;
|
import io.github.sspanak.tt9.db.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.ime.EmptyDatabaseWarning;
|
import io.github.sspanak.tt9.ime.EmptyDatabaseWarning;
|
||||||
import io.github.sspanak.tt9.languages.Characters;
|
import io.github.sspanak.tt9.languages.Characters;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
|
@ -32,7 +32,7 @@ public class Predictions {
|
||||||
|
|
||||||
|
|
||||||
public Predictions(SettingsStore settingsStore) {
|
public Predictions(SettingsStore settingsStore) {
|
||||||
emptyDbWarning = new EmptyDatabaseWarning(settingsStore);
|
emptyDbWarning = new EmptyDatabaseWarning();
|
||||||
settings = settingsStore;
|
settings = settingsStore;
|
||||||
|
|
||||||
// digitSequence limiter when selecting emoji
|
// digitSequence limiter when selecting emoji
|
||||||
|
|
@ -128,13 +128,13 @@ public class Predictions {
|
||||||
if (loadStatic()) {
|
if (loadStatic()) {
|
||||||
onWordsChanged.run();
|
onWordsChanged.run();
|
||||||
} else {
|
} else {
|
||||||
DictionaryDb.getWords(
|
WordStoreAsync.getWords(
|
||||||
(words) -> onDbWords(words, true),
|
(words) -> onDbWords(words, true),
|
||||||
language,
|
language,
|
||||||
digitSequence,
|
digitSequence,
|
||||||
stem,
|
stem,
|
||||||
settings.getSuggestionsMin(),
|
SettingsStore.SUGGESTIONS_MIN,
|
||||||
settings.getSuggestionsMax()
|
SettingsStore.SUGGESTIONS_MAX
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ public class Predictions {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadWithoutLeadingPunctuation() {
|
private void loadWithoutLeadingPunctuation() {
|
||||||
DictionaryDb.getWords(
|
WordStoreAsync.getWords(
|
||||||
(dbWords) -> {
|
(dbWords) -> {
|
||||||
char firstChar = inputWord.charAt(0);
|
char firstChar = inputWord.charAt(0);
|
||||||
for (int i = 0; i < dbWords.size(); i++) {
|
for (int i = 0; i < dbWords.size(); i++) {
|
||||||
|
|
@ -187,8 +187,8 @@ public class Predictions {
|
||||||
language,
|
language,
|
||||||
digitSequence.substring(1),
|
digitSequence.substring(1),
|
||||||
stem.length() > 1 ? stem.substring(1) : "",
|
stem.length() > 1 ? stem.substring(1) : "",
|
||||||
settings.getSuggestionsMin(),
|
SettingsStore.SUGGESTIONS_MIN,
|
||||||
settings.getSuggestionsMax()
|
SettingsStore.SUGGESTIONS_MAX
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@ public class Language {
|
||||||
return keyChars;
|
return keyChars;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final public int getId() {
|
final public int getId() {
|
||||||
if (id == 0) {
|
if (id == 0) {
|
||||||
id = generateId();
|
id = generateId();
|
||||||
|
|
@ -172,8 +171,8 @@ public class Language {
|
||||||
/**
|
/**
|
||||||
* generateId
|
* generateId
|
||||||
* Uses the letters of the Locale to generate an ID for the language.
|
* Uses the letters of the Locale to generate an ID for the language.
|
||||||
* Each letter is converted to uppercase and used as n 5-bit integer. Then the the 5-bits
|
* Each letter is converted to uppercase and used as a 5-bit integer. Then the 5-bits
|
||||||
* are packed to form a 10-bit or a 20-bit integer, depending on the Locale.
|
* are packed to form a 10-bit or a 20-bit integer, depending on the Locale length.
|
||||||
*
|
*
|
||||||
* Example (2-letter Locale)
|
* Example (2-letter Locale)
|
||||||
* "en"
|
* "en"
|
||||||
|
|
@ -186,12 +185,14 @@ public class Language {
|
||||||
* -> "B" | "G" | "B" | "G"
|
* -> "B" | "G" | "B" | "G"
|
||||||
* -> 2 | 224 | 2048 | 229376 (shift each 5-bit number, not overlap with the previous ones)
|
* -> 2 | 224 | 2048 | 229376 (shift each 5-bit number, not overlap with the previous ones)
|
||||||
* -> 231650
|
* -> 231650
|
||||||
|
*
|
||||||
|
* Maximum ID is: "zz-ZZ" -> 879450
|
||||||
*/
|
*/
|
||||||
private int generateId() {
|
private int generateId() {
|
||||||
String idString = (locale.getLanguage() + locale.getCountry()).toUpperCase();
|
String idString = (locale.getLanguage() + locale.getCountry()).toUpperCase();
|
||||||
int idInt = 0;
|
int idInt = 0;
|
||||||
for (int i = 0; i < idString.length(); i++) {
|
for (int i = 0; i < idString.length(); i++) {
|
||||||
idInt |= ((idString.charAt(i) & 31) << (i * 5));
|
idInt |= ((idString.codePointAt(i) & 31) << (i * 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
return idInt;
|
return idInt;
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,6 @@ public class LanguageCollection {
|
||||||
return getAll(context,false);
|
return getAll(context,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static String toString(ArrayList<Language> list) {
|
public static String toString(ArrayList<Language> list) {
|
||||||
StringBuilder stringList = new StringBuilder();
|
StringBuilder stringList = new StringBuilder();
|
||||||
int listSize = list.size();
|
int listSize = list.size();
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
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.WordStoreAsync;
|
||||||
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.db.LegacyDb;
|
||||||
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;
|
||||||
|
|
@ -28,6 +28,7 @@ import io.github.sspanak.tt9.preferences.screens.HotkeysScreen;
|
||||||
import io.github.sspanak.tt9.preferences.screens.KeyPadScreen;
|
import io.github.sspanak.tt9.preferences.screens.KeyPadScreen;
|
||||||
import io.github.sspanak.tt9.preferences.screens.MainSettingsScreen;
|
import io.github.sspanak.tt9.preferences.screens.MainSettingsScreen;
|
||||||
import io.github.sspanak.tt9.preferences.screens.SetupScreen;
|
import io.github.sspanak.tt9.preferences.screens.SetupScreen;
|
||||||
|
import io.github.sspanak.tt9.preferences.screens.UsageStatsScreen;
|
||||||
import io.github.sspanak.tt9.ui.DictionaryLoadingBar;
|
import io.github.sspanak.tt9.ui.DictionaryLoadingBar;
|
||||||
|
|
||||||
public class PreferencesActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
public class PreferencesActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||||
|
|
@ -42,9 +43,8 @@ public class PreferencesActivity extends AppCompatActivity implements Preference
|
||||||
applyTheme();
|
applyTheme();
|
||||||
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
|
Logger.enableDebugLevel(settings.getDebugLogsEnabled());
|
||||||
|
|
||||||
new SQLWords(this).clear();
|
try (LegacyDb db = new LegacyDb(this)) { db.clear(); }
|
||||||
DictionaryDb.init(this);
|
WordStoreAsync.init(this);
|
||||||
DictionaryDb.normalizeWordFrequencies(settings);
|
|
||||||
|
|
||||||
InputModeValidator.validateEnabledLanguages(this, settings.getEnabledLanguageIds());
|
InputModeValidator.validateEnabledLanguages(this, settings.getEnabledLanguageIds());
|
||||||
validateFunctionKeys();
|
validateFunctionKeys();
|
||||||
|
|
@ -99,6 +99,8 @@ public class PreferencesActivity extends AppCompatActivity implements Preference
|
||||||
return new KeyPadScreen(this);
|
return new KeyPadScreen(this);
|
||||||
case "Setup":
|
case "Setup":
|
||||||
return new SetupScreen(this);
|
return new SetupScreen(this);
|
||||||
|
case "SlowQueries":
|
||||||
|
return new UsageStatsScreen(this);
|
||||||
default:
|
default:
|
||||||
return new MainSettingsScreen(this);
|
return new MainSettingsScreen(this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -272,21 +272,19 @@ public class SettingsStore {
|
||||||
|
|
||||||
public boolean getDebugLogsEnabled() { return prefs.getBoolean("pref_enable_debug_logs", Logger.isDebugLevel()); }
|
public boolean getDebugLogsEnabled() { return prefs.getBoolean("pref_enable_debug_logs", Logger.isDebugLevel()); }
|
||||||
|
|
||||||
public int getDictionaryImportProgressUpdateInterval() { return 250; /* ms */ }
|
public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_BATCH_SIZE = 1000; // items
|
||||||
public int getDictionaryImportWordChunkSize() { return 1000; /* words */ }
|
public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms
|
||||||
|
public final static int DICTIONARY_MISSING_WARNING_INTERVAL = 30000; // ms
|
||||||
public int getDictionaryMissingWarningInterval() { return 30000; /* ms */ }
|
public final static int PREFERENCES_CLICK_DEBOUNCE_TIME = 250; // ms
|
||||||
|
public final static byte SLOW_QUERY_TIME = 50; // ms
|
||||||
public int getSuggestionsMax() { return 20; }
|
public final static int SOFT_KEY_REPEAT_DELAY = 40; // ms
|
||||||
public int getSuggestionsMin() { return 8; }
|
public final static int SUGGESTIONS_MAX = 20;
|
||||||
|
public final static int SUGGESTIONS_MIN = 8;
|
||||||
public int getSuggestionSelectAnimationDuration() { return 66; }
|
public final static int SUGGESTIONS_SELECT_ANIMATION_DURATION = 66;
|
||||||
public int getSuggestionTranslateAnimationDuration() { return 0; }
|
public final static int SUGGESTIONS_TRANSLATE_ANIMATION_DURATION = 0;
|
||||||
|
public final static int WORD_FREQUENCY_MAX = 25500;
|
||||||
public int getSoftKeyRepeatDelay() { return 40; /* ms */ }
|
public final static int WORD_FREQUENCY_NORMALIZATION_DIVIDER = 100; // normalized frequency = WORD_FREQUENCY_MAX / WORD_FREQUENCY_NORMALIZATION_DIVIDER
|
||||||
|
public final static int WORD_NORMALIZATION_DELAY = 120000; // ms
|
||||||
public int getWordFrequencyMax() { return 25500; }
|
|
||||||
public int getWordFrequencyNormalizationDivider() { return 100; } // normalized frequency = getWordFrequencyMax() / getWordFrequencyNormalizationDivider()
|
|
||||||
|
|
||||||
|
|
||||||
/************* hack settings *************/
|
/************* hack settings *************/
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
abstract public class ItemClickable {
|
abstract public class ItemClickable {
|
||||||
private final int CLICK_DEBOUNCE_TIME = 250;
|
|
||||||
private long lastClickTime = 0;
|
private long lastClickTime = 0;
|
||||||
|
|
||||||
protected final Preference item;
|
protected final Preference item;
|
||||||
|
|
@ -69,7 +69,7 @@ abstract public class ItemClickable {
|
||||||
*/
|
*/
|
||||||
protected boolean debounceClick(Preference p) {
|
protected boolean debounceClick(Preference p) {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (now - lastClickTime < CLICK_DEBOUNCE_TIME) {
|
if (now - lastClickTime < SettingsStore.PREFERENCES_CLICK_DEBOUNCE_TIME) {
|
||||||
Logger.d("debounceClick", "Preference click debounced.");
|
Logger.d("debounceClick", "Preference click debounced.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,11 @@ public class ItemDropDown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemDropDown preview() {
|
public void preview() {
|
||||||
try {
|
try {
|
||||||
setPreview(values.get(Integer.parseInt(item.getValue())));
|
setPreview(values.get(Integer.parseInt(item.getValue())));
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
setPreview("");
|
setPreview("");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@ package io.github.sspanak.tt9.preferences.items;
|
||||||
|
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
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.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.db.DictionaryLoader;
|
import io.github.sspanak.tt9.db.DictionaryLoader;
|
||||||
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
||||||
import io.github.sspanak.tt9.ui.UI;
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
|
|
||||||
|
|
@ -30,7 +34,11 @@ public class ItemTruncateAll extends ItemClickable {
|
||||||
}
|
}
|
||||||
|
|
||||||
onStartDeleting();
|
onStartDeleting();
|
||||||
DictionaryDb.deleteWords(activity.getApplicationContext(), this::onFinishDeleting);
|
ArrayList<Integer> languageIds = new ArrayList<>();
|
||||||
|
for (Language lang : LanguageCollection.getAll(activity, false)) {
|
||||||
|
languageIds.add(lang.getId());
|
||||||
|
}
|
||||||
|
WordStoreAsync.deleteWords(this::onFinishDeleting, languageIds);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.preference.Preference;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.db.DictionaryDb;
|
import io.github.sspanak.tt9.db.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.db.DictionaryLoader;
|
import io.github.sspanak.tt9.db.DictionaryLoader;
|
||||||
import io.github.sspanak.tt9.languages.Language;
|
import io.github.sspanak.tt9.languages.Language;
|
||||||
import io.github.sspanak.tt9.languages.LanguageCollection;
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
|
|
@ -40,7 +40,7 @@ public class ItemTruncateUnselected extends ItemTruncateAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
onStartDeleting();
|
onStartDeleting();
|
||||||
DictionaryDb.deleteWords(this::onFinishDeleting, unselectedLanguageIds);
|
WordStoreAsync.deleteWords(this::onFinishDeleting, unselectedLanguageIds);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ import io.github.sspanak.tt9.preferences.items.ItemTruncateUnselected;
|
||||||
|
|
||||||
public class DictionariesScreen extends BaseScreenFragment {
|
public class DictionariesScreen extends BaseScreenFragment {
|
||||||
private ItemLoadDictionary loadItem;
|
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); }
|
||||||
|
|
@ -37,14 +35,14 @@ public class DictionariesScreen extends BaseScreenFragment {
|
||||||
activity.getDictionaryProgressBar()
|
activity.getDictionaryProgressBar()
|
||||||
);
|
);
|
||||||
|
|
||||||
deleteItem = new ItemTruncateUnselected(
|
ItemTruncateUnselected deleteItem = new ItemTruncateUnselected(
|
||||||
findPreference(ItemTruncateUnselected.NAME),
|
findPreference(ItemTruncateUnselected.NAME),
|
||||||
activity,
|
activity,
|
||||||
activity.settings,
|
activity.settings,
|
||||||
activity.getDictionaryLoader()
|
activity.getDictionaryLoader()
|
||||||
);
|
);
|
||||||
|
|
||||||
truncateItem = new ItemTruncateAll(
|
ItemTruncateAll truncateItem = new ItemTruncateAll(
|
||||||
findPreference(ItemTruncateAll.NAME),
|
findPreference(ItemTruncateAll.NAME),
|
||||||
activity,
|
activity,
|
||||||
activity.getDictionaryLoader()
|
activity.getDictionaryLoader()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package io.github.sspanak.tt9.preferences.screens;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import io.github.sspanak.tt9.R;
|
||||||
|
import io.github.sspanak.tt9.db.SlowQueryStats;
|
||||||
|
import io.github.sspanak.tt9.preferences.PreferencesActivity;
|
||||||
|
import io.github.sspanak.tt9.ui.UI;
|
||||||
|
|
||||||
|
public class UsageStatsScreen extends BaseScreenFragment {
|
||||||
|
private final static String RESET_BUTTON = "pref_slow_queries_reset_stats";
|
||||||
|
private final static String SUMMARY_CONTAINER = "summary_container";
|
||||||
|
private final static String QUERY_LIST_CONTAINER = "query_list_container";
|
||||||
|
|
||||||
|
public UsageStatsScreen() { init(); }
|
||||||
|
public UsageStatsScreen(PreferencesActivity activity) { init(activity); }
|
||||||
|
|
||||||
|
@Override protected int getTitle() { return R.string.pref_category_usage_stats; }
|
||||||
|
@Override protected int getXml() { return R.xml.prefs_screen_usage_stats; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate() {
|
||||||
|
printSummary();
|
||||||
|
printSlowQueries();
|
||||||
|
enableLogsCopy();
|
||||||
|
|
||||||
|
Preference resetButton = findPreference(RESET_BUTTON);
|
||||||
|
if (resetButton != null) {
|
||||||
|
resetButton.setOnPreferenceClickListener((Preference p) -> {
|
||||||
|
SlowQueryStats.clear();
|
||||||
|
printSummary();
|
||||||
|
printSlowQueries();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printSummary() {
|
||||||
|
Preference logsContainer = findPreference(SUMMARY_CONTAINER);
|
||||||
|
if (logsContainer != null) {
|
||||||
|
logsContainer.setSummary(SlowQueryStats.getSummary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printSlowQueries() {
|
||||||
|
Preference queryListContainer = findPreference(QUERY_LIST_CONTAINER);
|
||||||
|
if (queryListContainer != null) {
|
||||||
|
String slowQueries = SlowQueryStats.getList();
|
||||||
|
queryListContainer.setSummary(slowQueries.isEmpty() ? "No slow queries." : slowQueries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void enableLogsCopy() {
|
||||||
|
Preference queryListContainer = findPreference(QUERY_LIST_CONTAINER);
|
||||||
|
if (activity == null || queryListContainer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
queryListContainer.setOnPreferenceClickListener((Preference p) -> {
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText("TT9 debug log", p.getSummary()));
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
UI.toast(activity, "Logs copied.");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,18 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.Logger;
|
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.WordStoreAsync;
|
||||||
import io.github.sspanak.tt9.db.exceptions.InsertBlankWordException;
|
|
||||||
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.languages.LanguageCollection;
|
import io.github.sspanak.tt9.languages.LanguageCollection;
|
||||||
|
|
||||||
public class AddWordAct extends AppCompatActivity {
|
public class AddWordAct extends AppCompatActivity {
|
||||||
|
public static final int CODE_SUCCESS = 0;
|
||||||
|
public static final int CODE_BLANK_WORD = 1;
|
||||||
|
public static final int CODE_INVALID_LANGUAGE = 2;
|
||||||
|
public static final int CODE_WORD_EXISTS = 3;
|
||||||
|
public static final int CODE_GENERAL_ERROR = 666;
|
||||||
|
|
||||||
public static final String INTENT_FILTER = "tt9.add_word";
|
public static final String INTENT_FILTER = "tt9.add_word";
|
||||||
|
|
||||||
private Language language;
|
private Language language;
|
||||||
|
|
@ -61,14 +65,22 @@ public class AddWordAct extends AppCompatActivity {
|
||||||
private void onAddedWord(int statusCode) {
|
private void onAddedWord(int statusCode) {
|
||||||
String message;
|
String message;
|
||||||
switch (statusCode) {
|
switch (statusCode) {
|
||||||
case 0:
|
case CODE_SUCCESS:
|
||||||
message = getString(R.string.add_word_success, word);
|
message = getString(R.string.add_word_success, word);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case CODE_WORD_EXISTS:
|
||||||
message = getResources().getString(R.string.add_word_exist, word);
|
message = getResources().getString(R.string.add_word_exist, word);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CODE_BLANK_WORD:
|
||||||
|
message = getString(R.string.add_word_blank);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CODE_INVALID_LANGUAGE:
|
||||||
|
message = getResources().getString(R.string.add_word_invalid_language);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
message = getString(R.string.error_unexpected);
|
message = getString(R.string.error_unexpected);
|
||||||
break;
|
break;
|
||||||
|
|
@ -80,22 +92,7 @@ public class AddWordAct extends AppCompatActivity {
|
||||||
|
|
||||||
|
|
||||||
public void addWord(View v) {
|
public void addWord(View v) {
|
||||||
try {
|
WordStoreAsync.put(this::onAddedWord, language, word);
|
||||||
Logger.d("addWord", "Attempting to add word: '" + word + "'...");
|
|
||||||
DictionaryDb.insertWord(this::onAddedWord, language, word);
|
|
||||||
} catch (InsertBlankWordException e) {
|
|
||||||
Logger.e("AddWordAct.addWord", e.getMessage());
|
|
||||||
finish();
|
|
||||||
sendMessageToMain(getString(R.string.add_word_blank));
|
|
||||||
} catch (InvalidLanguageException e) {
|
|
||||||
Logger.e("AddWordAct.addWord", "Cannot insert a word for language: '" + language.getName() + "'. " + e.getMessage());
|
|
||||||
finish();
|
|
||||||
sendMessageToMain(getString(R.string.add_word_invalid_language));
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.e("AddWordAct.addWord", e.getMessage());
|
|
||||||
finish();
|
|
||||||
sendMessageToMain(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ abstract class BaseMainLayout {
|
||||||
protected View view = null;
|
protected View view = null;
|
||||||
protected ArrayList<SoftKey> keys = new ArrayList<>();
|
protected ArrayList<SoftKey> keys = new ArrayList<>();
|
||||||
|
|
||||||
public BaseMainLayout(TraditionalT9 tt9, int xml) {
|
BaseMainLayout(TraditionalT9 tt9, int xml) {
|
||||||
this.tt9 = tt9;
|
this.tt9 = tt9;
|
||||||
this.xml = xml;
|
this.xml = xml;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||||
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
|
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
|
||||||
|
|
||||||
class MainLayoutNumpad extends BaseMainLayout {
|
class MainLayoutNumpad extends BaseMainLayout {
|
||||||
public MainLayoutNumpad(TraditionalT9 tt9) {
|
MainLayoutNumpad(TraditionalT9 tt9) {
|
||||||
super(tt9, R.layout.main_numpad);
|
super(tt9, R.layout.main_numpad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||||
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
|
import io.github.sspanak.tt9.ui.main.keys.SoftKey;
|
||||||
|
|
||||||
class MainLayoutSmall extends BaseMainLayout {
|
class MainLayoutSmall extends BaseMainLayout {
|
||||||
public MainLayoutSmall(TraditionalT9 tt9) {
|
MainLayoutSmall(TraditionalT9 tt9) {
|
||||||
super(tt9, R.layout.main_small);
|
super(tt9, R.layout.main_small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat;
|
||||||
import io.github.sspanak.tt9.Logger;
|
import io.github.sspanak.tt9.Logger;
|
||||||
import io.github.sspanak.tt9.R;
|
import io.github.sspanak.tt9.R;
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
public class SoftKey extends androidx.appcompat.widget.AppCompatButton implements View.OnTouchListener, View.OnLongClickListener {
|
public class SoftKey extends androidx.appcompat.widget.AppCompatButton implements View.OnTouchListener, View.OnLongClickListener {
|
||||||
protected TraditionalT9 tt9;
|
protected TraditionalT9 tt9;
|
||||||
|
|
@ -109,7 +110,7 @@ public class SoftKey extends androidx.appcompat.widget.AppCompatButton implement
|
||||||
handleHold();
|
handleHold();
|
||||||
lastPressedKey = getId();
|
lastPressedKey = getId();
|
||||||
repeatHandler.removeCallbacks(this::repeatOnLongPress);
|
repeatHandler.removeCallbacks(this::repeatOnLongPress);
|
||||||
repeatHandler.postDelayed(this::repeatOnLongPress, tt9.getSettings().getSoftKeyRepeatDelay());
|
repeatHandler.postDelayed(this::repeatOnLongPress, SettingsStore.SOFT_KEY_REPEAT_DELAY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import java.util.List;
|
||||||
|
|
||||||
import io.github.sspanak.tt9.R;
|
import io.github.sspanak.tt9.R;
|
||||||
import io.github.sspanak.tt9.ime.TraditionalT9;
|
import io.github.sspanak.tt9.ime.TraditionalT9;
|
||||||
|
import io.github.sspanak.tt9.preferences.SettingsStore;
|
||||||
|
|
||||||
public class SuggestionsBar {
|
public class SuggestionsBar {
|
||||||
private final List<String> suggestions = new ArrayList<>();
|
private final List<String> suggestions = new ArrayList<>();
|
||||||
|
|
@ -52,13 +53,10 @@ public class SuggestionsBar {
|
||||||
private void configureAnimation() {
|
private void configureAnimation() {
|
||||||
DefaultItemAnimator animator = new DefaultItemAnimator();
|
DefaultItemAnimator animator = new DefaultItemAnimator();
|
||||||
|
|
||||||
int translateDuration = tt9.getSettings().getSuggestionTranslateAnimationDuration();
|
animator.setMoveDuration(SettingsStore.SUGGESTIONS_SELECT_ANIMATION_DURATION);
|
||||||
int selectDuration = tt9.getSettings().getSuggestionSelectAnimationDuration();
|
animator.setChangeDuration(SettingsStore.SUGGESTIONS_TRANSLATE_ANIMATION_DURATION);
|
||||||
|
animator.setAddDuration(SettingsStore.SUGGESTIONS_TRANSLATE_ANIMATION_DURATION);
|
||||||
animator.setMoveDuration(selectDuration);
|
animator.setRemoveDuration(SettingsStore.SUGGESTIONS_TRANSLATE_ANIMATION_DURATION);
|
||||||
animator.setChangeDuration(translateDuration);
|
|
||||||
animator.setAddDuration(translateDuration);
|
|
||||||
animator.setRemoveDuration(translateDuration);
|
|
||||||
|
|
||||||
mView.setItemAnimator(animator);
|
mView.setItemAnimator(animator);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue