From 330c3b3ca3d5ed06b7a701eb63472a349e8afb88 Mon Sep 17 00:00:00 2001 From: Ikram Gabiyev Date: Fri, 23 Sep 2022 19:51:48 +0000 Subject: [PATCH] Add flicker test for pinch out pip screen resizing Create a helper class for injecting a pinching gesture and use it pinch out to a pip window in a flicker test. Make pip window area increases during the animation. Test: atest WMShellFlickerTests:ExpandPipOnPinchOpenTest Bug: 178407318 Change-Id: I0e3dc3d78cdcafcf1dff380d76d1bea4cf629f65 --- .../shell/pip/phone/PipDoubleTapHelper.java | 6 +- .../flicker/pip/ExpandPipOnPinchOpenTest.kt | 77 ++++++ .../wm/flicker/helpers/GestureHelper.java | 224 ++++++++++++++++++ .../server/wm/flicker/helpers/PipAppHelper.kt | 50 ++++ 4 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt create mode 100644 tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java index acc0caf95e358..d7d335b856be2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java @@ -43,16 +43,16 @@ public class PipDoubleTapHelper { *

MAX - maximum allowed screen size

*/ @IntDef(value = { - SIZE_SPEC_CUSTOM, SIZE_SPEC_DEFAULT, - SIZE_SPEC_MAX + SIZE_SPEC_MAX, + SIZE_SPEC_CUSTOM }) @Retention(RetentionPolicy.SOURCE) @interface PipSizeSpec {} - static final int SIZE_SPEC_CUSTOM = 2; static final int SIZE_SPEC_DEFAULT = 0; static final int SIZE_SPEC_MAX = 1; + static final int SIZE_SPEC_CUSTOM = 2; /** * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from. diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt new file mode 100644 index 0000000000000..bcd01a414fd37 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 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.wm.shell.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window via pinch out gesture. + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExpandPipOnPinchOpenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + transitions { + pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30) + } + } + + /** + * Checks that the visible region area of [pipApp] always increases during the animation. + */ + @Postsubmit + @Test + fun pipLayerAreaIncreases() { + testSpec.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + previous.visibleRegion.notBiggerThan(current.visibleRegion.region) + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0) + ) + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java new file mode 100644 index 0000000000000..858cd7672fe3f --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2022 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.server.wm.flicker.helpers; + +import android.annotation.NonNull; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; + +/** + * Injects gestures given an {@link Instrumentation} object. + */ +public class GestureHelper { + // Inserted after each motion event injection. + private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; + + private final UiAutomation mUiAutomation; + + /** + * A pair of floating point values. + */ + public static class Tuple { + public float x; + public float y; + + public Tuple(float x, float y) { + this.x = x; + this.y = y; + } + } + + public GestureHelper(Instrumentation instrumentation) { + mUiAutomation = instrumentation.getUiAutomation(); + } + + /** + * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture. + * + * @param startPoint1 initial coordinates of the first pointer + * @param startPoint2 initial coordinates of the second pointer + * @param endPoint1 final coordinates of the first pointer + * @param endPoint2 final coordinates of the second pointer + * @param steps number of steps to take to animate pinching + * @return true if gesture is injected successfully + */ + public boolean pinch(@NonNull Tuple startPoint1, @NonNull Tuple startPoint2, + @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps) { + PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); + PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER); + + PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1); + PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1); + + PointerProperties[] ptrProps = new PointerProperties[] { + ptrProp1, ptrProp2 + }; + + PointerCoords[] ptrCoords = new PointerCoords[] { + ptrCoord1, ptrCoord2 + }; + + long downTime = SystemClock.uptimeMillis(); + + if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) { + return false; + } + + if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) { + return false; + } + + if (!movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint1, endPoint2 }, + downTime, steps)) { + return false; + } + + if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) { + return false; + } + + return primaryPointerUp(ptrProp1, ptrCoord1, downTime); + } + + private boolean primaryPointerDown(@NonNull PointerProperties prop, + @NonNull PointerCoords coord, long downTime) { + MotionEvent event = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1, + new PointerProperties[]{ prop }, new PointerCoords[]{ coord }); + + return injectEventSync(event); + } + + private boolean nonPrimaryPointerDown(@NonNull PointerProperties[] props, + @NonNull PointerCoords[] coords, long downTime, int index) { + // at least 2 pointers are needed + if (props.length != coords.length || coords.length < 2) { + return false; + } + + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN + + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords); + + return injectEventSync(event); + } + + private boolean movePointers(@NonNull PointerProperties[] props, + @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps) { + // the number of endpoints should be the same as the number of pointers + if (props.length != coords.length || coords.length != endPoints.length) { + return false; + } + + // prevent division by 0 and negative number of steps + if (steps < 1) { + steps = 1; + } + + // save the starting points before updating any pointers + Tuple[] startPoints = new Tuple[coords.length]; + + for (int i = 0; i < coords.length; i++) { + startPoints[i] = new Tuple(coords[i].x, coords[i].y); + } + + MotionEvent event; + long eventTime; + + for (int i = 0; i < steps; i++) { + // inject a delay between movements + SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); + + // update the coordinates + for (int j = 0; j < coords.length; j++) { + coords[j].x += (endPoints[j].x - startPoints[j].x) / steps; + coords[j].y += (endPoints[j].y - startPoints[j].y) / steps; + } + + eventTime = SystemClock.uptimeMillis(); + + event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE, + coords.length, props, coords); + + boolean didInject = injectEventSync(event); + + if (!didInject) { + return false; + } + } + + return true; + } + + private boolean primaryPointerUp(@NonNull PointerProperties prop, + @NonNull PointerCoords coord, long downTime) { + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_UP, 1, + new PointerProperties[]{ prop }, new PointerCoords[]{ coord }); + + return injectEventSync(event); + } + + private boolean nonPrimaryPointerUp(@NonNull PointerProperties[] props, + @NonNull PointerCoords[] coords, long downTime, int index) { + // at least 2 pointers are needed + if (props.length != coords.length || coords.length < 2) { + return false; + } + + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_UP + + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords); + + return injectEventSync(event); + } + + private PointerCoords getPointerCoord(float x, float y, float pressure, float size) { + PointerCoords ptrCoord = new PointerCoords(); + ptrCoord.x = x; + ptrCoord.y = y; + ptrCoord.pressure = pressure; + ptrCoord.size = size; + return ptrCoord; + } + + private PointerProperties getPointerProp(int id, int toolType) { + PointerProperties ptrProp = new PointerProperties(); + ptrProp.id = id; + ptrProp.toolType = toolType; + return ptrProp; + } + + private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, + int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords) { + return MotionEvent.obtain(downTime, eventTime, action, pointerCount, + ptrProps, ptrCoords, 0, 0, 1.0f, 1.0f, + 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + } + + private boolean injectEventSync(InputEvent event) { + return mUiAutomation.injectInputEvent(event, true); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index e730f315392bd..19ee09a81ec53 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -22,6 +22,7 @@ import android.media.session.MediaSessionManager import android.util.Log import androidx.test.uiautomator.By import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.GestureHelper.Tuple import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.WindowManagerConditionsFactory @@ -44,6 +45,8 @@ open class PipAppHelper(instrumentation: Instrumentation) : get() = mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` } + private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation) + open fun clickObject(resId: String) { val selector = By.res(`package`, resId) val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") @@ -51,6 +54,50 @@ open class PipAppHelper(instrumentation: Instrumentation) : obj.click() } + /** + * Expands the PIP window my using the pinch out gesture. + * + * @param percent The percentage by which to increase the pip window size. + * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f + */ + fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) { + // the percentage must be between 0.0f and 1.0f + if (percent <= 0.0f || percent > 1.0f) { + throw IllegalArgumentException("Percent must be between 0.0f and 1.0f") + } + + val windowRect = getWindowRect(wmHelper) + + // first pointer's initial x coordinate is halfway between the left edge and the center + val initLeftX = (windowRect.centerX() - windowRect.width / 4).toFloat() + // second pointer's initial x coordinate is halfway between the right edge and the center + val initRightX = (windowRect.centerX() + windowRect.width / 4).toFloat() + + // horizontal distance the window should increase by + val distIncrease = windowRect.width * percent + + // final x-coordinates + val finalLeftX = initLeftX - (distIncrease / 2) + val finalRightX = initRightX + (distIncrease / 2) + + // y-coordinate is the same throughout this animation + val yCoord = windowRect.centerY().toFloat() + + var adjustedSteps = MIN_STEPS_TO_ANIMATE + + // if distance per step is at least 1, then we can use the number of steps requested + if (distIncrease.toInt() / (steps * 2) >= 1) { + adjustedSteps = steps + } + + // if the distance per step is less than 1, carry out the animation in two steps + gestureHelper.pinch( + Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord), + Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps) + + waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect)) + } + /** * Launches the app through an intent instead of interacting with the launcher and waits until * the app window is in PIP mode @@ -194,5 +241,8 @@ open class PipAppHelper(instrumentation: Instrumentation) : private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual" private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter" + // minimum number of steps to take, when animating gestures, needs to be 2 + // so that there is at least a single intermediate layer that flicker tests can check + private const val MIN_STEPS_TO_ANIMATE = 2 } }