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 com.android.inputmethod.stresstest; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 21 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; 22 23 import static com.android.compatibility.common.util.SystemUtil.eventually; 24 25 import static com.google.common.truth.Truth.assertWithMessage; 26 27 import android.app.Activity; 28 import android.app.Instrumentation; 29 import android.content.Intent; 30 import android.os.Build; 31 import android.os.Bundle; 32 import android.util.Log; 33 import android.view.View; 34 import android.view.WindowInsets; 35 import android.view.WindowInsetsAnimation; 36 import android.view.WindowInsetsController; 37 import android.view.WindowManager; 38 import android.view.WindowManager.LayoutParams; 39 import android.view.inputmethod.InputMethodManager; 40 import android.widget.EditText; 41 import android.widget.LinearLayout; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.test.platform.app.InstrumentationRegistry; 46 47 import com.android.compatibility.common.util.ThrowingRunnable; 48 49 import java.lang.ref.WeakReference; 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.concurrent.Callable; 53 import java.util.concurrent.TimeUnit; 54 import java.util.concurrent.atomic.AtomicReference; 55 56 /** Utility methods for IME stress test. */ 57 public final class ImeStressTestUtil { 58 59 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(3); 60 ImeStressTestUtil()61 private ImeStressTestUtil() {} 62 63 private static final int[] WINDOW_FOCUS_FLAGS = 64 new int[] { 65 LayoutParams.FLAG_NOT_FOCUSABLE, 66 LayoutParams.FLAG_ALT_FOCUSABLE_IM, 67 LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_ALT_FOCUSABLE_IM, 68 LayoutParams.FLAG_LOCAL_FOCUS_MODE 69 }; 70 71 private static final int[] SOFT_INPUT_VISIBILITY_FLAGS = 72 new int[] { 73 LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED, 74 LayoutParams.SOFT_INPUT_STATE_UNCHANGED, 75 LayoutParams.SOFT_INPUT_STATE_HIDDEN, 76 LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN, 77 LayoutParams.SOFT_INPUT_STATE_VISIBLE, 78 LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE, 79 }; 80 81 private static final int[] SOFT_INPUT_ADJUST_FLAGS = 82 new int[] { 83 LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED, 84 LayoutParams.SOFT_INPUT_ADJUST_RESIZE, 85 LayoutParams.SOFT_INPUT_ADJUST_PAN, 86 LayoutParams.SOFT_INPUT_ADJUST_NOTHING 87 }; 88 89 public static final String SOFT_INPUT_FLAGS = "soft_input_flags"; 90 public static final String WINDOW_FLAGS = "window_flags"; 91 public static final String UNFOCUSABLE_VIEW = "unfocusable_view"; 92 public static final String REQUEST_FOCUS_ON_CREATE = "request_focus_on_create"; 93 public static final String INPUT_METHOD_MANAGER_SHOW_ON_CREATE = 94 "input_method_manager_show_on_create"; 95 public static final String INPUT_METHOD_MANAGER_HIDE_ON_CREATE = 96 "input_method_manager_hide_on_create"; 97 public static final String WINDOW_INSETS_CONTROLLER_SHOW_ON_CREATE = 98 "window_insets_controller_show_on_create"; 99 public static final String WINDOW_INSETS_CONTROLLER_HIDE_ON_CREATE = 100 "window_insets_controller_hide_on_create"; 101 102 /** Parameters for show/hide ime parameterized tests. */ getWindowAndSoftInputFlagParameters()103 public static ArrayList<Object[]> getWindowAndSoftInputFlagParameters() { 104 ArrayList<Object[]> params = new ArrayList<>(); 105 106 // Set different window focus flags and keep soft input flags as default values (4 cases) 107 for (int windowFocusFlags : WINDOW_FOCUS_FLAGS) { 108 params.add( 109 new Object[] { 110 windowFocusFlags, 111 LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED, 112 LayoutParams.SOFT_INPUT_ADJUST_RESIZE 113 }); 114 } 115 // Set the combinations of different softInputVisibility, softInputAdjustment flags, 116 // keep the window focus flag as default value ( 6 * 4 = 24 cases) 117 for (int softInputVisibility : SOFT_INPUT_VISIBILITY_FLAGS) { 118 for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) { 119 params.add( 120 new Object[] { 121 0x0 /* No window focus flags */, softInputVisibility, softInputAdjust 122 }); 123 } 124 } 125 return params; 126 } 127 128 /** Checks if the IME is shown on the window that the given view belongs to. */ isImeShown(View view)129 public static boolean isImeShown(View view) { 130 WindowInsets insets = view.getRootWindowInsets(); 131 if (insets == null) { 132 return false; 133 } 134 return insets.isVisible(WindowInsets.Type.ime()); 135 } 136 137 /** Calls the callable on the main thread and returns the result. */ callOnMainSync(Callable<V> callable)138 public static <V> V callOnMainSync(Callable<V> callable) { 139 AtomicReference<V> result = new AtomicReference<>(); 140 InstrumentationRegistry.getInstrumentation() 141 .runOnMainSync( 142 () -> { 143 try { 144 result.set(callable.call()); 145 } catch (Exception e) { 146 throw new RuntimeException("Exception was thrown", e); 147 } 148 }); 149 return result.get(); 150 } 151 152 /** 153 * Requests EditText view focus on the main thread, and assert this returns {@code true}. 154 */ requestFocusAndVerify(TestActivity activity)155 public static void requestFocusAndVerify(TestActivity activity) { 156 boolean result = callOnMainSync(activity::requestFocus); 157 assertWithMessage("View focus request should have succeeded").that(result).isTrue(); 158 } 159 160 /** 161 * Waits until {@code pred} returns true, or throws on timeout. 162 * 163 * <p>The given {@code pred} will be called on the main thread. 164 */ waitOnMainUntil(String message, Callable<Boolean> pred)165 public static void waitOnMainUntil(String message, Callable<Boolean> pred) { 166 eventually(() -> assertWithMessage(message).that(callOnMainSync(pred)).isTrue(), TIMEOUT); 167 } 168 169 /** Waits until IME is shown, or throws on timeout. */ waitOnMainUntilImeIsShown(View view)170 public static void waitOnMainUntilImeIsShown(View view) { 171 eventually( 172 () -> 173 assertWithMessage("IME should have been shown") 174 .that(callOnMainSync(() -> isImeShown(view))) 175 .isTrue(), 176 TIMEOUT); 177 } 178 179 /** Waits until IME is hidden, or throws on timeout. */ waitOnMainUntilImeIsHidden(View view)180 public static void waitOnMainUntilImeIsHidden(View view) { 181 eventually( 182 () -> 183 assertWithMessage("IME should have been hidden") 184 .that(callOnMainSync(() -> isImeShown(view))) 185 .isFalse(), 186 TIMEOUT); 187 } 188 189 /** Waits until window gains focus, or throws on timeout. */ waitOnMainUntilWindowGainsFocus(View view)190 public static void waitOnMainUntilWindowGainsFocus(View view) { 191 eventually( 192 () -> 193 assertWithMessage( 194 "Window should have gained focus; value of hasWindowFocus:") 195 .that(callOnMainSync(view::hasWindowFocus)) 196 .isTrue(), 197 TIMEOUT); 198 } 199 200 /** Waits until view gains focus, or throws on timeout. */ waitOnMainUntilViewGainsFocus(View view)201 public static void waitOnMainUntilViewGainsFocus(View view) { 202 eventually( 203 () -> 204 assertWithMessage("View should have gained focus; value of hasFocus:") 205 .that(callOnMainSync(view::hasFocus)) 206 .isTrue(), 207 TIMEOUT); 208 } 209 210 /** Verify IME is always hidden within the given time duration. */ verifyImeIsAlwaysHidden(View view)211 public static void verifyImeIsAlwaysHidden(View view) { 212 always( 213 () -> 214 assertWithMessage("IME should have been hidden") 215 .that(callOnMainSync(() -> isImeShown(view))) 216 .isFalse(), 217 TIMEOUT); 218 } 219 220 /** Verify the window never gains focus within the given time duration. */ verifyWindowNeverGainsFocus(View view)221 public static void verifyWindowNeverGainsFocus(View view) { 222 always( 223 () -> 224 assertWithMessage( 225 "Window should not have gained focus; value of hasWindowFocus:") 226 .that(callOnMainSync(view::hasWindowFocus)) 227 .isFalse(), 228 TIMEOUT); 229 } 230 231 /** Verify the view never gains focus within the given time duration. */ verifyViewNeverGainsFocus(View view)232 public static void verifyViewNeverGainsFocus(View view) { 233 always( 234 () -> 235 assertWithMessage("View should not have gained focus; value of hasFocus:") 236 .that(callOnMainSync(view::hasFocus)) 237 .isFalse(), 238 TIMEOUT); 239 } 240 241 /** 242 * Make sure that a {@link Runnable} always finishes without throwing a {@link Exception} in the 243 * given duration 244 * 245 * @param r The {@link Runnable} to run. 246 * @param timeoutMillis The number of milliseconds to wait for {@code r} to not throw 247 */ always(ThrowingRunnable r, long timeoutMillis)248 public static void always(ThrowingRunnable r, long timeoutMillis) { 249 long start = System.currentTimeMillis(); 250 251 while (true) { 252 try { 253 r.run(); 254 if (System.currentTimeMillis() - start >= timeoutMillis) { 255 return; 256 } 257 try { 258 Thread.sleep(100); 259 } catch (InterruptedException ignored) { 260 // Do nothing 261 } 262 } catch (Throwable e) { 263 throw new RuntimeException(e); 264 } 265 } 266 } 267 268 /** 269 * Returns {@code true} if the activity can't receive IME focus, based on its window flags, 270 * and {@code false} otherwise. 271 * 272 * @param activity the activity to check. 273 */ hasUnfocusableWindowFlags(Activity activity)274 public static boolean hasUnfocusableWindowFlags(Activity activity) { 275 return hasUnfocusableWindowFlags(activity.getWindow().getAttributes().flags); 276 } 277 278 /** 279 * Returns {@code true} if the activity can't receive IME focus, based on its window flags, 280 * and {@code false} otherwise. 281 * 282 * @param windowFlags the window flags to check. 283 */ hasUnfocusableWindowFlags(int windowFlags)284 public static boolean hasUnfocusableWindowFlags(int windowFlags) { 285 return (windowFlags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0 286 || (windowFlags & LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0 287 || (windowFlags & LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0; 288 } 289 verifyWindowAndViewFocus( View view, boolean expectWindowFocus, boolean expectViewFocus)290 public static void verifyWindowAndViewFocus( 291 View view, boolean expectWindowFocus, boolean expectViewFocus) { 292 if (expectWindowFocus) { 293 waitOnMainUntilWindowGainsFocus(view); 294 } else { 295 verifyWindowNeverGainsFocus(view); 296 } 297 if (expectViewFocus) { 298 waitOnMainUntilViewGainsFocus(view); 299 } else { 300 verifyViewNeverGainsFocus(view); 301 } 302 } 303 verifyImeAlwaysHiddenWithWindowFlagSet(TestActivity activity)304 public static void verifyImeAlwaysHiddenWithWindowFlagSet(TestActivity activity) { 305 int windowFlags = activity.getWindow().getAttributes().flags; 306 View view = activity.getEditText(); 307 if ((windowFlags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) != 0) { 308 // When FLAG_NOT_FOCUSABLE is set true, the view will never gain window focus. The IME 309 // will always be hidden even though the view can get focus itself. 310 verifyWindowAndViewFocus(view, /*expectWindowFocus*/ false, /*expectViewFocus*/ true); 311 verifyImeIsAlwaysHidden(view); 312 } else if ((windowFlags & WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0 313 || (windowFlags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0) { 314 // When FLAG_ALT_FOCUSABLE_IM or FLAG_LOCAL_FOCUS_MODE is set, the view can gain both 315 // window focus and view focus but not IME focus. The IME will always be hidden. 316 verifyWindowAndViewFocus(view, /*expectWindowFocus*/ true, /*expectViewFocus*/ true); 317 verifyImeIsAlwaysHidden(view); 318 } 319 } 320 321 /** Activity to help test show/hide behavior of IME. */ 322 public static class TestActivity extends Activity { 323 private static final String TAG = "ImeStressTestUtil.TestActivity"; 324 private EditText mEditText; 325 private boolean mIsAnimating; 326 private static WeakReference<TestActivity> sLastCreatedInstance = 327 new WeakReference<>(null); 328 329 private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback = 330 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { 331 @NonNull 332 @Override 333 public WindowInsetsAnimation.Bounds onStart( 334 @NonNull WindowInsetsAnimation animation, 335 @NonNull WindowInsetsAnimation.Bounds bounds) { 336 mIsAnimating = true; 337 return super.onStart(animation, bounds); 338 } 339 340 @Override 341 public void onEnd(@NonNull WindowInsetsAnimation animation) { 342 super.onEnd(animation); 343 mIsAnimating = false; 344 } 345 346 @NonNull 347 @Override 348 public WindowInsets onProgress( 349 @NonNull WindowInsets insets, 350 @NonNull List<WindowInsetsAnimation> runningAnimations) { 351 return insets; 352 } 353 }; 354 355 /** Create intent with extras. */ createIntent( int windowFlags, int softInputFlags, List<String> extras)356 public static Intent createIntent( 357 int windowFlags, int softInputFlags, List<String> extras) { 358 Intent intent = 359 new Intent() 360 .putExtra(WINDOW_FLAGS, windowFlags) 361 .putExtra(SOFT_INPUT_FLAGS, softInputFlags); 362 for (String extra : extras) { 363 intent.putExtra(extra, true); 364 } 365 return intent; 366 } 367 368 @Override onCreate(@ullable Bundle savedInstanceState)369 protected void onCreate(@Nullable Bundle savedInstanceState) { 370 super.onCreate(savedInstanceState); 371 Log.i(TAG, "onCreate()"); 372 sLastCreatedInstance = new WeakReference<>(this); 373 boolean isUnfocusableView = getIntent().getBooleanExtra(UNFOCUSABLE_VIEW, false); 374 boolean requestFocus = getIntent().getBooleanExtra(REQUEST_FOCUS_ON_CREATE, false); 375 int softInputFlags = getIntent().getIntExtra(SOFT_INPUT_FLAGS, 0); 376 int windowFlags = getIntent().getIntExtra(WINDOW_FLAGS, 0); 377 boolean showWithInputMethodManagerOnCreate = 378 getIntent().getBooleanExtra(INPUT_METHOD_MANAGER_SHOW_ON_CREATE, false); 379 boolean hideWithInputMethodManagerOnCreate = 380 getIntent().getBooleanExtra(INPUT_METHOD_MANAGER_HIDE_ON_CREATE, false); 381 boolean showWithWindowInsetsControllerOnCreate = 382 getIntent().getBooleanExtra(WINDOW_INSETS_CONTROLLER_SHOW_ON_CREATE, false); 383 boolean hideWithWindowInsetsControllerOnCreate = 384 getIntent().getBooleanExtra(WINDOW_INSETS_CONTROLLER_HIDE_ON_CREATE, false); 385 386 getWindow().addFlags(windowFlags); 387 getWindow().setSoftInputMode(softInputFlags); 388 389 LinearLayout rootView = new LinearLayout(this); 390 rootView.setOrientation(LinearLayout.VERTICAL); 391 mEditText = new EditText(this); 392 if (isUnfocusableView) { 393 mEditText.setFocusableInTouchMode(false); 394 } 395 rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 396 rootView.setFitsSystemWindows(true); 397 setContentView(rootView); 398 399 if (requestFocus) { 400 requestFocus(); 401 } 402 if (showWithInputMethodManagerOnCreate) { 403 showImeWithInputMethodManager(); 404 } 405 if (hideWithInputMethodManagerOnCreate) { 406 hideImeWithInputMethodManager(); 407 } 408 if (showWithWindowInsetsControllerOnCreate) { 409 showImeWithWindowInsetsController(); 410 } 411 if (hideWithWindowInsetsControllerOnCreate) { 412 hideImeWithWindowInsetsController(); 413 } 414 } 415 416 /** Get the last created TestActivity instance. */ 417 @Nullable getLastCreatedInstance()418 public static TestActivity getLastCreatedInstance() { 419 return sLastCreatedInstance.get(); 420 } 421 422 /** Show IME with InputMethodManager. */ showImeWithInputMethodManager()423 public boolean showImeWithInputMethodManager() { 424 boolean showResult = 425 getInputMethodManager() 426 .showSoftInput(mEditText, 0 /* flags */); 427 if (showResult) { 428 Log.i(TAG, "IMM#showSoftInput succeeded"); 429 } else { 430 Log.i(TAG, "IMM#showSoftInput failed"); 431 } 432 return showResult; 433 } 434 435 /** Hide IME with InputMethodManager. */ hideImeWithInputMethodManager()436 public boolean hideImeWithInputMethodManager() { 437 boolean hideResult = 438 getInputMethodManager() 439 .hideSoftInputFromWindow(mEditText.getWindowToken(), 0 /* flags */); 440 if (hideResult) { 441 Log.i(TAG, "IMM#hideSoftInput succeeded"); 442 } else { 443 Log.i(TAG, "IMM#hideSoftInput failed"); 444 } 445 return hideResult; 446 } 447 448 /** Show IME with WindowInsetsController */ showImeWithWindowInsetsController()449 public void showImeWithWindowInsetsController() { 450 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 451 return; 452 } 453 Log.i(TAG, "showImeWithWIC()"); 454 WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController(); 455 assertWithMessage("WindowInsetsController") 456 .that(windowInsetsController) 457 .isNotNull(); 458 windowInsetsController.show(WindowInsets.Type.ime()); 459 } 460 461 /** Hide IME with WindowInsetsController. */ hideImeWithWindowInsetsController()462 public void hideImeWithWindowInsetsController() { 463 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 464 return; 465 } 466 Log.i(TAG, "hideImeWithWIC()"); 467 WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController(); 468 assertWithMessage("WindowInsetsController") 469 .that(windowInsetsController) 470 .isNotNull(); 471 windowInsetsController.hide(WindowInsets.Type.ime()); 472 } 473 getInputMethodManager()474 private InputMethodManager getInputMethodManager() { 475 return getSystemService(InputMethodManager.class); 476 } 477 getEditText()478 public EditText getEditText() { 479 return mEditText; 480 } 481 482 /** Start TestActivity with intent. */ start(Intent intent)483 public static TestActivity start(Intent intent) { 484 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 485 intent.setAction(Intent.ACTION_MAIN) 486 .setClass(instrumentation.getContext(), TestActivity.class) 487 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 488 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 489 return (TestActivity) instrumentation.startActivitySync(intent); 490 } 491 492 /** Start the second TestActivity with intent. */ startSecondTestActivity(Intent intent)493 public TestActivity startSecondTestActivity(Intent intent) { 494 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 495 intent.setClass(TestActivity.this, TestActivity.class); 496 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 497 return (TestActivity) instrumentation.startActivitySync(intent); 498 } 499 enableAnimationMonitoring()500 public void enableAnimationMonitoring() { 501 // Enable WindowInsetsAnimation. 502 // Note that this has a side effect of disabling InsetsAnimationThreadControlRunner. 503 InstrumentationRegistry.getInstrumentation() 504 .runOnMainSync( 505 () -> { 506 getWindow().setDecorFitsSystemWindows(false); 507 mEditText.setWindowInsetsAnimationCallback( 508 mWindowInsetsAnimationCallback); 509 }); 510 } 511 isAnimating()512 public boolean isAnimating() { 513 return mIsAnimating; 514 } 515 requestFocus()516 public boolean requestFocus() { 517 boolean requestFocusResult = mEditText.requestFocus(); 518 if (requestFocusResult) { 519 Log.i(TAG, "View#requestFocus succeeded"); 520 } else { 521 Log.i(TAG, "View#requestFocus failed"); 522 } 523 return requestFocusResult; 524 } 525 } 526 } 527