- add sources.
[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 com.google.common.annotations.VisibleForTesting;
8
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;
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  */
25 public 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             // 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) {
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         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         mNumNestedBatchEdits++;
276         return true;
277     }
278
279     /**
280      * @see BaseInputConnection#endBatchEdit()
281      */
282     @Override
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;
289     }
290
291     /**
292      * @see BaseInputConnection#deleteSurroundingText(int, int)
293      */
294     @Override
295     public boolean deleteSurroundingText(int beforeLength, int afterLength) {
296         if (DEBUG) {
297             Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
298         }
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);
307     }
308
309     /**
310      * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
311      */
312     @Override
313     public boolean sendKeyEvent(KeyEvent event) {
314         if (DEBUG) {
315             Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
316         }
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
319         // being edited).
320         if (event.getAction() == KeyEvent.ACTION_UP) {
321             if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
322                 deleteSurroundingText(1, 0);
323                 return true;
324             } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
325                 deleteSurroundingText(0, 1);
326                 return true;
327             } else {
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;
336                         selectionEnd = temp;
337                     }
338                     editable.replace(selectionStart, selectionEnd,
339                             Character.toString((char)unicodeChar));
340                 }
341             }
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) {
345                 beginBatchEdit();
346                 finishComposingText();
347                 mImeAdapter.translateAndSendNativeEvents(event);
348                 endBatchEdit();
349                 return true;
350             } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
351                 return true;
352             } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
353                 return true;
354             }
355         }
356         mImeAdapter.translateAndSendNativeEvents(event);
357         return true;
358     }
359
360     /**
361      * @see BaseInputConnection#finishComposingText()
362      */
363     @Override
364     public boolean finishComposingText() {
365         if (DEBUG) Log.w(TAG, "finishComposingText");
366         Editable editable = getEditable();
367         if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) {
368             return true;
369         }
370
371         super.finishComposingText();
372         updateSelectionIfRequired();
373         mImeAdapter.finishComposingText();
374
375         return true;
376     }
377
378     /**
379      * @see BaseInputConnection#setSelection(int, int)
380      */
381     @Override
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);
389     }
390
391     /**
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.
394      */
395     void restartInput() {
396         if (DEBUG) Log.w(TAG, "restartInput");
397         getInputMethodManagerWrapper().restartInput(mInternalView);
398         mNumNestedBatchEdits = 0;
399     }
400
401     /**
402      * @see BaseInputConnection#setComposingRegion(int, int)
403      */
404     @Override
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);
410         if (a < 0) a = 0;
411         if (b < 0) b = 0;
412         if (a > textLength) a = textLength;
413         if (b > textLength) b = textLength;
414
415         if (a == b) {
416             removeComposingSpans(getEditable());
417         } else {
418             super.setComposingRegion(a, b);
419         }
420         updateSelectionIfRequired();
421         return mImeAdapter.setComposingRegion(a, b);
422     }
423
424     boolean isActive() {
425         return getInputMethodManagerWrapper().isActive(mInternalView);
426     }
427
428     private InputMethodManagerWrapper getInputMethodManagerWrapper() {
429         return mImeAdapter.getInputMethodManagerWrapper();
430     }
431
432     @VisibleForTesting
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;
439
440         public ImeState(String text, int selectionStart, int selectionEnd,
441                 int compositionStart, int compositionEnd) {
442             this.text = text;
443             this.selectionStart = selectionStart;
444             this.selectionEnd = selectionEnd;
445             this.compositionStart = compositionStart;
446             this.compositionEnd = compositionEnd;
447         }
448     }
449
450     @VisibleForTesting
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);
459     }
460 }