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