Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2017 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.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
     20 import static android.view.inputmethod.cts.util.TestUtils.runOnMainSync;
     21 import static android.widget.PopupWindow.INPUT_METHOD_NOT_NEEDED;
     22 
     23 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
     24 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput;
     25 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
     26 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
     27 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
     28 
     29 import static org.junit.Assert.assertFalse;
     30 
     31 import android.app.Instrumentation;
     32 import android.content.Context;
     33 import android.os.Build;
     34 import android.os.IBinder;
     35 import android.os.Process;
     36 import android.os.SystemClock;
     37 import android.support.test.InstrumentationRegistry;
     38 import android.support.test.filters.MediumTest;
     39 import android.support.test.runner.AndroidJUnit4;
     40 import android.text.TextUtils;
     41 import android.view.View;
     42 import android.view.inputmethod.EditorInfo;
     43 import android.view.inputmethod.InputMethodManager;
     44 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
     45 import android.view.inputmethod.cts.util.TestActivity;
     46 import android.view.inputmethod.cts.util.TestUtils;
     47 import android.view.inputmethod.cts.util.WindowFocusStealer;
     48 import android.widget.EditText;
     49 import android.widget.LinearLayout;
     50 import android.widget.PopupWindow;
     51 import android.widget.TextView;
     52 
     53 import com.android.compatibility.common.util.CtsTouchUtils;
     54 import com.android.cts.mockime.ImeCommand;
     55 import com.android.cts.mockime.ImeEvent;
     56 import com.android.cts.mockime.ImeEventStream;
     57 import com.android.cts.mockime.ImeSettings;
     58 import com.android.cts.mockime.MockImeSession;
     59 
     60 import org.junit.Test;
     61 import org.junit.runner.RunWith;
     62 
     63 import java.util.concurrent.TimeUnit;
     64 import java.util.concurrent.atomic.AtomicReference;
     65 
     66 @MediumTest
     67 @RunWith(AndroidJUnit4.class)
     68 public class FocusHandlingTest extends EndToEndImeTestBase {
     69     static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
     70     static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
     71 
     72     private static final String TEST_MARKER_PREFIX =
     73         "android.view.inputmethod.cts.FocusHandlingTest";
     74 
     75     public EditText launchTestActivity(String marker) {
     76         final AtomicReference<EditText> editTextRef = new AtomicReference<>();
     77         TestActivity.startSync(activity-> {
     78             final LinearLayout layout = new LinearLayout(activity);
     79             layout.setOrientation(LinearLayout.VERTICAL);
     80 
     81             final EditText editText = new EditText(activity);
     82             editText.setPrivateImeOptions(marker);
     83             editText.setHint("editText");
     84             editText.requestFocus();
     85             editTextRef.set(editText);
     86 
     87             layout.addView(editText);
     88             return layout;
     89         });
     90         return editTextRef.get();
     91     }
     92 
     93     private static String getTestMarker() {
     94         return TEST_MARKER_PREFIX + "/"  + SystemClock.elapsedRealtimeNanos();
     95     }
     96 
     97     @Test
     98     public void testOnStartInputCalledOnceIme() throws Exception {
     99         try (MockImeSession imeSession = MockImeSession.create(
    100                 InstrumentationRegistry.getContext(),
    101                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
    102                 new ImeSettings.Builder())) {
    103             final ImeEventStream stream = imeSession.openEventStream();
    104 
    105             final String marker = getTestMarker();
    106             final EditText editText = launchTestActivity(marker);
    107 
    108             // Wait until the MockIme gets bound to the TestActivity.
    109             expectBindInput(stream, Process.myPid(), TIMEOUT);
    110 
    111             // Emulate tap event
    112             CtsTouchUtils.emulateTapOnViewCenter(
    113                     InstrumentationRegistry.getInstrumentation(), editText);
    114 
    115             // Wait until "onStartInput" gets called for the EditText.
    116             final ImeEvent onStart =
    117                     expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
    118 
    119             assertFalse(stream.dump(), onStart.getEnterState().hasDummyInputConnection());
    120             assertFalse(stream.dump(), onStart.getArguments().getBoolean("restarting"));
    121 
    122             // There shouldn't be onStartInput any more.
    123             notExpectEvent(stream, editorMatcher("onStartInput", marker), NOT_EXPECT_TIMEOUT);
    124         }
    125     }
    126 
    127     @Test
    128     public void testSoftInputStateAlwaysVisibleWithoutFocusedEditorView() throws Exception {
    129         try (MockImeSession imeSession = MockImeSession.create(
    130                 InstrumentationRegistry.getContext(),
    131                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
    132                 new ImeSettings.Builder())) {
    133             final ImeEventStream stream = imeSession.openEventStream();
    134 
    135             final String marker = getTestMarker();
    136             final TestActivity testActivity = TestActivity.startSync(activity -> {
    137                 final LinearLayout layout = new LinearLayout(activity);
    138                 layout.setOrientation(LinearLayout.VERTICAL);
    139 
    140                 final TextView textView = new TextView(activity) {
    141                     @Override
    142                     public boolean onCheckIsTextEditor() {
    143                         return false;
    144                     }
    145                 };
    146                 textView.setText("textView");
    147                 textView.setPrivateImeOptions(marker);
    148                 textView.requestFocus();
    149 
    150                 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
    151                 layout.addView(textView);
    152                 return layout;
    153             });
    154 
    155             if (testActivity.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
    156                 // Input shouldn't start
    157                 notExpectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
    158                 // There shouldn't be onStartInput because the focused view is not an editor.
    159                 notExpectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
    160             } else {
    161                 // Wait until the MockIme gets bound to the TestActivity.
    162                 expectBindInput(stream, Process.myPid(), TIMEOUT);
    163                 // For apps that target pre-P devices, onStartInput() should be called.
    164                 expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
    165             }
    166         }
    167     }
    168 
    169     @Test
    170     public void testEditorStartsInput() throws Exception {
    171         try (MockImeSession imeSession = MockImeSession.create(
    172                 InstrumentationRegistry.getContext(),
    173                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
    174                 new ImeSettings.Builder())) {
    175             final ImeEventStream stream = imeSession.openEventStream();
    176 
    177             final String marker = getTestMarker();
    178             TestActivity.startSync(activity -> {
    179                 final LinearLayout layout = new LinearLayout(activity);
    180                 layout.setOrientation(LinearLayout.VERTICAL);
    181 
    182                 final EditText editText = new EditText(activity);
    183                 editText.setPrivateImeOptions(marker);
    184                 editText.setText("Editable");
    185                 editText.requestFocus();
    186                 layout.addView(editText);
    187                 return layout;
    188             });
    189 
    190             // Input should start
    191             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
    192         }
    193     }
    194 
    195     @Test
    196     public void testSoftInputStateAlwaysVisibleFocusedEditorView() throws Exception {
    197         try (MockImeSession imeSession = MockImeSession.create(
    198                 InstrumentationRegistry.getContext(),
    199                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
    200                 new ImeSettings.Builder())) {
    201             final ImeEventStream stream = imeSession.openEventStream();
    202 
    203             TestActivity.startSync(activity -> {
    204                 final LinearLayout layout = new LinearLayout(activity);
    205                 layout.setOrientation(LinearLayout.VERTICAL);
    206 
    207                 final EditText editText = new EditText(activity);
    208                 editText.setText("editText");
    209                 editText.requestFocus();
    210 
    211                 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
    212                 layout.addView(editText);
    213                 return layout;
    214             });
    215 
    216             // Wait until the MockIme gets bound to the TestActivity.
    217             expectBindInput(stream, Process.myPid(), TIMEOUT);
    218 
    219             expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
    220         }
    221     }
    222 
    223     /**
    224      * Makes sure that an existing {@link android.view.inputmethod.InputConnection} will not be
    225      * invalidated by showing a focusable {@link PopupWindow} with
    226      * {@link PopupWindow#INPUT_METHOD_NOT_NEEDED}.
    227      *
    228      * <p>If {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} is set and
    229      * {@link android.view.WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} is not set to a
    230      * {@link android.view.Window}, showing that window must not invalidate an existing valid
    231      * {@link android.view.inputmethod.InputConnection}.</p>
    232      *
    233      * @see android.view.WindowManager.LayoutParams#mayUseInputMethod(int)
    234      */
    235     @Test
    236     public void testFocusableWindowDoesNotInvalidateExistingInputConnection() throws Exception {
    237         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    238         try (MockImeSession imeSession = MockImeSession.create(
    239                 InstrumentationRegistry.getContext(),
    240                 instrumentation.getUiAutomation(),
    241                 new ImeSettings.Builder())) {
    242             final ImeEventStream stream = imeSession.openEventStream();
    243 
    244             final String marker = getTestMarker();
    245             final EditText editText = launchTestActivity(marker);
    246             instrumentation.runOnMainSync(() -> editText.requestFocus());
    247 
    248             // Wait until the MockIme gets bound to the TestActivity.
    249             expectBindInput(stream, Process.myPid(), TIMEOUT);
    250 
    251             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
    252 
    253             // Make sure that InputConnection#commitText() works.
    254             final ImeCommand commit1 = imeSession.callCommitText("test commit", 1);
    255             expectCommand(stream, commit1, TIMEOUT);
    256             TestUtils.waitOnMainUntil(
    257                     () -> TextUtils.equals(editText.getText(), "test commit"), TIMEOUT);
    258             instrumentation.runOnMainSync(() -> editText.setText(""));
    259 
    260             // Create a popup window that cannot be the IME target.
    261             final PopupWindow popupWindow = TestUtils.getOnMainSync(() -> {
    262                 final Context context = instrumentation.getTargetContext();
    263                 final PopupWindow popup = new PopupWindow(context);
    264                 popup.setFocusable(true);
    265                 popup.setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
    266                 final TextView textView = new TextView(context);
    267                 textView.setText("Test Text");
    268                 popup.setContentView(textView);
    269                 return popup;
    270             });
    271 
    272             // Show the popup window.
    273             instrumentation.runOnMainSync(() -> popupWindow.showAsDropDown(editText));
    274             instrumentation.waitForIdleSync();
    275 
    276             // Make sure that the EditText no longer has window-focus
    277             TestUtils.waitOnMainUntil(() -> !editText.hasWindowFocus(), TIMEOUT);
    278 
    279             // Make sure that InputConnection#commitText() works.
    280             final ImeCommand commit2 = imeSession.callCommitText("Hello!", 1);
    281             expectCommand(stream, commit2, TIMEOUT);
    282             TestUtils.waitOnMainUntil(
    283                     () -> TextUtils.equals(editText.getText(), "Hello!"), TIMEOUT);
    284             instrumentation.runOnMainSync(() -> editText.setText(""));
    285 
    286             stream.skipAll();
    287 
    288             final String marker2 = getTestMarker();
    289             // Call InputMethodManager#restartInput()
    290             instrumentation.runOnMainSync(() -> {
    291                 editText.setPrivateImeOptions(marker2);
    292                 editText.getContext()
    293                         .getSystemService(InputMethodManager.class)
    294                         .restartInput(editText);
    295             });
    296 
    297             // Make sure that onStartInput() is called with restarting == true.
    298             expectEvent(stream, event -> {
    299                 if (!TextUtils.equals("onStartInput", event.getEventName())) {
    300                     return false;
    301                 }
    302                 if (!event.getArguments().getBoolean("restarting")) {
    303                     return false;
    304                 }
    305                 final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo");
    306                 return TextUtils.equals(marker2, editorInfo.privateImeOptions);
    307             }, TIMEOUT);
    308 
    309             // Make sure that InputConnection#commitText() works.
    310             final ImeCommand commit3 = imeSession.callCommitText("World!", 1);
    311             expectCommand(stream, commit3, TIMEOUT);
    312             TestUtils.waitOnMainUntil(
    313                     () -> TextUtils.equals(editText.getText(), "World!"), TIMEOUT);
    314             instrumentation.runOnMainSync(() -> editText.setText(""));
    315 
    316             // Dismiss the popup window.
    317             instrumentation.runOnMainSync(() -> popupWindow.dismiss());
    318             instrumentation.waitForIdleSync();
    319 
    320             // Make sure that the EditText now has window-focus again.
    321             TestUtils.waitOnMainUntil(() -> editText.hasWindowFocus(), TIMEOUT);
    322 
    323             // Make sure that InputConnection#commitText() works.
    324             final ImeCommand commit4 = imeSession.callCommitText("Done!", 1);
    325             expectCommand(stream, commit4, TIMEOUT);
    326             TestUtils.waitOnMainUntil(
    327                     () -> TextUtils.equals(editText.getText(), "Done!"), TIMEOUT);
    328             instrumentation.runOnMainSync(() -> editText.setText(""));
    329         }
    330     }
    331 
    332     /**
    333      * Test case for Bug 70629102.
    334      *
    335      * {@link InputMethodManager#restartInput(View)} can be called even when another process
    336      * temporarily owns focused window. {@link InputMethodManager} should continue to work after
    337      * the IME target application gains window focus again.
    338      */
    339     @Test
    340     public void testRestartInputWhileOtherProcessHasWindowFocus() throws Exception {
    341         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    342         try (MockImeSession imeSession = MockImeSession.create(
    343                 InstrumentationRegistry.getContext(),
    344                 instrumentation.getUiAutomation(),
    345                 new ImeSettings.Builder())) {
    346             final ImeEventStream stream = imeSession.openEventStream();
    347 
    348             final String marker = getTestMarker();
    349             final EditText editText = launchTestActivity(marker);
    350             instrumentation.runOnMainSync(() -> editText.requestFocus());
    351 
    352             // Wait until the MockIme gets bound to the TestActivity.
    353             expectBindInput(stream, Process.myPid(), TIMEOUT);
    354 
    355             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
    356 
    357             // Get app window token
    358             final IBinder appWindowToken = TestUtils.getOnMainSync(
    359                     () -> editText.getApplicationWindowToken());
    360 
    361             try (WindowFocusStealer focusStealer =
    362                          WindowFocusStealer.connect(instrumentation.getTargetContext(), TIMEOUT)) {
    363 
    364                 focusStealer.stealWindowFocus(appWindowToken, TIMEOUT);
    365 
    366                 // Wait until the edit text loses window focus.
    367                 TestUtils.waitOnMainUntil(() -> !editText.hasWindowFocus(), TIMEOUT);
    368 
    369                 // Call InputMethodManager#restartInput()
    370                 instrumentation.runOnMainSync(() -> {
    371                     editText.getContext()
    372                             .getSystemService(InputMethodManager.class)
    373                             .restartInput(editText);
    374                 });
    375             }
    376 
    377             // Wait until the edit text gains window focus again.
    378             TestUtils.waitOnMainUntil(() -> editText.hasWindowFocus(), TIMEOUT);
    379 
    380             // Make sure that InputConnection#commitText() still works.
    381             final ImeCommand command = imeSession.callCommitText("test commit", 1);
    382             expectCommand(stream, command, TIMEOUT);
    383 
    384             TestUtils.waitOnMainUntil(
    385                     () -> TextUtils.equals(editText.getText(), "test commit"), TIMEOUT);
    386         }
    387     }
    388 
    389     /**
    390      * Test {@link EditText#setShowSoftInputOnFocus(boolean)}.
    391      */
    392     @Test
    393     public void testSetShowInputOnFocus() throws Exception {
    394         try (MockImeSession imeSession = MockImeSession.create(
    395                 InstrumentationRegistry.getContext(),
    396                 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
    397                 new ImeSettings.Builder())) {
    398             final ImeEventStream stream = imeSession.openEventStream();
    399 
    400             final String marker = getTestMarker();
    401             final EditText editText = launchTestActivity(marker);
    402             runOnMainSync(() -> editText.setShowSoftInputOnFocus(false));
    403 
    404             // Wait until "onStartInput" gets called for the EditText.
    405             expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
    406 
    407             // Emulate tap event
    408             CtsTouchUtils.emulateTapOnViewCenter(
    409                     InstrumentationRegistry.getInstrumentation(), editText);
    410 
    411             // "showSoftInput" must not happen when setShowSoftInputOnFocus(false) is called.
    412             notExpectEvent(stream, event -> "showSoftInput".equals(event.getEventName()),
    413                     NOT_EXPECT_TIMEOUT);
    414         }
    415     }
    416 }
    417