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

Commit 70a58b70 authored by Ikram Gabiyev's avatar Ikram Gabiyev
Browse files

Enforce at least a minimal size change for resize

In PiP2, we use config-at-end transitions for
certain resize transitions as they help easily
run animations as a part of the transition with the draws
being deferred until the transition is over. This also helps
reduce latency of how long it takes until we reenable user interactions.

However, config-at-end transitions have shown to evince WindowState offset
flickers when their size isn't changing during such transition, as
described in b/393159816.

So as a workaround we enforce a minimal one-pixel size change in such
event.

This CL also refactors PipDoubleTapHelper and PipDoubleTapHelperTest,
as its implementation had some gaps after workaround was applied.

Bug: 381014478
Flag: com.android.wm.shell.enable_pip2
Test: atest PipDoubleTapHelperTest
Test: pinch resize to max allowed size and then keep resizing to max size
Test: stash, then unstash PiP repeatedly
Change-Id: I6f11e7f73567be79190b66a50b58a358272a3f3a
parent ff56fe99
Loading
Loading
Loading
Loading
+18 −32
Original line number Diff line number Diff line
@@ -52,24 +52,15 @@ public class PipDoubleTapHelper {
    public static final int SIZE_SPEC_MAX = 1;
    public static final int SIZE_SPEC_CUSTOM = 2;

    /**
     * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from.
     *
     * <p>Each double tap toggles back and forth between {@code PipSizeSpec.CUSTOM} and
     * either {@code PipSizeSpec.MAX} or {@code PipSizeSpec.DEFAULT}. The choice between
     * the latter two sizes is determined based on the current state of the pip screen.</p>
     *
     * @param mPipBoundsState current state of the pip screen
     */
    @PipSizeSpec
    private static int getMaxOrDefaultPipSizeSpec(@NonNull PipBoundsState mPipBoundsState) {
    private static int getMaxOrDefaultPipSizeSpec(@NonNull PipBoundsState pipBoundsState) {
        // determine the average pip screen width
        int averageWidth = (mPipBoundsState.getMaxSize().x
                + mPipBoundsState.getMinSize().x) / 2;
        int averageWidth = (pipBoundsState.getMaxSize().x
                + pipBoundsState.getMinSize().x) / 2;

        // If pip screen width is above average, DEFAULT is the size spec we need to
        // toggle to. Otherwise, we choose MAX.
        return (mPipBoundsState.getBounds().width() > averageWidth)
        return (pipBoundsState.getBounds().width() > averageWidth)
                ? SIZE_SPEC_DEFAULT
                : SIZE_SPEC_MAX;
    }
@@ -77,35 +68,33 @@ public class PipDoubleTapHelper {
    /**
     * Determines the {@link PipSizeSpec} to toggle to on double tap.
     *
     * @param mPipBoundsState current state of the pip screen
     * @param pipBoundsState current state of the pip bounds
     * @param userResizeBounds latest user resized bounds (by pinching in/out)
     * @return pip screen size to switch to
     */
    @PipSizeSpec
    public static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState,
    public static int nextSizeSpec(@NonNull PipBoundsState pipBoundsState,
            @NonNull Rect userResizeBounds) {
        // is pip screen at its maximum
        boolean isScreenMax = mPipBoundsState.getBounds().width()
                == mPipBoundsState.getMaxSize().x;

        // is pip screen at its normal default size
        boolean isScreenDefault = (mPipBoundsState.getBounds().width()
                == mPipBoundsState.getNormalBounds().width())
                && (mPipBoundsState.getBounds().height()
                == mPipBoundsState.getNormalBounds().height());
        boolean isScreenMax = pipBoundsState.getBounds().width() == pipBoundsState.getMaxSize().x
                && pipBoundsState.getBounds().height() == pipBoundsState.getMaxSize().y;
        boolean isScreenDefault = (pipBoundsState.getBounds().width()
                == pipBoundsState.getNormalBounds().width())
                && (pipBoundsState.getBounds().height()
                == pipBoundsState.getNormalBounds().height());

        // edge case 1
        // if user hasn't resized screen yet, i.e. CUSTOM size does not exist yet
        // or if user has resized exactly to DEFAULT, then we just want to maximize
        if (isScreenDefault
                && userResizeBounds.width() == mPipBoundsState.getNormalBounds().width()) {
                && userResizeBounds.width() == pipBoundsState.getNormalBounds().width()
                && userResizeBounds.height() == pipBoundsState.getNormalBounds().height()) {
            return SIZE_SPEC_MAX;
        }

        // edge case 2
        // if user has maximized, then we want to toggle to DEFAULT
        // if user has resized to max, then we want to toggle to DEFAULT
        if (isScreenMax
                && userResizeBounds.width() == mPipBoundsState.getMaxSize().x) {
                && userResizeBounds.width() == pipBoundsState.getMaxSize().x
                && userResizeBounds.height() == pipBoundsState.getMaxSize().y) {
            return SIZE_SPEC_DEFAULT;
        }

@@ -113,9 +102,6 @@ public class PipDoubleTapHelper {
        if (isScreenDefault || isScreenMax) {
            return SIZE_SPEC_CUSTOM;
        }

        // if we are currently in user resized CUSTOM size state
        // then we toggle either to MAX or DEFAULT depending on the current pip screen state
        return getMaxOrDefaultPipSizeSpec(mPipBoundsState);
        return getMaxOrDefaultPipSizeSpec(pipBoundsState);
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -180,10 +180,17 @@ public class PipScheduler implements PipTransitionState.PipTransitionStateChange
            return;
        }
        WindowContainerTransaction wct = new WindowContainerTransaction();
        wct.setBounds(pipTaskToken, toBounds);
        if (configAtEnd) {
            wct.deferConfigToTransitionEnd(pipTaskToken);

            if (mPipBoundsState.getBounds().width() == toBounds.width()
                    && mPipBoundsState.getBounds().height() == toBounds.height()) {
                // TODO (b/393159816): Config-at-End causes a flicker without size change.
                // If PiP size isn't changing enforce a minimal one-pixel change as a workaround.
                --toBounds.bottom;
            }
        }
        wct.setBounds(pipTaskToken, toBounds);
        mPipTransitionController.startResizeTransition(wct, duration);
    }

+1 −0
Original line number Diff line number Diff line
@@ -934,6 +934,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
                    }

                    // the size to toggle to after a double tap
                    mPipBoundsState.setNormalBounds(getAdjustedNormalBounds());
                    int nextSize = PipDoubleTapHelper
                            .nextSizeSpec(mPipBoundsState, getUserResizeBounds());

+53 −110
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAU
import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_MAX;
import static com.android.wm.shell.common.pip.PipDoubleTapHelper.nextSizeSpec;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.graphics.Point;
@@ -41,131 +40,75 @@ import org.mockito.Mock;
 */
@RunWith(AndroidTestingRunner.class)
public class PipDoubleTapHelperTest extends ShellTestCase {
    // represents the current pip window state and has information on current
    // max, min, and normal sizes
    @Mock private PipBoundsState mBoundStateMock;
    // tied to boundsStateMock.getBounds() in setUp()
    @Mock private Rect mBoundsMock;
    @Mock private PipBoundsState mMockPipBoundsState;

    // represents the most recent manually resized bounds
    // i.e. dimensions from the most recent pinch in/out
    @Mock private Rect mUserResizeBoundsMock;
    // Actual dimension guidelines of the PiP bounds.
    private static final int MAX_EDGE_SIZE = 100;
    private static final int DEFAULT_EDGE_SIZE = 60;
    private static final int MIN_EDGE_SIZE = 50;
    private static final int AVERAGE_EDGE_SIZE = (MAX_EDGE_SIZE + MIN_EDGE_SIZE) / 2;

    // actual dimensions of the pip screen bounds
    private static final int MAX_WIDTH = 100;
    private static final int DEFAULT_WIDTH = 40;
    private static final int MIN_WIDTH = 10;

    private static final int AVERAGE_WIDTH = (MAX_WIDTH + MIN_WIDTH) / 2;

    /**
     * Initializes mocks and assigns values for different pip screen bounds.
     */
    @Before
    public void setUp() {
        // define pip bounds
        when(mBoundStateMock.getMaxSize()).thenReturn(new Point(MAX_WIDTH, 20));
        when(mBoundStateMock.getMinSize()).thenReturn(new Point(MIN_WIDTH, 2));
        when(mMockPipBoundsState.getMaxSize()).thenReturn(new Point(MAX_EDGE_SIZE, MAX_EDGE_SIZE));
        when(mMockPipBoundsState.getMinSize()).thenReturn(new Point(MIN_EDGE_SIZE, MIN_EDGE_SIZE));

        Rect rectMock = mock(Rect.class);
        when(rectMock.width()).thenReturn(DEFAULT_WIDTH);
        when(mBoundStateMock.getNormalBounds()).thenReturn(rectMock);
        final Rect normalBounds = new Rect(0, 0, DEFAULT_EDGE_SIZE, DEFAULT_EDGE_SIZE);
        when(mMockPipBoundsState.getNormalBounds()).thenReturn(normalBounds);
    }

        when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH);
        when(mBoundStateMock.getBounds()).thenReturn(mBoundsMock);
    @Test
    public void nextSizeSpec_resizedWiderThanAverage_returnDefaultThenCustom() {
        final int resizeEdgeSize = (MAX_EDGE_SIZE + AVERAGE_EDGE_SIZE) / 2;
        final Rect userResizeBounds = new Rect(0, 0, resizeEdgeSize, resizeEdgeSize);
        when(mMockPipBoundsState.getBounds()).thenReturn(userResizeBounds);
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_DEFAULT);

        // once we toggle to DEFAULT only PiP bounds state gets updated - not the user resize bounds
        when(mMockPipBoundsState.getBounds()).thenReturn(
                new Rect(0, 0, DEFAULT_EDGE_SIZE, DEFAULT_EDGE_SIZE));
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_CUSTOM);
    }

    /**
     * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
     *
     * <p>when the user resizes the screen to a larger than the average but not the maximum width,
     * then we toggle between {@code PipSizeSpec.CUSTOM} and {@code PipSizeSpec.DEFAULT}
     */
    @Test
    public void testNextScreenSize_resizedWiderThanAverage_returnDefaultThenCustom() {
        // make the user resize width in between MAX and average
        when(mUserResizeBoundsMock.width()).thenReturn((MAX_WIDTH + AVERAGE_WIDTH) / 2);
        // make current bounds same as resized bound since no double tap yet
        when(mBoundsMock.width()).thenReturn((MAX_WIDTH + AVERAGE_WIDTH) / 2);

        // then nextScreenSize() i.e. double tapping should
        // toggle to DEFAULT state
        Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
                SIZE_SPEC_DEFAULT);

        // once we toggle to DEFAULT our screen size gets updated
        // but not the user resize bounds
        when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH);

        // then nextScreenSize() i.e. double tapping should
        // toggle to CUSTOM state
        Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
                SIZE_SPEC_CUSTOM);
    public void nextSizeSpec_resizedSmallerThanAverage_returnMaxThenCustom() {
        final int resizeEdgeSize = (AVERAGE_EDGE_SIZE + MIN_EDGE_SIZE) / 2;
        final Rect userResizeBounds = new Rect(0, 0, resizeEdgeSize, resizeEdgeSize);
        when(mMockPipBoundsState.getBounds()).thenReturn(userResizeBounds);
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_MAX);

        // Once we toggle to MAX our screen size gets updated but not the user resize bounds
        when(mMockPipBoundsState.getBounds()).thenReturn(
                new Rect(0, 0, MAX_EDGE_SIZE, MAX_EDGE_SIZE));
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_CUSTOM);
    }

    /**
     * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
     *
     * <p>when the user resizes the screen to a smaller than the average but not the default width,
     * then we toggle between {@code PipSizeSpec.CUSTOM} and {@code PipSizeSpec.MAX}
     */
    @Test
    public void testNextScreenSize_resizedNarrowerThanAverage_returnMaxThenCustom() {
        // make the user resize width in between MIN and average
        when(mUserResizeBoundsMock.width()).thenReturn((MIN_WIDTH + AVERAGE_WIDTH) / 2);
        // make current bounds same as resized bound since no double tap yet
        when(mBoundsMock.width()).thenReturn((MIN_WIDTH + AVERAGE_WIDTH) / 2);

        // then nextScreenSize() i.e. double tapping should
        // toggle to MAX state
        Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
                SIZE_SPEC_MAX);

        // once we toggle to MAX our screen size gets updated
        // but not the user resize bounds
        when(mBoundsMock.width()).thenReturn(MAX_WIDTH);

        // then nextScreenSize() i.e. double tapping should
        // toggle to CUSTOM state
        Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
                SIZE_SPEC_CUSTOM);
    public void nextSizeSpec_resizedToMax_returnDefault() {
        final Rect userResizeBounds = new Rect(0, 0, MAX_EDGE_SIZE, MAX_EDGE_SIZE);
        when(mMockPipBoundsState.getBounds()).thenReturn(userResizeBounds);
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_DEFAULT);
    }

    /**
     * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
     *
     * <p>when the user resizes the screen to exactly the maximum width
     * then we toggle to {@code PipSizeSpec.DEFAULT}
     */
    @Test
    public void testNextScreenSize_resizedToMax_returnDefault() {
        // the resized width is the same as MAX_WIDTH
        when(mUserResizeBoundsMock.width()).thenReturn(MAX_WIDTH);
        // the current bounds are also at MAX_WIDTH
        when(mBoundsMock.width()).thenReturn(MAX_WIDTH);

        // then nextScreenSize() i.e. double tapping should
        // toggle to DEFAULT state
        Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
                SIZE_SPEC_DEFAULT);
    public void nextSizeSpec_resizedToDefault_returnMax() {
        final Rect userResizeBounds = new Rect(0, 0, DEFAULT_EDGE_SIZE, DEFAULT_EDGE_SIZE);
        when(mMockPipBoundsState.getBounds()).thenReturn(userResizeBounds);
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_MAX);
    }

    @Test
    public void nextSizeSpec_resizedToAlmostMax_returnDefault() {
        final Rect userResizeBounds = new Rect(0, 0, MAX_EDGE_SIZE, MAX_EDGE_SIZE - 1);
        when(mMockPipBoundsState.getBounds()).thenReturn(userResizeBounds);
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_DEFAULT);
    }

    /**
     * Tests {@link PipDoubleTapHelper#nextSizeSpec(PipBoundsState, Rect)}.
     *
     * <p>when the user resizes the screen to exactly the default width
     * then we toggle to {@code PipSizeSpec.MAX}
     */
    @Test
    public void testNextScreenSize_resizedToDefault_returnMax() {
        // the resized width is the same as DEFAULT_WIDTH
        when(mUserResizeBoundsMock.width()).thenReturn(DEFAULT_WIDTH);
        // the current bounds are also at DEFAULT_WIDTH
        when(mBoundsMock.width()).thenReturn(DEFAULT_WIDTH);

        // then nextScreenSize() i.e. double tapping should
        // toggle to MAX state
        Assert.assertSame(nextSizeSpec(mBoundStateMock, mUserResizeBoundsMock),
                SIZE_SPEC_MAX);
    public void nextSizeSpec_resizedToAlmostMin_returnMax() {
        final Rect userResizeBounds = new Rect(0, 0, MIN_EDGE_SIZE, MIN_EDGE_SIZE + 1);
        when(mMockPipBoundsState.getBounds()).thenReturn(userResizeBounds);
        Assert.assertEquals(nextSizeSpec(mMockPipBoundsState, userResizeBounds), SIZE_SPEC_MAX);
    }
}