Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7306a40b authored by Ziqi Chen's avatar Ziqi Chen Committed by Android (Google) Code Review
Browse files

Merge "Extend InputMethodStressTest to add more IME show/hide e2e test cases."

parents e524c69c 19e01a7c
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -19,8 +19,8 @@
          package="com.android.inputmethod.stresstest">

    <application>
        <activity android:name=".AutoShowTest$TestActivity"/>
        <activity android:name=".ImeOpenCloseStressTest$TestActivity"/>
        <activity android:name=".ImeStressTestUtil$TestActivity"
                  android:configChanges="orientation|screenSize"/>
    </application>

    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+1 −1
Original line number Diff line number Diff line
{
  "presubmit": [
  "presubmit-large": [
    {
      "name": "InputMethodStressTest"
    }
+402 −101

File changed.

Preview size limit exceeded, changes collapsed.

+433 −122

File changed.

Preview size limit exceeded, changes collapsed.

+379 −16
Original line number Diff line number Diff line
@@ -16,17 +16,37 @@

package com.android.inputmethod.stresstest;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;

import static com.android.compatibility.common.util.SystemUtil.eventually;

import static com.google.common.truth.Truth.assertWithMessage;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.ThrowingRunnable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@@ -34,22 +54,90 @@ import java.util.concurrent.atomic.AtomicReference;
/** Utility methods for IME stress test. */
public final class ImeStressTestUtil {

    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
    private static final long VERIFY_DURATION = TimeUnit.SECONDS.toMillis(2);
    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(3);

    private ImeStressTestUtil() {}

    private static final int[] WINDOW_FOCUS_FLAGS =
            new int[] {
                LayoutParams.FLAG_NOT_FOCUSABLE,
                LayoutParams.FLAG_ALT_FOCUSABLE_IM,
                LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_ALT_FOCUSABLE_IM,
                LayoutParams.FLAG_LOCAL_FOCUS_MODE
            };

    private static final int[] SOFT_INPUT_VISIBILITY_FLAGS =
            new int[] {
                LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED,
                LayoutParams.SOFT_INPUT_STATE_UNCHANGED,
                LayoutParams.SOFT_INPUT_STATE_HIDDEN,
                LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN,
                LayoutParams.SOFT_INPUT_STATE_VISIBLE,
                LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE,
            };

    private static final int[] SOFT_INPUT_ADJUST_FLAGS =
            new int[] {
                LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED,
                LayoutParams.SOFT_INPUT_ADJUST_RESIZE,
                LayoutParams.SOFT_INPUT_ADJUST_PAN,
                LayoutParams.SOFT_INPUT_ADJUST_NOTHING
            };

    public static final String SOFT_INPUT_FLAGS = "soft_input_flags";
    public static final String WINDOW_FLAGS = "window_flags";
    public static final String UNFOCUSABLE_VIEW = "unfocusable_view";
    public static final String REQUEST_FOCUS_ON_CREATE = "request_focus_on_create";
    public static final String INPUT_METHOD_MANAGER_SHOW_ON_CREATE =
            "input_method_manager_show_on_create";
    public static final String INPUT_METHOD_MANAGER_HIDE_ON_CREATE =
            "input_method_manager_hide_on_create";
    public static final String WINDOW_INSETS_CONTROLLER_SHOW_ON_CREATE =
            "window_insets_controller_show_on_create";
    public static final String WINDOW_INSETS_CONTROLLER_HIDE_ON_CREATE =
            "window_insets_controller_hide_on_create";

    private ImeStressTestUtil() {
    /** Parameters for show/hide ime parameterized tests. */
    public static ArrayList<Object[]> getWindowAndSoftInputFlagParameters() {
        ArrayList<Object[]> params = new ArrayList<>();

        // Set different window focus flags and keep soft input flags as default values (4 cases)
        for (int windowFocusFlags : WINDOW_FOCUS_FLAGS) {
            params.add(
                    new Object[] {
                        windowFocusFlags,
                        LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED,
                        LayoutParams.SOFT_INPUT_ADJUST_RESIZE
                    });
        }
        // Set the combinations of different softInputVisibility, softInputAdjustment flags,
        // keep the window focus flag as default value ( 6 * 4 = 24 cases)
        for (int softInputVisibility : SOFT_INPUT_VISIBILITY_FLAGS) {
            for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) {
                params.add(
                        new Object[] {
                            0x0 /* No window focus flags */, softInputVisibility, softInputAdjust
                        });
            }
        }
        return params;
    }

    /** Checks if the IME is shown on the window that the given view belongs to. */
    public static boolean isImeShown(View view) {
        WindowInsets insets = view.getRootWindowInsets();
        if (insets == null) {
            return false;
        }
        return insets.isVisible(WindowInsets.Type.ime());
    }

    /** Calls the callable on the main thread and returns the result. */
    public static <V> V callOnMainSync(Callable<V> callable) {
        AtomicReference<V> result = new AtomicReference<>();
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
        InstrumentationRegistry.getInstrumentation()
                .runOnMainSync(
                        () -> {
                            try {
                                result.set(callable.call());
                            } catch (Exception e) {
@@ -70,15 +158,42 @@ public final class ImeStressTestUtil {

    /** Waits until IME is shown, or throws on timeout. */
    public static void waitOnMainUntilImeIsShown(View view) {
        eventually(() -> assertWithMessage("IME should be shown").that(
                callOnMainSync(() -> isImeShown(view))).isTrue(), TIMEOUT);
        eventually(
                () ->
                        assertWithMessage("IME should be shown")
                                .that(callOnMainSync(() -> isImeShown(view)))
                                .isTrue(),
                TIMEOUT);
    }

    /** Waits until IME is hidden, or throws on timeout. */
    public static void waitOnMainUntilImeIsHidden(View view) {
        //eventually(() -> assertThat(callOnMainSync(() -> isImeShown(view))).isFalse(), TIMEOUT);
        eventually(() -> assertWithMessage("IME should be hidden").that(
                callOnMainSync(() -> isImeShown(view))).isFalse(), TIMEOUT);
        eventually(
                () ->
                        assertWithMessage("IME should be hidden")
                                .that(callOnMainSync(() -> isImeShown(view)))
                                .isFalse(),
                TIMEOUT);
    }

    /** Waits until window get focus, or throws on timeout. */
    public static void waitOnMainUntilWindowGainsFocus(View view) {
        eventually(
                () ->
                        assertWithMessage("Window should gain focus")
                                .that(callOnMainSync(view::hasWindowFocus))
                                .isTrue(),
                TIMEOUT);
    }

    /** Waits until view get focus, or throws on timeout. */
    public static void waitOnMainUntilViewGainsFocus(View view) {
        eventually(
                () ->
                        assertWithMessage("View should gain focus")
                                .that(callOnMainSync(view::hasFocus))
                                .isTrue(),
                TIMEOUT);
    }

    /** Verify IME is always hidden within the given time duration. */
@@ -88,7 +203,27 @@ public final class ImeStressTestUtil {
                        assertWithMessage("IME should be hidden")
                                .that(callOnMainSync(() -> isImeShown(view)))
                                .isFalse(),
                VERIFY_DURATION);
                TIMEOUT);
    }

    /** Verify the window never gains focus within the given time duration. */
    public static void verifyWindowNeverGainsFocus(View view) {
        always(
                () ->
                        assertWithMessage("window should never gain focus")
                                .that(callOnMainSync(view::hasWindowFocus))
                                .isFalse(),
                TIMEOUT);
    }

    /** Verify the view never gains focus within the given time duration. */
    public static void verifyViewNeverGainsFocus(View view) {
        always(
                () ->
                        assertWithMessage("view should never gain ime focus")
                                .that(callOnMainSync(view::hasFocus))
                                .isFalse(),
                TIMEOUT);
    }

    /**
@@ -117,4 +252,232 @@ public final class ImeStressTestUtil {
            }
        }
    }

    public static boolean hasUnfocusableWindowFlags(Activity activity) {
        int windowFlags = activity.getWindow().getAttributes().flags;
        return (windowFlags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0
                || (windowFlags & LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0
                || (windowFlags & LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0;
    }

    public static void verifyWindowAndViewFocus(
            View view, boolean expectWindowFocus, boolean expectViewFocus) {
        if (expectWindowFocus) {
            waitOnMainUntilWindowGainsFocus(view);
        } else {
            verifyWindowNeverGainsFocus(view);
        }
        if (expectViewFocus) {
            waitOnMainUntilViewGainsFocus(view);
        } else {
            verifyViewNeverGainsFocus(view);
        }
    }

    public static void verifyImeAlwaysHiddenWithWindowFlagSet(TestActivity activity) {
        int windowFlags = activity.getWindow().getAttributes().flags;
        View view = activity.getEditText();
        if ((windowFlags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) != 0) {
            // When FLAG_NOT_FOCUSABLE is set true, the view will never gain window focus. The IME
            // will always be hidden even though the view can get focus itself.
            verifyWindowAndViewFocus(view, /*expectWindowFocus*/ false, /*expectViewFocus*/ true);
            verifyImeIsAlwaysHidden(view);
        } else if ((windowFlags & WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0
                || (windowFlags & WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE) != 0) {
            // When FLAG_ALT_FOCUSABLE_IM or FLAG_LOCAL_FOCUS_MODE is set, the view can gain both
            // window focus and view focus but not IME focus. The IME will always be hidden.
            verifyWindowAndViewFocus(view, /*expectWindowFocus*/ true, /*expectViewFocus*/ true);
            verifyImeIsAlwaysHidden(view);
        }
    }

    /** Activity to help test show/hide behavior of IME. */
    public static class TestActivity extends Activity {
        private static final String TAG = "ImeStressTestUtil.TestActivity";
        private EditText mEditText;
        private boolean mIsAnimating;

        private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
                new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
                    @Override
                    public WindowInsetsAnimation.Bounds onStart(
                            WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) {
                        mIsAnimating = true;
                        return super.onStart(animation, bounds);
                    }

                    @Override
                    public void onEnd(WindowInsetsAnimation animation) {
                        super.onEnd(animation);
                        mIsAnimating = false;
                    }

                    @Override
                    public WindowInsets onProgress(
                            WindowInsets insets, List<WindowInsetsAnimation> runningAnimations) {
                        return insets;
                    }
                };

        /** Create intent with extras. */
        public static Intent createIntent(
                int windowFlags, int softInputFlags, List<String> extras) {
            Intent intent =
                    new Intent()
                            .putExtra(WINDOW_FLAGS, windowFlags)
                            .putExtra(SOFT_INPUT_FLAGS, softInputFlags);
            for (String extra : extras) {
                intent.putExtra(extra, true);
            }
            return intent;
        }

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Log.i(TAG, "onCreate()");
            boolean isUnfocusableView = getIntent().getBooleanExtra(UNFOCUSABLE_VIEW, false);
            boolean requestFocus = getIntent().getBooleanExtra(REQUEST_FOCUS_ON_CREATE, false);
            int softInputFlags = getIntent().getIntExtra(SOFT_INPUT_FLAGS, 0);
            int windowFlags = getIntent().getIntExtra(WINDOW_FLAGS, 0);
            boolean showWithInputMethodManagerOnCreate =
                    getIntent().getBooleanExtra(INPUT_METHOD_MANAGER_SHOW_ON_CREATE, false);
            boolean hideWithInputMethodManagerOnCreate =
                    getIntent().getBooleanExtra(INPUT_METHOD_MANAGER_HIDE_ON_CREATE, false);
            boolean showWithWindowInsetsControllerOnCreate =
                    getIntent().getBooleanExtra(WINDOW_INSETS_CONTROLLER_SHOW_ON_CREATE, false);
            boolean hideWithWindowInsetsControllerOnCreate =
                    getIntent().getBooleanExtra(WINDOW_INSETS_CONTROLLER_HIDE_ON_CREATE, false);

            getWindow().addFlags(windowFlags);
            getWindow().setSoftInputMode(softInputFlags);

            LinearLayout rootView = new LinearLayout(this);
            rootView.setOrientation(LinearLayout.VERTICAL);
            mEditText = new EditText(this);
            if (isUnfocusableView) {
                mEditText.setFocusableInTouchMode(false);
            }
            rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
            setContentView(rootView);

            if (requestFocus) {
                requestFocus();
            }
            if (showWithInputMethodManagerOnCreate) {
                showImeWithInputMethodManager();
            }
            if (hideWithInputMethodManagerOnCreate) {
                hideImeWithInputMethodManager();
            }
            if (showWithWindowInsetsControllerOnCreate) {
                showImeWithWindowInsetsController();
            }
            if (hideWithWindowInsetsControllerOnCreate) {
                hideImeWithWindowInsetsController();
            }
        }

        /** Show IME with InputMethodManager. */
        public boolean showImeWithInputMethodManager() {
            boolean showResult =
                    getInputMethodManager()
                            .showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
            if (showResult) {
                Log.i(TAG, "IMM#showSoftInput successfully");
            } else {
                Log.i(TAG, "IMM#showSoftInput failed");
            }
            return showResult;
        }

        /** Hide IME with InputMethodManager. */
        public boolean hideImeWithInputMethodManager() {
            boolean hideResult =
                    getInputMethodManager().hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
            if (hideResult) {
                Log.i(TAG, "IMM#hideSoftInput successfully");
            } else {
                Log.i(TAG, "IMM#hideSoftInput failed");
            }
            return hideResult;
        }

        /** Show IME with WindowInsetsController */
        public void showImeWithWindowInsetsController() {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
                return;
            }
            Log.i(TAG, "showImeWithWIC()");
            WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController();
            assertWithMessage("WindowInsetsController shouldn't be null.")
                    .that(windowInsetsController)
                    .isNotNull();
            windowInsetsController.show(WindowInsets.Type.ime());
        }

        /** Hide IME with WindowInsetsController. */
        public void hideImeWithWindowInsetsController() {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
                return;
            }
            Log.i(TAG, "hideImeWithWIC()");
            WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController();
            assertWithMessage("WindowInsetsController shouldn't be null.")
                    .that(windowInsetsController)
                    .isNotNull();
            windowInsetsController.hide(WindowInsets.Type.ime());
        }

        private InputMethodManager getInputMethodManager() {
            return getSystemService(InputMethodManager.class);
        }

        public EditText getEditText() {
            return mEditText;
        }

        /** Start TestActivity with intent. */
        public static TestActivity start(Intent intent) {
            Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
            intent.setAction(Intent.ACTION_MAIN)
                    .setClass(instrumentation.getContext(), TestActivity.class)
                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            return (TestActivity) instrumentation.startActivitySync(intent);
        }

        /** Start the second TestActivity with intent. */
        public TestActivity startSecondTestActivity(Intent intent) {
            Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
            intent.setClass(TestActivity.this, TestActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            return (TestActivity) instrumentation.startActivitySync(intent);
        }

        public void enableAnimationMonitoring() {
            // Enable WindowInsetsAnimation.
            // Note that this has a side effect of disabling InsetsAnimationThreadControlRunner.
            InstrumentationRegistry.getInstrumentation()
                    .runOnMainSync(
                            () -> {
                                getWindow().setDecorFitsSystemWindows(false);
                                mEditText.setWindowInsetsAnimationCallback(
                                        mWindowInsetsAnimationCallback);
                            });
        }

        public boolean isAnimating() {
            return mIsAnimating;
        }

        public void requestFocus() {
            boolean requestFocusResult = getEditText().requestFocus();
            if (requestFocusResult) {
                Log.i(TAG, "Request focus successfully");
            } else {
                Log.i(TAG, "Request focus failed");
            }
        }
    }
}