• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.inputmethod.cts;
18 
19 import static android.view.inputmethod.cts.util.TestUtils.runOnMainSync;
20 
21 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
22 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
23 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
24 import static com.android.cts.mockime.ImeEventStreamTestUtils.hideSoftInputMatcher;
25 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
26 import static com.android.cts.mockime.ImeEventStreamTestUtils.showSoftInputMatcher;
27 
28 import static com.google.common.truth.Truth.assertThat;
29 
30 import static org.junit.Assert.assertEquals;
31 import static org.junit.Assert.assertFalse;
32 import static org.junit.Assert.assertNotEquals;
33 import static org.junit.Assert.assertNotNull;
34 import static org.junit.Assert.assertTrue;
35 import static org.junit.Assert.fail;
36 
37 import android.graphics.Color;
38 import android.platform.test.annotations.AppModeSdkSandbox;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.inputmethod.EditorInfo;
42 import android.view.inputmethod.InputConnection;
43 import android.view.inputmethod.InputMethodManager;
44 import android.view.inputmethod.SurroundingText;
45 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
46 import android.view.inputmethod.cts.util.TestActivity;
47 import android.view.inputmethod.cts.util.UnlockScreenRule;
48 import android.widget.EditText;
49 import android.widget.LinearLayout;
50 
51 import androidx.annotation.NonNull;
52 import androidx.annotation.Nullable;
53 import androidx.test.filters.MediumTest;
54 import androidx.test.platform.app.InstrumentationRegistry;
55 
56 import com.android.cts.mockime.ImeEvent;
57 import com.android.cts.mockime.ImeEventStream;
58 import com.android.cts.mockime.ImeSettings;
59 import com.android.cts.mockime.MockImeSession;
60 
61 import org.junit.Rule;
62 import org.junit.Test;
63 
64 import java.util.concurrent.TimeUnit;
65 import java.util.concurrent.atomic.AtomicReference;
66 
67 /**
68  * Contains test cases for some special IME-related behaviors of {@link EditText}.
69  */
70 @MediumTest
71 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
72 public final class EditTextImeSupportTest extends EndToEndImeTestBase {
73     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
74     private static final long NOT_EXPECT_TIMEOUT = 10;  // msec
75 
76     @Rule
77     public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
78 
launchTestActivity(String marker, String initialText, int initialSelectionStart, int initialSelectionEnd)79     public EditText launchTestActivity(String marker, String initialText,
80             int initialSelectionStart, int initialSelectionEnd) {
81         final AtomicReference<EditText> editTextRef = new AtomicReference<>();
82         TestActivity.startSync(activity-> {
83             final LinearLayout layout = new LinearLayout(activity);
84             layout.setOrientation(LinearLayout.VERTICAL);
85 
86             final EditText editText = new EditText(activity);
87             editText.setPrivateImeOptions(marker);
88             editText.setHint("editText");
89             editText.setText(initialText);
90             editText.setSelection(initialSelectionStart, initialSelectionEnd);
91             editText.requestFocus();
92             editTextRef.set(editText);
93 
94             layout.addView(editText);
95             return layout;
96         });
97         return editTextRef.get();
98     }
99 
100     /**
101      * A regression test for Bug 161330778.
102      */
103     @Test
testSetTextTriggersRestartInput()104     public void testSetTextTriggersRestartInput() throws Exception {
105         try (MockImeSession imeSession = MockImeSession.create(
106                 InstrumentationRegistry.getInstrumentation().getContext(),
107                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
108                 new ImeSettings.Builder())) {
109             final ImeEventStream stream = imeSession.openEventStream();
110 
111             final String initialText = "0123456789";
112             final int initialSelectionStart = 3;
113             final int initialSelectionEnd = 7;
114             final String marker = getTestMarker();
115             final EditText editText = launchTestActivity(marker, initialText, initialSelectionStart,
116                     initialSelectionEnd);
117 
118             // 1nd onStartInput() should be with restarting == false, with the correct initial
119             // surrounding text information.
120             final ImeEvent firstOnStartInput = expectEvent(stream,
121                     editorMatcher("onStartInput", marker), TIMEOUT);
122             assertFalse(firstOnStartInput.getArguments().getBoolean("restarting"));
123 
124             // Verify the initial surrounding text info.
125             final EditorInfo initialEditorInfo =
126                     firstOnStartInput.getArguments().getParcelable("editorInfo");
127             assertNotNull(initialEditorInfo);
128             assertInitialSurroundingText(initialEditorInfo, initialText, initialSelectionStart,
129                     initialSelectionEnd);
130 
131             final String updatedText = "NewText";
132 
133             // Create a copy of the stream to verify that there is no onUpdateSelection().
134             stream.skipAll();
135             final ImeEventStream copiedStream = stream.copy();
136 
137             // This should trigger InputMethodManager#restartInput(), which triggers the 2nd
138             // onStartInput() with restarting == true.
139             runOnMainSync(() -> editText.setText(updatedText));
140             final ImeEvent secondOnStartInput = expectEvent(stream,
141                     editorMatcher("onStartInput", marker), TIMEOUT);
142             assertTrue(secondOnStartInput.getArguments().getBoolean("restarting"));
143 
144             // Verify the initial surrounding text after TextView#setText(). The cursor must be
145             // placed at the beginning of the new text.
146             final EditorInfo restartingEditorInfo =
147                     secondOnStartInput.getArguments().getParcelable("editorInfo");
148             assertNotNull(restartingEditorInfo);
149             assertInitialSurroundingText(restartingEditorInfo, updatedText, 0, 0);
150 
151             assertFalse("TextView#setText() must not trigger onUpdateSelection",
152                     copiedStream.findFirst(
153                             event -> "onUpdateSelection".equals(event.getEventName())).isPresent());
154         }
155     }
156 
assertInitialSurroundingText(@onNull EditorInfo editorInfo, @NonNull String expectedText, int expectedSelectionStart, int expectedSelectionEnd)157     private static void assertInitialSurroundingText(@NonNull EditorInfo editorInfo,
158             @NonNull String expectedText, int expectedSelectionStart, int expectedSelectionEnd) {
159 
160         assertNotEquals("expectedText must has a selection", -1, expectedSelectionStart);
161         assertNotEquals("expectedText must has a selection", -1, expectedSelectionEnd);
162 
163         final CharSequence expectedTextBeforeCursor =
164                 expectedText.subSequence(0, expectedSelectionStart);
165         final CharSequence expectedSelectedText =
166                 expectedText.subSequence(expectedSelectionStart, expectedSelectionEnd);
167         final CharSequence expectedTextAfterCursor =
168                 expectedText.subSequence(expectedSelectionEnd, expectedText.length());
169         final int expectedTextLength = expectedText.length();
170 
171         assertEqualsWithIgnoringSpans(expectedTextBeforeCursor,
172                 editorInfo.getInitialTextBeforeCursor(expectedTextLength, 0));
173         assertEqualsWithIgnoringSpans(expectedSelectedText,
174                 editorInfo.getInitialSelectedText(0));
175         assertEqualsWithIgnoringSpans(expectedTextAfterCursor,
176                 editorInfo.getInitialTextAfterCursor(expectedTextLength, 0));
177 
178         final SurroundingText initialSurroundingText =
179                 editorInfo.getInitialSurroundingText(expectedTextLength, expectedTextLength, 0);
180         assertNotNull(initialSurroundingText);
181         assertEqualsWithIgnoringSpans(expectedText, initialSurroundingText.getText());
182         assertEquals(expectedSelectionStart, initialSurroundingText.getSelectionStart());
183         assertEquals(expectedSelectionEnd, initialSurroundingText.getSelectionEnd());
184     }
185 
assertEqualsWithIgnoringSpans(@ullable CharSequence expected, @Nullable CharSequence actual)186     private static void assertEqualsWithIgnoringSpans(@Nullable CharSequence expected,
187             @Nullable CharSequence actual) {
188         if (expected == actual) {
189             return;
190         }
191         if (expected == null) {
192             fail("must be null but was " + actual);
193         }
194         if (actual == null) {
195             fail("must be " + expected + " but was null");
196         }
197         assertEquals(expected.toString(), actual.toString());
198     }
199 
200     /**
201      * Test when to see {@link EditorInfo#IME_FLAG_NAVIGATE_NEXT} and
202      * {@link EditorInfo#IME_FLAG_NAVIGATE_PREVIOUS}.
203      *
204      * <p>This is also a regression test for Bug 31099943.</p>
205      */
206     @Test
testNavigateFlags()207     public void testNavigateFlags() throws Exception {
208         try (MockImeSession imeSession = MockImeSession.create(
209                 InstrumentationRegistry.getInstrumentation().getContext(),
210                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
211                 new ImeSettings.Builder())) {
212             final ImeEventStream stream = imeSession.openEventStream();
213 
214             // For a single EditText, there should be no navigate flag
215             verifyNavigateFlags(stream, new Control[]{
216                     Control.FOCUSED_EDIT_TEXT,
217             }, false /* navigateNext */, false /* navigatePrevious */);
218 
219             // For two EditText controls, there should be one navigate flag depending on the
220             // geometry.
221             verifyNavigateFlags(stream, new Control[]{
222                     Control.FOCUSED_EDIT_TEXT,
223                     Control.EDIT_TEXT,
224             }, true /* navigateNext */, false /* navigatePrevious */);
225             verifyNavigateFlags(stream, new Control[]{
226                     Control.EDIT_TEXT,
227                     Control.FOCUSED_EDIT_TEXT,
228             }, false /* navigateNext */, true /* navigatePrevious */);
229 
230             // Non focusable View controls should be ignored when determining navigation flags.
231             verifyNavigateFlags(stream, new Control[]{
232                     Control.NON_FOCUSABLE_VIEW,
233                     Control.FOCUSED_EDIT_TEXT,
234                     Control.NON_FOCUSABLE_VIEW,
235             }, false /* navigateNext */, false /* navigatePrevious */);
236 
237             // Even focusable View controls should be ignored when determining navigation flags if
238             // View#onCheckIsTextEditor() returns false. (Regression test for Bug 31099943)
239             verifyNavigateFlags(stream, new Control[]{
240                     Control.FOCUSABLE_VIEW,
241                     Control.FOCUSED_EDIT_TEXT,
242                     Control.FOCUSABLE_VIEW,
243             }, false /* navigateNext */, false /* navigatePrevious */);
244         }
245     }
246 
247     private enum Control {
248         EDIT_TEXT,
249         FOCUSED_EDIT_TEXT,
250         FOCUSABLE_VIEW,
251         NON_FOCUSABLE_VIEW,
252     }
253 
verifyNavigateFlags(@onNull ImeEventStream stream, @NonNull Control[] controls, boolean navigateNext, boolean navigatePrevious)254     private void verifyNavigateFlags(@NonNull ImeEventStream stream, @NonNull Control[] controls,
255             boolean navigateNext, boolean navigatePrevious) throws Exception {
256         final String marker = getTestMarker();
257         TestActivity.startSync(activity-> {
258             final LinearLayout layout = new LinearLayout(activity);
259             layout.setOrientation(LinearLayout.VERTICAL);
260             for (Control control : controls) {
261                 switch (control) {
262                     case EDIT_TEXT:
263                     case FOCUSED_EDIT_TEXT: {
264                         final boolean focused = (Control.FOCUSED_EDIT_TEXT == control);
265                         final EditText editText = new EditText(activity);
266                         editText.setHint("editText");
267                         layout.addView(editText);
268                         if (focused) {
269                             editText.setPrivateImeOptions(marker);
270                             editText.requestFocus();
271                         }
272                         break;
273                     }
274                     case FOCUSABLE_VIEW:
275                     case NON_FOCUSABLE_VIEW: {
276                         final boolean focusable = (Control.FOCUSABLE_VIEW == control);
277                         final View view = new View(activity);
278                         view.setBackgroundColor(focusable ? Color.YELLOW : Color.RED);
279                         view.setFocusable(focusable);
280                         view.setFocusableInTouchMode(focusable);
281                         view.setLayoutParams(new ViewGroup.LayoutParams(
282                                 ViewGroup.LayoutParams.MATCH_PARENT, 10 /* height */));
283                         layout.addView(view);
284                         break;
285                     }
286                     default:
287                         throw new UnsupportedOperationException("Unknown control=" + control);
288                 }
289             }
290             return layout;
291         });
292 
293         final ImeEvent startInput = expectEvent(stream,
294                 editorMatcher("onStartInput", marker), TIMEOUT);
295         final EditorInfo editorInfo = startInput.getArguments().getParcelable("editorInfo");
296         assertThat(editorInfo).isNotNull();
297         assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT)
298                 .isEqualTo(navigateNext ? EditorInfo.IME_FLAG_NAVIGATE_NEXT : 0);
299         assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS)
300                 .isEqualTo(navigatePrevious ? EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS : 0);
301     }
302 
303     /**
304      * Regression test for Bug 209958658.
305      */
306     @Test
testEndBatchEditReturnValue()307     public void testEndBatchEditReturnValue() {
308         EditText editText = new EditText(InstrumentationRegistry.getInstrumentation().getContext());
309         EditorInfo editorInfo = new EditorInfo();
310         InputConnection editableInputConnection = editText.onCreateInputConnection(editorInfo);
311         assertThat(editableInputConnection.beginBatchEdit()).isTrue();
312         assertThat(editableInputConnection.beginBatchEdit()).isTrue();
313         assertThat(editableInputConnection.endBatchEdit()).isTrue();
314         assertThat(editableInputConnection.endBatchEdit()).isFalse();
315 
316         // Extra invocations of endBatchEdit() continue to return false.
317         assertThat(editableInputConnection.endBatchEdit()).isFalse();
318     }
319 
320     /**
321      * Verifies that IME receives a hide request when an active {@link EditText} becomes
322      * disabled.
323      */
324     @Test
testHideSoftInputWhenDisabled()325     public void testHideSoftInputWhenDisabled() throws Exception {
326         try (MockImeSession imeSession = MockImeSession.create(
327                 InstrumentationRegistry.getInstrumentation().getContext(),
328                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
329                 new ImeSettings.Builder())) {
330             final ImeEventStream stream = imeSession.openEventStream();
331 
332             final String marker = getTestMarker();
333             final AtomicReference<EditText> editTextRef = new AtomicReference<>();
334             TestActivity.startSync(activity-> {
335                 final LinearLayout layout = new LinearLayout(activity);
336                 layout.setOrientation(LinearLayout.VERTICAL);
337 
338                 final EditText editText = new EditText(activity);
339                 editText.setPrivateImeOptions(marker);
340                 editText.requestFocus();
341                 editTextRef.set(editText);
342 
343                 layout.addView(editText);
344                 return layout;
345             });
346             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
347 
348             final var editText = editTextRef.get();
349             runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class)
350                     .showSoftInput(editText, 0));
351             expectEvent(stream, showSoftInputMatcher(0), TIMEOUT);
352 
353             runOnMainSync(() -> editText.setEnabled(false));
354             expectEvent(stream, hideSoftInputMatcher(), TIMEOUT);
355         }
356     }
357 
358     /**
359      * Verifies that IME receives a hide request when an active {@link EditText} receives
360      * {@link EditorInfo#IME_ACTION_DONE}.
361      */
362     @Test
testHideSoftInputByActionDone()363     public void testHideSoftInputByActionDone() throws Exception {
364         try (MockImeSession imeSession = MockImeSession.create(
365                 InstrumentationRegistry.getInstrumentation().getContext(),
366                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
367                 new ImeSettings.Builder())) {
368             final ImeEventStream stream = imeSession.openEventStream();
369 
370             final String marker = getTestMarker();
371             final AtomicReference<EditText> editTextRef = new AtomicReference<>();
372             TestActivity.startSync(activity-> {
373                 final LinearLayout layout = new LinearLayout(activity);
374                 layout.setOrientation(LinearLayout.VERTICAL);
375 
376                 final EditText editText = new EditText(activity);
377                 editText.setPrivateImeOptions(marker);
378                 editText.requestFocus();
379                 editTextRef.set(editText);
380 
381                 layout.addView(editText);
382                 return layout;
383             });
384             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
385 
386             final var editText = editTextRef.get();
387             runOnMainSync(() -> editText.getContext().getSystemService(InputMethodManager.class)
388                     .showSoftInput(editText, 0));
389             expectEvent(stream, showSoftInputMatcher(0), TIMEOUT);
390 
391             expectCommand(stream, imeSession.callPerformEditorAction(EditorInfo.IME_ACTION_DONE),
392                     TIMEOUT);
393             expectEvent(stream, hideSoftInputMatcher(), TIMEOUT);
394         }
395     }
396 
397     /**
398      * Verifies that disabling an {@link EditText} after its losing input focus will not hide the
399      * software keyboard.
400      *
401      * <p>This is a simplified repro code of Bug 332912075.</p>
402      */
403     @Test
testDisableImeAfterFocusChange()404     public void testDisableImeAfterFocusChange() throws Exception {
405         try (MockImeSession imeSession = MockImeSession.create(
406                 InstrumentationRegistry.getInstrumentation().getContext(),
407                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
408                 new ImeSettings.Builder())) {
409             final ImeEventStream stream = imeSession.openEventStream();
410 
411             final String marker1 = getTestMarker("EditText1");
412             final String marker2 = getTestMarker("EditText2");
413             final AtomicReference<EditText> editTextRef1 = new AtomicReference<>();
414             final AtomicReference<EditText> editTextRef2 = new AtomicReference<>();
415             TestActivity.startSync(activity-> {
416                 final LinearLayout layout = new LinearLayout(activity);
417                 layout.setOrientation(LinearLayout.VERTICAL);
418                 {
419                     final EditText editText = new EditText(activity);
420                     editText.setPrivateImeOptions(marker1);
421                     editText.requestFocus();
422                     editTextRef1.set(editText);
423                     layout.addView(editText);
424                 }
425                 {
426                     final EditText editText = new EditText(activity);
427                     editText.setPrivateImeOptions(marker2);
428                     editTextRef2.set(editText);
429                     layout.addView(editText);
430                 }
431                 return layout;
432             });
433             final var editText1 = editTextRef1.get();
434             final var editText2 = editTextRef2.get();
435 
436             expectEvent(stream, editorMatcher("onStartInput", marker1), TIMEOUT);
437             notExpectEvent(stream, showSoftInputMatcher(0), NOT_EXPECT_TIMEOUT);
438 
439             // Make sure to show the IME.
440             runOnMainSync(() -> editText1.getContext().getSystemService(InputMethodManager.class)
441                     .showSoftInput(editText1, 0));
442             expectEvent(stream, showSoftInputMatcher(0), TIMEOUT);
443 
444             runOnMainSync(() -> {
445                 editText2.requestFocus();
446                 editText1.setEnabled(false);
447             });
448 
449             var forkedStream = stream.copy();
450             expectEvent(stream, editorMatcher("onStartInput", marker2), TIMEOUT);
451             notExpectEvent(forkedStream, hideSoftInputMatcher(), NOT_EXPECT_TIMEOUT);
452         }
453     }
454 }
455