blob: a4c163ad631103bfd9de28281f46bcc3b46b6ac3 [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.server.wm;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW;
import static android.provider.Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS;
import static android.server.wm.UiDeviceUtils.pressUnlockButton;
import static android.server.wm.UiDeviceUtils.pressWakeupButton;
import static android.server.wm.WindowManagerState.STATE_RESUMED;
import static android.server.wm.overlay.Components.OverlayActivity.EXTRA_TOKEN;
import static android.view.WindowInsets.Type.navigationBars;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Instrumentation;
import android.app.NotificationManager;
import android.app.WindowConfiguration;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.server.wm.overlay.Components;
import android.server.wm.overlay.R;
import android.server.wm.settings.SettingsSession;
import android.server.wm.shared.BlockingResultReceiver;
import android.server.wm.shared.IUntrustedTouchTestService;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.view.Display;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.Toast;
import androidx.annotation.AnimRes;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import com.android.compatibility.common.util.AppOpsUtils;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@Presubmit
public class WindowUntrustedTouchTest {
private static final String TAG = "WindowUntrustedTouchTest";
/**
* Opacity (or alpha) is represented as a half-precision floating point number (16b) in surface
* flinger and the conversion from the single-precision float provided to window manager happens
* in Layer::setAlpha() by android::half::ftoh(). So, many small non-zero values provided to
* window manager end up becoming zero due to loss of precision (this is fine as long as the
* zeros are also used to render the pixels on the screen). So, the minimum opacity possible is
* actually the minimum positive value representable in half-precision float, which is
* 0_00001_0000000000, whose equivalent in float is 0_01110001_00000000000000000000000.
*
* Note that from float -> half conversion code we don't produce any subnormal half-precision
* floats during conversion.
*/
public static final float MIN_POSITIVE_OPACITY =
Float.intBitsToFloat(0b00111000100000000000000000000000);
private static final float MAXIMUM_OBSCURING_OPACITY = .8f;
private static final long TIMEOUT_MS = 3000L;
private static final long MAX_ANIMATION_DURATION_MS = 3000L;
private static final long ANIMATION_DURATION_TOLERANCE_MS = 500L;
private static final int OVERLAY_COLOR = 0xFFFF0000;
private static final int ACTIVITY_COLOR = 0xFFFFFFFF;
private static final int FEATURE_MODE_DISABLED = 0;
private static final int FEATURE_MODE_PERMISSIVE = 1;
private static final int FEATURE_MODE_BLOCK = 2;
private static final String APP_SELF =
WindowUntrustedTouchTest.class.getPackage().getName() + ".cts";
private static final String APP_A =
android.server.wm.second.Components.class.getPackage().getName();
private static final String APP_B =
android.server.wm.third.Components.class.getPackage().getName();
private static final String WINDOW_1 = "W1";
private static final String WINDOW_2 = "W2";
private static final String[] APPS = {APP_A, APP_B};
private static final String SETTING_MAXIMUM_OBSCURING_OPACITY =
"maximum_obscuring_opacity_for_touch";
private static SettingsSession<String> sImmersiveModeConfirmationSetting;
private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper();
private final Map<String, FutureConnection<IUntrustedTouchTestService>> mConnections =
new ArrayMap<>();
private Instrumentation mInstrumentation;
private Context mContext;
private Resources mResources;
private ContentResolver mContentResolver;
private TouchHelper mTouchHelper;
private Handler mMainHandler;
private InputManager mInputManager;
private WindowManager mWindowManager;
private ActivityManager mActivityManager;
private NotificationManager mNotificationManager;
private TestActivity mActivity;
private View mContainer;
private Toast mToast;
private float mPreviousTouchOpacity;
private int mPreviousMode;
private int mPreviousSawAppOp;
private final Set<String> mSawWindowsAdded = new ArraySet<>();
private final AtomicInteger mTouchesReceived = new AtomicInteger(0);
@Rule
public TestName testNameRule = new TestName();
@Rule
public ActivityScenarioRule<TestActivity> activityRule =
new ActivityScenarioRule<>(TestActivity.class);
@BeforeClass
public static void setUpClass() {
sImmersiveModeConfirmationSetting = new SettingsSession<>(
Settings.Secure.getUriFor(IMMERSIVE_MODE_CONFIRMATIONS),
Settings.Secure::getString, Settings.Secure::putString);
sImmersiveModeConfirmationSetting.set("confirmed");
}
@AfterClass
public static void tearDownClass() {
if (sImmersiveModeConfirmationSetting != null) {
sImmersiveModeConfirmationSetting.close();
}
}
@Before
public void setUp() throws Exception {
ActivityOptions options = ActivityOptions.makeBasic();
// Launch test in the fullscreen mode with navigation bar hidden,
// in order to ensure text toast is tappable and overlays above the test app
// on ARC++ and cf_pc devices. b/191075641.
options.setLaunchWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
activityRule.getScenario().launch(TestActivity.class, options.toBundle())
.onActivity(activity -> {
mActivity = activity;
mContainer = mActivity.view;
// On ARC++, text toast is fixed on the screen. Its position may overlays the navigation
// bar. Hide it to ensure the text toast overlays the app. b/191075641
mContainer.getWindowInsetsController().hide(navigationBars());
mContainer.setOnTouchListener(this::onTouchEvent);
});
mInstrumentation = getInstrumentation();
mContext = mInstrumentation.getContext();
mResources = mContext.getResources();
mContentResolver = mContext.getContentResolver();
mTouchHelper = new TouchHelper(mInstrumentation, mWmState);
mMainHandler = new Handler(Looper.getMainLooper());
mInputManager = mContext.getSystemService(InputManager.class);
mWindowManager = mContext.getSystemService(WindowManager.class);
mActivityManager = mContext.getSystemService(ActivityManager.class);
mNotificationManager = mContext.getSystemService(NotificationManager.class);
mPreviousSawAppOp = AppOpsUtils.getOpMode(APP_SELF, OPSTR_SYSTEM_ALERT_WINDOW);
AppOpsUtils.setOpMode(APP_SELF, OPSTR_SYSTEM_ALERT_WINDOW, MODE_ALLOWED);
mPreviousTouchOpacity = setMaximumObscuringOpacityForTouch(MAXIMUM_OBSCURING_OPACITY);
mPreviousMode = setBlockUntrustedTouchesMode(FEATURE_MODE_BLOCK);
SystemUtil.runWithShellPermissionIdentity(
() -> mNotificationManager.setToastRateLimitingEnabled(false));
pressWakeupButton();
pressUnlockButton();
}
@After
public void tearDown() throws Throwable {
mWmState.waitForAppTransitionIdleOnDisplay(Display.DEFAULT_DISPLAY);
mTouchesReceived.set(0);
removeOverlays();
for (FutureConnection<IUntrustedTouchTestService> connection : mConnections.values()) {
mContext.unbindService(connection);
}
mConnections.clear();
for (String app : APPS) {
stopPackage(app);
}
SystemUtil.runWithShellPermissionIdentity(
() -> mNotificationManager.setToastRateLimitingEnabled(true));
setBlockUntrustedTouchesMode(mPreviousMode);
setMaximumObscuringOpacityForTouch(mPreviousTouchOpacity);
AppOpsUtils.setOpMode(APP_SELF, OPSTR_SYSTEM_ALERT_WINDOW, mPreviousSawAppOp);
}
@Test
public void testWhenFeatureInDisabledModeAndActivityWindowAbove_allowsTouch()
throws Throwable {
setBlockUntrustedTouchesMode(FEATURE_MODE_DISABLED);
addActivityOverlay(APP_A, /* opacity */ .9f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenFeatureInPermissiveModeAndActivityWindowAbove_allowsTouch()
throws Throwable {
setBlockUntrustedTouchesMode(FEATURE_MODE_PERMISSIVE);
addActivityOverlay(APP_A, /* opacity */ .9f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenFeatureInBlockModeAndActivityWindowAbove_blocksTouch()
throws Throwable {
setBlockUntrustedTouchesMode(FEATURE_MODE_BLOCK);
addActivityOverlay(APP_A, /* opacity */ .9f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testMaximumObscuringOpacity() throws Throwable {
// Setting the previous value since we override this on setUp()
setMaximumObscuringOpacityForTouch(mPreviousTouchOpacity);
assertEquals(0.8f, mInputManager.getMaximumObscuringOpacityForTouch());
}
@Test
public void testAfterSettingThreshold_returnsThresholdSet()
throws Throwable {
float threshold = .123f;
setMaximumObscuringOpacityForTouch(threshold);
assertEquals(threshold, mInputManager.getMaximumObscuringOpacityForTouch());
}
@Test
public void testAfterSettingFeatureMode_returnsModeSet()
throws Throwable {
// Make sure the previous mode is different
setBlockUntrustedTouchesMode(FEATURE_MODE_BLOCK);
assertEquals(FEATURE_MODE_BLOCK, mInputManager.getBlockUntrustedTouchesMode(mContext));
setBlockUntrustedTouchesMode(FEATURE_MODE_PERMISSIVE);
assertEquals(FEATURE_MODE_PERMISSIVE, mInputManager.getBlockUntrustedTouchesMode(mContext));
}
@Test(expected = IllegalArgumentException.class)
public void testAfterSettingThresholdLessThan0_throws() throws Throwable {
setMaximumObscuringOpacityForTouch(-.5f);
}
@Test(expected = IllegalArgumentException.class)
public void testAfterSettingThresholdGreaterThan1_throws() throws Throwable {
setMaximumObscuringOpacityForTouch(1.5f);
}
/** This is testing what happens if setting is overridden manually */
@Test
public void testAfterSettingThresholdGreaterThan1ViaSettings_previousThresholdIsUsed()
throws Throwable {
setMaximumObscuringOpacityForTouch(.8f);
assertEquals(.8f, mInputManager.getMaximumObscuringOpacityForTouch());
SystemUtil.runWithShellPermissionIdentity(() -> {
Settings.Global.putFloat(mContentResolver, SETTING_MAXIMUM_OBSCURING_OPACITY, 1.5f);
});
addSawOverlay(APP_A, WINDOW_1, 9.f);
mTouchHelper.tapOnViewCenter(mContainer);
// Blocks because it's using previous maximum of .8
assertTouchNotReceived();
}
/** This is testing what happens if setting is overridden manually */
@Test
public void testAfterSettingThresholdLessThan0ViaSettings_previousThresholdIsUsed()
throws Throwable {
setMaximumObscuringOpacityForTouch(.8f);
assertEquals(.8f, mInputManager.getMaximumObscuringOpacityForTouch());
SystemUtil.runWithShellPermissionIdentity(() -> {
Settings.Global.putFloat(mContentResolver, SETTING_MAXIMUM_OBSCURING_OPACITY, -.5f);
});
addSawOverlay(APP_A, WINDOW_1, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
// Allows because it's using previous maximum of .8
assertTouchReceived();
}
/** SAWs */
@Test
public void testWhenOneSawWindowAboveThreshold_allowsTouch() throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .9f);
mTouchHelper.tapOnViewCenter(mContainer);
// Opacity will be automatically capped and touches will pass through.
assertTouchReceived();
}
@Test
public void testWhenOneSawWindowBelowThreshold_allowsTouch() throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenOneSawWindowWithZeroOpacity_allowsTouch() throws Throwable {
addSawOverlay(APP_A, WINDOW_1, 0f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenOneSawWindowAtThreshold_allowsTouch() throws Throwable {
addSawOverlay(APP_A, WINDOW_1, MAXIMUM_OBSCURING_OPACITY);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenTwoSawWindowsFromSameAppTogetherBelowThreshold_allowsTouch()
throws Throwable {
// Resulting opacity = 1 - (1 - 0.5)*(1 - 0.5) = .75
addSawOverlay(APP_A, WINDOW_1, .5f);
addSawOverlay(APP_A, WINDOW_2, .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenTwoSawWindowsFromSameAppTogetherAboveThreshold_blocksTouch()
throws Throwable {
// Resulting opacity = 1 - (1 - 0.7)*(1 - 0.7) = .91
addSawOverlay(APP_A, WINDOW_1, .7f);
addSawOverlay(APP_A, WINDOW_2, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenTwoSawWindowsFromDifferentAppsEachBelowThreshold_allowsTouch()
throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .7f);
addSawOverlay(APP_B, WINDOW_2, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenOneSawWindowAboveThresholdAndSelfSawWindow_allowsTouch()
throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .9f);
addSawOverlay(APP_SELF, WINDOW_1, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
// Opacity will be automatically capped and touches will pass through.
assertTouchReceived();
}
@Test
public void testWhenOneSawWindowBelowThresholdAndSelfSawWindow_allowsTouch()
throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .7f);
addSawOverlay(APP_SELF, WINDOW_1, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenTwoSawWindowsTogetherBelowThresholdAndSelfSawWindow_allowsTouch()
throws Throwable {
// Resulting opacity for A = 1 - (1 - 0.5)*(1 - 0.5) = .75
addSawOverlay(APP_A, WINDOW_1, .5f);
addSawOverlay(APP_A, WINDOW_1, .5f);
addSawOverlay(APP_SELF, WINDOW_1, .7f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenThresholdIs0AndSawWindowAtThreshold_allowsTouch()
throws Throwable {
setMaximumObscuringOpacityForTouch(0);
addSawOverlay(APP_A, WINDOW_1, 0);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenThresholdIs0AndSawWindowAboveThreshold_allowsTouch()
throws Throwable {
setMaximumObscuringOpacityForTouch(0);
addSawOverlay(APP_A, WINDOW_1, .1f);
mTouchHelper.tapOnViewCenter(mContainer);
// Opacity will be automatically capped and touches will pass through.
assertTouchReceived();
}
@Test
public void testWhenThresholdIs1AndSawWindowAtThreshold_allowsTouch()
throws Throwable {
setMaximumObscuringOpacityForTouch(1);
addSawOverlay(APP_A, WINDOW_1, 1);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenThresholdIs1AndSawWindowBelowThreshold_allowsTouch()
throws Throwable {
setMaximumObscuringOpacityForTouch(1);
addSawOverlay(APP_A, WINDOW_1, .9f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
/** Activity windows */
@Test
public void testWhenOneActivityWindowBelowThreshold_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowAboveThreshold_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .9f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowWithZeroOpacity_allowsTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ 0f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenOneActivityWindowWithMinPositiveOpacity_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ MIN_POSITIVE_OPACITY);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowWithSmallOpacity_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .01f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneSelfActivityWindow_allowsTouch() throws Throwable {
addActivityOverlay(APP_SELF, /* opacity */ .9f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenTwoActivityWindowsFromDifferentAppsTogetherBelowThreshold_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .7f);
addActivityOverlay(APP_B, /* opacity */ .7f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowAndOneSawWindowTogetherBelowThreshold_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .5f);
addSawOverlay(APP_A, WINDOW_1, .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowAndOneSelfCustomToastWindow_blocksTouch()
throws Throwable {
// Toast has to be before otherwise it would be blocked from background
addToastOverlay(APP_SELF, /* custom */ true);
addActivityOverlay(APP_A, /* opacity */ .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowAndOneSelfSawWindow_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .5f);
addSawOverlay(APP_SELF, WINDOW_1, .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowAndOneSawWindowBelowThreshold_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .5f);
addSawOverlay(APP_A, WINDOW_1, .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneActivityWindowAndOneSawWindowBelowThresholdFromDifferentApp_blocksTouch()
throws Throwable {
addActivityOverlay(APP_A, /* opacity */ .5f);
addSawOverlay(APP_B, WINDOW_1, .5f);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
/** Activity-type child windows on same activity */
@Test
public void testWhenActivityChildWindowWithSameTokenFromDifferentApp_allowsTouch()
throws Exception {
IBinder token = mActivity.getWindow().getAttributes().token;
addActivityChildWindow(APP_A, WINDOW_1, token);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenActivityChildWindowWithDifferentTokenFromDifferentApp_blocksTouch()
throws Exception {
// Creates a new activity with 0 opacity
BlockingResultReceiver receiver = new BlockingResultReceiver();
addActivityOverlay(APP_A, /* opacity */ 0f, receiver);
// Verify it allows touches
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
// Now get its token and put a child window from another app with it
IBinder token = receiver.getData(TIMEOUT_MS).getBinder(EXTRA_TOKEN);
addActivityChildWindow(APP_B, WINDOW_1, token);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenActivityChildWindowWithDifferentTokenFromSameApp_allowsTouch()
throws Exception {
// Creates a new activity with 0 opacity
BlockingResultReceiver receiver = new BlockingResultReceiver();
addActivityOverlay(APP_A, /* opacity */ 0f, receiver);
// Now get its token and put a child window owned by us
IBinder token = receiver.getData(TIMEOUT_MS).getBinder(EXTRA_TOKEN);
addActivityChildWindow(APP_SELF, WINDOW_1, token);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
/** Activity transitions */
@Test
public void testLongEnterAnimations_areLimited() {
long durationSet = mResources.getInteger(R.integer.long_animation_duration);
assertThat(durationSet).isGreaterThan(
MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
addAnimatedActivityOverlay(APP_A, /* touchable */ false, R.anim.long_alpha_0_7,
R.anim.long_alpha_1);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
long start = SystemClock.elapsedRealtime();
assertTrue(mWmState.waitForAppTransitionIdleOnDisplay(Display.DEFAULT_DISPLAY));
long duration = SystemClock.elapsedRealtime() - start;
assertThat(duration).isAtMost(MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
}
@Test
public void testLongExitAnimations_areLimited() {
long durationSet = mResources.getInteger(R.integer.long_animation_duration);
assertThat(durationSet).isGreaterThan(
MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
addExitAnimationActivity(APP_A);
sendFinishToExitAnimationActivity(APP_A,
Components.ExitAnimationActivityReceiver.EXTRA_VALUE_LONG_ANIMATION_0_7);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
long start = SystemClock.elapsedRealtime();
assertTrue(mWmState.waitForAppTransitionIdleOnDisplay(Display.DEFAULT_DISPLAY));
long duration = SystemClock.elapsedRealtime() - start;
assertThat(duration).isAtMost(MAX_ANIMATION_DURATION_MS + ANIMATION_DURATION_TOLERANCE_MS);
}
@Test
public void testWhenEnterAnimationAboveThresholdAndNewActivityNotTouchable_blocksTouch() {
addAnimatedActivityOverlay(APP_A, /* touchable */ false, R.anim.alpha_0_9, R.anim.alpha_1);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
assertAnimationRunning();
assertTouchNotReceived();
}
@Test
public void testWhenEnterAnimationBelowThresholdAndNewActivityNotTouchable_allowsTouch() {
addAnimatedActivityOverlay(APP_A, /* touchable */ false, R.anim.alpha_0_7, R.anim.alpha_1);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
assertAnimationRunning();
assertTouchReceived();
}
@Test
public void testWhenEnterAnimationBelowThresholdAndNewActivityTouchable_blocksTouch() {
addAnimatedActivityOverlay(APP_A, /* touchable */ true, R.anim.alpha_0_7, R.anim.alpha_1);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
assertAnimationRunning();
assertTouchNotReceived();
}
@Test
public void testWhenExitAnimationBelowThreshold_allowsTouch() {
addExitAnimationActivity(APP_A);
sendFinishToExitAnimationActivity(APP_A,
Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_7);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
assertAnimationRunning();
assertTouchReceived();
}
@Test
public void testWhenExitAnimationAboveThreshold_blocksTouch() {
addExitAnimationActivity(APP_A);
sendFinishToExitAnimationActivity(APP_A,
Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_9);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
assertAnimationRunning();
assertTouchNotReceived();
}
@Test
public void testWhenExitAnimationAboveThresholdFromSameUid_allowsTouch() {
addExitAnimationActivity(APP_SELF);
sendFinishToExitAnimationActivity(APP_SELF,
Components.ExitAnimationActivityReceiver.EXTRA_VALUE_ANIMATION_0_9);
assertTrue(mWmState.waitForAppTransitionRunningOnDisplay(Display.DEFAULT_DISPLAY));
mTouchHelper.tapOnViewCenter(mContainer, /* waitAnimations*/ false);
assertAnimationRunning();
assertTouchReceived();
}
/** Toast windows */
@Test
public void testWhenSelfTextToastWindow_allowsTouch() throws Throwable {
addToastOverlay(APP_SELF, /* custom */ false);
Rect toast = mWmState.waitForResult("toast bounds",
state -> state.findFirstWindowWithType(LayoutParams.TYPE_TOAST).getFrame());
mTouchHelper.tapOnCenter(toast, mActivity.getDisplayId());
assertTouchReceived();
}
@Test
public void testWhenTextToastWindow_allowsTouch() throws Throwable {
addToastOverlay(APP_A, /* custom */ false);
Rect toast = mWmState.waitForResult("toast bounds",
state -> state.findFirstWindowWithType(LayoutParams.TYPE_TOAST).getFrame());
mTouchHelper.tapOnCenter(toast, mActivity.getDisplayId());
assertTouchReceived();
}
@Test
public void testWhenOneCustomToastWindow_blocksTouch() throws Throwable {
addToastOverlay(APP_A, /* custom */ true);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneSelfCustomToastWindow_allowsTouch() throws Throwable {
addToastOverlay(APP_SELF, /* custom */ true);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
@Test
public void testWhenOneCustomToastWindowAndOneSelfSawWindow_blocksTouch()
throws Throwable {
addSawOverlay(APP_SELF, WINDOW_1, .9f);
addToastOverlay(APP_A, /* custom */ true);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneCustomToastWindowAndOneSawWindowBelowThreshold_blocksTouch()
throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .5f);
addToastOverlay(APP_A, /* custom */ true);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneCustomToastWindowAndOneSawWindowBelowThresholdFromDifferentApp_blocksTouch()
throws Throwable {
addSawOverlay(APP_A, WINDOW_1, .5f);
addToastOverlay(APP_B, /* custom */ true);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchNotReceived();
}
@Test
public void testWhenOneSelfCustomToastWindowOneSelfActivityWindowAndOneSawBelowThreshold_allowsTouch()
throws Throwable {
addActivityOverlay(APP_SELF, /* opacity */ .9f);
addSawOverlay(APP_A, WINDOW_1, .5f);
addToastOverlay(APP_SELF, /* custom */ true);
mTouchHelper.tapOnViewCenter(mContainer);
assertTouchReceived();
}
private boolean onTouchEvent(View view, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mTouchesReceived.incrementAndGet();
}
return true;
}
private void assertTouchReceived() {
mInstrumentation.waitForIdleSync();
assertThat(mTouchesReceived.get()).isEqualTo(1);
mTouchesReceived.set(0);
}
private void assertTouchNotReceived() {
mInstrumentation.waitForIdleSync();
assertThat(mTouchesReceived.get()).isEqualTo(0);
mTouchesReceived.set(0);
}
private void assertAnimationRunning() {
assertThat(mWmState.getDisplay(Display.DEFAULT_DISPLAY).getAppTransitionState()).isEqualTo(
WindowManagerStateHelper.APP_STATE_RUNNING);
}
private void addToastOverlay(String packageName, boolean custom) throws Exception {
// Making sure there are no toasts currently since we can only check for the presence of
// *any* toast afterwards and we don't want to be in a situation where this method returned
// because another toast was being displayed.
waitForNoToastOverlays();
if (custom) {
if (packageName.equals(APP_SELF)) {
// We add the custom toast here because we already have foreground status due to
// the activity rule, so no need to start another activity.
addMyCustomToastOverlay();
} else {
// We have to use an activity that will display the toast then finish itself because
// custom toasts cannot be posted from the background.
Intent intent = new Intent();
intent.setComponent(repackage(packageName, Components.ToastActivity.COMPONENT));
mActivity.startActivity(intent);
}
} else {
getService(packageName).showToast();
}
String message = "Toast from app " + packageName + " did not appear on time";
// TODO: WindowStateProto does not have package/UID information from the window, the current
// package test relies on the window name, which is not how toast windows are named. We
// should ideally incorporate that information in WindowStateProto and use here.
if (!mWmState.waitFor("toast window", this::hasVisibleToast)) {
fail(message);
}
}
private boolean hasVisibleToast(WindowManagerState state) {
return !state.getMatchingWindowType(LayoutParams.TYPE_TOAST).isEmpty()
&& state.findFirstWindowWithType(LayoutParams.TYPE_TOAST).isSurfaceShown();
}
private void addMyCustomToastOverlay() {
mActivity.runOnUiThread(() -> {
mToast = new Toast(mContext);
View view = new View(mContext);
view.setBackgroundColor(OVERLAY_COLOR);
mToast.setView(view);
mToast.setGravity(Gravity.FILL, 0, 0);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.show();
});
mInstrumentation.waitForIdleSync();
}
private void removeMyCustomToastOverlay() {
mActivity.runOnUiThread(() -> {
if (mToast != null) {
mToast.cancel();
mToast = null;
}
});
mInstrumentation.waitForIdleSync();
}
private void waitForNoToastOverlays() {
waitForNoToastOverlays("Toast windows did not hide on time");
}
private void waitForNoToastOverlays(String message) {
if (!mWmState.waitFor("no toast windows",
state -> state.getMatchingWindowType(LayoutParams.TYPE_TOAST).isEmpty())) {
fail(message);
}
}
private void addExitAnimationActivity(String packageName) {
// This activity responds to broadcasts to exit with animations and it's opaque (translucent
// activities don't honor custom exit animations).
addActivity(repackage(packageName, Components.ExitAnimationActivity.COMPONENT),
/* extras */ null, /* options */ null);
}
private void sendFinishToExitAnimationActivity(String packageName, int exitAnimation) {
Intent intent = new Intent(Components.ExitAnimationActivityReceiver.ACTION_FINISH);
intent.setPackage(packageName);
intent.putExtra(Components.ExitAnimationActivityReceiver.EXTRA_ANIMATION, exitAnimation);
mContext.sendBroadcast(intent);
}
private void addAnimatedActivityOverlay(String packageName, boolean touchable,
@AnimRes int enterAnim, @AnimRes int exitAnim) {
ConditionVariable animationsStarted = new ConditionVariable(false);
ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, enterAnim, exitAnim,
mMainHandler, animationsStarted::open, /* finishedListener */ null);
// We're testing the opacity coming from the animation here, not the one declared in the
// activity, so we set its opacity to 1
addActivityOverlay(packageName, /* opacity */ 1, touchable, options.toBundle());
animationsStarted.block();
}
private void addActivityChildWindow(String packageName, String windowSuffix, IBinder token)
throws Exception {
String name = getWindowName(packageName, windowSuffix);
getService(packageName).showActivityChildWindow(name, token);
if (!mWmState.waitFor("activity child window " + name,
state -> state.isWindowVisible(name) && state.isWindowSurfaceShown(name))) {
fail("Activity child window " + name + " did not appear on time");
}
}
private void addActivityOverlay(String packageName, float opacity) {
addActivityOverlay(packageName, opacity, /* touchable */ false, /* options */ null);
}
private void addActivityOverlay(String packageName, float opacity, boolean touchable,
@Nullable Bundle options) {
Bundle extras = new Bundle();
extras.putFloat(Components.OverlayActivity.EXTRA_OPACITY, opacity);
extras.putBoolean(Components.OverlayActivity.EXTRA_TOUCHABLE, touchable);
addActivityOverlay(packageName, extras, options);
}
private void addActivityOverlay(String packageName, float opacity,
BlockingResultReceiver tokenReceiver) {
Bundle extras = new Bundle();
extras.putFloat(Components.OverlayActivity.EXTRA_OPACITY, opacity);
extras.putParcelable(Components.OverlayActivity.EXTRA_TOKEN_RECEIVER, tokenReceiver);
addActivityOverlay(packageName, extras, /* options */ null);
}
private void addActivityOverlay(String packageName, @Nullable Bundle extras,
@Nullable Bundle options) {
addActivity(repackage(packageName, Components.OverlayActivity.COMPONENT), extras, options);
}
private void addActivity(ComponentName component, @Nullable Bundle extras,
@Nullable Bundle options) {
Intent intent = new Intent();
intent.setComponent(component);
if (extras != null) {
intent.putExtras(extras);
}
mActivity.startActivity(intent, options);
String packageName = component.getPackageName();
String activity = ComponentNameUtils.getActivityName(component);
if (!mWmState.waitFor("activity window " + activity,
state -> activity.equals(state.getFocusedActivity())
&& state.hasActivityState(component, STATE_RESUMED))) {
fail("Activity from app " + packageName + " did not appear on time");
}
}
private void removeActivityOverlays() {
Intent intent = new Intent(mContext, mActivity.getClass());
// Will clear any activity on top of it and it will become the new top
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mActivity.startActivity(intent);
}
private void waitForNoActivityOverlays(String message) {
// Base activity focused means no activities on top
ComponentName component = mActivity.getComponentName();
String name = ComponentNameUtils.getActivityName(component);
if (!mWmState.waitFor("test rule activity focused",
state -> name.equals(state.getFocusedActivity())
&& state.hasActivityState(component, STATE_RESUMED))) {
fail(message);
}
}
private void addSawOverlay(String packageName, String windowSuffix, float opacity)
throws Throwable {
String name = getWindowName(packageName, windowSuffix);
getService(packageName).showSystemAlertWindow(name, opacity);
mSawWindowsAdded.add(name);
if (!mWmState.waitFor("saw window " + name,
state -> state.isWindowVisible(name) && state.isWindowSurfaceShown(name))) {
fail("Saw window " + name + " did not appear on time");
}
}
private void waitForNoSawOverlays(String message) {
if (!mWmState.waitFor("no SAW windows",
state -> mSawWindowsAdded.stream().allMatch(w -> !state.isWindowVisible(w)))) {
fail(message);
}
mSawWindowsAdded.clear();
}
private void removeOverlays() throws Throwable {
for (FutureConnection<IUntrustedTouchTestService> connection : mConnections.values()) {
connection.getCurrent().removeOverlays();
}
// We need to stop the app because not every overlay is created via the service (eg.
// activity overlays and custom toasts)
for (String app : APPS) {
stopPackage(app);
}
waitForNoSawOverlays("SAWs not removed on time");
removeActivityOverlays();
waitForNoActivityOverlays("Activities not removed on time");
removeMyCustomToastOverlay();
waitForNoToastOverlays("Toasts not removed on time");
}
private void stopPackage(String packageName) {
SystemUtil.runWithShellPermissionIdentity(
() -> mActivityManager.forceStopPackage(packageName));
}
private int setBlockUntrustedTouchesMode(int mode) throws Exception {
return SystemUtil.callWithShellPermissionIdentity(() -> {
int previous = mInputManager.getBlockUntrustedTouchesMode(mContext);
mInputManager.setBlockUntrustedTouchesMode(mContext, mode);
return previous;
});
}
private float setMaximumObscuringOpacityForTouch(float opacity) throws Exception {
return SystemUtil.callWithShellPermissionIdentity(() -> {
float previous = mInputManager.getMaximumObscuringOpacityForTouch();
mInputManager.setMaximumObscuringOpacityForTouch(opacity);
return previous;
});
}
private IUntrustedTouchTestService getService(String packageName) throws Exception {
return mConnections.computeIfAbsent(packageName, this::connect).get(TIMEOUT_MS);
}
private FutureConnection<IUntrustedTouchTestService> connect(String packageName) {
FutureConnection<IUntrustedTouchTestService> connection =
new FutureConnection<>(IUntrustedTouchTestService.Stub::asInterface);
Intent intent = new Intent();
intent.setComponent(repackage(packageName, Components.UntrustedTouchTestService.COMPONENT));
assertTrue(mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE));
return connection;
}
private static String getWindowName(String packageName, String windowSuffix) {
return packageName + "." + windowSuffix;
}
private static ComponentName repackage(String packageName, ComponentName baseComponent) {
return new ComponentName(packageName, baseComponent.getClassName());
}
public static class TestActivity extends Activity {
public View view;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new View(this);
view.setBackgroundColor(ACTIVITY_COLOR);
setContentView(view);
}
}
}