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