1 /*
<lambda>null2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:Suppress("DEPRECATION")
18
19 package androidx.compose.ui.text.input
20
21 import android.graphics.Rect as AndroidRect
22 import android.text.InputType
23 import android.util.Log
24 import android.view.Choreographer
25 import android.view.KeyEvent
26 import android.view.View
27 import android.view.inputmethod.BaseInputConnection
28 import android.view.inputmethod.EditorInfo
29 import android.view.inputmethod.InputConnection
30 import androidx.compose.runtime.collection.mutableVectorOf
31 import androidx.compose.ui.geometry.Rect
32 import androidx.compose.ui.graphics.Matrix
33 import androidx.compose.ui.input.pointer.MatrixPositionCalculator
34 import androidx.compose.ui.text.TextLayoutResult
35 import androidx.compose.ui.text.TextRange
36 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.HideKeyboard
37 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.ShowKeyboard
38 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StartInput
39 import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StopInput
40 import androidx.core.view.inputmethod.EditorInfoCompat
41 import androidx.emoji2.text.EmojiCompat
42 import java.lang.ref.WeakReference
43 import java.util.concurrent.Executor
44 import kotlin.math.roundToInt
45
46 private const val DEBUG_CLASS = "TextInputServiceAndroid"
47
48 /**
49 * Provide Android specific input service with the Operating System.
50 *
51 * @param inputCommandProcessorExecutor [Executor] used to schedule the [processInputCommands]
52 * function when a input command is first requested for a frame.
53 */
54 @Deprecated(
55 "Only exists to support the legacy TextInputService APIs. It is not used by any Compose " +
56 "code. A copy of this class in foundation is used by the legacy BasicTextField."
57 )
58 internal class TextInputServiceAndroid(
59 val view: View,
60 rootPositionCalculator: MatrixPositionCalculator,
61 private val inputMethodManager: InputMethodManager,
62 private val inputCommandProcessorExecutor: Executor = Choreographer.getInstance().asExecutor(),
63 ) : PlatformTextInputService {
64
65 /**
66 * Commands that can be sent into [textInputCommandQueue] to be processed by
67 * [processInputCommands].
68 */
69 private enum class TextInputCommand {
70 StartInput,
71 StopInput,
72 ShowKeyboard,
73 HideKeyboard
74 }
75
76 /**
77 * True if the currently editable composable has connected. This is used to tell the platform
78 * when it asks if the compose view is a text editor.
79 */
80 private var editorHasFocus = false
81
82 /**
83 * The following three observers are set when the editable composable has initiated the input
84 * session
85 */
86 private var onEditCommand: (List<EditCommand>) -> Unit = {}
87 private var onImeActionPerformed: (ImeAction) -> Unit = {}
88
89 // Visible for testing
90 internal var state = TextFieldValue(text = "", selection = TextRange.Zero)
91 private set
92
93 private var imeOptions = ImeOptions.Default
94
95 // RecordingInputConnection has strong reference to the View through TextInputServiceAndroid and
96 // event callback. The connection should be closed when IME has changed and removed from this
97 // list in onConnectionClosed callback, but not clear it is guaranteed the close connection is
98 // called any time. So, keep it in WeakReference just in case.
99 private var ics = mutableListOf<WeakReference<RecordingInputConnection>>()
100
101 // used for sendKeyEvent delegation
102 private val baseInputConnection by
103 lazy(LazyThreadSafetyMode.NONE) { BaseInputConnection(view, false) }
104
105 private var focusedRect: AndroidRect? = null
106
107 private val cursorAnchorInfoController =
108 CursorAnchorInfoController(rootPositionCalculator, inputMethodManager)
109
110 /**
111 * A channel that is used to debounce rapid operations such as showing/hiding the keyboard and
112 * starting/stopping input, so we can make the minimal number of calls on the
113 * [inputMethodManager]. The [TextInputCommand]s sent to this channel are processed by
114 * [processInputCommands].
115 */
116 private val textInputCommandQueue = mutableVectorOf<TextInputCommand>()
117 private var frameCallback: Runnable? = null
118
119 constructor(
120 view: View,
121 positionCalculator: MatrixPositionCalculator
122 ) : this(
123 view,
124 positionCalculator,
125 InputMethodManagerImpl(view),
126 )
127
128 init {
129 if (DEBUG) {
130 Log.d(TAG, "$DEBUG_CLASS.create")
131 }
132 }
133
134 /** Creates new input connection. */
135 fun createInputConnection(outAttrs: EditorInfo): InputConnection? {
136 if (!editorHasFocus) {
137 return null
138 }
139
140 outAttrs.update(imeOptions, state)
141 outAttrs.updateWithEmojiCompat()
142
143 return RecordingInputConnection(
144 initState = state,
145 autoCorrect = imeOptions.autoCorrect,
146 eventCallback =
147 object : InputEventCallback2 {
148 override fun onEditCommands(editCommands: List<EditCommand>) {
149 onEditCommand(editCommands)
150 }
151
152 override fun onImeAction(imeAction: ImeAction) {
153 onImeActionPerformed(imeAction)
154 }
155
156 override fun onKeyEvent(event: KeyEvent) {
157 baseInputConnection.sendKeyEvent(event)
158 }
159
160 override fun onRequestCursorAnchorInfo(
161 immediate: Boolean,
162 monitor: Boolean,
163 includeInsertionMarker: Boolean,
164 includeCharacterBounds: Boolean,
165 includeEditorBounds: Boolean,
166 includeLineBounds: Boolean
167 ) {
168 cursorAnchorInfoController.requestUpdate(
169 immediate,
170 monitor,
171 includeInsertionMarker,
172 includeCharacterBounds,
173 includeEditorBounds,
174 includeLineBounds
175 )
176 }
177
178 override fun onConnectionClosed(inputConnection: RecordingInputConnection) {
179 for (i in 0 until ics.size) {
180 if (ics[i].get() == inputConnection) {
181 ics.removeAt(i)
182 return // No duplicated instances should be in the list.
183 }
184 }
185 }
186 }
187 )
188 .also {
189 ics.add(WeakReference(it))
190 if (DEBUG) {
191 Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics")
192 }
193 }
194 }
195
196 /** Returns true if some editable component is focused. */
197 fun isEditorFocused(): Boolean = editorHasFocus
198
199 override fun startInput(
200 value: TextFieldValue,
201 imeOptions: ImeOptions,
202 onEditCommand: (List<EditCommand>) -> Unit,
203 onImeActionPerformed: (ImeAction) -> Unit
204 ) {
205 if (DEBUG) {
206 Log.d(TAG, "$DEBUG_CLASS.startInput")
207 }
208
209 editorHasFocus = true
210 state = value
211 this.imeOptions = imeOptions
212 this.onEditCommand = onEditCommand
213 this.onImeActionPerformed = onImeActionPerformed
214
215 // Don't actually send the command to the IME yet, it may be overruled by a subsequent call
216 // to stopInput.
217 sendInputCommand(StartInput)
218 }
219
220 override fun startInput() {
221 if (DEBUG) {
222 Log.d(TAG, "$DEBUG_CLASS.startInput")
223 }
224
225 // Don't set editorHasFocus or any of the other properties used to support the legacy text
226 // input system.
227
228 // Don't actually send the command to the IME yet, it may be overruled by a subsequent call
229 // to stopInput.
230 sendInputCommand(StartInput)
231 }
232
233 override fun stopInput() {
234 if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.stopInput")
235
236 editorHasFocus = false
237 onEditCommand = {}
238 onImeActionPerformed = {}
239 focusedRect = null
240
241 // Don't actually send the command to the IME yet, it may be overruled by a subsequent call
242 // to startInput.
243 sendInputCommand(StopInput)
244 }
245
246 override fun showSoftwareKeyboard() {
247 if (DEBUG) {
248 Log.d(TAG, "$DEBUG_CLASS.showSoftwareKeyboard")
249 }
250 sendInputCommand(ShowKeyboard)
251 }
252
253 override fun hideSoftwareKeyboard() {
254 if (DEBUG) {
255 Log.d(TAG, "$DEBUG_CLASS.hideSoftwareKeyboard")
256 }
257 sendInputCommand(HideKeyboard)
258 }
259
260 private fun sendInputCommand(command: TextInputCommand) {
261 textInputCommandQueue += command
262 if (frameCallback == null) {
263 frameCallback =
264 Runnable {
265 frameCallback = null
266 processInputCommands()
267 }
268 .also(inputCommandProcessorExecutor::execute)
269 }
270 }
271
272 private fun processInputCommands() {
273 // If the associated view is not focused anymore, we should check whether the focus has
274 // transitioned into another Editor.
275 if (!view.isFocused) {
276 val focusedView = view.rootView.findFocus()
277 // If a view is focused and is an editor, we can skip the queued up commands since the
278 // new editor is going to manage the keyboard and the input session. Otherwise we should
279 // process the queue since it probably contains StopInput or HideKeyboard calls to
280 // clean up after us.
281 if (focusedView?.onCheckIsTextEditor() == true) {
282 textInputCommandQueue.clear()
283 return
284 }
285 }
286 // Multiple commands may have been queued up in the channel while this function was
287 // waiting to be resumed. We don't execute the commands as they come in because making a
288 // bunch of calls to change the actual IME quickly can result in flickers. Instead, we
289 // manually coalesce the commands to figure out the minimum number of IME operations we
290 // need to get to the desired final state.
291 // The queued commands effectively operate on a simple state machine consisting of two
292 // flags:
293 // 1. Whether to start a new input connection (true), tear down the input connection
294 // (false), or leave the current connection as-is (null).
295 var startInput: Boolean? = null
296 // 2. Whether to show the keyboard (true), hide the keyboard (false), or leave the
297 // keyboard visibility as-is (null).
298 var showKeyboard: Boolean? = null
299
300 // And a function that performs the appropriate state transition given a command.
301 fun TextInputCommand.applyToState() {
302 when (this) {
303 StartInput -> {
304 // Any commands before restarting the input are meaningless since they would
305 // apply to the connection we're going to tear down and recreate.
306 // Starting a new connection implicitly stops the previous connection.
307 startInput = true
308 // It doesn't make sense to start a new connection without the keyboard
309 // showing.
310 showKeyboard = true
311 }
312 StopInput -> {
313 startInput = false
314 // It also doesn't make sense to keep the keyboard visible if it's not
315 // connected to anything. Note that this is different than the Android
316 // default behavior for Views, which is to keep the keyboard showing even
317 // after the view that the IME was shown for loses focus.
318 // See this doc for some notes and discussion on whether we should auto-hide
319 // or match Android:
320 // https://docs.google.com/document/d/1o-y3NkfFPCBhfDekdVEEl41tqtjjqs8jOss6txNgqaw/edit?resourcekey=0-o728aLn51uXXnA4Pkpe88Q#heading=h.ieacosb5rizm
321 showKeyboard = false
322 }
323 ShowKeyboard,
324 HideKeyboard -> {
325 // Any keyboard visibility commands sent after input is stopped but before
326 // input is started should be ignored.
327 // Otherwise, the last visibility command sent either before the last stop
328 // command, or after the last start command, is the one that should take
329 // effect.
330 if (startInput != false) {
331 showKeyboard = this == ShowKeyboard
332 }
333 }
334 }
335 }
336
337 // Feed all the queued commands into the state machine.
338 textInputCommandQueue.forEach { command ->
339 command.applyToState()
340 if (DEBUG) {
341 Log.d(
342 TAG,
343 "$DEBUG_CLASS.textInputCommandEventLoop.$command " +
344 "(startInput=$startInput, showKeyboard=$showKeyboard)"
345 )
346 }
347 }
348 textInputCommandQueue.clear()
349
350 // Now that we've calculated what operations we need to perform on the actual input
351 // manager, perform them.
352 // If the keyboard visibility was changed after starting a new connection, we need to
353 // perform that operation change after starting it.
354 // If the keyboard visibility was changed before closing the connection, we need to
355 // perform that operation before closing the connection so it doesn't no-op.
356 if (startInput == true) {
357 restartInputImmediately()
358 }
359 showKeyboard?.also(::setKeyboardVisibleImmediately)
360 if (startInput == false) {
361 restartInputImmediately()
362 }
363
364 if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.textInputCommandEventLoop.finished")
365 }
366
367 override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
368 if (DEBUG) {
369 Log.d(TAG, "$DEBUG_CLASS.updateState called: $oldValue -> $newValue")
370 }
371
372 // If the selection has changed from the last time, we need to update selection even though
373 // the oldValue in EditBuffer is already in sync with the newValue.
374 // Same holds for composition b/207800945
375 val needUpdateSelection =
376 (this.state.selection != newValue.selection) ||
377 this.state.composition != newValue.composition
378 this.state = newValue
379 // update the latest TextFieldValue in InputConnection
380 for (i in 0 until ics.size) {
381 ics[i].get()?.mTextFieldValue = newValue
382 }
383 cursorAnchorInfoController.invalidate()
384
385 if (oldValue == newValue) {
386 if (DEBUG) {
387 Log.d(TAG, "$DEBUG_CLASS.updateState early return")
388 }
389 if (needUpdateSelection) {
390 // updateSelection API requires -1 if there is no composition
391 inputMethodManager.updateSelection(
392 selectionStart = newValue.selection.min,
393 selectionEnd = newValue.selection.max,
394 compositionStart = state.composition?.min ?: -1,
395 compositionEnd = state.composition?.max ?: -1
396 )
397 }
398 return
399 }
400
401 val restartInput =
402 oldValue?.let {
403 it.text != newValue.text ||
404 // when selection is the same but composition has changed, need to reset the
405 // input.
406 (it.selection == newValue.selection && it.composition != newValue.composition)
407 } ?: false
408
409 if (DEBUG) {
410 Log.d(TAG, "$DEBUG_CLASS.updateState: restart($restartInput), state: $state")
411 }
412
413 if (restartInput) {
414 restartInputImmediately()
415 } else {
416 for (i in 0 until ics.size) {
417 ics[i].get()?.updateInputState(this.state, inputMethodManager)
418 }
419 }
420 }
421
422 @Deprecated("This method should not be called, used BringIntoViewRequester instead.")
423 override fun notifyFocusedRect(rect: Rect) {
424 focusedRect =
425 AndroidRect(
426 rect.left.roundToInt(),
427 rect.top.roundToInt(),
428 rect.right.roundToInt(),
429 rect.bottom.roundToInt()
430 )
431
432 // Requesting rectangle too early after obtaining focus may bring view into wrong place
433 // probably due to transient IME inset change. We don't know the correct timing of calling
434 // requestRectangleOnScreen API, so try to call this API only after the IME is ready to
435 // use, i.e. InputConnection has created.
436 // Even if we miss all the timing of requesting rectangle during initial text field focus,
437 // focused rectangle will be requested when software keyboard has shown.
438 if (ics.isEmpty()) {
439 focusedRect?.let {
440 // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
441 // create another Rect and then pass it.
442 view.requestRectangleOnScreen(AndroidRect(it))
443 }
444 }
445 }
446
447 override fun updateTextLayoutResult(
448 textFieldValue: TextFieldValue,
449 offsetMapping: OffsetMapping,
450 textLayoutResult: TextLayoutResult,
451 textFieldToRootTransform: (Matrix) -> Unit,
452 innerTextFieldBounds: Rect,
453 decorationBoxBounds: Rect
454 ) {
455 cursorAnchorInfoController.updateTextLayoutResult(
456 textFieldValue,
457 offsetMapping,
458 textLayoutResult,
459 textFieldToRootTransform,
460 innerTextFieldBounds,
461 decorationBoxBounds
462 )
463 }
464
465 /** Immediately restart the IME connection, bypassing the [textInputCommandQueue]. */
466 private fun restartInputImmediately() {
467 if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.restartInputImmediately")
468 inputMethodManager.restartInput()
469 }
470
471 /** Immediately show or hide the keyboard, bypassing the [textInputCommandQueue]. */
472 private fun setKeyboardVisibleImmediately(visible: Boolean) {
473 if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.setKeyboardVisibleImmediately(visible=$visible)")
474 if (visible) {
475 inputMethodManager.showSoftInput()
476 } else {
477 inputMethodManager.hideSoftInput()
478 }
479 }
480 }
481
482 /** Call to update EditorInfo correctly when EmojiCompat is configured. */
EditorInfonull483 private fun EditorInfo.updateWithEmojiCompat() {
484 if (!EmojiCompat.isConfigured()) {
485 return
486 }
487
488 EmojiCompat.get().updateEditorInfo(this)
489 }
490
491 /** Fills necessary info of EditorInfo. */
updatenull492 internal fun EditorInfo.update(imeOptions: ImeOptions, textFieldValue: TextFieldValue) {
493 this.imeOptions =
494 when (imeOptions.imeAction) {
495 ImeAction.Default -> {
496 if (imeOptions.singleLine) {
497 // this is the last resort to enable single line
498 // Android IME still show return key even if multi line is not send
499 // TextView.java#onCreateInputConnection
500 EditorInfo.IME_ACTION_DONE
501 } else {
502 EditorInfo.IME_ACTION_UNSPECIFIED
503 }
504 }
505 ImeAction.None -> EditorInfo.IME_ACTION_NONE
506 ImeAction.Go -> EditorInfo.IME_ACTION_GO
507 ImeAction.Next -> EditorInfo.IME_ACTION_NEXT
508 ImeAction.Previous -> EditorInfo.IME_ACTION_PREVIOUS
509 ImeAction.Search -> EditorInfo.IME_ACTION_SEARCH
510 ImeAction.Send -> EditorInfo.IME_ACTION_SEND
511 ImeAction.Done -> EditorInfo.IME_ACTION_DONE
512 else -> error("invalid ImeAction")
513 }
514 imeOptions.platformImeOptions?.privateImeOptions?.let { privateImeOptions = it }
515 when (imeOptions.keyboardType) {
516 KeyboardType.Text -> this.inputType = InputType.TYPE_CLASS_TEXT
517 KeyboardType.Ascii -> {
518 this.inputType = InputType.TYPE_CLASS_TEXT
519 this.imeOptions = this.imeOptions or EditorInfo.IME_FLAG_FORCE_ASCII
520 }
521 KeyboardType.Number -> this.inputType = InputType.TYPE_CLASS_NUMBER
522 KeyboardType.Phone -> this.inputType = InputType.TYPE_CLASS_PHONE
523 KeyboardType.Uri ->
524 this.inputType = InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI
525 KeyboardType.Email ->
526 this.inputType =
527 InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
528 KeyboardType.Password -> {
529 this.inputType = InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
530 }
531 KeyboardType.NumberPassword -> {
532 this.inputType =
533 InputType.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD
534 }
535 KeyboardType.Decimal -> {
536 this.inputType = InputType.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_FLAG_DECIMAL
537 }
538 else -> error("Invalid Keyboard Type")
539 }
540
541 if (!imeOptions.singleLine) {
542 if (hasFlag(this.inputType, InputType.TYPE_CLASS_TEXT)) {
543 // TextView.java#setInputTypeSingleLine
544 this.inputType = this.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
545
546 if (imeOptions.imeAction == ImeAction.Default) {
547 this.imeOptions = this.imeOptions or EditorInfo.IME_FLAG_NO_ENTER_ACTION
548 }
549 }
550 }
551
552 if (hasFlag(this.inputType, InputType.TYPE_CLASS_TEXT)) {
553 when (imeOptions.capitalization) {
554 KeyboardCapitalization.Characters -> {
555 this.inputType = this.inputType or InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
556 }
557 KeyboardCapitalization.Words -> {
558 this.inputType = this.inputType or InputType.TYPE_TEXT_FLAG_CAP_WORDS
559 }
560 KeyboardCapitalization.Sentences -> {
561 this.inputType = this.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
562 }
563 else -> {
564 /* do nothing */
565 }
566 }
567
568 if (imeOptions.autoCorrect) {
569 this.inputType = this.inputType or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
570 }
571 }
572
573 this.initialSelStart = textFieldValue.selection.start
574 this.initialSelEnd = textFieldValue.selection.end
575
576 EditorInfoCompat.setInitialSurroundingText(this, textFieldValue.text)
577
578 this.imeOptions = this.imeOptions or EditorInfo.IME_FLAG_NO_FULLSCREEN
579 }
580
runnablenull581 internal fun Choreographer.asExecutor(): Executor = Executor { runnable ->
582 postFrameCallback { runnable.run() }
583 }
584
hasFlagnull585 private fun hasFlag(bits: Int, flag: Int): Boolean = (bits and flag) == flag
586