⎇ efc233ed86b31f83fb34050dd8d13e82c2d64cd3
keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt
Historia
crudo
Trazo: fix crash "ViewTreeLifecycleOwner not found" al abrir el teclado
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 // La ventana del IME es internamente un Dialog. Compose resuelve el
110 // ViewTreeLifecycleOwner desde el hijo de android.R.id.content y hacia arriba,
111 // así que los owners DEBEN estar en el decorView de la ventana (no basta el ComposeView).
112 window.window?.decorView?.let { decor ->
113 decor.setViewTreeLifecycleOwner(this)
114 decor.setViewTreeViewModelStoreOwner(this)
115 decor.setViewTreeSavedStateRegistryOwner(this)
116 }
117 return ComposeView(this).apply {
118 setViewTreeLifecycleOwner(this@TrazoImeService)
119 setViewTreeViewModelStoreOwner(this@TrazoImeService)
120 setViewTreeSavedStateRegistryOwner(this@TrazoImeService)
121 setContent {
122 TrazoTheme {
123 KeyboardScreen(controller)
124 }
125 }
126 }
127 }
128
129 override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
130 super.onStartInputView(info, restarting)
131 editorInputType = info?.inputType ?: InputType.TYPE_CLASS_TEXT
132 controller.reset()
133 controller.syncAutoShift()
134 lifecycleRegistry.currentState = Lifecycle.State.RESUMED
135 }
136
137 override fun onUpdateSelection(
138 oldSelStart: Int, oldSelEnd: Int,
139 newSelStart: Int, newSelEnd: Int,
140 candidatesStart: Int, candidatesEnd: Int,
141 ) {
142 super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd)
143 // Si el editor ya no tiene región en composición pero nosotros creíamos tenerla,
144 // significa que el cursor se movió fuera: abandonamos la palabra local.
145 if (candidatesStart == -1 && controller.hasComposing()) {
146 controller.abandonComposing()
147 }
148 }
149
150 override fun onFinishInputView(finishingInput: Boolean) {
151 super.onFinishInputView(finishingInput)
152 lifecycleRegistry.currentState = Lifecycle.State.CREATED
153 }
154
155 override fun onDestroy() {
156 lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
157 store.clear()
158 scope.cancel()
159 super.onDestroy()
160 }
161}