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