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.
5 package org.chromium.content.browser.input;
7 import com.google.common.annotations.VisibleForTesting;
9 import android.text.Editable;
10 import android.text.InputType;
11 import android.text.Selection;
12 import android.util.Log;
13 import android.view.KeyEvent;
14 import android.view.View;
15 import android.view.inputmethod.BaseInputConnection;
16 import android.view.inputmethod.EditorInfo;
17 import android.view.inputmethod.ExtractedText;
18 import android.view.inputmethod.ExtractedTextRequest;
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.
25 public class AdapterInputConnection extends BaseInputConnection {
26 private static final String TAG = "AdapterInputConnection";
27 private static final boolean DEBUG = false;
29 * Selection value should be -1 if not known. See EditorInfo.java for details.
31 public static final int INVALID_SELECTION = -1;
32 public static final int INVALID_COMPOSITION = -1;
34 private final View mInternalView;
35 private final ImeAdapter mImeAdapter;
37 private boolean mSingleLine;
38 private int mNumNestedBatchEdits = 0;
40 private int mLastUpdateSelectionStart = INVALID_SELECTION;
41 private int mLastUpdateSelectionEnd = INVALID_SELECTION;
42 private int mLastUpdateCompositionStart = INVALID_COMPOSITION;
43 private int mLastUpdateCompositionEnd = INVALID_COMPOSITION;
46 AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
49 mImeAdapter = imeAdapter;
50 mImeAdapter.setInputConnection(this);
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;
57 if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) {
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;
69 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) {
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) {
76 outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
77 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) {
79 // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
80 // exclude it for now.
81 outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
82 } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) {
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) {
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) {
95 outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
96 | InputType.TYPE_NUMBER_VARIATION_NORMAL;
97 outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
99 outAttrs.initialSelStart = imeAdapter.getInitialSelectionStart();
100 outAttrs.initialSelEnd = imeAdapter.getInitialSelectionEnd();
101 mLastUpdateSelectionStart = imeAdapter.getInitialSelectionStart();
102 mLastUpdateSelectionEnd = imeAdapter.getInitialSelectionEnd();
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.
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
116 * @param compositionStart The character offset of the composition start, or -1 if there is no
118 * @param compositionEnd The character offset of the composition end, or -1 if there is no
120 * @param requireAck True when the update was not caused by IME, false otherwise.
122 public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart,
123 int compositionEnd, boolean requireAck) {
125 Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] ["
126 + compositionStart + " " + compositionEnd + "] [" + requireAck + "]");
128 if (!requireAck) return;
130 // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
131 text = text.replace('\u00A0', ' ');
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());
138 Editable editable = getEditable();
139 String prevText = editable.toString();
140 boolean textUnchanged = prevText.equals(text);
142 if (!textUnchanged) {
143 editable.replace(0, editable.length(), text);
146 Selection.setSelection(editable, selectionStart, selectionEnd);
148 if (compositionStart == compositionEnd) {
149 removeComposingSpans(editable);
151 super.setComposingRegion(compositionStart, compositionEnd);
153 updateSelectionIfRequired();
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.
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) {
175 Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] ["
176 + compositionStart + " " + compositionEnd + "]");
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;
189 * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
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);
201 * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
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);
213 * @see BaseInputConnection#performEditorAction(int)
216 public boolean performEditorAction(int actionCode) {
217 if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
218 if (actionCode == EditorInfo.IME_ACTION_NEXT) {
220 // Send TAB key event
221 long timeStampMs = System.currentTimeMillis();
222 mImeAdapter.sendSyntheticKeyEvent(
223 ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
225 mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
226 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
227 | KeyEvent.FLAG_EDITOR_ACTION);
233 * @see BaseInputConnection#performContextMenuAction(int)
236 public boolean performContextMenuAction(int id) {
237 if (DEBUG) Log.w(TAG, "performContextMenuAction [" + 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();
253 * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
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;
270 * @see BaseInputConnection#beginBatchEdit()
273 public boolean beginBatchEdit() {
274 if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
275 mNumNestedBatchEdits++;
280 * @see BaseInputConnection#endBatchEdit()
283 public boolean endBatchEdit() {
284 if (mNumNestedBatchEdits == 0) return false;
285 --mNumNestedBatchEdits;
286 if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
287 if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
288 return mNumNestedBatchEdits != 0;
292 * @see BaseInputConnection#deleteSurroundingText(int, int)
295 public boolean deleteSurroundingText(int beforeLength, int afterLength) {
297 Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
299 Editable editable = getEditable();
300 int availableBefore = Selection.getSelectionStart(editable);
301 int availableAfter = editable.length() - Selection.getSelectionEnd(editable);
302 beforeLength = Math.min(beforeLength, availableBefore);
303 afterLength = Math.min(afterLength, availableAfter);
304 super.deleteSurroundingText(beforeLength, afterLength);
305 updateSelectionIfRequired();
306 return mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
310 * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
313 public boolean sendKeyEvent(KeyEvent event) {
315 Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
317 // If this is a key-up, and backspace/del or if the key has a character representation,
318 // need to update the underlying Editable (i.e. the local representation of the text
320 if (event.getAction() == KeyEvent.ACTION_UP) {
321 if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
322 deleteSurroundingText(1, 0);
324 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
325 deleteSurroundingText(0, 1);
328 int unicodeChar = event.getUnicodeChar();
329 if (unicodeChar != 0) {
330 Editable editable = getEditable();
331 int selectionStart = Selection.getSelectionStart(editable);
332 int selectionEnd = Selection.getSelectionEnd(editable);
333 if (selectionStart > selectionEnd) {
334 int temp = selectionStart;
335 selectionStart = selectionEnd;
338 editable.replace(selectionStart, selectionEnd,
339 Character.toString((char)unicodeChar));
342 } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
343 // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
344 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
346 finishComposingText();
347 mImeAdapter.translateAndSendNativeEvents(event);
350 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
352 } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
356 mImeAdapter.translateAndSendNativeEvents(event);
361 * @see BaseInputConnection#finishComposingText()
364 public boolean finishComposingText() {
365 if (DEBUG) Log.w(TAG, "finishComposingText");
366 Editable editable = getEditable();
367 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) {
371 super.finishComposingText();
372 updateSelectionIfRequired();
373 mImeAdapter.finishComposingText();
379 * @see BaseInputConnection#setSelection(int, int)
382 public boolean setSelection(int start, int end) {
383 if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
384 int textLength = getEditable().length();
385 if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
386 super.setSelection(start, end);
387 updateSelectionIfRequired();
388 return mImeAdapter.setEditableSelectionOffsets(start, end);
392 * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
393 * state is no longer what the IME has and that it needs to be updated.
395 void restartInput() {
396 if (DEBUG) Log.w(TAG, "restartInput");
397 getInputMethodManagerWrapper().restartInput(mInternalView);
398 mNumNestedBatchEdits = 0;
402 * @see BaseInputConnection#setComposingRegion(int, int)
405 public boolean setComposingRegion(int start, int end) {
406 if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
407 int textLength = getEditable().length();
408 int a = Math.min(start, end);
409 int b = Math.max(start, end);
412 if (a > textLength) a = textLength;
413 if (b > textLength) b = textLength;
416 removeComposingSpans(getEditable());
418 super.setComposingRegion(a, b);
420 updateSelectionIfRequired();
421 return mImeAdapter.setComposingRegion(a, b);
425 return getInputMethodManagerWrapper().isActive(mInternalView);
428 private InputMethodManagerWrapper getInputMethodManagerWrapper() {
429 return mImeAdapter.getInputMethodManagerWrapper();
433 static class ImeState {
434 public final String text;
435 public final int selectionStart;
436 public final int selectionEnd;
437 public final int compositionStart;
438 public final int compositionEnd;
440 public ImeState(String text, int selectionStart, int selectionEnd,
441 int compositionStart, int compositionEnd) {
443 this.selectionStart = selectionStart;
444 this.selectionEnd = selectionEnd;
445 this.compositionStart = compositionStart;
446 this.compositionEnd = compositionEnd;
451 ImeState getImeStateForTesting() {
452 Editable editable = getEditable();
453 String text = editable.toString();
454 int selectionStart = Selection.getSelectionStart(editable);
455 int selectionEnd = Selection.getSelectionEnd(editable);
456 int compositionStart = getComposingSpanStart(editable);
457 int compositionEnd = getComposingSpanEnd(editable);
458 return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);