Skip to content

Commit ff926aa

Browse files
authored
Code cleanup, dep updates etc (#535)
1 parent e2197bb commit ff926aa

22 files changed

Lines changed: 290 additions & 243 deletions

app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
22

33
plugins {
44
id("com.android.application")
5-
id("org.jetbrains.kotlin.android")
65
id("org.jetbrains.kotlin.plugin.serialization")
76
id("com.google.devtools.ksp")
87
id("com.google.dagger.hilt.android")

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.urik.keyboard
33
object KeyboardConstants {
44
object CacheConstants {
55
const val PROCESSING_CACHE_MAX_SIZE = 200
6-
const val PROCESSING_CACHE_CLEANUP_THRESHOLD = 250
76
const val CACHE_TTL_MS = 300000L
87

98
const val SUGGESTION_CACHE_SIZE = 500
@@ -65,12 +64,10 @@ object KeyboardConstants {
6564
const val COMPLETION_LENGTH_WEIGHT = 0.70
6665
const val COMPLETION_FREQUENCY_WEIGHT = 0.30
6766
const val COMPLETION_CONFIDENCE_MIN = 0.50
68-
const val COMPLETION_CONFIDENCE_MAX = 0.84
6967

7068
const val SYMSPELL_DISTANCE_WEIGHT = 0.45
7169
const val SYMSPELL_FREQUENCY_WEIGHT = 0.05
7270
const val SYMSPELL_CONFIDENCE_MIN = 0.0
73-
const val SYMSPELL_CONFIDENCE_MAX = 0.70
7471
const val MAX_DICT_FREQUENCY = 30_000_000.0
7572

7673
const val SAME_LENGTH_BONUS = 0.10
@@ -224,13 +221,10 @@ object KeyboardConstants {
224221
const val MAX_WORD_INPUT_LENGTH = 50
225222
const val MAX_CURSOR_POSITION_CHARS = 1000
226223
const val WORD_BOUNDARY_CONTEXT_LENGTH = 64
227-
const val PARAGRAPH_CONTEXT_LENGTH = 256
228224
}
229225

230226
object SelectionTrackingConstants {
231227
const val NON_SEQUENTIAL_JUMP_THRESHOLD = 5
232-
const val CURSOR_VALIDATION_TOLERANCE = 1
233-
const val MAX_COMPOSING_REGION_VALIDATION_DELAY_MS = 50L
234228
}
235229

236230
object InputTimingConstants {

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

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

33
import android.annotation.SuppressLint
4-
import android.content.Context
54
import android.graphics.Color
65
import android.inputmethodservice.InputMethodService
76
import android.os.Build
@@ -147,7 +146,7 @@ class UrikInputMethodService :
147146
private var postureDetector: com.urik.keyboard.service.PostureDetector? = null
148147

149148
private val inputMethodManager: android.view.inputmethod.InputMethodManager by lazy {
150-
getSystemService(Context.INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
149+
getSystemService(INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager
151150
}
152151

153152
private var serviceJob = SupervisorJob()
@@ -423,7 +422,6 @@ class UrikInputMethodService :
423422
* Learns word and invalidates relevant caches.
424423
*
425424
* @param word Word to learn
426-
* @param inputMethod Source of the word (TYPED, SWIPED, SELECTED_FROM_SUGGESTION)
427425
* @return True if learning succeeded or is disabled
428426
*/
429427
private suspend fun recordWordUsage(word: String) {
@@ -2455,7 +2453,7 @@ class UrikInputMethodService :
24552453
if (wordStart < 0) return null
24562454

24572455
if (paragraphBoundary >= 0) {
2458-
val paragraphStartInText = textBeforeCursor.length - textInParagraph.length
2456+
textBeforeCursor.length - textInParagraph.length
24592457
val absoluteParagraphBoundary = cursorPosition - textInParagraph.length
24602458
if (wordStart < absoluteParagraphBoundary) {
24612459
return null
@@ -2465,15 +2463,6 @@ class UrikInputMethodService :
24652463
return Triple(wordStart, cursorPosition, word)
24662464
}
24672465

2468-
/**
2469-
* Extracts word before cursor using boundary detection.
2470-
*
2471-
* @param textBeforeCursor Text before cursor position
2472-
* @return Pair of (word, boundary index) or null if no valid word found
2473-
*/
2474-
private fun extractWordBeforeCursor(textBeforeCursor: String): Pair<String, Int>? =
2475-
BackspaceUtils.extractWordBeforeCursor(textBeforeCursor)
2476-
24772466
/**
24782467
* Handles space key press.
24792468
*/
@@ -3106,6 +3095,8 @@ class UrikInputMethodService :
31063095

31073096
override fun onDestroy() {
31083097
serviceJob.cancel()
3098+
serviceJob = SupervisorJob()
3099+
serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
31093100

31103101
observerJobs.forEach { it.cancel() }
31113102
observerJobs.clear()

app/src/main/java/com/urik/keyboard/data/WordFrequencyRepository.kt

Lines changed: 144 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,32 @@ import com.urik.keyboard.utils.CacheMemoryManager
99
import com.urik.keyboard.utils.ErrorLogger
1010
import com.urik.keyboard.utils.ManagedCache
1111
import kotlinx.coroutines.CoroutineDispatcher
12+
import kotlinx.coroutines.CoroutineScope
1213
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.SupervisorJob
16+
import kotlinx.coroutines.delay
17+
import kotlinx.coroutines.launch
18+
import kotlinx.coroutines.sync.Mutex
19+
import kotlinx.coroutines.sync.withLock
1320
import kotlinx.coroutines.withContext
1421
import java.util.concurrent.ConcurrentHashMap
1522
import javax.inject.Inject
1623
import javax.inject.Singleton
1724

25+
private data class PendingFrequencyUpdate(
26+
val languageTag: String,
27+
val wordNormalized: String,
28+
var incrementCount: Int = 1,
29+
)
30+
31+
private data class PendingBigramUpdate(
32+
val languageTag: String,
33+
val wordANormalized: String,
34+
val wordBNormalized: String,
35+
var incrementCount: Int = 1,
36+
)
37+
1838
@Singleton
1939
class WordFrequencyRepository
2040
@Inject
@@ -41,6 +61,18 @@ class WordFrequencyRepository
4161
@Volatile
4262
private var topBigramsCache = ConcurrentHashMap<String, Map<String, List<String>>>()
4363

64+
private val writeScope = CoroutineScope(SupervisorJob() + ioDispatcher)
65+
private var frequencyFlushJob: Job? = null
66+
private var bigramFlushJob: Job? = null
67+
private val frequencyWriteMutex = Mutex()
68+
private val bigramWriteMutex = Mutex()
69+
private val pendingFrequencyUpdates = ConcurrentHashMap<String, PendingFrequencyUpdate>()
70+
private val pendingBigramUpdates = ConcurrentHashMap<String, PendingBigramUpdate>()
71+
72+
private companion object {
73+
const val WRITE_DEBOUNCE_MS = 300L
74+
}
75+
4476
private fun normalizeWord(
4577
word: String,
4678
languageTag: String,
@@ -51,38 +83,67 @@ class WordFrequencyRepository
5183
normalizedWord: String,
5284
): String = "${languageTag}_$normalizedWord"
5385

54-
suspend fun incrementFrequency(
86+
fun incrementFrequency(
5587
word: String,
5688
languageTag: String,
57-
): Result<Unit> =
58-
withContext(ioDispatcher) {
59-
try {
60-
if (word.isBlank()) {
61-
return@withContext Result.success(Unit)
62-
}
89+
): Result<Unit> {
90+
if (word.isBlank()) {
91+
return Result.success(Unit)
92+
}
6393

64-
val normalized = normalizeWord(word, languageTag)
65-
val cacheKey = buildCacheKey(languageTag, normalized)
94+
val normalized = normalizeWord(word, languageTag)
95+
val cacheKey = buildCacheKey(languageTag, normalized)
6696

67-
userWordFrequencyDao.incrementFrequency(
68-
languageTag = languageTag,
69-
wordNormalized = normalized,
70-
lastUsed = System.currentTimeMillis(),
71-
)
97+
frequencyCache.invalidate(cacheKey)
7298

73-
frequencyCache.invalidate(cacheKey)
99+
pendingFrequencyUpdates.compute(cacheKey) { _, existing ->
100+
existing?.apply { incrementCount++ }
101+
?: PendingFrequencyUpdate(languageTag, normalized)
102+
}
74103

75-
Result.success(Unit)
76-
} catch (e: Exception) {
77-
ErrorLogger.logException(
78-
component = "WordFrequencyRepository",
79-
severity = ErrorLogger.Severity.HIGH,
80-
exception = e,
81-
context = mapOf("operation" to "incrementFrequency"),
82-
)
83-
Result.failure(e)
104+
scheduleFrequencyFlush()
105+
106+
return Result.success(Unit)
107+
}
108+
109+
private fun scheduleFrequencyFlush() {
110+
frequencyFlushJob?.cancel()
111+
frequencyFlushJob =
112+
writeScope.launch {
113+
delay(WRITE_DEBOUNCE_MS)
114+
flushPendingFrequencyUpdates()
115+
}
116+
}
117+
118+
private suspend fun flushPendingFrequencyUpdates() {
119+
frequencyWriteMutex.withLock {
120+
if (pendingFrequencyUpdates.isEmpty()) return
121+
122+
val updates = pendingFrequencyUpdates.toMap()
123+
pendingFrequencyUpdates.clear()
124+
125+
val timestamp = System.currentTimeMillis()
126+
127+
updates.values.forEach { update ->
128+
try {
129+
repeat(update.incrementCount) {
130+
userWordFrequencyDao.incrementFrequency(
131+
languageTag = update.languageTag,
132+
wordNormalized = update.wordNormalized,
133+
lastUsed = timestamp,
134+
)
135+
}
136+
} catch (e: Exception) {
137+
ErrorLogger.logException(
138+
component = "WordFrequencyRepository",
139+
severity = ErrorLogger.Severity.HIGH,
140+
exception = e,
141+
context = mapOf("operation" to "flushPendingFrequencyUpdates"),
142+
)
143+
}
84144
}
85145
}
146+
}
86147

87148
suspend fun getFrequency(
88149
word: String,
@@ -179,65 +240,80 @@ class WordFrequencyRepository
179240
}
180241
}
181242

182-
suspend fun clearAllFrequencies(): Result<Unit> =
183-
withContext(ioDispatcher) {
184-
try {
185-
userWordFrequencyDao.clearAll()
186-
frequencyCache.invalidateAll()
187-
Result.success(Unit)
188-
} catch (e: Exception) {
189-
ErrorLogger.logException(
190-
component = "WordFrequencyRepository",
191-
severity = ErrorLogger.Severity.HIGH,
192-
exception = e,
193-
context = mapOf("operation" to "clearAllFrequencies"),
194-
)
195-
Result.failure(e)
196-
}
197-
}
198-
199243
fun clearCache() {
200244
frequencyCache.invalidateAll()
201245
bigramCache.invalidateAll()
202246
topBigramsCache.clear()
247+
pendingFrequencyUpdates.clear()
248+
pendingBigramUpdates.clear()
203249
}
204250

205-
suspend fun recordBigram(
251+
fun recordBigram(
206252
wordA: String,
207253
wordB: String,
208254
languageTag: String,
209-
): Result<Unit> =
210-
withContext(ioDispatcher) {
211-
try {
212-
if (wordA.isBlank() || wordB.isBlank()) {
213-
return@withContext Result.success(Unit)
214-
}
255+
): Result<Unit> {
256+
if (wordA.isBlank() || wordB.isBlank()) {
257+
return Result.success(Unit)
258+
}
215259

216-
val normalizedA = normalizeWord(wordA, languageTag)
217-
val normalizedB = normalizeWord(wordB, languageTag)
218-
val cacheKey = buildBigramCacheKey(languageTag, normalizedA)
260+
val normalizedA = normalizeWord(wordA, languageTag)
261+
val normalizedB = normalizeWord(wordB, languageTag)
262+
val cacheKey = buildBigramCacheKey(languageTag, normalizedA)
263+
val bigramKey = "${languageTag}_${normalizedA}_$normalizedB"
219264

220-
userWordBigramDao.incrementBigram(
221-
languageTag = languageTag,
222-
wordANormalized = normalizedA,
223-
wordBNormalized = normalizedB,
224-
lastUsed = System.currentTimeMillis(),
225-
)
265+
bigramCache.invalidate(cacheKey)
266+
topBigramsCache.remove(languageTag)
267+
268+
pendingBigramUpdates.compute(bigramKey) { _, existing ->
269+
existing?.apply { incrementCount++ }
270+
?: PendingBigramUpdate(languageTag, normalizedA, normalizedB)
271+
}
226272

227-
bigramCache.invalidate(cacheKey)
228-
topBigramsCache.remove(languageTag)
273+
scheduleBigramFlush()
229274

230-
Result.success(Unit)
231-
} catch (e: Exception) {
232-
ErrorLogger.logException(
233-
component = "WordFrequencyRepository",
234-
severity = ErrorLogger.Severity.HIGH,
235-
exception = e,
236-
context = mapOf("operation" to "recordBigram"),
237-
)
238-
Result.failure(e)
275+
return Result.success(Unit)
276+
}
277+
278+
private fun scheduleBigramFlush() {
279+
bigramFlushJob?.cancel()
280+
bigramFlushJob =
281+
writeScope.launch {
282+
delay(WRITE_DEBOUNCE_MS)
283+
flushPendingBigramUpdates()
284+
}
285+
}
286+
287+
private suspend fun flushPendingBigramUpdates() {
288+
bigramWriteMutex.withLock {
289+
if (pendingBigramUpdates.isEmpty()) return
290+
291+
val updates = pendingBigramUpdates.toMap()
292+
pendingBigramUpdates.clear()
293+
294+
val timestamp = System.currentTimeMillis()
295+
296+
updates.values.forEach { update ->
297+
try {
298+
repeat(update.incrementCount) {
299+
userWordBigramDao.incrementBigram(
300+
languageTag = update.languageTag,
301+
wordANormalized = update.wordANormalized,
302+
wordBNormalized = update.wordBNormalized,
303+
lastUsed = timestamp,
304+
)
305+
}
306+
} catch (e: Exception) {
307+
ErrorLogger.logException(
308+
component = "WordFrequencyRepository",
309+
severity = ErrorLogger.Severity.HIGH,
310+
exception = e,
311+
context = mapOf("operation" to "flushPendingBigramUpdates"),
312+
)
313+
}
239314
}
240315
}
316+
}
241317

242318
suspend fun getBigramPredictions(
243319
wordA: String,
@@ -308,24 +384,6 @@ class WordFrequencyRepository
308384
}
309385
}
310386

311-
suspend fun clearAllBigrams(): Result<Unit> =
312-
withContext(ioDispatcher) {
313-
try {
314-
userWordBigramDao.clearAll()
315-
bigramCache.invalidateAll()
316-
topBigramsCache.clear()
317-
Result.success(Unit)
318-
} catch (e: Exception) {
319-
ErrorLogger.logException(
320-
component = "WordFrequencyRepository",
321-
severity = ErrorLogger.Severity.HIGH,
322-
exception = e,
323-
context = mapOf("operation" to "clearAllBigrams"),
324-
)
325-
Result.failure(e)
326-
}
327-
}
328-
329387
private fun buildBigramCacheKey(
330388
languageTag: String,
331389
normalizedWordA: String,

0 commit comments

Comments
 (0)