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