1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.content.browser.input; 6 7 import android.os.SystemClock; 8 import android.text.Editable; 9 import android.text.InputType; 10 import android.text.Selection; 11 import android.text.TextUtils; 12 import android.util.Log; 13 import android.view.KeyEvent; 14 import android.view.View; 15 import android.view.inputmethod.BaseInputConnection; 16 import android.view.inputmethod.EditorInfo; 17 import android.view.inputmethod.ExtractedText; 18 import android.view.inputmethod.ExtractedTextRequest; 19 20 import org.chromium.base.VisibleForTesting; 21 22 /** 23 * InputConnection is created by ContentView.onCreateInputConnection. 24 * It then adapts android's IME to chrome's RenderWidgetHostView using the 25 * native ImeAdapterAndroid via the class ImeAdapter. 26 */ 27 public class AdapterInputConnection extends BaseInputConnection { 28 private static final String TAG = "AdapterInputConnection"; 29 private static final boolean DEBUG = false; 30 /** 31 * Selection value should be -1 if not known. See EditorInfo.java for details. 32 */ 33 public static final int INVALID_SELECTION = -1; 34 public static final int INVALID_COMPOSITION = -1; 35 36 private final View mInternalView; 37 private final ImeAdapter mImeAdapter; 38 private final Editable mEditable; 39 40 private boolean mSingleLine; 41 private int mNumNestedBatchEdits = 0; 42 43 private int mLastUpdateSelectionStart = INVALID_SELECTION; 44 private int mLastUpdateSelectionEnd = INVALID_SELECTION; 45 private int mLastUpdateCompositionStart = INVALID_COMPOSITION; 46 private int mLastUpdateCompositionEnd = INVALID_COMPOSITION; 47 48 @VisibleForTesting AdapterInputConnection(View view, ImeAdapter imeAdapter, Editable editable, EditorInfo outAttrs)49 AdapterInputConnection(View view, ImeAdapter imeAdapter, Editable editable, 50 EditorInfo outAttrs) { 51 super(view, true); 52 mInternalView = view; 53 mImeAdapter = imeAdapter; 54 mImeAdapter.setInputConnection(this); 55 mEditable = editable; 56 // The editable passed in might have been in use by a prior keyboard and could have had 57 // prior composition spans set. To avoid keyboard conflicts, remove all composing spans 58 // when taking ownership of an existing Editable. 59 removeComposingSpans(mEditable); 60 mSingleLine = true; 61 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN 62 | EditorInfo.IME_FLAG_NO_EXTRACT_UI; 63 outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT 64 | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; 65 66 int inputType = imeAdapter.getTextInputType(); 67 int inputFlags = imeAdapter.getTextInputFlags(); 68 if ((inputFlags & imeAdapter.sTextInputFlagAutocompleteOff) != 0) { 69 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; 70 } 71 72 if (inputType == ImeAdapter.sTextInputTypeText) { 73 // Normal text field 74 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 75 if ((inputFlags & imeAdapter.sTextInputFlagAutocorrectOff) == 0) { 76 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; 77 } 78 } else if (inputType == ImeAdapter.sTextInputTypeTextArea || 79 inputType == ImeAdapter.sTextInputTypeContentEditable) { 80 // TextArea or contenteditable. 81 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE 82 | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; 83 if ((inputFlags & imeAdapter.sTextInputFlagAutocorrectOff) == 0) { 84 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; 85 } 86 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE; 87 mSingleLine = false; 88 } else if (inputType == ImeAdapter.sTextInputTypePassword) { 89 // Password 90 outAttrs.inputType = InputType.TYPE_CLASS_TEXT 91 | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; 92 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 93 } else if (inputType == ImeAdapter.sTextInputTypeSearch) { 94 // Search 95 outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH; 96 } else if (inputType == ImeAdapter.sTextInputTypeUrl) { 97 // Url 98 outAttrs.inputType = InputType.TYPE_CLASS_TEXT 99 | InputType.TYPE_TEXT_VARIATION_URI; 100 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 101 } else if (inputType == ImeAdapter.sTextInputTypeEmail) { 102 // Email 103 outAttrs.inputType = InputType.TYPE_CLASS_TEXT 104 | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; 105 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 106 } else if (inputType == ImeAdapter.sTextInputTypeTel) { 107 // Telephone 108 // Number and telephone do not have both a Tab key and an 109 // action in default OSK, so set the action to NEXT 110 outAttrs.inputType = InputType.TYPE_CLASS_PHONE; 111 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; 112 } else if (inputType == ImeAdapter.sTextInputTypeNumber) { 113 // Number 114 outAttrs.inputType = InputType.TYPE_CLASS_NUMBER 115 | InputType.TYPE_NUMBER_VARIATION_NORMAL 116 | InputType.TYPE_NUMBER_FLAG_DECIMAL; 117 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; 118 } 119 outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); 120 outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); 121 mLastUpdateSelectionStart = Selection.getSelectionStart(mEditable); 122 mLastUpdateSelectionEnd = Selection.getSelectionEnd(mEditable); 123 124 Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd); 125 updateSelectionIfRequired(); 126 } 127 128 /** 129 * Updates the AdapterInputConnection's internal representation of the text being edited and 130 * its selection and composition properties. The resulting Editable is accessible through the 131 * getEditable() method. If the text has not changed, this also calls updateSelection on the 132 * InputMethodManager. 133 * 134 * @param text The String contents of the field being edited. 135 * @param selectionStart The character offset of the selection start, or the caret position if 136 * there is no selection. 137 * @param selectionEnd The character offset of the selection end, or the caret position if there 138 * is no selection. 139 * @param compositionStart The character offset of the composition start, or -1 if there is no 140 * composition. 141 * @param compositionEnd The character offset of the composition end, or -1 if there is no 142 * selection. 143 * @param isNonImeChange True when the update was caused by non-IME (e.g. Javascript). 144 */ 145 @VisibleForTesting updateState(String text, int selectionStart, int selectionEnd, int compositionStart, int compositionEnd, boolean isNonImeChange)146 public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart, 147 int compositionEnd, boolean isNonImeChange) { 148 if (DEBUG) { 149 Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] [" 150 + compositionStart + " " + compositionEnd + "] [" + isNonImeChange + "]"); 151 } 152 // If this update is from the IME, no further state modification is necessary because the 153 // state should have been updated already by the IM framework directly. 154 if (!isNonImeChange) return; 155 156 // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces. 157 text = text.replace('\u00A0', ' '); 158 159 selectionStart = Math.min(selectionStart, text.length()); 160 selectionEnd = Math.min(selectionEnd, text.length()); 161 compositionStart = Math.min(compositionStart, text.length()); 162 compositionEnd = Math.min(compositionEnd, text.length()); 163 164 String prevText = mEditable.toString(); 165 boolean textUnchanged = prevText.equals(text); 166 167 if (!textUnchanged) { 168 mEditable.replace(0, mEditable.length(), text); 169 } 170 171 Selection.setSelection(mEditable, selectionStart, selectionEnd); 172 173 if (compositionStart == compositionEnd) { 174 removeComposingSpans(mEditable); 175 } else { 176 super.setComposingRegion(compositionStart, compositionEnd); 177 } 178 updateSelectionIfRequired(); 179 } 180 181 /** 182 * @return Editable object which contains the state of current focused editable element. 183 */ 184 @Override getEditable()185 public Editable getEditable() { 186 return mEditable; 187 } 188 189 /** 190 * Sends selection update to the InputMethodManager unless we are currently in a batch edit or 191 * if the exact same selection and composition update was sent already. 192 */ updateSelectionIfRequired()193 private void updateSelectionIfRequired() { 194 if (mNumNestedBatchEdits != 0) return; 195 int selectionStart = Selection.getSelectionStart(mEditable); 196 int selectionEnd = Selection.getSelectionEnd(mEditable); 197 int compositionStart = getComposingSpanStart(mEditable); 198 int compositionEnd = getComposingSpanEnd(mEditable); 199 // Avoid sending update if we sent an exact update already previously. 200 if (mLastUpdateSelectionStart == selectionStart && 201 mLastUpdateSelectionEnd == selectionEnd && 202 mLastUpdateCompositionStart == compositionStart && 203 mLastUpdateCompositionEnd == compositionEnd) { 204 return; 205 } 206 if (DEBUG) { 207 Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] [" 208 + compositionStart + " " + compositionEnd + "]"); 209 } 210 // updateSelection should be called every time the selection or composition changes 211 // if it happens not within a batch edit, or at the end of each top level batch edit. 212 getInputMethodManagerWrapper().updateSelection(mInternalView, 213 selectionStart, selectionEnd, compositionStart, compositionEnd); 214 mLastUpdateSelectionStart = selectionStart; 215 mLastUpdateSelectionEnd = selectionEnd; 216 mLastUpdateCompositionStart = compositionStart; 217 mLastUpdateCompositionEnd = compositionEnd; 218 } 219 220 /** 221 * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int) 222 */ 223 @Override setComposingText(CharSequence text, int newCursorPosition)224 public boolean setComposingText(CharSequence text, int newCursorPosition) { 225 if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]"); 226 if (maybePerformEmptyCompositionWorkaround(text)) return true; 227 super.setComposingText(text, newCursorPosition); 228 updateSelectionIfRequired(); 229 return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition, false); 230 } 231 232 /** 233 * @see BaseInputConnection#commitText(java.lang.CharSequence, int) 234 */ 235 @Override commitText(CharSequence text, int newCursorPosition)236 public boolean commitText(CharSequence text, int newCursorPosition) { 237 if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]"); 238 if (maybePerformEmptyCompositionWorkaround(text)) return true; 239 super.commitText(text, newCursorPosition); 240 updateSelectionIfRequired(); 241 return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition, 242 text.length() > 0); 243 } 244 245 /** 246 * @see BaseInputConnection#performEditorAction(int) 247 */ 248 @Override performEditorAction(int actionCode)249 public boolean performEditorAction(int actionCode) { 250 if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]"); 251 if (actionCode == EditorInfo.IME_ACTION_NEXT) { 252 restartInput(); 253 // Send TAB key event 254 long timeStampMs = SystemClock.uptimeMillis(); 255 mImeAdapter.sendSyntheticKeyEvent( 256 ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0, 0); 257 } else { 258 mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER, 259 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE 260 | KeyEvent.FLAG_EDITOR_ACTION); 261 } 262 return true; 263 } 264 265 /** 266 * @see BaseInputConnection#performContextMenuAction(int) 267 */ 268 @Override performContextMenuAction(int id)269 public boolean performContextMenuAction(int id) { 270 if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]"); 271 switch (id) { 272 case android.R.id.selectAll: 273 return mImeAdapter.selectAll(); 274 case android.R.id.cut: 275 return mImeAdapter.cut(); 276 case android.R.id.copy: 277 return mImeAdapter.copy(); 278 case android.R.id.paste: 279 return mImeAdapter.paste(); 280 default: 281 return false; 282 } 283 } 284 285 /** 286 * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest, 287 * int) 288 */ 289 @Override getExtractedText(ExtractedTextRequest request, int flags)290 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 291 if (DEBUG) Log.w(TAG, "getExtractedText"); 292 ExtractedText et = new ExtractedText(); 293 et.text = mEditable.toString(); 294 et.partialEndOffset = mEditable.length(); 295 et.selectionStart = Selection.getSelectionStart(mEditable); 296 et.selectionEnd = Selection.getSelectionEnd(mEditable); 297 et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0; 298 return et; 299 } 300 301 /** 302 * @see BaseInputConnection#beginBatchEdit() 303 */ 304 @Override beginBatchEdit()305 public boolean beginBatchEdit() { 306 if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); 307 mNumNestedBatchEdits++; 308 return true; 309 } 310 311 /** 312 * @see BaseInputConnection#endBatchEdit() 313 */ 314 @Override endBatchEdit()315 public boolean endBatchEdit() { 316 if (mNumNestedBatchEdits == 0) return false; 317 --mNumNestedBatchEdits; 318 if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); 319 if (mNumNestedBatchEdits == 0) updateSelectionIfRequired(); 320 return mNumNestedBatchEdits != 0; 321 } 322 323 /** 324 * @see BaseInputConnection#deleteSurroundingText(int, int) 325 */ 326 @Override deleteSurroundingText(int beforeLength, int afterLength)327 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 328 if (DEBUG) { 329 Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]"); 330 } 331 int originalBeforeLength = beforeLength; 332 int originalAfterLength = afterLength; 333 int availableBefore = Selection.getSelectionStart(mEditable); 334 int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable); 335 beforeLength = Math.min(beforeLength, availableBefore); 336 afterLength = Math.min(afterLength, availableAfter); 337 super.deleteSurroundingText(beforeLength, afterLength); 338 updateSelectionIfRequired(); 339 340 // For single-char deletion calls |ImeAdapter.sendKeyEventWithKeyCode| with the real key 341 // code. For multi-character deletion, executes deletion by calling 342 // |ImeAdapter.deleteSurroundingText| and sends synthetic key events with a dummy key code. 343 int keyCode = KeyEvent.KEYCODE_UNKNOWN; 344 if (originalBeforeLength == 1 && originalAfterLength == 0) 345 keyCode = KeyEvent.KEYCODE_DEL; 346 else if (originalBeforeLength == 0 && originalAfterLength == 1) 347 keyCode = KeyEvent.KEYCODE_FORWARD_DEL; 348 349 boolean result = true; 350 if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { 351 result = mImeAdapter.sendSyntheticKeyEvent( 352 ImeAdapter.sEventTypeRawKeyDown, SystemClock.uptimeMillis(), keyCode, 0, 0); 353 result &= mImeAdapter.deleteSurroundingText(beforeLength, afterLength); 354 result &= mImeAdapter.sendSyntheticKeyEvent( 355 ImeAdapter.sEventTypeKeyUp, SystemClock.uptimeMillis(), keyCode, 0, 0); 356 } else { 357 mImeAdapter.sendKeyEventWithKeyCode( 358 keyCode, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); 359 } 360 return result; 361 } 362 363 /** 364 * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) 365 */ 366 @Override sendKeyEvent(KeyEvent event)367 public boolean sendKeyEvent(KeyEvent event) { 368 if (DEBUG) { 369 Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]"); 370 } 371 // If this is a key-up, and backspace/del or if the key has a character representation, 372 // need to update the underlying Editable (i.e. the local representation of the text 373 // being edited). 374 if (event.getAction() == KeyEvent.ACTION_UP) { 375 if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 376 deleteSurroundingText(1, 0); 377 return true; 378 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 379 deleteSurroundingText(0, 1); 380 return true; 381 } else { 382 int unicodeChar = event.getUnicodeChar(); 383 if (unicodeChar != 0) { 384 int selectionStart = Selection.getSelectionStart(mEditable); 385 int selectionEnd = Selection.getSelectionEnd(mEditable); 386 if (selectionStart > selectionEnd) { 387 int temp = selectionStart; 388 selectionStart = selectionEnd; 389 selectionEnd = temp; 390 } 391 mEditable.replace(selectionStart, selectionEnd, 392 Character.toString((char) unicodeChar)); 393 } 394 } 395 } else if (event.getAction() == KeyEvent.ACTION_DOWN) { 396 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed. 397 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { 398 beginBatchEdit(); 399 finishComposingText(); 400 mImeAdapter.translateAndSendNativeEvents(event); 401 endBatchEdit(); 402 return true; 403 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 404 return true; 405 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 406 return true; 407 } 408 } 409 mImeAdapter.translateAndSendNativeEvents(event); 410 return true; 411 } 412 413 /** 414 * @see BaseInputConnection#finishComposingText() 415 */ 416 @Override finishComposingText()417 public boolean finishComposingText() { 418 if (DEBUG) Log.w(TAG, "finishComposingText"); 419 if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) { 420 return true; 421 } 422 423 super.finishComposingText(); 424 updateSelectionIfRequired(); 425 mImeAdapter.finishComposingText(); 426 427 return true; 428 } 429 430 /** 431 * @see BaseInputConnection#setSelection(int, int) 432 */ 433 @Override setSelection(int start, int end)434 public boolean setSelection(int start, int end) { 435 if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]"); 436 int textLength = mEditable.length(); 437 if (start < 0 || end < 0 || start > textLength || end > textLength) return true; 438 super.setSelection(start, end); 439 updateSelectionIfRequired(); 440 return mImeAdapter.setEditableSelectionOffsets(start, end); 441 } 442 443 /** 444 * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text 445 * state is no longer what the IME has and that it needs to be updated. 446 */ restartInput()447 void restartInput() { 448 if (DEBUG) Log.w(TAG, "restartInput"); 449 getInputMethodManagerWrapper().restartInput(mInternalView); 450 mNumNestedBatchEdits = 0; 451 } 452 453 /** 454 * @see BaseInputConnection#setComposingRegion(int, int) 455 */ 456 @Override setComposingRegion(int start, int end)457 public boolean setComposingRegion(int start, int end) { 458 if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); 459 int textLength = mEditable.length(); 460 int a = Math.min(start, end); 461 int b = Math.max(start, end); 462 if (a < 0) a = 0; 463 if (b < 0) b = 0; 464 if (a > textLength) a = textLength; 465 if (b > textLength) b = textLength; 466 467 if (a == b) { 468 removeComposingSpans(mEditable); 469 } else { 470 super.setComposingRegion(a, b); 471 } 472 updateSelectionIfRequired(); 473 474 CharSequence regionText = null; 475 if (b > a) { 476 regionText = mEditable.subSequence(a, b); 477 } 478 return mImeAdapter.setComposingRegion(regionText, a, b); 479 } 480 isActive()481 boolean isActive() { 482 return getInputMethodManagerWrapper().isActive(mInternalView); 483 } 484 getInputMethodManagerWrapper()485 private InputMethodManagerWrapper getInputMethodManagerWrapper() { 486 return mImeAdapter.getInputMethodManagerWrapper(); 487 } 488 489 /** 490 * This method works around the issue crbug.com/373934 where Blink does not cancel 491 * the composition when we send a commit with the empty text. 492 * 493 * TODO(aurimas) Remove this once crbug.com/373934 is fixed. 494 * 495 * @param text Text that software keyboard requested to commit. 496 * @return Whether the workaround was performed. 497 */ maybePerformEmptyCompositionWorkaround(CharSequence text)498 private boolean maybePerformEmptyCompositionWorkaround(CharSequence text) { 499 int selectionStart = Selection.getSelectionStart(mEditable); 500 int selectionEnd = Selection.getSelectionEnd(mEditable); 501 int compositionStart = getComposingSpanStart(mEditable); 502 int compositionEnd = getComposingSpanEnd(mEditable); 503 if (TextUtils.isEmpty(text) && (selectionStart == selectionEnd) 504 && compositionStart != INVALID_COMPOSITION 505 && compositionEnd != INVALID_COMPOSITION) { 506 beginBatchEdit(); 507 finishComposingText(); 508 int selection = Selection.getSelectionStart(mEditable); 509 deleteSurroundingText(selection - compositionStart, selection - compositionEnd); 510 endBatchEdit(); 511 return true; 512 } 513 return false; 514 } 515 516 @VisibleForTesting 517 static class ImeState { 518 public final String text; 519 public final int selectionStart; 520 public final int selectionEnd; 521 public final int compositionStart; 522 public final int compositionEnd; 523 ImeState(String text, int selectionStart, int selectionEnd, int compositionStart, int compositionEnd)524 public ImeState(String text, int selectionStart, int selectionEnd, 525 int compositionStart, int compositionEnd) { 526 this.text = text; 527 this.selectionStart = selectionStart; 528 this.selectionEnd = selectionEnd; 529 this.compositionStart = compositionStart; 530 this.compositionEnd = compositionEnd; 531 } 532 } 533 534 @VisibleForTesting getImeStateForTesting()535 ImeState getImeStateForTesting() { 536 String text = mEditable.toString(); 537 int selectionStart = Selection.getSelectionStart(mEditable); 538 int selectionEnd = Selection.getSelectionEnd(mEditable); 539 int compositionStart = getComposingSpanStart(mEditable); 540 int compositionEnd = getComposingSpanEnd(mEditable); 541 return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd); 542 } 543 } 544