diff options
5 files changed, 329 insertions, 27 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index bc0918331168..e901c39b8792 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -322,7 +322,7 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); mPipBoundsState.setBounds(toBounds); } - t.setBounds(mPipTransitionState.mPipTaskToken, mPipBoundsState.getBounds()); + t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds()); } private void setDisplayLayout(DisplayLayout layout) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 607de0eccd77..5438a014af00 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -25,10 +25,13 @@ import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; import android.view.SurfaceControl; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; @@ -48,11 +51,13 @@ public class PipScheduler { private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private PipTransitionController mPipTransitionController; - private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; @Nullable private Runnable mUpdateMovementBoundsRunnable; + private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier; + public PipScheduler(Context context, PipBoundsState pipBoundsState, ShellExecutor mainExecutor, @@ -64,10 +69,7 @@ public class PipScheduler { mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); - } - - ShellExecutor getMainExecutor() { - return mMainExecutor; + mPipAlphaAnimatorSupplier = PipAlphaAnimator::new; } void setPipTransitionController(PipTransitionController pipTransitionController) { @@ -76,27 +78,29 @@ public class PipScheduler { @Nullable private WindowContainerTransaction getExitPipViaExpandTransaction() { - if (mPipTransitionState.mPipTaskToken == null) { + WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); + if (pipTaskToken == null) { return null; } WindowContainerTransaction wct = new WindowContainerTransaction(); // final expanded bounds to be inherited from the parent - wct.setBounds(mPipTransitionState.mPipTaskToken, null); + wct.setBounds(pipTaskToken, null); // if we are hitting a multi-activity case // windowing mode change will reparent to original host task - wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED); + wct.setWindowingMode(pipTaskToken, WINDOWING_MODE_UNDEFINED); return wct; } @Nullable private WindowContainerTransaction getRemovePipTransaction() { - if (mPipTransitionState.mPipTaskToken == null) { + WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); + if (pipTaskToken == null) { return null; } WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mPipTransitionState.mPipTaskToken, null); - wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED); - wct.reorder(mPipTransitionState.mPipTaskToken, false); + wct.setBounds(pipTaskToken, null); + wct.setWindowingMode(pipTaskToken, WINDOWING_MODE_UNDEFINED); + wct.reorder(pipTaskToken, false); return wct; } @@ -117,7 +121,7 @@ public class PipScheduler { /** Runs remove PiP animation and schedules remove PiP transition after the animation ends. */ public void removePipAfterAnimation() { SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - PipAlphaAnimator animator = new PipAlphaAnimator(mContext, + PipAlphaAnimator animator = mPipAlphaAnimatorSupplier.get(mContext, mPipTransitionState.getPinnedTaskLeash(), tx, PipAlphaAnimator.FADE_OUT); animator.setAnimationEndCallback(this::scheduleRemovePipImmediately); animator.start(); @@ -159,13 +163,14 @@ public class PipScheduler { * for running the animator will get this as an extra. */ public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd, int duration) { - if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { + WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); + if (pipTaskToken == null || !mPipTransitionState.isInPip()) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + wct.setBounds(pipTaskToken, toBounds); if (configAtEnd) { - wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken); + wct.deferConfigToTransitionEnd(pipTaskToken); } mPipTransitionController.startResizeTransition(wct, duration); } @@ -204,7 +209,7 @@ public class PipScheduler { return; } SurfaceControl leash = mPipTransitionState.getPinnedTaskLeash(); - final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); Matrix transformTensor = new Matrix(); final float[] mMatrixTmp = new float[9]; @@ -218,7 +223,7 @@ public class PipScheduler { tx.apply(); } - void setUpdateMovementBoundsRunnable(Runnable updateMovementBoundsRunnable) { + void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) { mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; } @@ -235,4 +240,23 @@ public class PipScheduler { mPipBoundsState.setBounds(newBounds); maybeUpdateMovementBounds(); } + + @VisibleForTesting + void setSurfaceControlTransactionFactory( + @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; + } + + @VisibleForTesting + interface PipAlphaAnimatorSupplier { + PipAlphaAnimator get(@NonNull Context context, + SurfaceControl leash, + SurfaceControl.Transaction tx, + @PipAlphaAnimator.Fade int direction); + } + + @VisibleForTesting + void setPipAlphaAnimatorSupplier(@NonNull PipAlphaAnimatorSupplier supplier) { + mPipAlphaAnimatorSupplier = supplier; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 3caad0966b1f..02f595537d03 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -543,7 +543,7 @@ public class PipTransition extends PipTransitionController implements @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - WindowContainerToken pipToken = mPipTransitionState.mPipTaskToken; + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); if (pipChange == null) { @@ -773,11 +773,11 @@ public class PipTransition extends PipTransitionController implements } private boolean isRemovePipTransition(@NonNull TransitionInfo info) { - if (mPipTransitionState.mPipTaskToken == null) { + if (mPipTransitionState.getPipTaskToken() == null) { // PiP removal makes sense if enter-PiP has cached a valid pinned task token. return false; } - TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.mPipTaskToken); + TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.getPipTaskToken()); if (pipChange == null) { // Search for the PiP change by token since the windowing mode might be FULLSCREEN now. return false; @@ -859,18 +859,18 @@ public class PipTransition extends PipTransitionController implements Preconditions.checkState(extra != null, "No extra bundle for " + mPipTransitionState); - mPipTransitionState.mPipTaskToken = extra.getParcelable( - PIP_TASK_TOKEN, WindowContainerToken.class); + mPipTransitionState.setPipTaskToken(extra.getParcelable( + PIP_TASK_TOKEN, WindowContainerToken.class)); mPipTransitionState.setPinnedTaskLeash(extra.getParcelable( PIP_TASK_LEASH, SurfaceControl.class)); - boolean hasValidTokenAndLeash = mPipTransitionState.mPipTaskToken != null + boolean hasValidTokenAndLeash = mPipTransitionState.getPipTaskToken() != null && mPipTransitionState.getPinnedTaskLeash() != null; Preconditions.checkState(hasValidTokenAndLeash, "Unexpected bundle for " + mPipTransitionState); break; case PipTransitionState.EXITED_PIP: - mPipTransitionState.mPipTaskToken = null; + mPipTransitionState.setPipTaskToken(null); mPipTransitionState.setPinnedTaskLeash(null); break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 03e06f906015..8e90bfee2636 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -138,7 +138,7 @@ public class PipTransitionState { // pinned PiP task's WC token @Nullable - WindowContainerToken mPipTaskToken; + private WindowContainerToken mPipTaskToken; // pinned PiP task's leash @Nullable @@ -304,6 +304,14 @@ public class PipTransitionState { mSwipePipToHomeAppBounds.setEmpty(); } + @Nullable WindowContainerToken getPipTaskToken() { + return mPipTaskToken; + } + + public void setPipTaskToken(@Nullable WindowContainerToken token) { + mPipTaskToken = token; + } + @Nullable SurfaceControl getPinnedTaskLeash() { return mPinnedTaskLeash; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java new file mode 100644 index 000000000000..cab625216236 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2024 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.pip2.phone; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.MatchersKt.eq; +import static org.mockito.kotlin.VerificationKt.times; +import static org.mockito.kotlin.VerificationKt.verify; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip2.animation.PipAlphaAnimator; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipScheduler} + */ + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipSchedulerTest { + private static final int TEST_RESIZE_DURATION = 1; + private static final Rect TEST_STARTING_BOUNDS = new Rect(0, 0, 10, 10); + private static final Rect TEST_BOUNDS = new Rect(0, 0, 20, 20); + + @Mock private Context mMockContext; + @Mock private Resources mMockResources; + @Mock private PipBoundsState mMockPipBoundsState; + @Mock private ShellExecutor mMockMainExecutor; + @Mock private PipTransitionState mMockPipTransitionState; + @Mock private PipTransitionController mMockPipTransitionController; + @Mock private Runnable mMockUpdateMovementBoundsRunnable; + @Mock private WindowContainerToken mMockPipTaskToken; + @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; + @Mock private SurfaceControl.Transaction mMockTransaction; + @Mock private PipAlphaAnimator mMockAlphaAnimator; + + @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; + @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor; + + private PipScheduler mPipScheduler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockResources.getInteger(anyInt())).thenReturn(0); + when(mMockPipBoundsState.getBounds()).thenReturn(TEST_STARTING_BOUNDS); + when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); + when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockTransaction); + + mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, + mMockPipTransitionState); + mPipScheduler.setPipTransitionController(mMockPipTransitionController); + mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); + mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, tx, direction) -> + mMockAlphaAnimator); + + SurfaceControl testLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipSchedulerTest") + .setCallsite("PipSchedulerTest") + .build(); + when(mMockPipTransitionState.getPinnedTaskLeash()).thenReturn(testLeash); + } + + @Test + public void scheduleExitPipViaExpand_nullTaskToken_noop() { + setNullPipTaskToken(); + + mPipScheduler.scheduleExitPipViaExpand(); + + verify(mMockMainExecutor, never()).execute(any()); + } + + @Test + public void scheduleExitPipViaExpand_exitTransitionCalled() { + setMockPipTaskToken(); + + mPipScheduler.scheduleExitPipViaExpand(); + + verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + mRunnableArgumentCaptor.getValue().run(); + + verify(mMockPipTransitionController, times(1)) + .startExitTransition(eq(TRANSIT_EXIT_PIP), any(), isNull()); + } + + @Test + public void removePipAfterAnimation() { + //TODO: Update once this is changed to run animation as part of transition + setMockPipTaskToken(); + + mPipScheduler.removePipAfterAnimation(); + verify(mMockAlphaAnimator, times(1)) + .setAnimationEndCallback(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + verify(mMockAlphaAnimator, times(1)).start(); + + mRunnableArgumentCaptor.getValue().run(); + + verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + + mRunnableArgumentCaptor.getValue().run(); + + verify(mMockPipTransitionController, times(1)) + .startExitTransition(eq(TRANSIT_REMOVE_PIP), any(), isNull()); + } + + @Test + public void scheduleAnimateResizePip_bounds_nullTaskToken_noop() { + setNullPipTaskToken(); + + mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS); + + verify(mMockPipTransitionController, never()).startResizeTransition(any(), anyInt()); + } + + @Test + public void scheduleAnimateResizePip_boundsConfig_nullTaskToken_noop() { + setNullPipTaskToken(); + + mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true); + + verify(mMockPipTransitionController, never()).startResizeTransition(any(), anyInt()); + } + + @Test + public void scheduleAnimateResizePip_boundsConfig_setsConfigAtEnd() { + setMockPipTaskToken(); + when(mMockPipTransitionState.isInPip()).thenReturn(true); + + mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true); + + verify(mMockPipTransitionController, times(1)) + .startResizeTransition(mWctArgumentCaptor.capture(), anyInt()); + assertNotNull(mWctArgumentCaptor.getValue()); + assertNotNull(mWctArgumentCaptor.getValue().getChanges()); + boolean hasConfigAtEndChange = false; + for (WindowContainerTransaction.Change change : + mWctArgumentCaptor.getValue().getChanges().values()) { + if (change.getConfigAtTransitionEnd()) { + hasConfigAtEndChange = true; + break; + } + } + assertTrue(hasConfigAtEndChange); + } + + @Test + public void scheduleAnimateResizePip_boundsConfigDuration_nullTaskToken_noop() { + setNullPipTaskToken(); + + mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true, TEST_RESIZE_DURATION); + + verify(mMockPipTransitionController, never()).startResizeTransition(any(), anyInt()); + } + + @Test + public void scheduleAnimateResizePip_notInPip_noop() { + setMockPipTaskToken(); + when(mMockPipTransitionState.isInPip()).thenReturn(false); + + mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true, TEST_RESIZE_DURATION); + + verify(mMockPipTransitionController, never()).startResizeTransition(any(), anyInt()); + } + + @Test + public void scheduleAnimateResizePip_resizeTransition() { + setMockPipTaskToken(); + when(mMockPipTransitionState.isInPip()).thenReturn(true); + + mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true, TEST_RESIZE_DURATION); + + verify(mMockPipTransitionController, times(1)) + .startResizeTransition(any(), eq(TEST_RESIZE_DURATION)); + } + + @Test + public void scheduleUserResizePip_emptyBounds_noop() { + setMockPipTaskToken(); + + mPipScheduler.scheduleUserResizePip(new Rect()); + + verify(mMockTransaction, never()).apply(); + } + + @Test + public void scheduleUserResizePip_rotation_emptyBounds_noop() { + setMockPipTaskToken(); + + mPipScheduler.scheduleUserResizePip(new Rect(), 90); + + verify(mMockTransaction, never()).apply(); + } + + @Test + public void scheduleUserResizePip_applyTransaction() { + setMockPipTaskToken(); + + mPipScheduler.scheduleUserResizePip(TEST_BOUNDS, 90); + + verify(mMockTransaction, times(1)).apply(); + } + + @Test + public void finishResize_movementBoundsRunnableCalled() { + mPipScheduler.setUpdateMovementBoundsRunnable(mMockUpdateMovementBoundsRunnable); + mPipScheduler.scheduleFinishResizePip(TEST_BOUNDS); + + verify(mMockUpdateMovementBoundsRunnable, times(1)).run(); + } + + private void setNullPipTaskToken() { + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(null); + } + + private void setMockPipTaskToken() { + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken); + } +} |