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