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 package androidx.compose.ui.text.input
18 
19 import android.os.Build
20 import android.os.Bundle
21 import android.os.Handler
22 import android.text.TextUtils
23 import android.util.Log
24 import android.view.KeyEvent
25 import android.view.inputmethod.CompletionInfo
26 import android.view.inputmethod.CorrectionInfo
27 import android.view.inputmethod.EditorInfo
28 import android.view.inputmethod.ExtractedText
29 import android.view.inputmethod.ExtractedTextRequest
30 import android.view.inputmethod.InputConnection
31 import android.view.inputmethod.InputContentInfo
32 
33 internal const val DEBUG = false
34 internal const val TAG = "RecordingIC"
35 private const val DEBUG_CLASS = "RecordingInputConnection"
36 
37 /**
38  * [InputConnection] implementation that binds Android IME to Compose.
39  *
40  * @param initState The initial input state.
41  * @param eventCallback An input event listener.
42  * @param autoCorrect Whether autoCorrect is enabled.
43  */
44 @Deprecated(
45     "Only exists to support the legacy TextInputService APIs. It is not used by any Compose " +
46         "code. A copy of this class in foundation is used by the legacy BasicTextField."
47 )
48 internal class RecordingInputConnection(
49     initState: TextFieldValue,
50     @Suppress("DEPRECATION") val eventCallback: InputEventCallback2,
51     val autoCorrect: Boolean
52 ) : InputConnection {
53 
54     // The depth of the batch session. 0 means no session.
55     private var batchDepth: Int = 0
56 
57     // The input state.
58     internal var mTextFieldValue: TextFieldValue = initState
59         set(value) {
60             if (DEBUG) {
61                 logDebug("mTextFieldValue : $field -> $value")
62             }
63             field = value
64         }
65 
66     /**
67      * The token to be used for reporting updateExtractedText API.
68      *
69      * 0 if no token was specified from IME.
70      */
71     private var currentExtractedTextRequestToken = 0
72 
73     /**
74      * True if IME requested extracted text monitor mode.
75      *
76      * If extracted text monitor mode is ON, need to call updateExtractedText API whenever the text
77      * is changed.
78      */
79     private var extractedTextMonitorMode = false
80 
81     // The recoding editing ops.
82     private val editCommands = mutableListOf<EditCommand>()
83 
84     private var isActive: Boolean = true
85 
86     private inline fun ensureActive(block: () -> Unit): Boolean {
87         return isActive.also { applying ->
88             if (applying) {
89                 block()
90             }
91         }
92     }
93 
94     /**
95      * Updates the input state and tells it to the IME.
96      *
97      * This function may emits updateSelection and updateExtractedText to notify IMEs that the text
98      * contents has changed if needed.
99      */
100     fun updateInputState(
101         state: TextFieldValue,
102         @Suppress("DEPRECATION") inputMethodManager: InputMethodManager,
103     ) {
104         if (!isActive) return
105 
106         if (DEBUG) {
107             logDebug("RecordingInputConnection.updateInputState: $state")
108         }
109 
110         mTextFieldValue = state
111 
112         if (extractedTextMonitorMode) {
113             inputMethodManager.updateExtractedText(
114                 currentExtractedTextRequestToken,
115                 state.toExtractedText()
116             )
117         }
118 
119         // updateSelection API requires -1 if there is no composition
120         val compositionStart = state.composition?.min ?: -1
121         val compositionEnd = state.composition?.max ?: -1
122         if (DEBUG) {
123             logDebug(
124                 "updateSelection(" +
125                     "selection = (${state.selection.min},${state.selection.max}), " +
126                     "composition = ($compositionStart, $compositionEnd))"
127             )
128         }
129         inputMethodManager.updateSelection(
130             state.selection.min,
131             state.selection.max,
132             compositionStart,
133             compositionEnd
134         )
135     }
136 
137     // Add edit op to internal list with wrapping batch edit.
138     private fun addEditCommandWithBatch(editCommand: EditCommand) {
139         beginBatchEditInternal()
140         try {
141             editCommands.add(editCommand)
142         } finally {
143             endBatchEditInternal()
144         }
145     }
146 
147     // /////////////////////////////////////////////////////////////////////////////////////////////
148     // Callbacks for text editing session
149     // /////////////////////////////////////////////////////////////////////////////////////////////
150 
151     override fun beginBatchEdit(): Boolean = ensureActive {
152         if (DEBUG) {
153             logDebug("beginBatchEdit()")
154         }
155         return beginBatchEditInternal()
156     }
157 
158     private fun beginBatchEditInternal(): Boolean {
159         batchDepth++
160         return true
161     }
162 
163     override fun endBatchEdit(): Boolean {
164         if (DEBUG) {
165             logDebug("endBatchEdit()")
166         }
167         return endBatchEditInternal()
168     }
169 
170     private fun endBatchEditInternal(): Boolean {
171         batchDepth--
172         if (batchDepth == 0 && editCommands.isNotEmpty()) {
173             eventCallback.onEditCommands(editCommands.toMutableList())
174             editCommands.clear()
175         }
176         return batchDepth > 0
177     }
178 
179     override fun closeConnection() {
180         if (DEBUG) {
181             logDebug("closeConnection()")
182         }
183         editCommands.clear()
184         batchDepth = 0
185         isActive = false
186         eventCallback.onConnectionClosed(this)
187     }
188 
189     // /////////////////////////////////////////////////////////////////////////////////////////////
190     // Callbacks for text editing
191     // /////////////////////////////////////////////////////////////////////////////////////////////
192 
193     override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean = ensureActive {
194         if (DEBUG) {
195             logDebug("commitText(\"$text\", $newCursorPosition)")
196         }
197         addEditCommandWithBatch(CommitTextCommand(text.toString(), newCursorPosition))
198     }
199 
200     override fun setComposingRegion(start: Int, end: Int): Boolean = ensureActive {
201         if (DEBUG) {
202             logDebug("setComposingRegion($start, $end)")
203         }
204         addEditCommandWithBatch(SetComposingRegionCommand(start, end))
205     }
206 
207     override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean =
208         ensureActive {
209             if (DEBUG) {
210                 logDebug("setComposingText(\"$text\", $newCursorPosition)")
211             }
212             addEditCommandWithBatch(SetComposingTextCommand(text.toString(), newCursorPosition))
213         }
214 
215     override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean =
216         ensureActive {
217             if (DEBUG) {
218                 logDebug("deleteSurroundingTextInCodePoints($beforeLength, $afterLength)")
219             }
220             addEditCommandWithBatch(
221                 DeleteSurroundingTextInCodePointsCommand(beforeLength, afterLength)
222             )
223             return true
224         }
225 
226     override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean =
227         ensureActive {
228             if (DEBUG) {
229                 logDebug("deleteSurroundingText($beforeLength, $afterLength)")
230             }
231             addEditCommandWithBatch(DeleteSurroundingTextCommand(beforeLength, afterLength))
232             return true
233         }
234 
235     override fun setSelection(start: Int, end: Int): Boolean = ensureActive {
236         if (DEBUG) {
237             logDebug("setSelection($start, $end)")
238         }
239         addEditCommandWithBatch(SetSelectionCommand(start, end))
240         return true
241     }
242 
243     override fun finishComposingText(): Boolean = ensureActive {
244         if (DEBUG) {
245             logDebug("finishComposingText()")
246         }
247         addEditCommandWithBatch(FinishComposingTextCommand())
248         return true
249     }
250 
251     override fun sendKeyEvent(event: KeyEvent): Boolean = ensureActive {
252         if (DEBUG) {
253             logDebug("sendKeyEvent($event)")
254         }
255         eventCallback.onKeyEvent(event)
256         return true
257     }
258 
259     // /////////////////////////////////////////////////////////////////////////////////////////////
260     // Callbacks for retrieving editing buffers
261     // /////////////////////////////////////////////////////////////////////////////////////////////
262 
263     override fun getTextBeforeCursor(maxChars: Int, flags: Int): CharSequence {
264         // TODO(b/135556699) should return styled text
265         val result = mTextFieldValue.getTextBeforeSelection(maxChars).toString()
266         if (DEBUG) {
267             logDebug("getTextBeforeCursor($maxChars, $flags): $result")
268         }
269         return result
270     }
271 
272     override fun getTextAfterCursor(maxChars: Int, flags: Int): CharSequence {
273         // TODO(b/135556699) should return styled text
274         val result = mTextFieldValue.getTextAfterSelection(maxChars).toString()
275         if (DEBUG) {
276             logDebug("getTextAfterCursor($maxChars, $flags): $result")
277         }
278         return result
279     }
280 
281     override fun getSelectedText(flags: Int): CharSequence? {
282         // https://source.chromium.org/chromium/chromium/src/+/master:content/public/android/java/src/org/chromium/content/browser/input/TextInputState.java;l=56;drc=0e20d1eb38227949805a4c0e9d5cdeddc8d23637
283         val result: CharSequence? =
284             if (mTextFieldValue.selection.collapsed) {
285                 null
286             } else {
287                 // TODO(b/135556699) should return styled text
288                 mTextFieldValue.getSelectedText().toString()
289             }
290         if (DEBUG) {
291             logDebug("getSelectedText($flags): $result")
292         }
293         return result
294     }
295 
296     override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean = ensureActive {
297         val immediate = cursorUpdateMode and InputConnection.CURSOR_UPDATE_IMMEDIATE != 0
298         val monitor = cursorUpdateMode and InputConnection.CURSOR_UPDATE_MONITOR != 0
299         if (DEBUG) {
300             logDebug(
301                 "requestCursorUpdates($cursorUpdateMode=[immediate:$immediate, monitor: $monitor])"
302             )
303         }
304 
305         // Before Android T, filter flags are not used, and insertion marker and character bounds
306         // info are always included.
307         var includeInsertionMarker = true
308         var includeCharacterBounds = true
309         var includeEditorBounds = false
310         var includeLineBounds = false
311         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
312             includeInsertionMarker =
313                 cursorUpdateMode and InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER != 0
314             includeCharacterBounds =
315                 cursorUpdateMode and InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS != 0
316             includeEditorBounds =
317                 cursorUpdateMode and InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS != 0
318             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
319                 includeLineBounds =
320                     cursorUpdateMode and InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS !=
321                         0
322             }
323             // If no filter flags are used, then all info should be included.
324             if (
325                 !includeInsertionMarker &&
326                     !includeCharacterBounds &&
327                     !includeEditorBounds &&
328                     !includeLineBounds
329             ) {
330                 includeInsertionMarker = true
331                 includeCharacterBounds = true
332                 includeEditorBounds = true
333                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
334                     includeLineBounds = true
335                 }
336             }
337         }
338 
339         eventCallback.onRequestCursorAnchorInfo(
340             immediate,
341             monitor,
342             includeInsertionMarker,
343             includeCharacterBounds,
344             includeEditorBounds,
345             includeLineBounds
346         )
347         return true
348     }
349 
350     override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText {
351         if (DEBUG) {
352             logDebug("getExtractedText($request, $flags)")
353         }
354         extractedTextMonitorMode = (flags and InputConnection.GET_EXTRACTED_TEXT_MONITOR) != 0
355         if (extractedTextMonitorMode) {
356             currentExtractedTextRequestToken = request?.token ?: 0
357         }
358         // TODO(b/135556699) should return styled text
359         val extractedText = mTextFieldValue.toExtractedText()
360 
361         if (DEBUG) {
362             with(extractedText) {
363                 logDebug(
364                     "getExtractedText() return: text: \"$text\"" +
365                         ",partialStartOffset $partialStartOffset" +
366                         ",partialEndOffset $partialEndOffset" +
367                         ",selectionStart $selectionStart" +
368                         ",selectionEnd $selectionEnd" +
369                         ",flags $flags"
370                 )
371             }
372         }
373 
374         return extractedText
375     }
376 
377     // /////////////////////////////////////////////////////////////////////////////////////////////
378     // Editor action and Key events.
379     // /////////////////////////////////////////////////////////////////////////////////////////////
380 
381     override fun performContextMenuAction(id: Int): Boolean = ensureActive {
382         if (DEBUG) {
383             logDebug("performContextMenuAction($id)")
384         }
385         when (id) {
386             android.R.id.selectAll -> {
387                 addEditCommandWithBatch(SetSelectionCommand(0, mTextFieldValue.text.length))
388             }
389             // TODO(siyamed): Need proper connection to cut/copy/paste
390             android.R.id.cut -> sendSynthesizedKeyEvent(KeyEvent.KEYCODE_CUT)
391             android.R.id.copy -> sendSynthesizedKeyEvent(KeyEvent.KEYCODE_COPY)
392             android.R.id.paste -> sendSynthesizedKeyEvent(KeyEvent.KEYCODE_PASTE)
393             android.R.id.startSelectingText -> {} // not supported
394             android.R.id.stopSelectingText -> {} // not supported
395             android.R.id.copyUrl -> {} // not supported
396             android.R.id.switchInputMethod -> {} // not supported
397             else -> {
398                 // not supported
399             }
400         }
401         return false
402     }
403 
404     private fun sendSynthesizedKeyEvent(code: Int) {
405         sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, code))
406         sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, code))
407     }
408 
409     override fun performEditorAction(editorAction: Int): Boolean = ensureActive {
410         if (DEBUG) {
411             logDebug("performEditorAction($editorAction)")
412         }
413         val imeAction =
414             when (editorAction) {
415                 EditorInfo.IME_ACTION_UNSPECIFIED -> ImeAction.Default
416                 EditorInfo.IME_ACTION_DONE -> ImeAction.Done
417                 EditorInfo.IME_ACTION_SEND -> ImeAction.Send
418                 EditorInfo.IME_ACTION_SEARCH -> ImeAction.Search
419                 EditorInfo.IME_ACTION_PREVIOUS -> ImeAction.Previous
420                 EditorInfo.IME_ACTION_NEXT -> ImeAction.Next
421                 EditorInfo.IME_ACTION_GO -> ImeAction.Go
422                 else -> {
423                     Log.w(TAG, "IME sends unsupported Editor Action: $editorAction")
424                     ImeAction.Default
425                 }
426             }
427         eventCallback.onImeAction(imeAction)
428         return true
429     }
430 
431     // /////////////////////////////////////////////////////////////////////////////////////////////
432     // Unsupported callbacks
433     // /////////////////////////////////////////////////////////////////////////////////////////////
434 
435     override fun commitCompletion(text: CompletionInfo?): Boolean = ensureActive {
436         if (DEBUG) {
437             logDebug("commitCompletion(${text?.text})")
438         }
439         // We don't support this callback.
440         // The API documents says this should return if the input connection is no longer valid, but
441         // The Chromium implementation already returning false, so assuming it is safe to return
442         // false if not supported.
443         // see
444         // https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
445         return false
446     }
447 
448     override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean = ensureActive {
449         if (DEBUG) {
450             logDebug("commitCorrection($correctionInfo),autoCorrect:$autoCorrect")
451         }
452         // Should add an event here so that we can implement the autocorrect highlight
453         // Bug: 170647219
454         return autoCorrect
455     }
456 
457     override fun getHandler(): Handler? {
458         if (DEBUG) {
459             logDebug("getHandler()")
460         }
461         return null // Returns null means using default Handler
462     }
463 
464     override fun clearMetaKeyStates(states: Int): Boolean = ensureActive {
465         if (DEBUG) {
466             logDebug("clearMetaKeyStates($states)")
467         }
468         // We don't support this callback.
469         // The API documents says this should return if the input connection is no longer valid, but
470         // The Chromium implementation already returning false, so assuming it is safe to return
471         // false if not supported.
472         // see
473         // https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
474         return false
475     }
476 
477     override fun reportFullscreenMode(enabled: Boolean): Boolean {
478         if (DEBUG) {
479             logDebug("reportFullscreenMode($enabled)")
480         }
481         return false // This value is ignored according to the API docs.
482     }
483 
484     override fun getCursorCapsMode(reqModes: Int): Int {
485         if (DEBUG) {
486             logDebug("getCursorCapsMode($reqModes)")
487         }
488         return TextUtils.getCapsMode(mTextFieldValue.text, mTextFieldValue.selection.min, reqModes)
489     }
490 
491     override fun performPrivateCommand(action: String?, data: Bundle?): Boolean = ensureActive {
492         if (DEBUG) {
493             logDebug("performPrivateCommand($action, $data)")
494         }
495         return true // API doc says we should return true even if we didn't understand the command.
496     }
497 
498     override fun commitContent(
499         inputContentInfo: InputContentInfo,
500         flags: Int,
501         opts: Bundle?
502     ): Boolean = ensureActive {
503         if (DEBUG) {
504             logDebug("commitContent($inputContentInfo, $flags, $opts)")
505         }
506         return false // We don't accept any contents.
507     }
508 
509     private fun logDebug(message: String) {
510         if (DEBUG) {
511             Log.d(TAG, "$DEBUG_CLASS.$message, $isActive")
512         }
513     }
514 }
515