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