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