1 /* 2 * Copyright (C) 2022 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.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; 21 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeInvisible; 22 import static android.view.inputmethod.cts.util.InputMethodVisibilityVerifier.expectImeVisible; 23 24 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 25 26 import static com.google.common.truth.Truth.assertThat; 27 import static com.google.common.truth.Truth.assertWithMessage; 28 29 import static org.junit.Assert.fail; 30 31 import android.app.Activity; 32 import android.app.Instrumentation; 33 import android.platform.test.annotations.AppModeSdkSandbox; 34 import android.platform.test.annotations.RequiresFlagsDisabled; 35 import android.platform.test.flag.junit.CheckFlagsRule; 36 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 37 import android.support.test.uiautomator.UiDevice; 38 import android.view.MotionEvent; 39 import android.view.WindowInsets; 40 import android.view.WindowInsetsController.OnControllableInsetsChangedListener; 41 import android.view.WindowManager; 42 import android.view.inputmethod.Flags; 43 import android.view.inputmethod.InputMethodManager; 44 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 45 import android.view.inputmethod.cts.util.MetricsRecorder; 46 import android.view.inputmethod.cts.util.TestActivity; 47 import android.view.inputmethod.cts.util.TestUtils; 48 import android.view.inputmethod.nano.ImeProtoEnums; 49 import android.widget.EditText; 50 import android.widget.LinearLayout; 51 import android.widget.TextView; 52 53 import androidx.annotation.NonNull; 54 import androidx.test.ext.junit.runners.AndroidJUnit4; 55 import androidx.test.platform.app.InstrumentationRegistry; 56 57 import com.android.compatibility.common.util.PollingCheck; 58 import com.android.cts.mockime.ImeSettings; 59 import com.android.cts.mockime.MockImeSession; 60 import com.android.os.nano.AtomsProto; 61 62 import org.junit.After; 63 import org.junit.Before; 64 import org.junit.Rule; 65 import org.junit.Test; 66 import org.junit.runner.RunWith; 67 68 import java.util.List; 69 import java.util.concurrent.CountDownLatch; 70 import java.util.concurrent.TimeUnit; 71 72 /** 73 * Test suite to ensure IME stats get tracked and logged correctly. 74 */ 75 @RunWith(AndroidJUnit4.class) 76 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 77 public class InputMethodStatsTest extends EndToEndImeTestBase { 78 79 private static final String TAG = "InputMethodStatsTest"; 80 81 @Rule 82 public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); 83 84 private static final int EDIT_TEXT_ID = 1; 85 private static final int TEXT_VIEW_ID = 2; 86 87 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); 88 89 /** Time to wait for statsd to setup. */ 90 private static final long WAIT_TIME_LONG = 1000; 91 92 /** Time to wait for atoms to be reported. */ 93 private static final long WAIT_TIME_SHORT = 500; 94 95 private Instrumentation mInstrumentation; 96 97 /** The test app package name from which atoms will be logged. */ 98 private String mPkgName; 99 100 @Before setUp()101 public void setUp() throws Exception { 102 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 103 mPkgName = mInstrumentation.getContext().getPackageName(); 104 105 // Finish tracking any pending IME visibility requests from previous tests to avoid issues. 106 final var imm = mInstrumentation.getContext().getSystemService(InputMethodManager.class); 107 runWithShellPermissionIdentity(() -> { 108 imm.finishTrackingPendingImeVisibilityRequests(); 109 }); 110 111 MetricsRecorder.removeConfig(); 112 MetricsRecorder.clearReports(); 113 // TODO(b/330143218): Add a proper fence for statsd 114 Thread.sleep(WAIT_TIME_LONG); 115 } 116 117 @After tearDown()118 public void tearDown() throws Exception { 119 MetricsRecorder.removeConfig(); 120 MetricsRecorder.clearReports(); 121 122 mInstrumentation = null; 123 mPkgName = ""; 124 } 125 126 /** 127 * Creates and launches a test activity. 128 * 129 * @param mode the {@link WindowManager.LayoutParams#softInputMode softInputMode} for the 130 * activity. 131 * 132 * @return the created activity. 133 */ createTestActivity(final int mode)134 private TestActivity createTestActivity(final int mode) { 135 return TestActivity.startSync(activity -> createLayout(mode, activity)); 136 } 137 138 /** 139 * Creates a linear layout with one EditText. 140 * 141 * @param mode the {@link WindowManager.LayoutParams#softInputMode softInputMode} for the 142 * activity. 143 * @param activity the activity to create the layout for. 144 * 145 * @return the created layout. 146 */ createLayout(final int mode, final Activity activity)147 private LinearLayout createLayout(final int mode, final Activity activity) { 148 final var layout = new LinearLayout(activity); 149 layout.setOrientation(LinearLayout.VERTICAL); 150 151 final var editText = new EditText(activity); 152 editText.setId(EDIT_TEXT_ID); 153 editText.setText("Editable"); 154 155 final var textView = new TextView(activity); 156 textView.setId(TEXT_VIEW_ID); 157 textView.setText("Not Editable"); 158 159 layout.addView(editText); 160 layout.addView(textView); 161 editText.requestFocus(); 162 activity.getWindow().setSoftInputMode(mode); 163 return layout; 164 } 165 166 /** 167 * Waits for the given inset type to be controllable on the given activity's 168 * {@link android.view.WindowInsetsController}. 169 * 170 * @param type the inset type waiting to be controllable. 171 * @param activity the activity whose Window Insets Controller to wait on. 172 * 173 * @implNote This is used to avoid the case where 174 * {@link android.view.InsetsController#show(int)} 175 * is called before IME insets control is available, starting a more complex flow which is 176 * currently harder to track with the {@link com.android.server.inputmethod.ImeTrackerService} 177 * system. 178 * 179 * TODO(b/263069667): Remove this method when the ImeInsetsSourceConsumer show flow is fixed. 180 */ awaitControl(final int type, final Activity activity)181 private void awaitControl(final int type, final Activity activity) { 182 final var latch = new CountDownLatch(1); 183 final OnControllableInsetsChangedListener listener = (controller, typeMask) -> { 184 if ((typeMask & type) != 0) { 185 latch.countDown(); 186 } 187 }; 188 TestUtils.runOnMainSync(() -> activity.getWindow() 189 .getDecorView() 190 .getWindowInsetsController() 191 .addOnControllableInsetsChangedListener(listener)); 192 193 try { 194 if (!latch.await(TIMEOUT, TimeUnit.SECONDS)) { 195 fail("IME insets controls not available"); 196 } 197 } catch (InterruptedException e) { 198 fail("Waiting for IMe insets controls to be available failed"); 199 } finally { 200 TestUtils.runOnMainSync(() -> activity.getWindow() 201 .getDecorView() 202 .getWindowInsetsController() 203 .removeOnControllableInsetsChangedListener(listener)); 204 } 205 } 206 207 /** 208 * Test the logging for an IME show request from the client. 209 */ 210 @Test 211 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testClientShowImeRequestFinished()212 public void testClientShowImeRequestFinished() throws Throwable { 213 verifyLogging(true /* show */, 214 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT), 215 false /* fromUser */, (imeSession, activity) -> { 216 awaitControl(WindowInsets.Type.ime(), activity); 217 expectImeInvisible(TIMEOUT); 218 219 TestUtils.runOnMainSync(() -> activity.getWindow() 220 .getDecorView() 221 .getWindowInsetsController() 222 .show(WindowInsets.Type.ime())); 223 224 expectImeVisible(TIMEOUT); 225 }); 226 } 227 228 /** 229 * Test the logging for an IME hide request from the client. 230 */ 231 @Test 232 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testClientHideImeRequestFinished()233 public void testClientHideImeRequestFinished() throws Exception { 234 verifyLogging(false /* show */, 235 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT), 236 false /* fromUser */, (imeSession, activity) -> { 237 TestUtils.runOnMainSync(() -> activity.getWindow() 238 .getDecorView() 239 .getWindowInsetsController() 240 .hide(WindowInsets.Type.ime())); 241 242 expectImeInvisible(TIMEOUT); 243 }); 244 } 245 246 /** 247 * Test the logging for an IME show request from the server. 248 */ 249 @Test 250 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testServerShowImeRequestFinished()251 public void testServerShowImeRequestFinished() throws Exception { 252 verifyLogging(true /* show */, 253 List.of(ImeProtoEnums.ORIGIN_SERVER, ImeProtoEnums.ORIGIN_SERVER_START_INPUT), 254 false /* fromUser */, (imeSession, activity) -> { 255 createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 256 257 expectImeVisible(TIMEOUT); 258 }); 259 } 260 261 /** 262 * Test the logging for an IME hide request from the server. 263 */ 264 @Test 265 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testServerHideImeRequestFinished()266 public void testServerHideImeRequestFinished() throws Exception { 267 verifyLogging(false /* show */, 268 List.of(ImeProtoEnums.ORIGIN_SERVER, ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT), 269 false /* fromUser */, (imeSession, activity) -> { 270 imeSession.hideSoftInputFromServerForTest(); 271 272 expectImeInvisible(TIMEOUT); 273 }); 274 } 275 276 /** 277 * Test the logging for an IME show request from the IME. 278 */ 279 @Test 280 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testImeShowImeRequestFinished()281 public void testImeShowImeRequestFinished() throws Exception { 282 // In the past, the origin of this request was considered in the server. 283 verifyLogging(true /* show */, 284 List.of(ImeProtoEnums.ORIGIN_IME, ImeProtoEnums.ORIGIN_SERVER_START_INPUT), 285 false /* fromUser */, (imeSession, activity) -> { 286 imeSession.callRequestShowSelf(0 /* flags */); 287 288 expectImeVisible(TIMEOUT); 289 }); 290 291 } 292 293 /** 294 * Test the logging for an IME hide request from the IME. 295 */ 296 @Test 297 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testImeHideImeRequestFinished()298 public void testImeHideImeRequestFinished() throws Exception { 299 verifyLogging(false /* show */, 300 List.of(ImeProtoEnums.ORIGIN_IME, ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT), 301 false /* fromUser */, (imeSession, activity) -> { 302 imeSession.callRequestHideSelf(0 /* flags */); 303 304 expectImeInvisible(TIMEOUT); 305 }); 306 } 307 308 /** 309 * Test the logging for an IME show request from a user interaction using InputMethodManager. 310 */ 311 @Test 312 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testFromUser_withImm_showImeRequestFinished()313 public void testFromUser_withImm_showImeRequestFinished() throws Exception { 314 verifyLogging(true /* show */, 315 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT), 316 true /* fromUser */, (imeSession, activity) -> { 317 final EditText editText = activity.requireViewById(EDIT_TEXT_ID); 318 editText.setShowSoftInputOnFocus(false); 319 // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will 320 // be false. onTouchListener runs immediately, so the value will be true. 321 editText.setOnTouchListener((v, ev) -> { 322 // Three motion events are sent, only react to one of them. 323 if (ev.getAction() != MotionEvent.ACTION_DOWN) { 324 return false; 325 } 326 editText.getContext().getSystemService(InputMethodManager.class) 327 .showSoftInput(editText, 0 /* flags */); 328 return true; 329 }); 330 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, editText); 331 332 expectImeVisible(TIMEOUT); 333 }); 334 } 335 336 /** 337 * Test the logging for an IME hide request from a user interaction using InputMethodManager. 338 */ 339 @Test 340 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testFromUser_withImm_hideImeRequestFinished()341 public void testFromUser_withImm_hideImeRequestFinished() throws Exception { 342 verifyLogging(false /* show */, 343 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT), 344 true /* formUser */, (imeSession, activity) -> { 345 final TextView textView = activity.requireViewById(TEXT_VIEW_ID); 346 // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will 347 // be false. onTouchListener runs immediately, so the value will be true. 348 textView.setOnTouchListener((v, ev) -> { 349 // Three motion events are sent, only react to one of them. 350 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { 351 return false; 352 } 353 textView.getContext().getSystemService(InputMethodManager.class) 354 .hideSoftInputFromWindow(textView.getWindowToken(), 0 /* flags */); 355 return true; 356 }); 357 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, textView); 358 359 expectImeInvisible(TIMEOUT); 360 }); 361 } 362 363 /** 364 * Test the logging for an IME show request from a user interaction using 365 * WindowInsetsController. 366 */ 367 @Test 368 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testFromUser_withWic_showImeRequestFinished()369 public void testFromUser_withWic_showImeRequestFinished() throws Exception { 370 verifyLogging(true /* show */, 371 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT), 372 true /* fromUser */, (imeSession, activity) -> { 373 final EditText editText = activity.requireViewById(EDIT_TEXT_ID); 374 editText.setShowSoftInputOnFocus(false); 375 // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will 376 // be false. onTouchListener runs immediately, so the value will be true. 377 editText.setOnTouchListener((v, ev) -> { 378 // Three motion events are sent, only react to one of them. 379 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { 380 return false; 381 } 382 activity.getWindow().getInsetsController().show(WindowInsets.Type.ime()); 383 return true; 384 }); 385 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, editText); 386 387 expectImeVisible(TIMEOUT); 388 }); 389 } 390 391 /** 392 * Test the logging for an IME hide request from a user interaction using 393 * WindowInsetsController. 394 */ 395 @Test 396 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testFromUser_withWic_hideImeRequestFinished()397 public void testFromUser_withWic_hideImeRequestFinished() throws Exception { 398 verifyLogging(false /* show */, 399 List.of(ImeProtoEnums.ORIGIN_CLIENT, ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT), 400 true /* fromUser */, (imeSession, activity) -> { 401 final TextView textView = activity.requireViewById(TEXT_VIEW_ID); 402 // onClickListener is run later, so ViewRootImpl#isHandlingPointeEvent will 403 // be false. onTouchListener runs immediately, so the value will be true. 404 textView.setOnTouchListener((v, ev) -> { 405 // Three motion events are sent, only react to one of them. 406 if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { 407 return false; 408 } 409 activity.getWindow().getInsetsController().hide(WindowInsets.Type.ime()); 410 return true; 411 }); 412 mCtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, null, textView); 413 414 expectImeInvisible(TIMEOUT); 415 }); 416 } 417 418 /** 419 * Test the logging for an IME hide request from a user interaction using back button press. 420 */ 421 @Test 422 @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) testFromUser_withBackPress_hideImeRequestFinished()423 public void testFromUser_withBackPress_hideImeRequestFinished() throws Exception { 424 verifyLogging(false /* show */, 425 List.of(ImeProtoEnums.ORIGIN_IME, ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT), 426 true /* fromUser */, (imeSession, activity) -> { 427 UiDevice.getInstance(mInstrumentation) 428 .pressBack(); 429 430 expectImeInvisible(TIMEOUT); 431 }); 432 } 433 434 /** 435 * Verifies the logged atom events for the given test runnable and expected values. 436 * 437 * @param show whether this is testing a show request (starts with IME hidden), 438 * or hide request (starts with IME shown). 439 * @param origins the expected IME request origins. This is a list of possible origins, 440 * to also allow previously deprecated ones. 441 * @param fromUser whether this request is expected to be created from user interaction. 442 * @param runnable the runnable with the test code to execute. 443 */ verifyLogging(boolean show, @NonNull List<Integer> origins, boolean fromUser, @NonNull TestRunnable runnable)444 private void verifyLogging(boolean show, @NonNull List<Integer> origins, boolean fromUser, 445 @NonNull TestRunnable runnable) throws Exception { 446 // Create mockImeSession to decouple from real IMEs, 447 // and enable calling expectImeVisible and expectImeInvisible. 448 try (var imeSession = MockImeSession.create( 449 mInstrumentation.getContext(), 450 mInstrumentation.getUiAutomation(), 451 new ImeSettings.Builder())) { 452 // Wait for any outstanding IME requests to finish, to not interfere with test. 453 PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(), 454 "Test Setup Failed: There should be no pending IME requests present when the " 455 + "test starts."); 456 457 final TestActivity activity; 458 if (show) { 459 // Use STATE_UNCHANGED to not trigger any other IME requests. 460 activity = createTestActivity(SOFT_INPUT_STATE_UNCHANGED); 461 expectImeInvisible(TIMEOUT); 462 } else { 463 // If running a hide test, start with the IME showing already. 464 activity = createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 465 expectImeVisible(TIMEOUT); 466 // Wait for any outstanding IME requests to finish, to capture all atoms. 467 PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(), 468 "Test Error: Pending IME requests took too long, likely timing out."); 469 } 470 471 // Wait for any atoms from activity start to be sent. 472 // TODO(b/330143218): Add a proper fence for statsd 473 Thread.sleep(WAIT_TIME_SHORT); 474 475 // Expect atoms pushed from either the IME process, or from the test app process. 476 MetricsRecorder.uploadConfigForPushedAtomWithUid( 477 new String[]{mPkgName, imeSession.getMockImePackageName()}, 478 AtomsProto.Atom.IME_REQUEST_FINISHED_FIELD_NUMBER, 479 false /* useUidAttributionChain */); 480 481 // Run the given test. 482 runnable.run(imeSession, activity); 483 484 // Wait for any outstanding IME requests to finish, to capture all atoms. 485 PollingCheck.waitFor(() -> !imeSession.hasPendingImeVisibilityRequests(), 486 "Test Error: Pending IME requests took too long, likely timing out."); 487 488 // Wait for any atoms from the test runnable to be sent. 489 // TODO(b/330143218): Add a proper fence for statsd 490 Thread.sleep(WAIT_TIME_SHORT); 491 492 // Must have at least one atom received. 493 final var data = MetricsRecorder.getEventMetricDataList(); 494 assertWithMessage("Number of atoms logged") 495 .that(data.size()) 496 .isAtLeast(1); 497 498 // Check received atom data. 499 try { 500 int successfulAtoms = 0; 501 for (int i = 0; i < data.size(); i++) { 502 final var atom = data.get(i).atom; 503 assertThat(atom).isNotNull(); 504 505 final var imeRequestFinished = atom.getImeRequestFinished(); 506 assertThat(imeRequestFinished).isNotNull(); 507 508 // Skip cancelled requests. 509 if (imeRequestFinished.status == ImeProtoEnums.STATUS_CANCEL) continue; 510 511 successfulAtoms++; 512 513 assertWithMessage("Ime Request type") 514 .that(imeRequestFinished.type) 515 .isEqualTo(show ? ImeProtoEnums.TYPE_SHOW : ImeProtoEnums.TYPE_HIDE); 516 assertWithMessage("Ime Request status") 517 .that(imeRequestFinished.status) 518 .isEqualTo(ImeProtoEnums.STATUS_SUCCESS); 519 assertWithMessage("Ime Request origin") 520 .that(imeRequestFinished.origin) 521 .isIn(origins); 522 if (fromUser) { 523 // Assert only when fromUser was expected to be true. 524 assertWithMessage("Ime Request fromUser") 525 .that(imeRequestFinished.fromUser) 526 .isEqualTo(true); 527 } 528 } 529 530 // Must have at least one successful request received. 531 assertWithMessage("Number of successful atoms logged") 532 .that(successfulAtoms) 533 .isAtLeast(1); 534 } catch (AssertionError e) { 535 throw new AssertionError(e.getMessage() + "\natoms data:\n" + data, e); 536 } 537 } 538 } 539 540 /** Interface for the test code to be ran. */ 541 private interface TestRunnable { 542 543 /** 544 * Execute the given test code given the ime session and activity. 545 * 546 * @param imeSession the initialized mock ime session. 547 * @param activity the initialized test activity. 548 */ run(@onNull MockImeSession imeSession, @NonNull TestActivity activity)549 void run(@NonNull MockImeSession imeSession, @NonNull TestActivity activity); 550 } 551 } 552