Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions app/src/main/java/com/urik/keyboard/UrikInputMethodService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.urik.keyboard.service.AutofillStateTracker
import com.urik.keyboard.service.CharacterVariationService
import com.urik.keyboard.service.ClipboardMonitorService
import com.urik.keyboard.service.EmojiSearchManager
import com.urik.keyboard.service.EnglishPronounI
import com.urik.keyboard.service.InputMethod
import com.urik.keyboard.service.InputStateManager
import com.urik.keyboard.service.LanguageManager
Expand All @@ -47,7 +48,6 @@ import com.urik.keyboard.service.SpellConfirmationState
import com.urik.keyboard.service.SuggestionPipeline
import com.urik.keyboard.service.TextInputProcessor
import com.urik.keyboard.service.ViewCallback
import com.urik.keyboard.service.EnglishPronounI
import com.urik.keyboard.service.WordLearningEngine
import com.urik.keyboard.service.WordState
import com.urik.keyboard.settings.KeyboardSettings
Expand Down Expand Up @@ -95,6 +95,9 @@ class UrikInputMethodService :
@Inject
lateinit var swipeDetector: SwipeDetector

@Inject
lateinit var streamingScoringEngine: com.urik.keyboard.ui.keyboard.components.StreamingScoringEngine

@Inject
lateinit var languageManager: LanguageManager

Expand Down Expand Up @@ -229,6 +232,7 @@ class UrikInputMethodService :
private fun isSentenceEndingPunctuation(char: Char): Boolean = UCharacter.hasBinaryProperty(char.code, UProperty.S_TERM)

private fun coordinateStateClear() {
streamingScoringEngine.cancelActiveGesture()
outputBridge.coordinateStateClear()
}

Expand Down Expand Up @@ -1159,7 +1163,11 @@ class UrikInputMethodService :
val cursorPosInWord =
if (inputState.composingRegionStart != -1 && inputState.displayBuffer.isNotEmpty()) {
val absoluteCursorPos = outputBridge.safeGetCursorPosition()
CursorEditingUtils.calculateCursorPositionInWord(absoluteCursorPos, inputState.composingRegionStart, inputState.displayBuffer.length)
CursorEditingUtils.calculateCursorPositionInWord(
absoluteCursorPos,
inputState.composingRegionStart,
inputState.displayBuffer.length,
)
} else {
inputState.displayBuffer.length
}
Expand Down Expand Up @@ -1313,7 +1321,11 @@ class UrikInputMethodService :
outputBridge.highlightCurrentWord()

val suggestions = textInputProcessor.getSuggestions(inputState.displayBuffer)
val displaySuggestions = suggestionPipeline.storeAndCapitalizeSuggestions(suggestions, inputState.isCurrentWordAtSentenceStart)
val displaySuggestions =
suggestionPipeline.storeAndCapitalizeSuggestions(
suggestions,
inputState.isCurrentWordAtSentenceStart,
)
inputState.pendingSuggestions = displaySuggestions
if (displaySuggestions.isNotEmpty()) {
swipeKeyboardView?.updateSuggestions(displaySuggestions)
Expand Down Expand Up @@ -1520,8 +1532,7 @@ class UrikInputMethodService :
return caseTransformer.applyCasing(suggestion, keyboardState, isSentenceStart)
}

private fun getEnglishPronounIForm(normalizedWord: String): String? =
EnglishPronounI.capitalize(normalizedWord)
private fun getEnglishPronounIForm(normalizedWord: String): String? = EnglishPronounI.capitalize(normalizedWord)

private fun handleSuggestionSelected(suggestion: String) {
serviceScope.launch {
Expand Down Expand Up @@ -1701,7 +1712,8 @@ class UrikInputMethodService :
val actualCursorPos = outputBridge.safeGetCursorPosition()

if (inputState.displayBuffer.isNotEmpty() && inputState.composingRegionStart != -1) {
val expectedCursorRange = inputState.composingRegionStart..(inputState.composingRegionStart + inputState.displayBuffer.length)
val expectedCursorRange =
inputState.composingRegionStart..(inputState.composingRegionStart + inputState.displayBuffer.length)
if (actualCursorPos !in expectedCursorRange) {
invalidateComposingStateOnCursorJump()
}
Expand Down Expand Up @@ -1811,12 +1823,20 @@ class UrikInputMethodService :

val cursorPosInWord =
if (inputState.composingRegionStart != -1) {
CursorEditingUtils.calculateCursorPositionInWord(absoluteCursorPos, inputState.composingRegionStart, inputState.displayBuffer.length)
CursorEditingUtils.calculateCursorPositionInWord(
absoluteCursorPos,
inputState.composingRegionStart,
inputState.displayBuffer.length,
)
} else {
val potentialStart = absoluteCursorPos - inputState.displayBuffer.length
if (potentialStart >= 0) {
inputState.composingRegionStart = potentialStart
CursorEditingUtils.calculateCursorPositionInWord(absoluteCursorPos, inputState.composingRegionStart, inputState.displayBuffer.length)
CursorEditingUtils.calculateCursorPositionInWord(
absoluteCursorPos,
inputState.composingRegionStart,
inputState.displayBuffer.length,
)
} else {
inputState.displayBuffer.length
}
Expand Down Expand Up @@ -2099,7 +2119,11 @@ class UrikInputMethodService :
inputState.pendingWordForLearning = inputState.displayBuffer

outputBridge.highlightCurrentWord()
val displaySuggestions = suggestionPipeline.storeAndCapitalizeSuggestions(suggestions, inputState.isCurrentWordAtSentenceStart)
val displaySuggestions =
suggestionPipeline.storeAndCapitalizeSuggestions(
suggestions,
inputState.isCurrentWordAtSentenceStart,
)
inputState.pendingSuggestions = displaySuggestions
if (displaySuggestions.isNotEmpty()) {
swipeKeyboardView?.updateSuggestions(displaySuggestions)
Expand Down Expand Up @@ -2483,6 +2507,7 @@ class UrikInputMethodService :

override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig)
streamingScoringEngine.cancelActiveGesture()

val currentDensity = resources.displayMetrics.density

Expand Down Expand Up @@ -2645,6 +2670,7 @@ class UrikInputMethodService :
}

override fun onDestroy() {
streamingScoringEngine.shutdown()
wordFrequencyRepository.clearCache()
autofillStateTracker.cleanup()

Expand Down
13 changes: 10 additions & 3 deletions app/src/main/java/com/urik/keyboard/di/KeyboardModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.urik.keyboard.service.WordNormalizer
import com.urik.keyboard.settings.SettingsRepository
import com.urik.keyboard.ui.keyboard.components.PathGeometryAnalyzer
import com.urik.keyboard.ui.keyboard.components.ResidualScorer
import com.urik.keyboard.ui.keyboard.components.StreamingScoringEngine
import com.urik.keyboard.ui.keyboard.components.SwipeDetector
import com.urik.keyboard.ui.keyboard.components.ZipfCheck
import com.urik.keyboard.utils.CacheMemoryManager
Expand Down Expand Up @@ -104,16 +105,16 @@ object KeyboardModule {

@Provides
@Singleton
fun provideSwipeDetector(
fun provideStreamingScoringEngine(
spellCheckManager: SpellCheckManager,
wordLearningEngine: WordLearningEngine,
pathGeometryAnalyzer: PathGeometryAnalyzer,
wordFrequencyRepository: WordFrequencyRepository,
residualScorer: ResidualScorer,
zipfCheck: ZipfCheck,
wordNormalizer: WordNormalizer,
): SwipeDetector =
SwipeDetector(
): StreamingScoringEngine =
StreamingScoringEngine(
spellCheckManager,
wordLearningEngine,
pathGeometryAnalyzer,
Expand All @@ -123,6 +124,12 @@ object KeyboardModule {
wordNormalizer,
)

@Provides
@Singleton
fun provideSwipeDetector(
streamingScoringEngine: StreamingScoringEngine,
): SwipeDetector = SwipeDetector(streamingScoringEngine)

@Provides
@Singleton
fun provideSpellCheckManager(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.database.sqlite.SQLiteDatabaseCorruptException
import android.database.sqlite.SQLiteDatabaseLockedException
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteFullException
import com.urik.keyboard.KeyboardConstants.MemoryConstants
import com.urik.keyboard.data.database.LearnedWord
import com.urik.keyboard.data.database.LearnedWordDao
import com.urik.keyboard.data.database.WordSource
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.urik.keyboard.ui.keyboard.components

import kotlin.math.sqrt

/** Catmull-Rom spline interpolation for raw touch input. */
class GestureInterpolator(private val ringBuffer: SwipePointRingBuffer) {

private val windowX = FloatArray(WINDOW_SIZE)
private val windowY = FloatArray(WINDOW_SIZE)
private val windowTimestamp = LongArray(WINDOW_SIZE)
private val windowPressure = FloatArray(WINDOW_SIZE)
private val windowVelocity = FloatArray(WINDOW_SIZE)
private var windowCount = 0
private var rawPointIndex = 0
val rawPointCount: Int get() = rawPointIndex

fun onRawPoint(x: Float, y: Float, timestamp: Long, pressure: Float, velocity: Float) {
rawPointIndex++
if (windowCount < WINDOW_SIZE) {
val i = windowCount
windowX[i] = x
windowY[i] = y
windowTimestamp[i] = timestamp
windowPressure[i] = pressure
windowVelocity[i] = velocity
windowCount++

if (windowCount == WINDOW_SIZE) {
interpolateSegment()
}

ringBuffer.write(x, y, timestamp, pressure, velocity)
return
}

windowX[0] = windowX[1]
windowY[0] = windowY[1]
windowTimestamp[0] = windowTimestamp[1]
windowPressure[0] = windowPressure[1]
windowVelocity[0] = windowVelocity[1]

windowX[1] = windowX[2]
windowY[1] = windowY[2]
windowTimestamp[1] = windowTimestamp[2]
windowPressure[1] = windowPressure[2]
windowVelocity[1] = windowVelocity[2]

windowX[2] = windowX[3]
windowY[2] = windowY[3]
windowTimestamp[2] = windowTimestamp[3]
windowPressure[2] = windowPressure[3]
windowVelocity[2] = windowVelocity[3]

windowX[3] = x
windowY[3] = y
windowTimestamp[3] = timestamp
windowPressure[3] = pressure
windowVelocity[3] = velocity

interpolateSegment()

ringBuffer.write(x, y, timestamp, pressure, velocity)
}

private fun interpolateSegment() {
val dx = windowX[3] - windowX[2]
val dy = windowY[3] - windowY[2]
val segmentLength = sqrt(dx * dx + dy * dy)

if (segmentLength < MIN_SEGMENT_FOR_INTERPOLATION) {
return
}

val pointCount = ((segmentLength / TARGET_DENSITY_PX).toInt() - 1)
.coerceIn(0, MAX_INTERPOLATED_PER_SEGMENT)

if (pointCount <= 0) return

val p1x = windowX[1]; val p1y = windowY[1]
val p2x = windowX[2]; val p2y = windowY[2]
val p3x = windowX[3]; val p3y = windowY[3]

val t1 = windowTimestamp[2]
val t2 = windowTimestamp[3]
val pr1 = windowPressure[2]
val pr2 = windowPressure[3]
val v1 = windowVelocity[2]
val v2 = windowVelocity[3]

for (i in 1..pointCount) {
val t = i.toFloat() / (pointCount + 1)
val t2f = t * t
val t3f = t2f * t

val interpX = ALPHA * (
(-p1x + 2f * p2x - p3x) * t3f +
(2f * p1x - 4f * p2x + 2f * p3x) * t2f +
(-p1x + p3x) * t +
2f * p2x
)

val interpY = ALPHA * (
(-p1y + 2f * p2y - p3y) * t3f +
(2f * p1y - 4f * p2y + 2f * p3y) * t2f +
(-p1y + p3y) * t +
2f * p2y
)

val interpTimestamp = t1 + ((t2 - t1) * t).toLong()
val interpPressure = pr1 + (pr2 - pr1) * t
val interpVelocity = v1 + (v2 - v1) * t

ringBuffer.write(interpX, interpY, interpTimestamp, interpPressure, interpVelocity)
}
}

fun reset() {
windowCount = 0
rawPointIndex = 0
}

companion object {
private const val WINDOW_SIZE = 4
private const val TARGET_DENSITY_PX = 6f
private const val MIN_SEGMENT_FOR_INTERPOLATION = 6f
private const val MAX_INTERPOLATED_PER_SEGMENT = 10
private const val ALPHA = 0.5f
}
}
Loading