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