summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ikram Gabiyev <gabiyev@google.com> 2025-02-13 12:57:24 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-02-13 12:57:24 -0800
commitf7f6931f1d7ea585f7cb70ac6e34abc2cfad45e7 (patch)
tree237d5670c27846d65607a8685de2739808db1dae
parent3e3a4c3b25523271094191a812a3bb5b846ae176 (diff)
parent5da67811e114bfc31ce1286ce8421d1ac5eb76d1 (diff)
Merge "[PiP2] Implement expand PiP to Split CUJ" into main
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java191
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java331
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java133
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java51
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java188
9 files changed, 758 insertions, 177 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 7d80ee5f3bb6..f8b18f29c797 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -53,6 +53,7 @@ import com.android.wm.shell.pip2.phone.PipTransition;
import com.android.wm.shell.pip2.phone.PipTransitionState;
import com.android.wm.shell.pip2.phone.PipUiStateChangeController;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -85,11 +86,13 @@ public abstract class Pip2Module {
@NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipUiStateChangeController pipUiStateChangeController,
DisplayController displayController,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
pipScheduler, pipStackListenerController, pipDisplayLayoutState,
- pipUiStateChangeController, displayController, pipDesktopState);
+ pipUiStateChangeController, displayController, splitScreenControllerOptional,
+ pipDesktopState);
}
@WMSingleton
@@ -140,9 +143,10 @@ public abstract class Pip2Module {
PipBoundsState pipBoundsState,
@ShellMainThread ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
- pipDesktopState);
+ splitScreenControllerOptional, pipDesktopState);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index da3181096d98..cef18f55b86d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -145,7 +145,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH
/**
* Called when the Shell wants to start an exit-via-expand from Pip transition/animation.
*/
- public void startExpandTransition(WindowContainerTransaction out) {
+ public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) {
// Default implementation does nothing.
}
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 e17587ff18bc..df7a25af8376 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
@@ -35,6 +35,10 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.split.SplitScreenConstants;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+
+import java.util.Optional;
/**
* Scheduler for Shell initiated PiP transitions and animations.
@@ -47,6 +51,7 @@ public class PipScheduler {
private final ShellExecutor mMainExecutor;
private final PipTransitionState mPipTransitionState;
private final PipDesktopState mPipDesktopState;
+ private final Optional<SplitScreenController> mSplitScreenControllerOptional;
private PipTransitionController mPipTransitionController;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
@@ -59,12 +64,14 @@ public class PipScheduler {
PipBoundsState pipBoundsState,
ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
mContext = context;
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
mPipTransitionState = pipTransitionState;
mPipDesktopState = pipDesktopState;
+ mSplitScreenControllerOptional = splitScreenControllerOptional;
mSurfaceControlTransactionFactory =
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
@@ -96,10 +103,23 @@ public class PipScheduler {
public void scheduleExitPipViaExpand() {
mMainExecutor.execute(() -> {
if (!mPipTransitionState.isInPip()) return;
- WindowContainerTransaction wct = getExitPipViaExpandTransaction();
- if (wct != null) {
- mPipTransitionController.startExpandTransition(wct);
- }
+
+ final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction();
+ if (expandWct == null) return;
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mSplitScreenControllerOptional.ifPresent(splitScreenController -> {
+ int lastParentTaskId = mPipTransitionState.getPipTaskInfo()
+ .lastParentTaskIdBeforePip;
+ if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) {
+ splitScreenController.prepareEnterSplitScreen(wct,
+ null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED);
+ }
+ });
+
+ boolean toSplit = !wct.isEmpty();
+ wct.merge(expandWct, true /* transfer */);
+ mPipTransitionController.startExpandTransition(wct, toSplit);
});
}
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 9adaa3614a0f..e7bffe3bc4bc 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
@@ -16,7 +16,6 @@
package com.android.wm.shell.pip2.phone;
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Surface.ROTATION_0;
@@ -29,7 +28,13 @@ import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipChange;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
import static com.android.wm.shell.transition.Transitions.transitTypeToString;
@@ -45,7 +50,6 @@ import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -70,11 +74,14 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.pip2.animation.PipEnterAnimator;
-import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.transition.PipExpandHandler;
import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import java.util.Optional;
+
/**
* Implementation of transitions for PiP on phone.
*/
@@ -130,6 +137,7 @@ public class PipTransition extends PipTransitionController implements
//
// Internal state and relevant cached info
//
+ private final PipExpandHandler mExpandHandler;
private Transitions.TransitionFinishCallback mFinishCallback;
@@ -151,6 +159,7 @@ public class PipTransition extends PipTransitionController implements
PipDisplayLayoutState pipDisplayLayoutState,
PipUiStateChangeController pipUiStateChangeController,
DisplayController displayController,
+ Optional<SplitScreenController> splitScreenControllerOptional,
PipDesktopState pipDesktopState) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
@@ -165,6 +174,9 @@ public class PipTransition extends PipTransitionController implements
mDisplayController = displayController;
mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext);
mPipDesktopState = pipDesktopState;
+
+ mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm,
+ pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional);
}
@Override
@@ -184,10 +196,11 @@ public class PipTransition extends PipTransitionController implements
//
@Override
- public void startExpandTransition(WindowContainerTransaction out) {
+ public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) {
if (out == null) return;
mPipTransitionState.setState(PipTransitionState.EXITING_PIP);
- mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this);
+ mExitViaExpandTransition = mTransitions.startTransition(toSplit ? TRANSIT_EXIT_PIP_TO_SPLIT
+ : TRANSIT_EXIT_PIP, out, this);
}
@Override
@@ -239,10 +252,11 @@ public class PipTransition extends PipTransitionController implements
@NonNull SurfaceControl.Transaction finishT,
@NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- // Just jump-cut the current animation if any, but do not merge.
if (info.getType() == TRANSIT_EXIT_PIP) {
end();
}
+ mExpandHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget,
+ finishCallback);
}
@Override
@@ -290,7 +304,8 @@ public class PipTransition extends PipTransitionController implements
finishCallback);
} else if (transition == mExitViaExpandTransition) {
mExitViaExpandTransition = null;
- return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
+ return mExpandHandler.startAnimation(transition, info, startTransaction,
+ finishTransaction, finishCallback);
} else if (transition == mResizeTransition) {
mResizeTransition = null;
return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback);
@@ -436,7 +451,7 @@ public class PipTransition extends PipTransitionController implements
(destinationBounds.height() - overlaySize) / 2f);
}
- final int delta = getFixedRotationDelta(info, pipChange);
+ final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
if (delta != ROTATION_0) {
// Update transition target changes in place to prepare for fixed rotation.
handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
@@ -496,7 +511,7 @@ public class PipTransition extends PipTransitionController implements
final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange,
pipActivityChange);
- final int delta = getFixedRotationDelta(info, pipChange);
+ final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
if (delta != ROTATION_0) {
// Update transition target changes in place to prepare for fixed rotation.
handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
@@ -585,27 +600,6 @@ public class PipTransition extends PipTransitionController implements
endBounds.top + activityEndOffset.y);
}
- private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
- final Rect endBounds = outPipTaskChange.getEndAbsBounds();
- final int width = endBounds.width();
- final int height = endBounds.height();
- final int left = endBounds.left;
- final int top = endBounds.top;
- int newTop, newLeft;
-
- if (delta == Surface.ROTATION_90) {
- newLeft = top;
- newTop = -(left + width);
- } else {
- newLeft = -(height + top);
- newTop = left;
- }
- // Modify the endBounds, rotating and placing them potentially off-screen, so that
- // as we translate and rotate around the origin, we place them right into the target.
- endBounds.set(newLeft, newTop, newLeft + height, newTop + width);
- }
-
-
private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -633,83 +627,6 @@ public class PipTransition extends PipTransitionController implements
return true;
}
- private boolean startExpandAnimation(@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
-
- TransitionInfo.Change pipChange = getChangeByToken(info, pipToken);
- if (pipChange == null) {
- // pipChange is null, check to see if we've reparented the PIP activity for
- // the multi activity case. If so we should use the activity leash instead
- for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getTaskInfo() == null
- && change.getLastParent() != null
- && change.getLastParent().equals(pipToken)) {
- pipChange = change;
- break;
- }
- }
-
- // failsafe
- if (pipChange == null) {
- return false;
- }
- }
- mFinishCallback = finishCallback;
-
- // The parent change if we were in a multi-activity PiP; null if single activity PiP.
- final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null
- ? getChangeByToken(info, pipChange.getParent()) : null;
- if (parentBeforePip != null) {
- // For multi activity, we need to manually set the leash layer
- startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1);
- }
-
- final Rect startBounds = pipChange.getStartAbsBounds();
- final Rect endBounds = pipChange.getEndAbsBounds();
- final SurfaceControl pipLeash = getLeash(pipChange);
-
- PictureInPictureParams params = null;
- if (pipChange.getTaskInfo() != null) {
- // single activity
- params = getPipParams(pipChange);
- } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) {
- // multi activity
- params = getPipParams(parentBeforePip);
- }
- final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
- startBounds);
-
- // We define delta = startRotation - endRotation, so we need to flip the sign.
- final int delta = -getFixedRotationDelta(info, pipChange);
- if (delta != ROTATION_0) {
- // Update PiP target change in place to prepare for fixed rotation;
- handleExpandFixedRotation(pipChange, delta);
- }
-
- PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash,
- startTransaction, finishTransaction, endBounds, startBounds, endBounds,
- sourceRectHint, delta);
- animator.setAnimationEndCallback(() -> {
- if (parentBeforePip != null) {
- // TODO b/377362511: Animate local leash instead to also handle letterbox case.
- // For multi-activity, set the crop to be null
- finishTransaction.setCrop(pipLeash, null);
- }
- finishTransition();
- });
- cacheAndStartTransitionAnimator(animator);
-
- // Save the PiP bounds in case, we re-enter the PiP with the same component.
- float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
- mPipBoundsState.getBounds());
- mPipBoundsState.saveReentryState(snapFraction);
-
- return true;
- }
-
private boolean startRemoveAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -743,29 +660,6 @@ public class PipTransition extends PipTransitionController implements
// Various helpers to resolve transition requests and infos
//
- @Nullable
- private TransitionInfo.Change getPipChange(TransitionInfo info) {
- for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getTaskInfo() != null
- && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
- return change;
- }
- }
- return null;
- }
-
- @Nullable
- private TransitionInfo.Change getChangeByToken(TransitionInfo info,
- WindowContainerToken token) {
- for (TransitionInfo.Change change : info.getChanges()) {
- if (change.getTaskInfo() != null
- && change.getTaskInfo().getToken().equals(token)) {
- return change;
- }
- }
- return null;
- }
-
@NonNull
private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change pipTaskChange,
@@ -789,8 +683,8 @@ public class PipTransition extends PipTransitionController implements
Rect cutoutInsets = parentBeforePip != null
? parentBeforePip.getTaskInfo().displayCutoutInsets
: pipTaskChange.getTaskInfo().displayCutoutInsets;
- if (cutoutInsets != null
- && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
+ if (cutoutInsets != null && getFixedRotationDelta(info, pipTaskChange,
+ mPipDisplayLayoutState) == ROTATION_90) {
adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
}
if (mPipDesktopState.isDesktopWindowingPipEnabled()) {
@@ -807,25 +701,6 @@ public class PipTransition extends PipTransitionController implements
return adjustedSourceRectHint;
}
- @Surface.Rotation
- private int getFixedRotationDelta(@NonNull TransitionInfo info,
- @NonNull TransitionInfo.Change pipChange) {
- TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
- int startRotation = pipChange.getStartRotation();
- if (pipChange.getEndRotation() != ROTATION_UNDEFINED
- && startRotation != pipChange.getEndRotation()) {
- // If PiP change was collected along with the display change and the orientation change
- // happened in sync with the PiP change, then do not treat this as fixed-rotation case.
- return ROTATION_0;
- }
-
- int endRotation = fixedRotationChange != null
- ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation();
- int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
- : startRotation - endRotation;
- return delta;
- }
-
private void prepareOtherTargetTransforms(TransitionInfo info,
SurfaceControl.Transaction startTransaction,
SurfaceControl.Transaction finishTransaction) {
@@ -1012,20 +887,6 @@ public class PipTransition extends PipTransitionController implements
mTransitionAnimator.start();
}
- @NonNull
- private static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) {
- return pipChange.getTaskInfo().pictureInPictureParams != null
- ? pipChange.getTaskInfo().pictureInPictureParams
- : new PictureInPictureParams.Builder().build();
- }
-
- @NonNull
- private static SurfaceControl getLeash(TransitionInfo.Change change) {
- SurfaceControl leash = change.getLeash();
- Preconditions.checkNotNull(leash, "Leash is null for change=" + change);
- return leash;
- }
-
//
// Miscellaneous callbacks and listeners
//
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 8805cbb0dfbd..18c9a705dcf7 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
@@ -314,7 +314,8 @@ public class PipTransitionState {
mSwipePipToHomeAppBounds.setEmpty();
}
- @Nullable WindowContainerToken getPipTaskToken() {
+ @Nullable
+ public WindowContainerToken getPipTaskToken() {
return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java
new file mode 100644
index 000000000000..db4942b2fb95
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2025 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.transition;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash;
+import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
+
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PictureInPictureParams;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.Optional;
+
+public class PipExpandHandler implements Transitions.TransitionHandler {
+ private final Context mContext;
+ private final PipBoundsState mPipBoundsState;
+ private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private final PipTransitionState mPipTransitionState;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final Optional<SplitScreenController> mSplitScreenControllerOptional;
+
+ @Nullable
+ private Transitions.TransitionFinishCallback mFinishCallback;
+ @Nullable
+ private ValueAnimator mTransitionAnimator;
+
+ private PipExpandAnimatorSupplier mPipExpandAnimatorSupplier;
+
+ public PipExpandHandler(Context context,
+ PipBoundsState pipBoundsState,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipTransitionState pipTransitionState,
+ PipDisplayLayoutState pipDisplayLayoutState,
+ Optional<SplitScreenController> splitScreenControllerOptional) {
+ mContext = context;
+ mPipBoundsState = pipBoundsState;
+ mPipBoundsAlgorithm = pipBoundsAlgorithm;
+ mPipTransitionState = pipTransitionState;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mSplitScreenControllerOptional = splitScreenControllerOptional;
+
+ mPipExpandAnimatorSupplier = PipExpandAnimator::new;
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ // All Exit-via-Expand from PiP transitions are Shell initiated.
+ return null;
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ switch (info.getType()) {
+ case TRANSIT_EXIT_PIP:
+ return startExpandAnimation(info, startTransaction, finishTransaction,
+ finishCallback);
+ case TRANSIT_EXIT_PIP_TO_SPLIT:
+ return startExpandToSplitAnimation(info, startTransaction, finishTransaction,
+ finishCallback);
+ }
+ return false;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ end();
+ }
+
+ /**
+ * Ends the animation if such is running in the context of expanding out of PiP.
+ */
+ public void end() {
+ if (mTransitionAnimator != null && mTransitionAnimator.isRunning()) {
+ mTransitionAnimator.end();
+ mTransitionAnimator = null;
+ }
+ }
+
+ private boolean startExpandAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
+
+ TransitionInfo.Change pipChange = getChangeByToken(info, pipToken);
+ if (pipChange == null) {
+ // pipChange is null, check to see if we've reparented the PIP activity for
+ // the multi activity case. If so we should use the activity leash instead
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() == null
+ && change.getLastParent() != null
+ && change.getLastParent().equals(pipToken)) {
+ pipChange = change;
+ break;
+ }
+ }
+
+ // failsafe
+ if (pipChange == null) {
+ return false;
+ }
+ }
+ mFinishCallback = finishCallback;
+
+ // The parent change if we were in a multi-activity PiP; null if single activity PiP.
+ final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null
+ ? getChangeByToken(info, pipChange.getParent()) : null;
+ if (parentBeforePip != null) {
+ // For multi activity, we need to manually set the leash layer
+ startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1);
+ }
+
+ final Rect startBounds = pipChange.getStartAbsBounds();
+ final Rect endBounds = pipChange.getEndAbsBounds();
+ final SurfaceControl pipLeash = getLeash(pipChange);
+
+ PictureInPictureParams params = null;
+ if (pipChange.getTaskInfo() != null) {
+ // single activity
+ params = getPipParams(pipChange);
+ } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) {
+ // multi activity
+ params = getPipParams(parentBeforePip);
+ }
+ final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
+ startBounds);
+
+ // We define delta = startRotation - endRotation, so we need to flip the sign.
+ final int delta = -getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState);
+ if (delta != ROTATION_0) {
+ // Update PiP target change in place to prepare for fixed rotation;
+ handleExpandFixedRotation(pipChange, delta);
+ }
+
+ PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash,
+ startTransaction, finishTransaction, endBounds, startBounds, endBounds,
+ sourceRectHint, delta);
+ animator.setAnimationEndCallback(() -> {
+ if (parentBeforePip != null) {
+ // TODO b/377362511: Animate local leash instead to also handle letterbox case.
+ // For multi-activity, set the crop to be null
+ finishTransaction.setCrop(pipLeash, null);
+ }
+ finishTransition();
+ });
+ cacheAndStartTransitionAnimator(animator);
+ saveReentryState();
+ return true;
+ }
+
+ private boolean startExpandToSplitAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken();
+
+ // Expanding PiP to Split-screen makes sense only if we are dealing with multi-activity PiP
+ // and the lastParentBeforePip is still in one of the split-stages.
+ //
+ // This means we should be animating the PiP activity leash, since we do the reparenting
+ // of the PiP activity back to its original task in startWCT.
+ TransitionInfo.Change pipChange = null;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() == null
+ && change.getLastParent() != null
+ && change.getLastParent().equals(pipToken)) {
+ pipChange = change;
+ break;
+ }
+ }
+ // failsafe
+ if (pipChange == null || pipChange.getLeash() == null) {
+ return false;
+ }
+ mFinishCallback = finishCallback;
+
+ // Get the original parent before PiP. If original task hosting the PiP activity was
+ // already visible, then it's not participating in this transition; in that case,
+ // parentBeforePip would be null.
+ final TransitionInfo.Change parentBeforePip = getChangeByToken(info, pipChange.getParent());
+
+ final Rect startBounds = pipChange.getStartAbsBounds();
+ final Rect endBounds = pipChange.getEndAbsBounds();
+ if (parentBeforePip != null) {
+ // Since we have the parent task amongst the targets, all PiP activity
+ // leash translations will be relative to the original task, NOT the root leash.
+ startBounds.offset(-parentBeforePip.getStartAbsBounds().left,
+ -parentBeforePip.getStartAbsBounds().top);
+ endBounds.offset(-parentBeforePip.getEndAbsBounds().left,
+ -parentBeforePip.getEndAbsBounds().top);
+ }
+
+ final SurfaceControl pipLeash = pipChange.getLeash();
+ PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash,
+ startTransaction, finishTransaction, endBounds, startBounds, endBounds,
+ null /* srcRectHint */, ROTATION_0 /* delta */);
+
+
+ mSplitScreenControllerOptional.ifPresent(splitController -> {
+ splitController.finishEnterSplitScreen(finishTransaction);
+ });
+
+ animator.setAnimationEndCallback(() -> {
+ if (parentBeforePip == null) {
+ // After PipExpandAnimator is done modifying finishTransaction, we need to make
+ // sure PiP activity leash is offset at origin relative to its task as we reparent
+ // targets back from the transition root leash.
+ finishTransaction.setPosition(pipLeash, 0, 0);
+ }
+ finishTransition();
+ });
+ cacheAndStartTransitionAnimator(animator);
+ saveReentryState();
+ return true;
+ }
+
+ private void finishTransition() {
+ final int currentState = mPipTransitionState.getState();
+ if (currentState != PipTransitionState.EXITING_PIP) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "Unexpected state %s as we are finishing an exit-via-expand transition",
+ mPipTransitionState);
+ }
+ mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+
+ if (mFinishCallback != null) {
+ // Need to unset mFinishCallback first because onTransitionFinished can re-enter this
+ // handler if there is a pending PiP animation.
+ final Transitions.TransitionFinishCallback finishCallback = mFinishCallback;
+ mFinishCallback = null;
+ finishCallback.onTransitionFinished(null /* finishWct */);
+ }
+ }
+
+ private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
+ final Rect endBounds = outPipTaskChange.getEndAbsBounds();
+ final int width = endBounds.width();
+ final int height = endBounds.height();
+ final int left = endBounds.left;
+ final int top = endBounds.top;
+ int newTop, newLeft;
+
+ if (delta == Surface.ROTATION_90) {
+ newLeft = top;
+ newTop = -(left + width);
+ } else {
+ newLeft = -(height + top);
+ newTop = left;
+ }
+ // Modify the endBounds, rotating and placing them potentially off-screen, so that
+ // as we translate and rotate around the origin, we place them right into the target.
+ endBounds.set(newLeft, newTop, newLeft + height, newTop + width);
+ }
+
+ private void saveReentryState() {
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+ mPipBoundsState.getBounds());
+ mPipBoundsState.saveReentryState(snapFraction);
+ }
+
+ private void cacheAndStartTransitionAnimator(@NonNull ValueAnimator animator) {
+ mTransitionAnimator = animator;
+ mTransitionAnimator.start();
+ }
+
+ @VisibleForTesting
+ interface PipExpandAnimatorSupplier {
+ PipExpandAnimator get(Context context,
+ @NonNull SurfaceControl leash,
+ SurfaceControl.Transaction startTransaction,
+ SurfaceControl.Transaction finishTransaction,
+ @NonNull Rect baseBounds,
+ @NonNull Rect startBounds,
+ @NonNull Rect endBounds,
+ @Nullable Rect sourceRectHint,
+ @Surface.Rotation int rotation);
+ }
+
+ @VisibleForTesting
+ void setPipExpandAnimatorSupplier(@NonNull PipExpandAnimatorSupplier supplier) {
+ mPipExpandAnimatorSupplier = supplier;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java
new file mode 100644
index 000000000000..01cda6c91108
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2025 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.transition;
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Surface.ROTATION_0;
+
+import android.annotation.NonNull;
+import android.app.PictureInPictureParams;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+
+/**
+ * A set of utility methods to help resolve PiP transitions.
+ */
+public class PipTransitionUtils {
+
+ /**
+ * @return change for a pinned mode task; null if no such task is in the list of changes.
+ */
+ @Nullable
+ public static TransitionInfo.Change getPipChange(TransitionInfo info) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return change for a task with the provided token; null if no task with such token found.
+ */
+ @Nullable
+ public static TransitionInfo.Change getChangeByToken(TransitionInfo info,
+ WindowContainerToken token) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getToken().equals(token)) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the leash to interact with the container this change represents.
+ * @throws NullPointerException if the leash is null.
+ */
+ @NonNull
+ public static SurfaceControl getLeash(TransitionInfo.Change change) {
+ SurfaceControl leash = change.getLeash();
+ Preconditions.checkNotNull(leash, "Leash is null for change=" + change);
+ return leash;
+ }
+
+ /**
+ * Get the rotation delta in a potential fixed rotation transition.
+ *
+ * Whenever PiP participates in fixed rotation, its actual orientation isn't updated
+ * in the initial transition as per the async rotation convention.
+ *
+ * @param pipChange PiP change to verify that PiP task's rotation wasn't updated already.
+ * @param pipDisplayLayoutState display layout state that PiP component keeps track of.
+ */
+ @Surface.Rotation
+ public static int getFixedRotationDelta(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change pipChange,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState) {
+ TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
+ int startRotation = pipChange.getStartRotation();
+ if (pipChange.getEndRotation() != ROTATION_UNDEFINED
+ && startRotation != pipChange.getEndRotation()) {
+ // If PiP change was collected along with the display change and the orientation change
+ // happened in sync with the PiP change, then do not treat this as fixed-rotation case.
+ return ROTATION_0;
+ }
+
+ int endRotation = fixedRotationChange != null
+ ? fixedRotationChange.getEndFixedRotation() : pipDisplayLayoutState.getRotation();
+ int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
+ : startRotation - endRotation;
+ return delta;
+ }
+
+ /**
+ * Gets a change amongst the transition targets that is in a different final orientation than
+ * the display, signalling a potential fixed rotation transition.
+ */
+ @Nullable
+ public static TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getEndFixedRotation() != ROTATION_UNDEFINED) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return {@link PictureInPictureParams} provided by the client from the PiP change.
+ */
+ @NonNull
+ public static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) {
+ return pipChange.getTaskInfo().pictureInPictureParams != null
+ ? pipChange.getTaskInfo().pictureInPictureParams
+ : new PictureInPictureParams.Builder().build();
+ }
+}
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
index 8e0381e4f933..0c1952910d1a 100644
--- 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
@@ -26,6 +27,7 @@ import static org.mockito.kotlin.MatchersKt.eq;
import static org.mockito.kotlin.VerificationKt.times;
import static org.mockito.kotlin.VerificationKt.verify;
+import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Matrix;
@@ -44,15 +46,19 @@ import com.android.wm.shell.common.pip.PipDesktopState;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Optional;
+
/**
* Unit test against {@link PipScheduler}
*/
@@ -77,6 +83,8 @@ public class PipSchedulerTest {
@Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
@Mock private SurfaceControl.Transaction mMockTransaction;
@Mock private PipAlphaAnimator mMockAlphaAnimator;
+ @Mock private SplitScreenController mMockSplitScreenController;
+
@Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor;
@@ -93,7 +101,8 @@ public class PipSchedulerTest {
.thenReturn(mMockTransaction);
mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
- mMockPipTransitionState, mMockPipDesktopState);
+ mMockPipTransitionState, Optional.of(mMockSplitScreenController),
+ mMockPipDesktopState);
mPipScheduler.setPipTransitionController(mMockPipTransitionController);
mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) ->
@@ -119,12 +128,18 @@ public class PipSchedulerTest {
assertNotNull(mRunnableArgumentCaptor.getValue());
mRunnableArgumentCaptor.getValue().run();
- verify(mMockPipTransitionController, never()).startExpandTransition(any());
+ verify(mMockPipTransitionController, never()).startExpandTransition(any(), anyBoolean());
}
@Test
- public void scheduleExitPipViaExpand_exitTransitionCalled() {
+ public void scheduleExitPipViaExpand_noSplit_expandTransitionCalled() {
setMockPipTaskToken();
+ ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1);
+ when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo);
+
+ // Make sure task with the id = 1 isn't in split-screen.
+ when(mMockSplitScreenController.isTaskInSplitScreen(
+ ArgumentMatchers.eq(1))).thenReturn(false);
mPipScheduler.scheduleExitPipViaExpand();
@@ -132,7 +147,29 @@ public class PipSchedulerTest {
assertNotNull(mRunnableArgumentCaptor.getValue());
mRunnableArgumentCaptor.getValue().run();
- verify(mMockPipTransitionController, times(1)).startExpandTransition(any());
+ verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean());
+ }
+
+ @Test
+ public void scheduleExitPipViaExpand_lastParentInSplit_prepareSplitAndExpand() {
+ setMockPipTaskToken();
+ ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1);
+ when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo);
+
+ // Make sure task with the id = 1 is in split-screen.
+ when(mMockSplitScreenController.isTaskInSplitScreen(
+ ArgumentMatchers.eq(1))).thenReturn(true);
+
+ mPipScheduler.scheduleExitPipViaExpand();
+
+ verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture());
+ assertNotNull(mRunnableArgumentCaptor.getValue());
+ mRunnableArgumentCaptor.getValue().run();
+
+ // We need to both prepare the split screen with the last parent and start expanding.
+ verify(mMockSplitScreenController,
+ times(1)).prepareEnterSplitScreen(any(), any(), anyInt());
+ verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean());
}
@Test
@@ -259,4 +296,10 @@ public class PipSchedulerTest {
private void setMockPipTaskToken() {
when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken);
}
+
+ private ActivityManager.RunningTaskInfo getTaskInfoWithLastParentBeforePip(int lastParentId) {
+ final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.lastParentTaskIdBeforePip = lastParentId;
+ return taskInfo;
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java
new file mode 100644
index 000000000000..2a22842eda1a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2025 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.transition;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.kotlin.VerificationKt.times;
+import static org.mockito.kotlin.VerificationKt.verify;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PictureInPictureParams;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.pip2.animation.PipExpandAnimator;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.transition.TransitionInfoBuilder;
+import com.android.wm.shell.util.StubTransaction;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Unit test against {@link PipExpandHandler}
+ */
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class PipExpandHandlerTest {
+ @Mock private Context mMockContext;
+ @Mock private PipBoundsState mMockPipBoundsState;
+ @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm;
+ @Mock private PipTransitionState mMockPipTransitionState;
+ @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
+ @Mock private SplitScreenController mMockSplitScreenController;
+
+ @Mock private IBinder mMockTransitionToken;
+ @Mock private TransitionRequestInfo mMockRequestInfo;
+ @Mock private StubTransaction mStartT;
+ @Mock private StubTransaction mFinishT;
+ @Mock private SurfaceControl mPipLeash;
+
+ @Mock private PipExpandAnimator mMockPipExpandAnimator;
+
+ @Surface.Rotation
+ private static final int DISPLAY_ROTATION = Surface.ROTATION_0;
+
+ private static final float SNAP_FRACTION = 1.5f;
+ private static final Rect PIP_BOUNDS = new Rect(0, 0, 100, 100);
+ private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000);
+ private static final Rect RIGHT_HALF_DISPLAY_BOUNDS = new Rect(500, 0, 1000, 1000);
+
+ private PipExpandHandler mPipExpandHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mMockPipBoundsState.getBounds()).thenReturn(PIP_BOUNDS);
+ when(mMockPipBoundsAlgorithm.getSnapFraction(eq(PIP_BOUNDS))).thenReturn(SNAP_FRACTION);
+ when(mMockPipDisplayLayoutState.getRotation()).thenReturn(DISPLAY_ROTATION);
+
+ mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState,
+ mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState,
+ Optional.of(mMockSplitScreenController));
+ mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction,
+ finishTransaction, baseBounds, startBounds, endBounds,
+ sourceRectHint, rotation) -> mMockPipExpandAnimator);
+ }
+
+ @Test
+ public void handleRequest_returnNull() {
+ // All expand from PiP transitions are started in Shell, so handleRequest shouldn't be
+ // returning any non-null WCT
+ WindowContainerTransaction wct = mPipExpandHandler.handleRequest(
+ mMockTransitionToken, mMockRequestInfo);
+ assertNull(wct);
+ }
+
+ @Test
+ public void startAnimation_transitExit_startExpandAnimator() {
+ final ActivityManager.RunningTaskInfo pipTaskInfo = createPipTaskInfo(
+ 1, WINDOWING_MODE_FULLSCREEN, new PictureInPictureParams.Builder().build());
+
+ final TransitionInfo info = getExpandFromPipTransitionInfo(
+ TRANSIT_EXIT_PIP, pipTaskInfo, null /* lastParent */, false /* toSplit */);
+ final WindowContainerToken pipToken = pipTaskInfo.getToken();
+ when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken);
+
+ mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT,
+ (wct) -> {});
+
+ verify(mMockPipExpandAnimator, times(1)).start();
+ verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION);
+ }
+
+ @Test
+ public void startAnimation_transitExitToSplit_startExpandAnimator() {
+ // The task info of the task that was pinned while we were in PiP.
+ final WindowContainerToken pipToken = createPipTaskInfo(1, WINDOWING_MODE_FULLSCREEN,
+ new PictureInPictureParams.Builder().build()).getToken();
+ when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken);
+
+ // Change representing the ActivityRecord we are animating in the multi-activity PiP case;
+ // make sure change's taskInfo=null as this is an activity, but let lastParent be PiP token.
+ final TransitionInfo info = getExpandFromPipTransitionInfo(
+ TRANSIT_EXIT_PIP_TO_SPLIT, null /* taskInfo */, pipToken, true /* toSplit */);
+
+ mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT,
+ (wct) -> {});
+
+ verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT));
+ verify(mMockPipExpandAnimator, times(1)).start();
+ verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION);
+ }
+
+ private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type,
+ @Nullable ActivityManager.RunningTaskInfo pipTaskInfo,
+ @Nullable WindowContainerToken lastParent, boolean toSplit) {
+ final TransitionInfo info = new TransitionInfoBuilder(type)
+ .addChange(TRANSIT_CHANGE, pipTaskInfo).build();
+ final TransitionInfo.Change pipChange = info.getChanges().getFirst();
+ pipChange.setRotation(DISPLAY_ROTATION,
+ WindowConfiguration.ROTATION_UNDEFINED);
+ pipChange.setStartAbsBounds(PIP_BOUNDS);
+ pipChange.setEndAbsBounds(toSplit ? RIGHT_HALF_DISPLAY_BOUNDS : DISPLAY_BOUNDS);
+ pipChange.setLeash(mPipLeash);
+ pipChange.setLastParent(lastParent);
+ return info;
+ }
+
+ private static ActivityManager.RunningTaskInfo createPipTaskInfo(int taskId,
+ int windowingMode, PictureInPictureParams params) {
+ ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ taskInfo.token = mock(WindowContainerToken.class);
+ taskInfo.baseIntent = mock(Intent.class);
+ taskInfo.pictureInPictureParams = params;
+ return taskInfo;
+ }
+}