0
0
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:
Patrick Goldinger 2021-05-05 18:32:48 +02:00
parent dcd20e4b73
commit 2b1951ea5f
10 changed files with 285 additions and 26 deletions

3
.gitignore vendored
View File

@ -40,3 +40,6 @@ captures/
# Keystore files
*.jks
crowdin.properties
# AndroidX Room schema JSONs
/app/schemas/

View File

@ -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")

View File

@ -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)
}
/**

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -40,7 +40,7 @@ class TypingInnerFragment : PreferenceFragmentCompat() {
startActivity(intent)
true
}
PrefHelper.Dictionary.MANAGE_INTERNAL_USER_DICTIONARY -> {
PrefHelper.Dictionary.MANAGE_FLORIS_USER_DICTIONARY -> {
// NYI
true
}

View File

@ -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>

View File

@ -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>

View File

@ -9,7 +9,7 @@ subprojects {
repositories {
mavenCentral()
google()
jcenter()
jcenter() // Cannot remove jcenter as of now because flexbox depends on it
}
}