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