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) { 314 Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]"); 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 deleteSurroundingText(1, 0); 322 return true; 323 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 324 deleteSurroundingText(0, 1); 325 return true; 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 if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 343 return true; 344 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { 345 return true; 346 } 347 } 348 mImeAdapter.translateAndSendNativeEvents(event); 349 return true; 350 } 351 352 /** 353 * @see BaseInputConnection#finishComposingText() 354 */ 355 @Override 356 public boolean finishComposingText() { 357 if (DEBUG) Log.w(TAG, "finishComposingText"); 358 Editable editable = getEditable(); 359 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) { 360 return true; 361 } 362 363 super.finishComposingText(); 364 mImeAdapter.finishComposingText(); 365 366 return true; 367 } 368 369 /** 370 * @see BaseInputConnection#setSelection(int, int) 371 */ 372 @Override 373 public boolean setSelection(int start, int end) { 374 if (DEBUG) Log.w(TAG, "setSelection"); 375 if (start < 0 || end < 0) return true; 376 super.setSelection(start, end); 377 return mImeAdapter.setEditableSelectionOffsets(start, end); 378 } 379 380 /** 381 * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text 382 * state is no longer what the IME has and that it needs to be updated. 383 */ 384 void restartInput() { 385 if (DEBUG) Log.w(TAG, "restartInput"); 386 getInputMethodManagerWrapper().restartInput(mInternalView); 387 mIgnoreTextInputStateUpdates = false; 388 mNumNestedBatchEdits = 0; 389 } 390 391 /** 392 * @see BaseInputConnection#setComposingRegion(int, int) 393 */ 394 @Override 395 public boolean setComposingRegion(int start, int end) { 396 if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); 397 int a = Math.min(start, end); 398 int b = Math.max(start, end); 399 if (a < 0) a = 0; 400 if (b < 0) b = 0; 401 402 if (a == b) { 403 removeComposingSpans(getEditable()); 404 } else { 405 super.setComposingRegion(a, b); 406 } 407 return mImeAdapter.setComposingRegion(a, b); 408 } 409 410 boolean isActive() { 411 return getInputMethodManagerWrapper().isActive(mInternalView); 412 } 413 414 public void setIgnoreTextInputStateUpdates(boolean shouldIgnore) { 415 mIgnoreTextInputStateUpdates = shouldIgnore; 416 if (shouldIgnore) return; 417 418 Editable editable = getEditable(); 419 updateSelection(Selection.getSelectionStart(editable), 420 Selection.getSelectionEnd(editable), 421 getComposingSpanStart(editable), 422 getComposingSpanEnd(editable)); 423 } 424 425 @VisibleForTesting 426 protected boolean isIgnoringTextInputStateUpdates() { 427 return mIgnoreTextInputStateUpdates; 428 } 429 430 private InputMethodManagerWrapper getInputMethodManagerWrapper() { 431 return mImeAdapter.getInputMethodManagerWrapper(); 432 } 433} 434