mirror of
https://github.com/florisboard/florisboard.git
synced 2024-09-19 19:42:20 +02:00
Add internal and system user dictionary
This commit is contained in:
parent
dcd20e4b73
commit
2b1951ea5f
3
.gitignore
vendored
3
.gitignore
vendored
@ -40,3 +40,6 @@ captures/
|
||||
# Keystore files
|
||||
*.jks
|
||||
crowdin.properties
|
||||
|
||||
# AndroidX Room schema JSONs
|
||||
/app/schemas/
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "4.1.3"
|
||||
id("com.android.application") version "4.2.0"
|
||||
kotlin("android") version "1.5.0-RC"
|
||||
kotlin("kapt") version "1.5.0-RC"
|
||||
kotlin("plugin.serialization") version "1.5.0-RC"
|
||||
@ -28,6 +28,16 @@ android {
|
||||
versionName("0.3.11")
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += mapOf(
|
||||
Pair("room.schemaLocation", "$projectDir/schemas"),
|
||||
Pair("room.incremental", "true"),
|
||||
Pair("room.expandProjection", "true")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@ -84,7 +94,7 @@ dependencies {
|
||||
implementation("androidx.preference", "preference-ktx", "1.1.1")
|
||||
implementation("androidx.constraintlayout", "constraintlayout", "2.0.4")
|
||||
implementation("androidx.lifecycle", "lifecycle-service", "2.2.0")
|
||||
implementation("com.google.android", "flexbox", "2.0.1") // requires jcenter as of version 2.0.1
|
||||
implementation("com.google.android", "flexbox", "2.0.1")
|
||||
implementation("com.google.android.material", "material", "1.3.0")
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-android", "1.4.2")
|
||||
implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.1.0")
|
||||
|
@ -248,16 +248,16 @@ class PrefHelper(
|
||||
companion object {
|
||||
const val ENABLE_SYSTEM_USER_DICTIONARY = "suggestion__enable_system_user_dictionary"
|
||||
const val MANAGE_SYSTEM_USER_DICTIONARY = "suggestion__manage_system_user_dictionary"
|
||||
const val ENABLE_INTERNAL_USER_DICTIONARY = "suggestion__enable_internal_user_dictionary"
|
||||
const val MANAGE_INTERNAL_USER_DICTIONARY = "suggestion__manage_internal_user_dictionary"
|
||||
const val ENABLE_FLORIS_USER_DICTIONARY = "suggestion__enable_floris_user_dictionary"
|
||||
const val MANAGE_FLORIS_USER_DICTIONARY = "suggestion__manage_floris_user_dictionary"
|
||||
}
|
||||
|
||||
var enableSystemUserDictionary: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_SYSTEM_USER_DICTIONARY, true)
|
||||
set(v) = prefHelper.setPref(ENABLE_SYSTEM_USER_DICTIONARY, v)
|
||||
var enableInternalUserDictionary: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_INTERNAL_USER_DICTIONARY, true)
|
||||
set(v) = prefHelper.setPref(ENABLE_INTERNAL_USER_DICTIONARY, v)
|
||||
var enableFlorisUserDictionary: Boolean
|
||||
get() = prefHelper.getPref(ENABLE_FLORIS_USER_DICTIONARY, true)
|
||||
set(v) = prefHelper.setPref(ENABLE_FLORIS_USER_DICTIONARY, v)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,15 +17,24 @@
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dev.patrickgold.florisboard.ime.core.PrefHelper
|
||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* TODO: document
|
||||
*/
|
||||
class DictionaryManager private constructor(private val applicationContext: Context) {
|
||||
class DictionaryManager private constructor(context: Context) {
|
||||
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
|
||||
private val prefs: PrefHelper = PrefHelper.getDefaultInstance(context)
|
||||
|
||||
private val dictionaryCache: MutableMap<String, Dictionary<String, Int>> = mutableMapOf()
|
||||
|
||||
private var florisUserDictionaryDatabase: FlorisUserDictionaryDatabase? = null
|
||||
private var systemUserDictionaryDatabase: SystemUserDictionaryDatabase? = null
|
||||
|
||||
companion object {
|
||||
private var defaultInstance: DictionaryManager? = null
|
||||
|
||||
@ -53,16 +62,65 @@ class DictionaryManager private constructor(private val applicationContext: Cont
|
||||
}
|
||||
if (ref.path.endsWith(".flict")) {
|
||||
// Assume this is a Flictionary
|
||||
Flictionary.load(applicationContext, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Result.success(flict)
|
||||
}.onFailure { err ->
|
||||
Timber.i(err)
|
||||
return Result.failure(err)
|
||||
applicationContext.get()?.let {
|
||||
Flictionary.load(it, ref).onSuccess { flict ->
|
||||
dictionaryCache[ref.toString()] = flict
|
||||
return Result.success(flict)
|
||||
}.onFailure { err ->
|
||||
Timber.i(err)
|
||||
return Result.failure(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Result.failure(Exception("Unable to determine supported type for given AssetRef!"))
|
||||
}
|
||||
return Result.failure(Exception("If this message is ever thrown, something is completely broken..."))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun florisUserDictionaryDao(): UserDictionaryDao? {
|
||||
return if (prefs.suggestion.enabled && prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisUserDictionaryDatabase?.userDictionaryDao()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun systemUserDictionaryDao(): UserDictionaryDao? {
|
||||
return if (prefs.suggestion.enabled && prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemUserDictionaryDatabase?.userDictionaryDao()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun loadUserDictionariesIfNecessary() {
|
||||
val context = applicationContext.get() ?: return
|
||||
|
||||
if (prefs.suggestion.enabled) {
|
||||
if (florisUserDictionaryDatabase == null && prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisUserDictionaryDatabase = Room.databaseBuilder(
|
||||
context,
|
||||
FlorisUserDictionaryDatabase::class.java,
|
||||
FlorisUserDictionaryDatabase.DB_FILE_NAME
|
||||
).build()
|
||||
}
|
||||
if (systemUserDictionaryDatabase == null && prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemUserDictionaryDatabase = SystemUserDictionaryDatabase(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun unloadUserDictionariesIfNecessary() {
|
||||
if (florisUserDictionaryDatabase != null) {
|
||||
florisUserDictionaryDatabase?.close()
|
||||
florisUserDictionaryDatabase = null
|
||||
}
|
||||
if (systemUserDictionaryDatabase != null) {
|
||||
systemUserDictionaryDatabase = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Patrick Goldinger
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package dev.patrickgold.florisboard.ime.dictionary
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.provider.UserDictionary
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import dev.patrickgold.florisboard.util.LocaleUtils
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
private const val WORDS_TABLE = "words"
|
||||
|
||||
private const val SORT_BY_WORD_ASC = "${UserDictionary.Words.WORD} ASC"
|
||||
private const val SORT_BY_WORD_DESC = "${UserDictionary.Words.WORD} DESC"
|
||||
private const val SORT_BY_FREQ_ASC = "${UserDictionary.Words.FREQUENCY} ASC"
|
||||
private const val SORT_BY_FREQ_DESC = "${UserDictionary.Words.FREQUENCY} DESC"
|
||||
|
||||
private val PROJECTIONS: Array<String> = arrayOf(
|
||||
UserDictionary.Words._ID,
|
||||
UserDictionary.Words.WORD,
|
||||
UserDictionary.Words.FREQUENCY,
|
||||
UserDictionary.Words.LOCALE,
|
||||
UserDictionary.Words.SHORTCUT
|
||||
)
|
||||
|
||||
@Entity(tableName = WORDS_TABLE)
|
||||
data class UserDictionaryEntry(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = UserDictionary.Words._ID, index = true)
|
||||
val id: Long,
|
||||
@ColumnInfo(name = UserDictionary.Words.WORD)
|
||||
val word: String,
|
||||
@ColumnInfo(name = UserDictionary.Words.FREQUENCY)
|
||||
val freq: Int,
|
||||
@ColumnInfo(name = UserDictionary.Words.LOCALE)
|
||||
val locale: String?,
|
||||
@ColumnInfo(name = UserDictionary.Words.SHORTCUT)
|
||||
val shortcut: String?,
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface UserDictionaryDao {
|
||||
@Query("SELECT * FROM $WORDS_TABLE")
|
||||
fun queryAll(): List<UserDictionaryEntry>
|
||||
|
||||
@Query("SELECT * FROM $WORDS_TABLE WHERE ${UserDictionary.Words.WORD} LIKE :word")
|
||||
fun query(word: String): List<UserDictionaryEntry>
|
||||
|
||||
@Query("SELECT * FROM $WORDS_TABLE WHERE ${UserDictionary.Words.WORD} LIKE :word AND (${UserDictionary.Words.LOCALE} = :locale OR ${UserDictionary.Words.LOCALE} IS NULL)")
|
||||
fun query(word: String, locale: Locale): List<UserDictionaryEntry>
|
||||
}
|
||||
|
||||
interface UserDictionaryDatabase {
|
||||
fun userDictionaryDao(): UserDictionaryDao
|
||||
}
|
||||
|
||||
@Database(entities = [UserDictionaryEntry::class], version = 1)
|
||||
@TypeConverters(FlorisUserDictionaryDatabase.Converters::class)
|
||||
abstract class FlorisUserDictionaryDatabase : RoomDatabase(), UserDictionaryDatabase {
|
||||
companion object {
|
||||
const val DB_FILE_NAME = "floris_user_dictionary"
|
||||
}
|
||||
|
||||
abstract override fun userDictionaryDao(): UserDictionaryDao
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun localeToString(locale: Locale): String {
|
||||
return locale.toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToLocale(string: String): Locale {
|
||||
return LocaleUtils.stringToLocale(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SystemUserDictionaryDatabase(context: Context) : UserDictionaryDatabase {
|
||||
private val applicationContext: WeakReference<Context> = WeakReference(context.applicationContext ?: context)
|
||||
|
||||
private val dao = object : UserDictionaryDao {
|
||||
override fun queryAll(): List<UserDictionaryEntry> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun query(word: String): List<UserDictionaryEntry> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun query(word: String, locale: Locale): List<UserDictionaryEntry> {
|
||||
val resolver = applicationContext.get()?.contentResolver ?: return listOf()
|
||||
val cursor = resolver.query(
|
||||
UserDictionary.Words.CONTENT_URI,
|
||||
PROJECTIONS,
|
||||
"${UserDictionary.Words.WORD} LIKE '%$word%' AND (${UserDictionary.Words.LOCALE} = '$locale' OR ${UserDictionary.Words.LOCALE} = '${locale.language}' OR ${UserDictionary.Words.LOCALE} IS NULL)",
|
||||
null,
|
||||
SORT_BY_FREQ_DESC
|
||||
) ?: return listOf()
|
||||
return parseEntries(cursor).also { cursor.close() }
|
||||
}
|
||||
|
||||
private fun parseEntries(cursor: Cursor): List<UserDictionaryEntry> {
|
||||
if (cursor.count <= 0) {
|
||||
return listOf()
|
||||
}
|
||||
val idIndex = cursor.getColumnIndex(UserDictionary.Words._ID)
|
||||
val wordIndex = cursor.getColumnIndex(UserDictionary.Words.WORD)
|
||||
val freqIndex = cursor.getColumnIndex(UserDictionary.Words.FREQUENCY)
|
||||
val localeIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE)
|
||||
val shortcutIndex = cursor.getColumnIndex(UserDictionary.Words.SHORTCUT)
|
||||
val retList = mutableListOf<UserDictionaryEntry>()
|
||||
while (cursor.moveToNext()) {
|
||||
retList.add(
|
||||
UserDictionaryEntry(
|
||||
id = cursor.getLong(idIndex),
|
||||
word = cursor.getString(wordIndex),
|
||||
freq = cursor.getInt(freqIndex),
|
||||
locale = cursor.getString(localeIndex),
|
||||
shortcut = cursor.getString(shortcutIndex)
|
||||
)
|
||||
)
|
||||
}
|
||||
return retList
|
||||
}
|
||||
}
|
||||
|
||||
override fun userDictionaryDao(): UserDictionaryDao {
|
||||
return dao
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ import dev.patrickgold.florisboard.ime.text.layout.LayoutManager
|
||||
import dev.patrickgold.florisboard.ime.text.smartbar.SmartbarView
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONArray
|
||||
import java.util.*
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
@ -299,6 +300,7 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
keyboards.clear()
|
||||
inputEventDispatcher.keyEventReceiver = null
|
||||
inputEventDispatcher.close()
|
||||
dictionaryManager.unloadUserDictionariesIfNecessary()
|
||||
cancel()
|
||||
layoutManager.onDestroy()
|
||||
instance = null
|
||||
@ -375,6 +377,9 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
launch(Dispatchers.Default) {
|
||||
dictionaryManager.loadUserDictionariesIfNecessary()
|
||||
}
|
||||
smartbarView?.updateSmartbarState()
|
||||
}
|
||||
|
||||
@ -446,12 +451,16 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
activeDictionary?.let {
|
||||
launch(Dispatchers.Default) {
|
||||
val startTime = System.nanoTime()
|
||||
val suggestions = it.getTokenPredictions(
|
||||
val suggestions = queryUserDictionary(
|
||||
activeEditorInstance.cachedInput.currentWord.text,
|
||||
florisboard.activeSubtype.locale
|
||||
).toMutableList()
|
||||
suggestions.addAll(it.getTokenPredictions(
|
||||
precedingTokens = listOf(),
|
||||
currentToken = Token(activeEditorInstance.cachedInput.currentWord.text),
|
||||
maxSuggestionCount = 16,
|
||||
allowPossiblyOffensive = !prefs.suggestion.blockPossiblyOffensive
|
||||
).toStringList()
|
||||
).toStringList())
|
||||
if (BuildConfig.DEBUG) {
|
||||
val elapsed = (System.nanoTime() - startTime) / 1000.0
|
||||
flogInfo { "sugg fetch time: $elapsed us" }
|
||||
@ -472,6 +481,30 @@ class TextInputManager private constructor() : CoroutineScope by MainScope(), In
|
||||
smartbarView?.onPrimaryClipChanged()
|
||||
}
|
||||
|
||||
private fun queryUserDictionary(word: String, locale: Locale): List<String> {
|
||||
val florisDao = dictionaryManager.florisUserDictionaryDao()
|
||||
val systemDao = dictionaryManager.systemUserDictionaryDao()
|
||||
if (florisDao == null && systemDao == null) {
|
||||
return listOf()
|
||||
}
|
||||
val retList = mutableListOf<String>()
|
||||
if (prefs.dictionary.enableFlorisUserDictionary) {
|
||||
florisDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
retList.add(entry.word)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prefs.dictionary.enableSystemUserDictionary) {
|
||||
systemDao?.query(word, locale)?.let {
|
||||
for (entry in it) {
|
||||
retList.add(entry.word)
|
||||
}
|
||||
}
|
||||
}
|
||||
return retList
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current caps state according to the [EditorInstance.cursorCapsMode], while
|
||||
* respecting [capsLock] property and the correction.autoCapitalization preference.
|
||||
|
@ -40,7 +40,7 @@ class TypingInnerFragment : PreferenceFragmentCompat() {
|
||||
startActivity(intent)
|
||||
true
|
||||
}
|
||||
PrefHelper.Dictionary.MANAGE_INTERNAL_USER_DICTIONARY -> {
|
||||
PrefHelper.Dictionary.MANAGE_FLORIS_USER_DICTIONARY -> {
|
||||
// NYI
|
||||
true
|
||||
}
|
||||
|
@ -247,8 +247,8 @@
|
||||
<string name="pref__dictionary__manage_system_user_dictionary__summary" comment="Preference summary">Add, view, and remove entries for the system user dictionary</string>
|
||||
<string name="pref__dictionary__enable_internal_user_dictionary__label" comment="Preference title">Enable internal user dictionary</string>
|
||||
<string name="pref__dictionary__enable_internal_user_dictionary__summary" comment="Preference summary">Suggest words stored in the internal user dictionary</string>
|
||||
<string name="pref__dictionary__manage_internal_user_dictionary__label" comment="Preference title">Manage internal user dictionary</string>
|
||||
<string name="pref__dictionary__manage_internal_user_dictionary__summary" comment="Preference summary">Add, view, and remove entries for the internal user dictionary</string>
|
||||
<string name="pref__dictionary__manage_floris_user_dictionary__label" comment="Preference title">Manage internal user dictionary</string>
|
||||
<string name="pref__dictionary__manage_floris_user_dictionary__summary" comment="Preference summary">Add, view, and remove entries for the internal user dictionary</string>
|
||||
<string name="pref__correction__title" comment="Preference group title">Corrections</string>
|
||||
<string name="pref__correction__auto_capitalization__label" comment="Preference title">Auto-capitalization</string>
|
||||
<string name="pref__correction__auto_capitalization__summary" comment="Preference summary">Capitalize words based on the current input context</string>
|
||||
|
@ -91,17 +91,17 @@
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
app:dependency="suggestion__enabled"
|
||||
app:key="suggestion__enable_internal_user_dictionary"
|
||||
app:key="suggestion__enable_floris_user_dictionary"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__dictionary__enable_internal_user_dictionary__label"
|
||||
app:summary="@string/pref__dictionary__enable_internal_user_dictionary__summary"/>
|
||||
|
||||
<Preference
|
||||
app:dependency="suggestion__enable_internal_user_dictionary"
|
||||
app:key="suggestion__manage_internal_user_dictionary"
|
||||
app:dependency="suggestion__enable_floris_user_dictionary"
|
||||
app:key="suggestion__manage_floris_user_dictionary"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref__dictionary__manage_internal_user_dictionary__label"
|
||||
app:summary="@string/pref__dictionary__manage_internal_user_dictionary__summary"/>
|
||||
app:title="@string/pref__dictionary__manage_floris_user_dictionary__label"
|
||||
app:summary="@string/pref__dictionary__manage_floris_user_dictionary__summary"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
@ -9,7 +9,7 @@ subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
jcenter()
|
||||
jcenter() // Cannot remove jcenter as of now because flexbox depends on it
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user