blob: c9b5c96c99200b45cbeeed88fc07d7176bba40e5 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/** Utility methods for IME stress test. */
public final class ImeStressTestUtil {
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";
/** 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(
() -> {
try {
result.set(callable.call());
} catch (Exception e) {
throw new RuntimeException("Exception was thrown", e);
}
});
return result.get();
}
/**
* Waits until {@code pred} returns true, or throws on timeout.
*
* <p>The given {@code pred} will be called on the main thread.
*/
public static void waitOnMainUntil(String message, Callable<Boolean> pred) {
eventually(() -> assertWithMessage(message).that(pred.call()).isTrue(), TIMEOUT);
}
/** 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);
}
/** Waits until IME is hidden, or throws on timeout. */
public static void waitOnMainUntilImeIsHidden(View view) {
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. */
public static void verifyImeIsAlwaysHidden(View view) {
always(
() ->
assertWithMessage("IME should be hidden")
.that(callOnMainSync(() -> isImeShown(view)))
.isFalse(),
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);
}
/**
* Make sure that a {@link Runnable} always finishes without throwing a {@link Exception} in the
* given duration
*
* @param r The {@link Runnable} to run.
* @param timeoutMillis The number of milliseconds to wait for {@code r} to not throw
*/
public static void always(ThrowingRunnable r, long timeoutMillis) {
long start = System.currentTimeMillis();
while (true) {
try {
r.run();
if (System.currentTimeMillis() - start >= timeoutMillis) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
// Do nothing
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
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 static WeakReference<TestActivity> sLastCreatedInstance =
new WeakReference<>(null);
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()");
sLastCreatedInstance = new WeakReference<>(this);
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();
}
}
/** Get the last created TestActivity instance. */
@Nullable
public static TestActivity getLastCreatedInstance() {
return sLastCreatedInstance.get();
}
/** Show IME with InputMethodManager. */
public boolean showImeWithInputMethodManager() {
boolean showResult =
getInputMethodManager()
.showSoftInput(mEditText, 0 /* flags */);
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 /* flags */);
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");
}
}
}
}