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
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 acc0caf..d7d335b 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 @@
      * <p>MAX - maximum allowed screen size</p>
      */
     @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 0000000..bcd01a4
--- /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<FlickerTestParameter> {
+            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 0000000..858cd76
--- /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 e730f31..19ee09a 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.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 @@
         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")
@@ -52,6 +55,50 @@
     }
 
     /**
+     * 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 @@
         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
     }
 }