gioser

sergio/humanoid

HTML 13TOML 10 Definiciones

⎇ efc233ed86b31f83fb34050dd8d13e82c2d64cd3 keyboard/src/main/java/com/rizoma/trazo/TrazoImeService.kt 161 líneas · 6.4 KB Historia crudo

Trazo: fix crash "ViewTreeLifecycleOwner not found" al abrir el teclado efc233ed8 · sergio ·

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}