⎇ 8380e44a0947bafb169773353ce2b2036aff9e70
keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt
Ñawpa willay
ch'uya
Trazo: predicción, aprendizaje y gestos de espacio/borrado (v0.2)
1package com.rizoma.trazo
2
3import android.inputmethodservice.InputMethodService
4import android.text.InputType
5import android.view.KeyEvent
6import android.view.View
7import android.view.inputmethod.EditorInfo
8import androidx.compose.ui.platform.ComposeView
9import androidx.lifecycle.Lifecycle
10import androidx.lifecycle.LifecycleOwner
11import androidx.lifecycle.LifecycleRegistry
12import androidx.lifecycle.ViewModelStore
13import androidx.lifecycle.ViewModelStoreOwner
14import androidx.lifecycle.setViewTreeLifecycleOwner
15import androidx.lifecycle.setViewTreeViewModelStoreOwner
16import androidx.savedstate.SavedStateRegistry
17import androidx.savedstate.SavedStateRegistryController
18import androidx.savedstate.SavedStateRegistryOwner
19import androidx.savedstate.setViewTreeSavedStateRegistryOwner
20import com.rizoma.trazo.data.SuggestionEngine
21import com.rizoma.trazo.ime.ImeHost
22import com.rizoma.trazo.ime.KeyboardController
23import com.rizoma.trazo.ui.KeyboardScreen
24import com.rizoma.trazo.ui.theme.TrazoTheme
25import kotlinx.coroutines.CoroutineScope
26import kotlinx.coroutines.Dispatchers
27import kotlinx.coroutines.SupervisorJob
28import kotlinx.coroutines.cancel
29import kotlin.math.abs
30
31/**
32 * Trazo IME. Hospeda un teclado Compose dentro del InputMethodService, para lo cual
33 * el servicio actúa como Lifecycle/ViewModelStore/SavedState owner del ComposeView.
34 */
35class TrazoImeService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
36
37 private val lifecycleRegistry = LifecycleRegistry(this)
38 override val lifecycle: Lifecycle get() = lifecycleRegistry
39
40 private val store = ViewModelStore()
41 override val viewModelStore: ViewModelStore get() = store
42
43 private val savedStateController = SavedStateRegistryController.create(this)
44 override val savedStateRegistry: SavedStateRegistry get() = savedStateController.savedStateRegistry
45
46 private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
47
48 private var editorInputType: Int = InputType.TYPE_CLASS_TEXT
49
50 private val host = object : ImeHost {
51 override fun commit(text: String) {
52 currentInputConnection?.commitText(text, 1)
53 }
54
55 override fun backspace() {
56 val ic = currentInputConnection ?: return
57 val selected = ic.getSelectedText(0)
58 if (selected.isNullOrEmpty()) ic.deleteSurroundingText(1, 0) else ic.commitText("", 1)
59 }
60
61 override fun newline() {
62 val info = currentInputEditorInfo
63 val opt = (info?.imeOptions ?: 0) and EditorInfo.IME_MASK_ACTION
64 val noEnterAction = ((info?.imeOptions ?: 0) and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0
65 if (!noEnterAction && opt != EditorInfo.IME_ACTION_NONE && opt != EditorInfo.IME_ACTION_UNSPECIFIED) {
66 currentInputConnection?.performEditorAction(opt)
67 } else {
68 currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
69 currentInputConnection?.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER))
70 }
71 }
72
73 override fun setComposing(text: String) {
74 currentInputConnection?.setComposingText(text, 1)
75 }
76
77 override fun finishComposing() {
78 currentInputConnection?.finishComposingText()
79 }
80
81 override fun moveCursor(steps: Int) {
82 val ic = currentInputConnection ?: return
83 val code = if (steps > 0) KeyEvent.KEYCODE_DPAD_RIGHT else KeyEvent.KEYCODE_DPAD_LEFT
84 repeat(abs(steps)) {
85 ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, code))
86 ic.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, code))
87 }
88 }
89
90 override fun textBeforeCursor(n: Int): CharSequence =
91 currentInputConnection?.getTextBeforeCursor(n, 0) ?: ""
92
93 override fun cursorCapsMode(): Int =
94 currentInputConnection?.getCursorCapsMode(editorInputType) ?: 0
95 }
96
97 private lateinit var engine: SuggestionEngine
98 private lateinit var controller: KeyboardController
99
100 override fun onCreate() {
101 super.onCreate()
102 savedStateController.performRestore(null)
103 engine = SuggestionEngine(this, scope)
104 controller = KeyboardController(host, engine)
105 lifecycleRegistry.currentState = Lifecycle.State.CREATED
106 }
107
108 override fun onCreateInputView(): View {
109 return ComposeView(this).apply {
110 setViewTreeLifecycleOwner(this@TrazoImeService)
111 setViewTreeViewModelStoreOwner(this@TrazoImeService)
112 setViewTreeSavedStateRegistryOwner(this@TrazoImeService)
113 setContent {
114 TrazoTheme {
115 KeyboardScreen(controller)
116 }
117 }
118 }
119 }
120
121 override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
122 super.onStartInputView(info, restarting)
123 editorInputType = info?.inputType ?: InputType.TYPE_CLASS_TEXT
124 controller.reset()
125 controller.syncAutoShift()
126 lifecycleRegistry.currentState = Lifecycle.State.RESUMED
127 }
128
129 override fun onUpdateSelection(
130 oldSelStart: Int, oldSelEnd: Int,
131 newSelStart: Int, newSelEnd: Int,
132 candidatesStart: Int, candidatesEnd: Int,
133 ) {
134 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd)
135 // Si el editor ya no tiene región en composición pero nosotros creíamos tenerla,
136 // significa que el cursor se movió fuera: abandonamos la palabra local.
137 if (candidatesStart == -1 && controller.hasComposing()) {
138 controller.abandonComposing()
139 }
140 }
141
142 override fun onFinishInputView(finishingInput: Boolean) {
143 super.onFinishInputView(finishingInput)
144 lifecycleRegistry.currentState = Lifecycle.State.CREATED
145 }
146
147 override fun onDestroy() {
148 lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
149 store.clear()
150 scope.cancel()
151 super.onDestroy()
152 }
153}