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

Commit ebc7ee82 authored by Ikram Gabiyev's avatar Ikram Gabiyev Committed by Android (Google) Code Review
Browse files

Merge "Add flicker test for pinch out pip screen resizing"

parents 70fea19d 330c3b3c
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -43,16 +43,16 @@ public class PipDoubleTapHelper {
     * <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.
+77 −0
Original line number Diff line number Diff line
/*
 * 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)
                )
        }
    }
}
+224 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+50 −0
Original line number Diff line number Diff line
@@ -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
    }
}