Skip to content

Commit f42ab90

Browse files
authored
Code cleanup, release prep (#831)
1 parent 0b7e3ae commit f42ab90

43 files changed

Lines changed: 1220 additions & 124 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/main/java/com/urik/keyboard/UrikApplication.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.urik.keyboard
22

33
import android.app.Application
4+
import android.database.sqlite.SQLiteDatabaseCorruptException
5+
import com.urik.keyboard.data.database.KeyboardDatabase
46
import com.urik.keyboard.di.ApplicationScope
7+
import com.urik.keyboard.di.DatabaseModule
58
import com.urik.keyboard.service.ClipboardMonitorService
69
import com.urik.keyboard.settings.SettingsRepository
710
import com.urik.keyboard.utils.ErrorLogger
@@ -11,6 +14,7 @@ import kotlinx.coroutines.CoroutineScope
1114
import kotlinx.coroutines.flow.distinctUntilChanged
1215
import kotlinx.coroutines.flow.map
1316
import kotlinx.coroutines.launch
17+
import net.zetetic.database.sqlcipher.SQLiteNotADatabaseException
1418

1519
/**
1620
* SQLCipher is loaded before any database access (must precede Room init).
@@ -43,6 +47,10 @@ class UrikApplication : Application() {
4347
context = mapOf("thread" to thread.name)
4448
)
4549

50+
if (isDatabaseCorruptionException(throwable)) {
51+
recoverFromDatabaseCorruption()
52+
}
53+
4654
previousHandler?.uncaughtException(thread, throwable)
4755
}
4856

@@ -71,6 +79,28 @@ class UrikApplication : Application() {
7179
observeClipboardSettings()
7280
}
7381

82+
/**
83+
* Room opens its connection lazily on a background coroutine, so SQLCipher corruption
84+
* errors can arrive here uncaught instead of via [DatabaseModule.provideKeyboardDatabase]'s
85+
* recovery path. Deleting the database files lets the next launch recreate it cleanly.
86+
*/
87+
private fun isDatabaseCorruptionException(throwable: Throwable): Boolean = generateSequence(throwable) { it.cause }
88+
.any { it is SQLiteNotADatabaseException || it is SQLiteDatabaseCorruptException }
89+
90+
private fun recoverFromDatabaseCorruption() {
91+
try {
92+
KeyboardDatabase.resetInstance()
93+
DatabaseModule.deleteDatabaseFiles(applicationContext)
94+
} catch (e: Exception) {
95+
ErrorLogger.logException(
96+
component = "Application",
97+
severity = ErrorLogger.Severity.CRITICAL,
98+
exception = e,
99+
context = mapOf("phase" to "uncaught_corruption_recovery_failed")
100+
)
101+
}
102+
}
103+
74104
private fun observeClipboardSettings() {
75105
applicationScope.launch {
76106
settingsRepository.settings

app/src/main/java/com/urik/keyboard/UrikInputMethodService.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ import com.urik.keyboard.ui.keyboard.components.SwipeDetector
7575
import com.urik.keyboard.ui.keyboard.components.SwipeKeyboardView
7676
import com.urik.keyboard.utils.BackspaceUtils
7777
import com.urik.keyboard.utils.CacheMemoryManager
78-
import com.urik.keyboard.utils.CursorEditingUtils
7978
import com.urik.keyboard.utils.ErrorLogger
8079
import com.urik.keyboard.utils.KanaTransformUtils
8180
import com.urik.keyboard.utils.KeyboardModeUtils
@@ -238,8 +237,6 @@ open class UrikInputMethodService :
238237

239238
private fun clearSecureFieldState() = imeStateCoordinator.clearSecureFieldState()
240239

241-
private fun isValidTextInput(text: String): Boolean = CursorEditingUtils.isValidTextInput(text)
242-
243240
private fun updateScriptContext(locale: ULocale) {
244241
val currentLayout = viewModel.layout.value
245242
val isRTL = currentLayout?.isRTL ?: false
@@ -483,7 +480,6 @@ open class UrikInputMethodService :
483480
outputBridge = outputBridge,
484481
suggestionPipeline = suggestionPipeline,
485482
autoCorrectionEngine = autoCorrectionEngine,
486-
textInputProcessor = textInputProcessor,
487483
swipeSpaceManager = swipeSpaceManager,
488484
swipeDetector = swipeDetector,
489485
candidateBarController = candidateBarController,
@@ -1425,8 +1421,7 @@ open class UrikInputMethodService :
14251421
textInputProcessor.removeSuggestion(suggestion)
14261422

14271423
withContext(Dispatchers.Main) {
1428-
val currentSuggestions = inputState.pendingSuggestions.filter { it != suggestion }
1429-
inputState.pendingSuggestions = currentSuggestions
1424+
val currentSuggestions = inputState.removeSuggestionFromState(suggestion)
14301425
if (currentSuggestions.isNotEmpty()) {
14311426
candidateBarController.updateSuggestions(currentSuggestions)
14321427
} else {
@@ -1969,10 +1964,7 @@ open class UrikInputMethodService :
19691964
}
19701965

19711966
private companion object {
1972-
const val DOUBLE_TAP_SPACE_THRESHOLD_MS = 250L
19731967
const val DOUBLE_SHIFT_THRESHOLD_MS = 400L
1974-
const val NON_SEQUENTIAL_JUMP_THRESHOLD = 5
1975-
const val WORD_BOUNDARY_CONTEXT_LENGTH = 64
19761968
}
19771969
}
19781970

app/src/main/java/com/urik/keyboard/di/DatabaseModule.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ object DatabaseModule {
6868
@ApplicationContext context: Context,
6969
securityManager: DatabaseSecurityManager
7070
): KeyboardDatabase {
71-
var passphrase: ByteArray? = null
7271
var alreadyLogged = false
7372
try {
7473
if (securityManager.shouldMigrateToEncrypted(context)) {
@@ -86,7 +85,7 @@ object DatabaseModule {
8685
}
8786
}
8887

89-
passphrase =
88+
val passphrase =
9089
try {
9190
securityManager.getDatabasePassphrase()
9291
} catch (e: Exception) {
@@ -102,7 +101,6 @@ object DatabaseModule {
102101

103102
val passphraseWasAvailable = passphrase != null
104103
val initialPassphrase = passphrase
105-
passphrase = null
106104
return try {
107105
opener.open(context, initialPassphrase)
108106
} catch (e: Exception) {
@@ -184,8 +182,6 @@ object DatabaseModule {
184182
initialPassphrase?.fill(0)
185183
}
186184
} catch (e: Exception) {
187-
passphrase?.fill(0)
188-
189185
if (!alreadyLogged) {
190186
ErrorLogger.logException(
191187
component = "DatabaseModule",
@@ -198,7 +194,7 @@ object DatabaseModule {
198194
}
199195
}
200196

201-
private fun deleteDatabaseFiles(context: Context) {
197+
internal fun deleteDatabaseFiles(context: Context) {
202198
val dbPath = context.applicationContext.getDatabasePath(KeyboardDatabase.DATABASE_NAME)
203199
dbPath.delete()
204200
if (dbPath.exists()) {

app/src/main/java/com/urik/keyboard/di/KeyboardModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.urik.keyboard.data.database.KeyboardDatabase
77
import com.urik.keyboard.data.database.LearnedWordDao
88
import com.urik.keyboard.data.database.UserWordBigramDao
99
import com.urik.keyboard.data.database.UserWordFrequencyDao
10+
import com.urik.keyboard.service.BlacklistRepository
1011
import com.urik.keyboard.service.CharacterVariationService
1112
import com.urik.keyboard.service.DictionaryBackupManager
1213
import com.urik.keyboard.service.EmojiSearchManager
@@ -142,6 +143,7 @@ object KeyboardModule {
142143
wordLearningEngine: WordLearningEngine,
143144
wordFrequencyRepository: WordFrequencyRepository,
144145
cacheMemoryManager: CacheMemoryManager,
146+
blacklistRepository: BlacklistRepository,
145147
wordNormalizer: WordNormalizer,
146148
fatFingerExpander: FatFingerExpander
147149
): SpellCheckManager = SpellCheckManager(
@@ -151,6 +153,7 @@ object KeyboardModule {
151153
wordFrequencyRepository,
152154
wordNormalizer,
153155
cacheMemoryManager,
156+
blacklistRepository,
154157
fatFingerExpander = fatFingerExpander
155158
)
156159

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.urik.keyboard.dictionary
2+
3+
/**
4+
* Bare forms present in source frequency corpora only as encoding artifacts of
5+
* apostrophe forms (e.g. "im" for "i'm", "jai" for "j'ai") plus OCR garbage
6+
* (e.g. "weii" for "well"). Filtered out of dictionary lookups, candidates and
7+
* completions at runtime so the apostrophe form wins spell check and autocorrect.
8+
*
9+
* All entries must be lowercase.
10+
*/
11+
object BareFormRemovelist {
12+
private val REMOVELIST: Map<String, Set<String>> = mapOf(
13+
"en" to setOf(
14+
"dont", "wont", "cant", "didnt", "doesnt", "wasnt", "isnt", "arent",
15+
"wouldnt", "couldnt", "shouldnt", "hadnt", "hasnt", "havent",
16+
"thats", "whats", "whos", "hows", "heres", "theres", "wheres",
17+
"hes", "shes", "youre", "theyre", "weve", "theyve",
18+
"youll", "theyll", "itll", "youve", "youd", "hed", "shed", "wed",
19+
"wouldve", "couldve", "shouldve", "werent", "aint",
20+
"howd", "whatre", "lm", "ld", "lll", "lts", "lve", "weii",
21+
"im", "ive", "theyd"
22+
),
23+
"fr" to setOf(
24+
"jai", "cest", "tai", "lai", "nai", "quil", "nest",
25+
"aujourdhui", "taime", "cetait"
26+
),
27+
"de" to setOf(
28+
"gibts",
29+
"gehts",
30+
"habs",
31+
"stimmts",
32+
"bins",
33+
"sies"
34+
),
35+
"el" to setOf(
36+
"μένα", "σένα", "κάντο", "σενα", "παρόλα", "βάλτο", "δώστο",
37+
"κόφτο", "πάρτο", "γιαυτό", "βούλωστο", "σόλο", "απέξω",
38+
"πάρτα", "δώστου", "πάρτον", "δώσμου", "πάρτην", "βάλτα",
39+
"φέρτον", "απτο", "γιαυτο", "ρίξτου", "απτην", "ναναι",
40+
"σευχαριστώ", "μαρέσει", "σαυτό", "απτη", "θαναι", "σαγαπώ",
41+
"απτον", "σουπα", "γιαυτόν", "απαυτά", "απτα", "απότι",
42+
"νασαι", "σαρέσει", "απαυτό", "γιαυτήν", "σαυτή", "μαυτό",
43+
"σαγαπάω", "ναχει", "θαπρεπε", "σαυτόν", "απόλα", "ναμαι",
44+
"γιαυτά", "απτους", "γιαυτή", "θαμαι", "γιαυτούς", "θαθελα",
45+
"απτις", "απόσο", "οσι", "σαυτο", "μαρέσουν", "σόλους",
46+
"θαρθει", "ναμαστε", "σταλήθεια", "μόλα", "αποτι", "θαταν",
47+
"εφόσων", "τοχω", "γιαυτον", "θασαι", "μακούς", "γιαυτα",
48+
"μαυτόν", "σαφήσω", "απαυτούς", "σαυτά", "θαχει", "γιαυτην",
49+
"τοξερα", "μαυτή", "τόνομα", "θαχεις", "σέχω", "μαυτά",
50+
"σέναν", "τοκανες"
51+
),
52+
"cs" to setOf("dont", "its"),
53+
"nl" to setOf("fotos", "autos"),
54+
"it" to setOf("lho", "dacqua")
55+
)
56+
57+
fun forLanguage(languageCode: String): Set<String> = REMOVELIST[languageCode] ?: emptySet()
58+
}

app/src/main/java/com/urik/keyboard/dictionary/UrikDictionary.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package com.urik.keyboard.dictionary
44

55
import java.io.InputStream
66

7-
class UrikDictionary(inputStream: InputStream) {
7+
class UrikDictionary(inputStream: InputStream, private val removedWords: Set<String> = emptySet()) {
88
private val data: ByteArray = inputStream.readBytes()
99

1010
val wordCount: Int
@@ -33,7 +33,10 @@ class UrikDictionary(inputStream: InputStream) {
3333
return UrikFormat.dequantizeFreq(b)
3434
}
3535

36+
private fun isRemoved(word: String): Boolean = removedWords.isNotEmpty() && word.lowercase() in removedWords
37+
3638
private fun getFreqByte(word: String): Int? {
39+
if (isRemoved(word)) return null
3740
var stateOffset = stateTableOffset
3841
for ((idx, ch) in word.withIndex()) {
3942
val arc = findArc(stateOffset, ch) ?: return null
@@ -67,7 +70,10 @@ class UrikDictionary(inputStream: InputStream) {
6770
val isFinalDawg = (stateHeader and 0x80) != 0
6871

6972
if (isFinalDawg && auto.isAccepting(autoState)) {
70-
results.add(path.toString() to autoState.row.last())
73+
val word = path.toString()
74+
if (!isRemoved(word)) {
75+
results.add(word to autoState.row.last())
76+
}
7177
}
7278

7379
var arcOffset = stateAbsOffset + 1
@@ -113,7 +119,12 @@ class UrikDictionary(inputStream: InputStream) {
113119
val arcCount = stateHeader and 0x7F
114120
val isFinal = (stateHeader and 0x80) != 0
115121

116-
if (isFinal) results.add(path.toString() to UrikFormat.dequantizeFreq(incomingFreqByte))
122+
if (isFinal) {
123+
val word = path.toString()
124+
if (!isRemoved(word)) {
125+
results.add(word to UrikFormat.dequantizeFreq(incomingFreqByte))
126+
}
127+
}
117128
if (results.size >= maxResults) return
118129

119130
var arcOffset = stateAbsOffset + 1

app/src/main/java/com/urik/keyboard/service/AutoCorrectionEngine.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import javax.inject.Singleton
88
sealed class AutocorrectDecision {
99
data object None : AutocorrectDecision()
1010
data class Correct(val suggestion: String) : AutocorrectDecision()
11-
data object Pause : AutocorrectDecision()
12-
data object ContractionBypass : AutocorrectDecision()
11+
data class Pause(val suggestions: List<SpellingSuggestion>) : AutocorrectDecision()
12+
data class ContractionBypass(val suggestions: List<SpellingSuggestion>) : AutocorrectDecision()
1313
data class Suggestions(val list: List<SpellingSuggestion>) : AutocorrectDecision()
1414
}
1515

@@ -45,7 +45,7 @@ constructor(private val textInputProcessor: TextInputProcessor) {
4545
textInputProcessor.hasDominantContractionForm(buffer)
4646

4747
if (bypassForContraction) {
48-
return AutocorrectDecision.ContractionBypass
48+
return AutocorrectDecision.ContractionBypass(textInputProcessor.getSuggestions(buffer))
4949
}
5050

5151
if (isValid) {
@@ -79,7 +79,7 @@ constructor(private val textInputProcessor: TextInputProcessor) {
7979
val suggestions = textInputProcessor.getSuggestions(buffer)
8080

8181
if (pauseOnMisspelledWord) {
82-
return AutocorrectDecision.Pause
82+
return AutocorrectDecision.Pause(suggestions)
8383
}
8484

8585
if (autocorrectionEnabled && suggestions.isNotEmpty() && lastAutocorrection == null) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.urik.keyboard.service
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.preferences.core.Preferences
6+
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.stringSetPreferencesKey
8+
import androidx.datastore.preferences.preferencesDataStore
9+
import dagger.hilt.android.qualifiers.ApplicationContext
10+
import javax.inject.Inject
11+
import javax.inject.Singleton
12+
import kotlinx.coroutines.flow.first
13+
14+
private val Context.blacklistDataStore by preferencesDataStore(name = "blacklist_words")
15+
16+
/**
17+
* Persists user-rejected suggestion words so removals survive IME process death.
18+
* Global across languages.
19+
*/
20+
@Singleton
21+
class BlacklistRepository internal constructor(private val dataStore: DataStore<Preferences>) {
22+
@Inject
23+
constructor(
24+
@ApplicationContext context: Context
25+
) : this(context.blacklistDataStore)
26+
27+
private object PreferenceKeys {
28+
val BLACKLISTED_WORDS = stringSetPreferencesKey("blacklisted_words")
29+
}
30+
31+
suspend fun getAll(): Set<String> = dataStore.data.first()[PreferenceKeys.BLACKLISTED_WORDS] ?: emptySet()
32+
33+
suspend fun add(word: String) {
34+
dataStore.edit { preferences ->
35+
val current = preferences[PreferenceKeys.BLACKLISTED_WORDS] ?: emptySet()
36+
preferences[PreferenceKeys.BLACKLISTED_WORDS] = current + word
37+
}
38+
}
39+
40+
suspend fun remove(word: String) {
41+
dataStore.edit { preferences ->
42+
val current = preferences[PreferenceKeys.BLACKLISTED_WORDS] ?: emptySet()
43+
preferences[PreferenceKeys.BLACKLISTED_WORDS] = current - word
44+
}
45+
}
46+
}

app/src/main/java/com/urik/keyboard/service/CustomKeyMappingService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,13 @@ constructor(
7474
if (raw.isBlank()) return emptyList()
7575

7676
return raw
77-
.split(LONG_PRESS_DELIMITER)
77+
.splitToSequence(LONG_PRESS_DELIMITER)
7878
.map { it.trim() }
7979
.filter { it.isNotBlank() }
8080
.map { Normalizer.normalize(it, Normalizer.Form.NFC) }
8181
.distinct()
8282
.take(MAX_CUSTOM_SYMBOLS)
83+
.toList()
8384
}
8485
}
8586
}

app/src/main/java/com/urik/keyboard/service/FatFingerExpander.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.urik.keyboard.service
22

33
import android.graphics.PointF
4+
import com.urik.keyboard.service.FatFingerExpander.Companion.ADJACENT_KEY_THRESHOLD_MULTIPLIER
45
import javax.inject.Inject
56
import javax.inject.Singleton
67
import kotlin.math.sqrt

0 commit comments

Comments
 (0)