AdapterInputConnection.java revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
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(int selectionStart, int selectionEnd, int compositionStart, 168 int compositionEnd) { 169 // Avoid sending update if we sent an exact update already previously. 170 if (mLastUpdateSelectionStart == selectionStart && 171 mLastUpdateSelectionEnd == selectionEnd && 172 mLastUpdateCompositionStart == compositionStart && 173 mLastUpdateCompositionEnd == compositionEnd) { 174 return; 175 } 176 if (DEBUG) { 177 Log.w(TAG, "updateSelection [" + selectionStart + " " + selectionEnd + "] [" 178 + compositionStart + " " + compositionEnd + "]"); 179 } 180 // updateSelection should be called every time the selection or composition changes 181 // if it happens not within a batch edit, or at the end of each top level batch edit. 182 getInputMethodManagerWrapper().updateSelection(mInternalView, 183 selectionStart, selectionEnd, compositionStart, compositionEnd); 184 mLastUpdateSelectionStart = selectionStart; 185 mLastUpdateSelectionEnd = selectionEnd; 186 mLastUpdateCompositionStart = compositionStart; 187 mLastUpdateCompositionEnd = compositionEnd; 188 } 189 190 /** 191 * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int) 192 */ 193 @Override 194 public boolean setComposingText(CharSequence text, int newCursorPosition) { 195 if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]"); 196 super.setComposingText(text, newCursorPosition); 197 return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(), 198 newCursorPosition, false); 199 } 200 201 /** 202 * @see BaseInputConnection#commitText(java.lang.CharSequence, int) 203 */ 204 @Override 205 public boolean commitText(CharSequence text, int newCursorPosition) { 206 if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]"); 207 super.commitText(text, newCursorPosition); 208 return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(), 209 newCursorPosition, text.length() > 0); 210 } 211 212 /** 213 * @see BaseInputConnection#performEditorAction(int) 214 */ 215 @Override 216 public boolean performEditorAction(int actionCode) { 217 if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]"); 218 if (actionCode == EditorInfo.IME_ACTION_NEXT) { 219 restartInput(); 220 // Send TAB key event 221 long timeStampMs = System.currentTimeMillis(); 222 mImeAdapter.sendSyntheticKeyEvent( 223 ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0); 224 } else { 225 mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER, 226 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE 227 | KeyEvent.FLAG_EDITOR_ACTION); 228 } 229 return true; 230 } 231 232 /** 233 * @see BaseInputConnection#performContextMenuAction(int) 234 */ 235 @Override 236 public boolean performContextMenuAction(int id) { 237 if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]"); 238 switch (id) { 239 case android.R.id.selectAll: 240 return mImeAdapter.selectAll(); 241 case android.R.id.cut: 242 return mImeAdapter.cut(); 243 case android.R.id.copy: 244 return mImeAdapter.copy(); 245 case android.R.id.paste: 246 return mImeAdapter.paste(); 247 default: 248 return false; 249 } 250 } 251 252 /** 253 * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest, 254 * int) 255 */ 256 @Override 257 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 258 if (DEBUG) Log.w(TAG, "getExtractedText"); 259 ExtractedText et = new ExtractedText(); 260 Editable editable = getEditable(); 261 et.text = editable.toString(); 262 et.partialEndOffset = editable.length(); 263 et.selectionStart = Selection.getSelectionStart(editable); 264 et.selectionEnd = Selection.getSelectionEnd(editable); 265 et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0; 266 return et; 267 } 268 269 /** 270 * @see BaseInputConnection#beginBatchEdit() 271 */ 272 @Override 273 public boolean beginBatchEdit() { 274 if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); 275 if (mNumNestedBatchEdits == 0) mImeAdapter.batchStateChanged(true); 276 277 mNumNestedBatchEdits++; 278 return false; 279 } 280 281 /** 282 * @see BaseInputConnection#endBatchEdit() 283 */ 284 @Override 285 public boolean endBatchEdit() { 286 if (mNumNestedBatchEdits == 0) return false; 287 288 --mNumNestedBatchEdits; 289 if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); 290 if (mNumNestedBatchEdits == 0) mImeAdapter.batchStateChanged(false); 291 return false; 292 } 293 294 /** 295 * @see BaseInputConnection#deleteSurroundingText(int, int) 296 */ 297 @Override 298 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 299 if (DEBUG) { 300 Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]"); 301 } 302 Editable editable = getEditable(); 303 int availableBefore = Selection.getSelectionStart(editable); 304 int availableAfter = editable.length() - Selection.getSelectionEnd(editable); 305 beforeLength = Math.min(beforeLength, availableBefore); 306 afterLength = Math.min(afterLength, availableAfter); 307 super.deleteSurroundingText(beforeLength, afterLength); 308 return mImeAdapter.deleteSurroundingText(beforeLength, afterLength); 309 } 310 311 /** 312 * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) 313 */ 314 @Override 315 public boolean sendKeyEvent(KeyEvent event) { 316 if (DEBUG) Log.w(TAG, "sendKeyEvent [" + event.getAction() + "]"); 317 318 // If this is a key-up, and backspace/del or if the key has a character representation, 319 // need to update the underlying Editable (i.e. the local representation of the text 320 // being edited). 321 if (event.getAction() == KeyEvent.ACTION_UP) { 322 if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 323 super.deleteSurroundingText(1, 0); 324 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 325 super.deleteSurroundingText(0, 1); 326 } else { 327 int unicodeChar = event.getUnicodeChar(); 328 if (unicodeChar != 0) { 329 Editable editable = getEditable(); 330 int selectionStart = Selection.getSelectionStart(editable); 331 int selectionEnd = Selection.getSelectionEnd(editable); 332 if (selectionStart > selectionEnd) { 333 int temp = selectionStart; 334 selectionStart = selectionEnd; 335 selectionEnd = temp; 336 } 337 editable.replace(selectionStart, selectionEnd, 338 Character.toString((char)unicodeChar)); 339 } 340 } 341 } else if (event.getAction() == KeyEvent.ACTION_DOWN) { 342 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed. 343 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { 344 beginBatchEdit(); 345 finishComposingText(); 346 mImeAdapter.translateAndSendNativeEvents(event); 347 endBatchEdit(); 348 return true; 349 } 350 } 351 mImeAdapter.translateAndSendNativeEvents(event); 352 return true; 353 } 354 355 /** 356 * @see BaseInputConnection#finishComposingText() 357 */ 358 @Override 359 public boolean finishComposingText() { 360 if (DEBUG) Log.w(TAG, "finishComposingText"); 361 Editable editable = getEditable(); 362 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) { 363 return true; 364 } 365 366 super.finishComposingText(); 367 mImeAdapter.finishComposingText(); 368 369 return true; 370 } 371 372 /** 373 * @see BaseInputConnection#setSelection(int, int) 374 */ 375 @Override 376 public boolean setSelection(int start, int end) { 377 if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]"); 378 int textLength = getEditable().length(); 379 if (start < 0 || end < 0 || start > textLength || end > textLength) return true; 380 super.setSelection(start, end); 381 return mImeAdapter.setEditableSelectionOffsets(start, end); 382 } 383 384 /** 385 * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text 386 * state is no longer what the IME has and that it needs to be updated. 387 */ 388 void restartInput() { 389 if (DEBUG) Log.w(TAG, "restartInput"); 390 getInputMethodManagerWrapper().restartInput(mInternalView); 391 mIgnoreTextInputStateUpdates = false; 392 mNumNestedBatchEdits = 0; 393 } 394 395 /** 396 * @see BaseInputConnection#setComposingRegion(int, int) 397 */ 398 @Override 399 public boolean setComposingRegion(int start, int end) { 400 if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); 401 int textLength = getEditable().length(); 402 int a = Math.min(start, end); 403 int b = Math.max(start, end); 404 if (a < 0) a = 0; 405 if (b < 0) b = 0; 406 if (a > textLength) a = textLength; 407 if (b > textLength) b = textLength; 408 409 if (a == b) { 410 removeComposingSpans(getEditable()); 411 } else { 412 super.setComposingRegion(a, b); 413 } 414 return mImeAdapter.setComposingRegion(a, b); 415 } 416 417 boolean isActive() { 418 return getInputMethodManagerWrapper().isActive(mInternalView); 419 } 420 421 public void setIgnoreTextInputStateUpdates(boolean shouldIgnore) { 422 mIgnoreTextInputStateUpdates = shouldIgnore; 423 if (shouldIgnore) return; 424 425 Editable editable = getEditable(); 426 updateSelection(Selection.getSelectionStart(editable), 427 Selection.getSelectionEnd(editable), 428 getComposingSpanStart(editable), 429 getComposingSpanEnd(editable)); 430 } 431 432 @VisibleForTesting 433 protected boolean isIgnoringTextInputStateUpdates() { 434 return mIgnoreTextInputStateUpdates; 435 } 436 437 private InputMethodManagerWrapper getInputMethodManagerWrapper() { 438 return mImeAdapter.getInputMethodManagerWrapper(); 439 } 440} 441