AdapterInputConnection.java revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
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.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 // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so 81 // exclude it for now. 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.getInitialSelectionStart(); 102 } 103 104 /** 105 * Updates the AdapterInputConnection's internal representation of the text 106 * being edited and its selection and composition properties. The resulting 107 * Editable is accessible through the getEditable() method. 108 * If the text has not changed, this also calls updateSelection on the InputMethodManager. 109 * @param text The String contents of the field being edited 110 * @param selectionStart The character offset of the selection start, or the caret 111 * position if there is no selection 112 * @param selectionEnd The character offset of the selection end, or the caret 113 * position if there is no selection 114 * @param compositionStart The character offset of the composition start, or -1 115 * if there is no composition 116 * @param compositionEnd The character offset of the composition end, or -1 117 * if there is no selection 118 */ 119 public void setEditableText(String text, int selectionStart, int selectionEnd, 120 int compositionStart, int compositionEnd) { 121 if (DEBUG) { 122 Log.w(TAG, "setEditableText [" + text + "] [" + selectionStart + " " + selectionEnd 123 + "] [" + compositionStart + " " + compositionEnd + "]"); 124 } 125 // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces. 126 text = text.replace('\u00A0', ' '); 127 128 selectionStart = Math.min(selectionStart, text.length()); 129 selectionEnd = Math.min(selectionEnd, text.length()); 130 compositionStart = Math.min(compositionStart, text.length()); 131 compositionEnd = Math.min(compositionEnd, text.length()); 132 133 Editable editable = getEditable(); 134 String prevText = editable.toString(); 135 boolean textUnchanged = prevText.equals(text); 136 137 if (!textUnchanged) { 138 editable.replace(0, editable.length(), text); 139 } 140 141 int prevSelectionStart = Selection.getSelectionStart(editable); 142 int prevSelectionEnd = Selection.getSelectionEnd(editable); 143 int prevCompositionStart = getComposingSpanStart(editable); 144 int prevCompositionEnd = getComposingSpanEnd(editable); 145 146 if (prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd 147 && prevCompositionStart == compositionStart 148 && prevCompositionEnd == compositionEnd) { 149 // Nothing has changed; don't need to do anything 150 return; 151 } 152 153 Selection.setSelection(editable, selectionStart, selectionEnd); 154 155 if (compositionStart == compositionEnd) { 156 removeComposingSpans(editable); 157 } else { 158 super.setComposingRegion(compositionStart, compositionEnd); 159 } 160 161 if (mIgnoreTextInputStateUpdates) return; 162 updateSelection(selectionStart, selectionEnd, compositionStart, compositionEnd); 163 } 164 165 @VisibleForTesting 166 protected void updateSelection( 167 int selectionStart, int selectionEnd, 168 int compositionStart, 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 leftLength, int rightLength) { 299 if (DEBUG) { 300 Log.w(TAG, "deleteSurroundingText [" + leftLength + " " + rightLength + "]"); 301 } 302 if (!super.deleteSurroundingText(leftLength, rightLength)) { 303 return false; 304 } 305 return mImeAdapter.deleteSurroundingText(leftLength, rightLength); 306 } 307 308 /** 309 * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) 310 */ 311 @Override 312 public boolean sendKeyEvent(KeyEvent event) { 313 if (DEBUG) Log.w(TAG, "sendKeyEvent [" + event.getAction() + "]"); 314 mImeAdapter.hideSelectionAndInsertionHandleControllers(); 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 } 340 mImeAdapter.translateAndSendNativeEvents(event); 341 return true; 342 } 343 344 /** 345 * @see BaseInputConnection#finishComposingText() 346 */ 347 @Override 348 public boolean finishComposingText() { 349 if (DEBUG) Log.w(TAG, "finishComposingText"); 350 Editable editable = getEditable(); 351 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) { 352 return true; 353 } 354 355 super.finishComposingText(); 356 mImeAdapter.finishComposingText(); 357 358 return true; 359 } 360 361 /** 362 * @see BaseInputConnection#setSelection(int, int) 363 */ 364 @Override 365 public boolean setSelection(int start, int end) { 366 if (DEBUG) Log.w(TAG, "setSelection"); 367 if (start < 0 || end < 0) return true; 368 super.setSelection(start, end); 369 return mImeAdapter.setEditableSelectionOffsets(start, end); 370 } 371 372 /** 373 * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text 374 * state is no longer what the IME has and that it needs to be updated. 375 */ 376 void restartInput() { 377 if (DEBUG) Log.w(TAG, "restartInput"); 378 getInputMethodManagerWrapper().restartInput(mInternalView); 379 mIgnoreTextInputStateUpdates = false; 380 mNumNestedBatchEdits = 0; 381 } 382 383 /** 384 * @see BaseInputConnection#setComposingRegion(int, int) 385 */ 386 @Override 387 public boolean setComposingRegion(int start, int end) { 388 if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); 389 int a = Math.min(start, end); 390 int b = Math.max(start, end); 391 if (a < 0) a = 0; 392 if (b < 0) b = 0; 393 394 if (a == b) { 395 removeComposingSpans(getEditable()); 396 } else { 397 super.setComposingRegion(a, b); 398 } 399 return mImeAdapter.setComposingRegion(a, b); 400 } 401 402 boolean isActive() { 403 return getInputMethodManagerWrapper().isActive(mInternalView); 404 } 405 406 public void setIgnoreTextInputStateUpdates(boolean shouldIgnore) { 407 mIgnoreTextInputStateUpdates = shouldIgnore; 408 if (shouldIgnore) return; 409 410 Editable editable = getEditable(); 411 updateSelection(Selection.getSelectionStart(editable), 412 Selection.getSelectionEnd(editable), 413 getComposingSpanStart(editable), 414 getComposingSpanEnd(editable)); 415 } 416 417 @VisibleForTesting 418 protected boolean isIgnoringTextInputStateUpdates() { 419 return mIgnoreTextInputStateUpdates; 420 } 421 422 private InputMethodManagerWrapper getInputMethodManagerWrapper() { 423 return mImeAdapter.getInputMethodManagerWrapper(); 424 } 425} 426