ImeTest.java revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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 android.app.Activity;
8import android.content.ClipData;
9import android.content.ClipboardManager;
10import android.content.Context;
11import android.os.IBinder;
12import android.os.ResultReceiver;
13import android.test.suitebuilder.annotation.MediumTest;
14import android.test.suitebuilder.annotation.SmallTest;
15import android.text.TextUtils;
16import android.view.View;
17import android.view.inputmethod.EditorInfo;
18import android.view.inputmethod.InputConnection;
19
20import org.chromium.base.ThreadUtils;
21import org.chromium.base.test.util.Feature;
22import org.chromium.base.test.util.UrlUtils;
23import org.chromium.content.browser.ContentView;
24import org.chromium.content.browser.ContentViewCore;
25import org.chromium.content.browser.test.util.Criteria;
26import org.chromium.content.browser.test.util.CriteriaHelper;
27import org.chromium.content.browser.test.util.DOMUtils;
28import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
29import org.chromium.content_shell_apk.ContentShellTestBase;
30
31import java.util.ArrayList;
32import java.util.concurrent.Callable;
33
34public class ImeTest extends ContentShellTestBase {
35
36    private static final String DATA_URL = UrlUtils.encodeHtmlDataUri(
37            "<html><head><meta name=\"viewport\"" +
38            "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>" +
39            "<body><form action=\"about:blank\">" +
40            "<input id=\"input_text\" type=\"text\" />" +
41            "<input id=\"input_radio\" type=\"radio\" />" +
42            "</form></body></html>");
43
44    private TestAdapterInputConnection mConnection;
45    private ImeAdapter mImeAdapter;
46    private ContentView mContentView;
47    private TestCallbackHelperContainer mCallbackContainer;
48    private TestInputMethodManagerWrapper mInputMethodManagerWrapper;
49
50    @Override
51    public void setUp() throws Exception {
52        super.setUp();
53
54        launchContentShellWithUrl(DATA_URL);
55        assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
56
57        mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(getContentViewCore());
58        getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper);
59        assertEquals(0, mInputMethodManagerWrapper.mShowSoftInputCounter);
60        getContentViewCore().setAdapterInputConnectionFactory(
61                new TestAdapterInputConnectionFactory());
62
63        mContentView = getActivity().getActiveContentView();
64        mCallbackContainer = new TestCallbackHelperContainer(mContentView);
65        // TODO(aurimas) remove this wait once crbug.com/179511 is fixed.
66        assertWaitForPageScaleFactor(1);
67        DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_text");
68        assertWaitForKeyboardStatus(true);
69
70        mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
71        mImeAdapter = getImeAdapter();
72
73        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
74        assertEquals(1, mInputMethodManagerWrapper.mShowSoftInputCounter);
75        assertEquals(0, mInputMethodManagerWrapper.mEditorInfo.initialSelStart);
76        assertEquals(0, mInputMethodManagerWrapper.mEditorInfo.initialSelEnd);
77    }
78
79    @MediumTest
80    @Feature({"TextInput", "Main"})
81    public void testKeyboardDismissedAfterClickingGo() throws Throwable {
82        mConnection.setComposingText("hello", 1);
83        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, 0, 5);
84
85        performGo(getAdapterInputConnection(), mCallbackContainer);
86
87        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "", 0, 0, -1, -1);
88        assertWaitForKeyboardStatus(false);
89    }
90
91    @SmallTest
92    @Feature({"TextInput", "Main"})
93    public void testGetTextUpdatesAfterEnteringText() throws Throwable {
94        mConnection.setComposingText("h", 1);
95        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1);
96        assertEquals(1, mInputMethodManagerWrapper.mShowSoftInputCounter);
97
98        mConnection.setComposingText("he", 1);
99        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
100        assertEquals(1, mInputMethodManagerWrapper.mShowSoftInputCounter);
101
102        mConnection.setComposingText("hel", 1);
103        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
104        assertEquals(1, mInputMethodManagerWrapper.mShowSoftInputCounter);
105
106        mConnection.commitText("hel", 1);
107        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hel", 3, 3, -1, -1);
108        assertEquals(1, mInputMethodManagerWrapper.mShowSoftInputCounter);
109    }
110
111    @SmallTest
112    @Feature({"TextInput"})
113    public void testImeCopy() throws Exception {
114        mConnection.commitText("hello", 1);
115        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
116
117        mConnection.setSelection(2, 5);
118        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 2, 5, -1, -1);
119
120        mImeAdapter.copy();
121        assertClipboardContents(getActivity(), "llo");
122    }
123
124    @SmallTest
125    @Feature({"TextInput"})
126    public void testEnterTextAndRefocus() throws Exception {
127        mConnection.commitText("hello", 1);
128        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
129
130        DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_radio");
131        assertWaitForKeyboardStatus(false);
132
133        DOMUtils.clickNode(this, mContentView, mCallbackContainer, "input_text");
134        assertWaitForKeyboardStatus(true);
135        assertEquals(5, mInputMethodManagerWrapper.mEditorInfo.initialSelStart);
136        assertEquals(5, mInputMethodManagerWrapper.mEditorInfo.initialSelEnd);
137    }
138
139    @SmallTest
140    @Feature({"TextInput"})
141    public void testImeCut() throws Exception {
142        mConnection.commitText("snarful", 1);
143        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "snarful", 7, 7, -1, -1);
144
145        mConnection.setSelection(1, 5);
146        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "snarful", 1, 5, -1, -1);
147
148        mImeAdapter.cut();
149        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "sul", 1, 1, -1, -1);
150
151        assertClipboardContents(getActivity(), "narf");
152    }
153
154    @SmallTest
155    @Feature({"TextInput"})
156    public void testImePaste() throws Exception {
157        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
158            @Override
159            public void run() {
160                ClipboardManager clipboardManager =
161                        (ClipboardManager) getActivity().getSystemService(
162                                Context.CLIPBOARD_SERVICE);
163                clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
164            }
165        });
166
167        mImeAdapter.paste();
168        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "blarg", 5, 5, -1, -1);
169
170        mConnection.setSelection(3, 5);
171        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "blarg", 3, 5, -1, -1);
172
173        mImeAdapter.paste();
174        // Paste is a two step process when there is a non-zero selection.
175        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "bla", 3, 3, -1, -1);
176        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "blablarg", 8, 8, -1, -1);
177
178        mImeAdapter.paste();
179        waitAndVerifyEditableCallback(
180                mConnection.mImeUpdateQueue, 5, "blablargblarg", 13, 13, -1, -1);
181    }
182
183    @SmallTest
184    @Feature({"TextInput"})
185    public void testImeSelectAndUnSelectAll() throws Exception {
186        mConnection.commitText("hello", 1);
187        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hello", 5, 5, -1, -1);
188
189        mImeAdapter.selectAll();
190        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hello", 0, 5, -1, -1);
191
192        mImeAdapter.unselect();
193        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "", 0, 0, -1, -1);
194
195        assertWaitForKeyboardStatus(false);
196    }
197
198    @SmallTest
199    @Feature({"TextInput", "Main"})
200    public void testUpdatesGetIgnoredDuringBatchEdits() throws Throwable {
201        mConnection.beginBatchEdit();
202        assertWaitForSetIgnoreUpdates(true, mConnection);
203
204        mConnection.setComposingText("h", 1);
205        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "h", 1, 1, 0, 1);
206        assertTrue(mConnection.isIgnoringTextInputStateUpdates());
207
208        mConnection.setComposingText("he", 1);
209        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "he", 2, 2, 0, 2);
210        assertTrue(mConnection.isIgnoringTextInputStateUpdates());
211
212        mConnection.setComposingText("hel", 1);
213        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hel", 3, 3, 0, 3);
214
215        assertEquals(0, mConnection.mUpdateSelectionCounter);
216        assertTrue(mConnection.isIgnoringTextInputStateUpdates());
217        mConnection.endBatchEdit();
218        assertWaitForSetIgnoreUpdates(false, mConnection);
219    }
220
221    @SmallTest
222    @Feature({"TextInput", "Main"})
223    public void testShowImeIfNeeded() throws Throwable {
224        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_radio");
225        assertWaitForKeyboardStatus(false);
226
227        performShowImeIfNeeded();
228        assertWaitForKeyboardStatus(false);
229
230        DOMUtils.focusNode(this, mContentView, mCallbackContainer, "input_text");
231        assertWaitForKeyboardStatus(false);
232
233        performShowImeIfNeeded();
234        assertWaitForKeyboardStatus(true);
235    }
236
237    @SmallTest
238    @Feature({"TextInput", "Main"})
239    public void testFinishComposingText() throws Throwable {
240        mConnection.commitText("hllo", 1);
241        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 1, "hllo", 4, 4, -1, -1);
242
243        mConnection.commitText(" ", 1);
244        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 2, "hllo ", 5, 5, -1, -1);
245
246        mConnection.setSelection(1, 1);
247        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 3, "hllo ", 1, 1, -1, -1);
248
249        mConnection.setComposingRegion(0, 4);
250        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 4, "hllo ", 1, 1, 0, 4);
251
252        mConnection.finishComposingText();
253        // finishComposingText() is a two step IME event.
254        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 5, "hllo ", 4, 4, -1, -1);
255        waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "hllo ", 1, 1, -1, -1);
256    }
257
258    private void performShowImeIfNeeded() {
259        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
260            @Override
261            public void run() {
262                mContentView.getContentViewCore().showImeIfNeeded();
263            }
264        });
265    }
266
267    private void performGo(final AdapterInputConnection inputConnection,
268            TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable {
269        handleBlockingCallbackAction(
270                testCallbackHelperContainer.getOnPageFinishedHelper(),
271                new Runnable() {
272                    @Override
273                    public void run() {
274                        inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO);
275                    }
276                });
277    }
278
279
280
281    private void assertWaitForPageScaleFactor(final float scale) throws InterruptedException {
282        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
283            @Override
284            public boolean isSatisfied() {
285                return getContentViewCore().getScale() == scale;
286            }
287        }));
288    }
289
290    private void assertWaitForKeyboardStatus(final boolean show) throws InterruptedException {
291        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
292            @Override
293            public boolean isSatisfied() {
294                return show == getImeAdapter().mIsShowWithoutHideOutstanding &&
295                        (!show || getAdapterInputConnection() != null);
296            }
297        }));
298    }
299
300    private void waitAndVerifyEditableCallback(final ArrayList<TestImeState> states,
301            final int index, String text, int selectionStart, int selectionEnd,
302            int compositionStart, int compositionEnd) throws InterruptedException {
303        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
304            @Override
305            public boolean isSatisfied() {
306                return states.size() > index;
307            }
308        }));
309        states.get(index).assertEqualState(
310                text, selectionStart, selectionEnd, compositionStart, compositionEnd);
311    }
312
313    private void assertClipboardContents(final Activity activity, final String expectedContents)
314            throws InterruptedException {
315        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
316            @Override
317            public boolean isSatisfied() {
318                return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
319                    @Override
320                    public Boolean call() throws Exception {
321                        ClipboardManager clipboardManager =
322                                (ClipboardManager) activity.getSystemService(
323                                        Context.CLIPBOARD_SERVICE);
324                        ClipData clip = clipboardManager.getPrimaryClip();
325                        return clip != null && clip.getItemCount() == 1
326                                && TextUtils.equals(clip.getItemAt(0).getText(), expectedContents);
327                    }
328                });
329            }
330        }));
331    }
332
333    private void assertWaitForSetIgnoreUpdates(final boolean ignore,
334            final TestAdapterInputConnection connection) throws Throwable {
335        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
336            @Override
337            public boolean isSatisfied() {
338                return ignore == connection.isIgnoringTextInputStateUpdates();
339            }
340        }));
341    }
342
343    private ImeAdapter getImeAdapter() {
344        return getContentViewCore().getImeAdapterForTest();
345    }
346
347    private AdapterInputConnection getAdapterInputConnection() {
348        return getContentViewCore().getInputConnectionForTest();
349    }
350
351    private static class TestAdapterInputConnectionFactory extends
352            ImeAdapter.AdapterInputConnectionFactory {
353        @Override
354        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
355                EditorInfo outAttrs) {
356            return new TestAdapterInputConnection(view, imeAdapter, outAttrs);
357        }
358    }
359
360    private static class TestAdapterInputConnection extends AdapterInputConnection {
361        private int mUpdateSelectionCounter = 0;
362        private ArrayList<TestImeState> mImeUpdateQueue = new ArrayList<ImeTest.TestImeState>();
363
364        public TestAdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
365            super(view, imeAdapter, outAttrs);
366        }
367
368        @Override
369        public void setEditableText(String text, int selectionStart, int selectionEnd,
370                int compositionStart, int compositionEnd) {
371            mImeUpdateQueue.add(new TestImeState(text, selectionStart, selectionEnd,
372                    compositionStart, compositionEnd));
373            super.setEditableText(
374                    text, selectionStart, selectionEnd, compositionStart, compositionEnd);
375        }
376
377        @Override
378        protected void updateSelection(
379                int selectionStart, int selectionEnd,
380                int compositionStart, int compositionEnd) {
381            mUpdateSelectionCounter++;
382        }
383    }
384
385    private static class TestInputMethodManagerWrapper extends InputMethodManagerWrapper {
386        private ContentViewCore mContentViewCore;
387        private InputConnection mInputConnection;
388        private int mShowSoftInputCounter = 0;
389        private EditorInfo mEditorInfo;
390
391        public TestInputMethodManagerWrapper(ContentViewCore contentViewCore) {
392            super(null);
393            mContentViewCore = contentViewCore;
394        }
395
396        @Override
397        public void restartInput(View view) {
398            mEditorInfo = new EditorInfo();
399            mInputConnection = mContentViewCore.onCreateInputConnection(mEditorInfo);
400        }
401
402        @Override
403        public void showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
404            mShowSoftInputCounter++;
405            if (mInputConnection != null) return;
406            mEditorInfo = new EditorInfo();
407            mInputConnection = mContentViewCore.onCreateInputConnection(mEditorInfo);
408        }
409
410        @Override
411        public boolean isActive(View view) {
412            if (mInputConnection == null) return false;
413            return true;
414        }
415
416        @Override
417        public boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
418                ResultReceiver resultReceiver) {
419            boolean retVal = mInputConnection == null;
420            mInputConnection = null;
421            return retVal;
422        }
423
424        @Override
425        public void updateSelection(View view, int selStart, int selEnd,
426                int candidatesStart, int candidatesEnd) {
427        }
428    }
429
430    private static class TestImeState {
431        private final String mText;
432        private final int mSelectionStart;
433        private final int mSelectionEnd;
434        private final int mCompositionStart;
435        private final int mCompositionEnd;
436
437        public TestImeState(String text, int selectionStart, int selectionEnd,
438                int compositionStart, int compositionEnd) {
439            mText = text;
440            mSelectionStart = selectionStart;
441            mSelectionEnd = selectionEnd;
442            mCompositionStart = compositionStart;
443            mCompositionEnd = compositionEnd;
444        }
445
446        public void assertEqualState(String text, int selectionStart, int selectionEnd,
447                int compositionStart, int compositionEnd) {
448            assertEquals("Text did not match", mText, text);
449            assertEquals("Selection start did not match", mSelectionStart, selectionStart);
450            assertEquals("Selection end did not match", mSelectionEnd, selectionEnd);
451            assertEquals("Composition start did not match", mCompositionStart, compositionStart);
452            assertEquals("Composition end did not match", mCompositionEnd, compositionEnd);
453        }
454    }
455}
456