AdapterInputConnection.java revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 38 private boolean mSingleLine; 39 private int mNumNestedBatchEdits = 0; 40 41 private int mLastUpdateSelectionStart = INVALID_SELECTION; 42 private int mLastUpdateSelectionEnd = INVALID_SELECTION; 43 private int mLastUpdateCompositionStart = INVALID_COMPOSITION; 44 private int mLastUpdateCompositionEnd = INVALID_COMPOSITION; 45 46 @VisibleForTesting 47 AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) { 48 super(view, true); 49 mInternalView = view; 50 mImeAdapter = imeAdapter; 51 mImeAdapter.setInputConnection(this); 52 mSingleLine = true; 53 outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN 54 | EditorInfo.IME_FLAG_NO_EXTRACT_UI; 55 outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT 56 | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; 57 58 if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) { 59 // Normal text field 60 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; 61 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 62 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTextArea || 63 imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeContentEditable) { 64 // TextArea or contenteditable. 65 outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE 66 | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES 67 | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; 68 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE; 69 mSingleLine = false; 70 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) { 71 // Password 72 outAttrs.inputType = InputType.TYPE_CLASS_TEXT 73 | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; 74 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 75 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeSearch) { 76 // Search 77 outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH; 78 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) { 79 // Url 80 outAttrs.inputType = InputType.TYPE_CLASS_TEXT 81 | InputType.TYPE_TEXT_VARIATION_URI; 82 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 83 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) { 84 // Email 85 outAttrs.inputType = InputType.TYPE_CLASS_TEXT 86 | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; 87 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; 88 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTel) { 89 // Telephone 90 // Number and telephone do not have both a Tab key and an 91 // action in default OSK, so set the action to NEXT 92 outAttrs.inputType = InputType.TYPE_CLASS_PHONE; 93 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; 94 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeNumber) { 95 // Number 96 outAttrs.inputType = InputType.TYPE_CLASS_NUMBER 97 | InputType.TYPE_NUMBER_VARIATION_NORMAL; 98 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; 99 } 100 outAttrs.initialSelStart = imeAdapter.getInitialSelectionStart(); 101 outAttrs.initialSelEnd = imeAdapter.getInitialSelectionEnd(); 102 mLastUpdateSelectionStart = imeAdapter.getInitialSelectionStart(); 103 mLastUpdateSelectionEnd = imeAdapter.getInitialSelectionEnd(); 104 } 105 106 /** 107 * Updates the AdapterInputConnection's internal representation of the text being edited and 108 * its selection and composition properties. The resulting Editable is accessible through the 109 * getEditable() method. If the text has not changed, this also calls updateSelection on the 110 * InputMethodManager. 111 * 112 * @param text The String contents of the field being edited. 113 * @param selectionStart The character offset of the selection start, or the caret position if 114 * there is no selection. 115 * @param selectionEnd The character offset of the selection end, or the caret position if there 116 * is no selection. 117 * @param compositionStart The character offset of the composition start, or -1 if there is no 118 * composition. 119 * @param compositionEnd The character offset of the composition end, or -1 if there is no 120 * selection. 121 * @param requireAck True when the update was not caused by IME, false otherwise. 122 */ 123 public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart, 124 int compositionEnd, boolean requireAck) { 125 if (DEBUG) { 126 Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] [" 127 + compositionStart + " " + compositionEnd + "] [" + requireAck + "]"); 128 } 129 if (!requireAck) return; 130 131 // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces. 132 text = text.replace('\u00A0', ' '); 133 134 selectionStart = Math.min(selectionStart, text.length()); 135 selectionEnd = Math.min(selectionEnd, text.length()); 136 compositionStart = Math.min(compositionStart, text.length()); 137 compositionEnd = Math.min(compositionEnd, text.length()); 138 139 Editable editable = getEditable(); 140 String prevText = editable.toString(); 141 boolean textUnchanged = prevText.equals(text); 142 143 if (!textUnchanged) { 144 editable.replace(0, editable.length(), text); 145 } 146 147 Selection.setSelection(editable, selectionStart, selectionEnd); 148 149 if (compositionStart == compositionEnd) { 150 removeComposingSpans(editable); 151 } else { 152 super.setComposingRegion(compositionStart, compositionEnd); 153 } 154 updateSelectionIfRequired(); 155 } 156 157 /** 158 * Sends selection update to the InputMethodManager unless we are currently in a batch edit or 159 * if the exact same selection and composition update was sent already. 160 */ 161 private void updateSelectionIfRequired() { 162 if (mNumNestedBatchEdits != 0) return; 163 Editable editable = getEditable(); 164 int selectionStart = Selection.getSelectionStart(editable); 165 int selectionEnd = Selection.getSelectionEnd(editable); 166 int compositionStart = getComposingSpanStart(editable); 167 int compositionEnd = getComposingSpanEnd(editable); 168 // Avoid sending update if we sent an exact update already previously. 169 if (mLastUpdateSelectionStart == selectionStart && 170 mLastUpdateSelectionEnd == selectionEnd && 171 mLastUpdateCompositionStart == compositionStart && 172 mLastUpdateCompositionEnd == compositionEnd) { 173 return; 174 } 175 if (DEBUG) { 176 Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] [" 177 + compositionStart + " " + compositionEnd + "]"); 178 } 179 // updateSelection should be called every time the selection or composition changes 180 // if it happens not within a batch edit, or at the end of each top level batch edit. 181 getInputMethodManagerWrapper().updateSelection(mInternalView, 182 selectionStart, selectionEnd, compositionStart, compositionEnd); 183 mLastUpdateSelectionStart = selectionStart; 184 mLastUpdateSelectionEnd = selectionEnd; 185 mLastUpdateCompositionStart = compositionStart; 186 mLastUpdateCompositionEnd = compositionEnd; 187 } 188 189 /** 190 * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int) 191 */ 192 @Override 193 public boolean setComposingText(CharSequence text, int newCursorPosition) { 194 if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]"); 195 super.setComposingText(text, newCursorPosition); 196 updateSelectionIfRequired(); 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 updateSelectionIfRequired(); 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 = SystemClock.uptimeMillis(); 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 mNumNestedBatchEdits++; 277 return true; 278 } 279 280 /** 281 * @see BaseInputConnection#endBatchEdit() 282 */ 283 @Override 284 public boolean endBatchEdit() { 285 if (mNumNestedBatchEdits == 0) return false; 286 --mNumNestedBatchEdits; 287 if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); 288 if (mNumNestedBatchEdits == 0) updateSelectionIfRequired(); 289 return mNumNestedBatchEdits != 0; 290 } 291 292 /** 293 * @see BaseInputConnection#deleteSurroundingText(int, int) 294 */ 295 @Override 296 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 297 if (DEBUG) { 298 Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]"); 299 } 300 Editable editable = getEditable(); 301 int availableBefore = Selection.getSelectionStart(editable); 302 int availableAfter = editable.length() - Selection.getSelectionEnd(editable); 303 beforeLength = Math.min(beforeLength, availableBefore); 304 afterLength = Math.min(afterLength, availableAfter); 305 super.deleteSurroundingText(beforeLength, afterLength); 306 updateSelectionIfRequired(); 307 return mImeAdapter.deleteSurroundingText(beforeLength, afterLength); 308 } 309 310 /** 311 * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) 312 */ 313 @Override 314 public boolean sendKeyEvent(KeyEvent event) { 315 if (DEBUG) { 316 Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]"); 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 deleteSurroundingText(1, 0); 324 return true; 325 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 326 deleteSurroundingText(0, 1); 327 return true; 328 } else { 329 int unicodeChar = event.getUnicodeChar(); 330 if (unicodeChar != 0) { 331 Editable editable = getEditable(); 332 int selectionStart = Selection.getSelectionStart(editable); 333 int selectionEnd = Selection.getSelectionEnd(editable); 334 if (selectionStart > selectionEnd) { 335 int temp = selectionStart; 336 selectionStart = selectionEnd; 337 selectionEnd = temp; 338 } 339 editable.replace(selectionStart, selectionEnd, 340 Character.toString((char) unicodeChar)); 341 } 342 } 343 } else if (event.getAction() == KeyEvent.ACTION_DOWN) { 344 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed. 345 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { 346 beginBatchEdit(); 347 finishComposingText(); 348 mImeAdapter.translateAndSendNativeEvents(event); 349 endBatchEdit(); 350 return true; 351 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 352 return true; 353 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 354 return true; 355 } 356 } 357 mImeAdapter.translateAndSendNativeEvents(event); 358 return true; 359 } 360 361 /** 362 * @see BaseInputConnection#finishComposingText() 363 */ 364 @Override 365 public boolean finishComposingText() { 366 if (DEBUG) Log.w(TAG, "finishComposingText"); 367 Editable editable = getEditable(); 368 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) { 369 return true; 370 } 371 372 super.finishComposingText(); 373 updateSelectionIfRequired(); 374 mImeAdapter.finishComposingText(); 375 376 return true; 377 } 378 379 /** 380 * @see BaseInputConnection#setSelection(int, int) 381 */ 382 @Override 383 public boolean setSelection(int start, int end) { 384 if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]"); 385 int textLength = getEditable().length(); 386 if (start < 0 || end < 0 || start > textLength || end > textLength) return true; 387 super.setSelection(start, end); 388 updateSelectionIfRequired(); 389 return mImeAdapter.setEditableSelectionOffsets(start, end); 390 } 391 392 /** 393 * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text 394 * state is no longer what the IME has and that it needs to be updated. 395 */ 396 void restartInput() { 397 if (DEBUG) Log.w(TAG, "restartInput"); 398 getInputMethodManagerWrapper().restartInput(mInternalView); 399 mNumNestedBatchEdits = 0; 400 } 401 402 /** 403 * @see BaseInputConnection#setComposingRegion(int, int) 404 */ 405 @Override 406 public boolean setComposingRegion(int start, int end) { 407 if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); 408 int textLength = getEditable().length(); 409 int a = Math.min(start, end); 410 int b = Math.max(start, end); 411 if (a < 0) a = 0; 412 if (b < 0) b = 0; 413 if (a > textLength) a = textLength; 414 if (b > textLength) b = textLength; 415 416 if (a == b) { 417 removeComposingSpans(getEditable()); 418 } else { 419 super.setComposingRegion(a, b); 420 } 421 updateSelectionIfRequired(); 422 return mImeAdapter.setComposingRegion(a, b); 423 } 424 425 boolean isActive() { 426 return getInputMethodManagerWrapper().isActive(mInternalView); 427 } 428 429 private InputMethodManagerWrapper getInputMethodManagerWrapper() { 430 return mImeAdapter.getInputMethodManagerWrapper(); 431 } 432 433 @VisibleForTesting 434 static class ImeState { 435 public final String text; 436 public final int selectionStart; 437 public final int selectionEnd; 438 public final int compositionStart; 439 public final int compositionEnd; 440 441 public ImeState(String text, int selectionStart, int selectionEnd, 442 int compositionStart, int compositionEnd) { 443 this.text = text; 444 this.selectionStart = selectionStart; 445 this.selectionEnd = selectionEnd; 446 this.compositionStart = compositionStart; 447 this.compositionEnd = compositionEnd; 448 } 449 } 450 451 @VisibleForTesting 452 ImeState getImeStateForTesting() { 453 Editable editable = getEditable(); 454 String text = editable.toString(); 455 int selectionStart = Selection.getSelectionStart(editable); 456 int selectionEnd = Selection.getSelectionEnd(editable); 457 int compositionStart = getComposingSpanStart(editable); 458 int compositionEnd = getComposingSpanEnd(editable); 459 return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd); 460 } 461} 462