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