diff options
7 files changed, 608 insertions, 141 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 4eff3f03670e..6e61f22ca563 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 @@ -26,6 +26,7 @@ import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; @@ -43,6 +44,7 @@ import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; +import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -69,9 +71,11 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, PipTouchHandler pipTouchHandler, - @NonNull PipScheduler pipScheduler) { + @NonNull PipScheduler pipScheduler, + @NonNull PipTransitionState pipStackListenerController) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, - pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); + pipBoundsState, null, pipBoundsAlgorithm, pipScheduler, + pipStackListenerController); } @WMSingleton @@ -85,6 +89,9 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, @ShellMainThread ShellExecutor mainExecutor) { if (!PipUtils.isPip2ExperimentEnabled()) { return Optional.empty(); @@ -92,7 +99,7 @@ public abstract class Pip2Module { return Optional.ofNullable(PipController.create( context, shellInit, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler, - mainExecutor)); + taskStackListener, shellTaskOrganizer, pipTransitionState, mainExecutor)); } } @@ -101,8 +108,8 @@ public abstract class Pip2Module { static PipScheduler providePipScheduler(Context context, PipBoundsState pipBoundsState, @ShellMainThread ShellExecutor mainExecutor, - ShellTaskOrganizer shellTaskOrganizer) { - return new PipScheduler(context, pipBoundsState, mainExecutor, shellTaskOrganizer); + PipTransitionState pipTransitionState) { + return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState); } @WMSingleton @@ -146,4 +153,10 @@ public abstract class Pip2Module { return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm, floatingContentCoordinator, pipPerfHintControllerOptional); } + + @WMSingleton + @Provides + static PipTransitionState providePipStackListenerController() { + return new PipTransitionState(); + } } 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 4f71a02528c3..8c360859ff72 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 @@ -54,7 +54,6 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; /** * Responsible supplying PiP Transitions. @@ -125,12 +124,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start resizing Pip transition/animation. - * - * @param onFinishResizeCallback callback guaranteed to execute when animation ends and - * client completes any potential draws upon WM state updates. */ - public void startResizeTransition(WindowContainerTransaction wct, - Consumer<Rect> onFinishResizeCallback) { + public void startResizeTransition(WindowContainerTransaction wct) { // Default implementation does nothing. } 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 1e18b8c002db..a12882f56eb7 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 @@ -16,25 +16,31 @@ package com.android.wm.shell.pip2.phone; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Bundle; import android.view.InsetsState; import android.view.SurfaceControl; import androidx.annotation.BinderThread; +import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.Preconditions; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; @@ -42,6 +48,8 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SingleInstanceRemoteListener; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.pip.IPip; import com.android.wm.shell.common.pip.IPipAnimationListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -57,8 +65,11 @@ import com.android.wm.shell.sysui.ShellInit; * Manages the picture-in-picture (PIP) UI and states for Phones. */ public class PipController implements ConfigurationChangeListener, + PipTransitionState.PipTransitionStateChangedListener, DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> { private static final String TAG = PipController.class.getSimpleName(); + private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds"; + private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay"; private final Context mContext; private final ShellController mShellController; @@ -68,6 +79,9 @@ public class PipController implements ConfigurationChangeListener, private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipScheduler mPipScheduler; + private final TaskStackListenerImpl mTaskStackListener; + private final ShellTaskOrganizer mShellTaskOrganizer; + private final PipTransitionState mPipTransitionState; private final ShellExecutor mMainExecutor; // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. @@ -104,6 +118,9 @@ public class PipController implements ConfigurationChangeListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; @@ -113,6 +130,10 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipDisplayLayoutState = pipDisplayLayoutState; mPipScheduler = pipScheduler; + mTaskStackListener = taskStackListener; + mShellTaskOrganizer = shellTaskOrganizer; + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); mMainExecutor = mainExecutor; if (PipUtils.isPip2ExperimentEnabled()) { @@ -132,6 +153,9 @@ public class PipController implements ConfigurationChangeListener, PipBoundsAlgorithm pipBoundsAlgorithm, PipDisplayLayoutState pipDisplayLayoutState, PipScheduler pipScheduler, + TaskStackListenerImpl taskStackListener, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, ShellExecutor mainExecutor) { if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, @@ -140,7 +164,8 @@ public class PipController implements ConfigurationChangeListener, } return new PipController(context, shellInit, shellController, displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, - pipScheduler, mainExecutor); + pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState, + mainExecutor); } private void onInit() { @@ -164,6 +189,17 @@ public class PipController implements ConfigurationChangeListener, mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, this::createExternalInterface, this); mShellController.addConfigurationChangeListener(this); + + mTaskStackListener.addListener(new TaskStackListenerCallback() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { + return; + } + mPipScheduler.scheduleExitPipViaExpand(); + } + }); } private ExternalInterfaceBinder createExternalInterface() { @@ -245,11 +281,46 @@ public class PipController implements ConfigurationChangeListener, Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "onSwipePipToHomeAnimationStart: %s", componentName); - mPipScheduler.onSwipePipToHomeAnimationStart(taskId, componentName, destinationBounds, - overlay, appBounds); + Bundle extra = new Bundle(); + extra.putParcelable(SWIPE_TO_PIP_OVERLAY, overlay); + extra.putParcelable(SWIPE_TO_PIP_APP_BOUNDS, appBounds); + mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP, extra); + if (overlay != null) { + // Shell transitions might use a root animation leash, which will be removed when + // the Recents transition is finished. Launcher attaches the overlay leash to this + // animation target leash; thus, we need to reparent it to the actual Task surface now. + // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP + // transition. + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + mShellTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, tx); + tx.setLayer(overlay, Integer.MAX_VALUE); + tx.apply(); + } mPipRecentsAnimationListener.onPipAnimationStarted(); } + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + if (newState == PipTransitionState.SWIPING_TO_PIP) { + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + SurfaceControl overlay = extra.getParcelable( + SWIPE_TO_PIP_OVERLAY, SurfaceControl.class); + Rect appBounds = extra.getParcelable( + SWIPE_TO_PIP_APP_BOUNDS, Rect.class); + + Preconditions.checkState(appBounds != null, + "App bounds can't be null for " + mPipTransitionState); + mPipTransitionState.setSwipePipToHomeState(overlay, appBounds); + } else if (newState == PipTransitionState.ENTERED_PIP) { + if (mPipTransitionState.isInSwipePipToHomeTransition()) { + mPipTransitionState.resetSwipePipToHomeState(); + } + } + } + // // IPipAnimationListener Binder proxy helpers // 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 b4ca7df10292..72fa3badeb93 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 @@ -21,21 +21,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Rect; -import android.view.SurfaceControl; -import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; @@ -43,7 +38,6 @@ import com.android.wm.shell.pip.PipTransitionController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.function.Consumer; /** * Scheduler for Shell initiated PiP transitions and animations. @@ -55,31 +49,10 @@ public class PipScheduler { private final Context mContext; private final PipBoundsState mPipBoundsState; private final ShellExecutor mMainExecutor; - private final ShellTaskOrganizer mShellTaskOrganizer; + private final PipTransitionState mPipTransitionState; private PipSchedulerReceiver mSchedulerReceiver; private PipTransitionController mPipTransitionController; - // pinned PiP task's WC token - @Nullable - private WindowContainerToken mPipTaskToken; - - // pinned PiP task's leash - @Nullable - private SurfaceControl mPinnedTaskLeash; - - // true if Launcher has started swipe PiP to home animation - private boolean mInSwipePipToHomeTransition; - - // Overlay leash potentially used during swipe PiP to home transition; - // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. - @Nullable - SurfaceControl mSwipePipToHomeOverlay; - - // App bounds used when as a starting point to swipe PiP to home animation in Launcher; - // these are also used to calculate the app icon overlay buffer size. - @NonNull - final Rect mSwipePipToHomeAppBounds = new Rect(); - /** * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. * This is used for a broadcast receiver to resolve intents. This should be removed once @@ -118,11 +91,11 @@ public class PipScheduler { public PipScheduler(Context context, PipBoundsState pipBoundsState, ShellExecutor mainExecutor, - ShellTaskOrganizer shellTaskOrganizer) { + PipTransitionState pipTransitionState) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; - mShellTaskOrganizer = shellTaskOrganizer; + mPipTransitionState = pipTransitionState; if (PipUtils.isPip2ExperimentEnabled()) { // temporary broadcast receiver to initiate exit PiP via expand @@ -140,25 +113,17 @@ public class PipScheduler { mPipTransitionController = pipTransitionController; } - void setPinnedTaskLeash(SurfaceControl pinnedTaskLeash) { - mPinnedTaskLeash = pinnedTaskLeash; - } - - void setPipTaskToken(@Nullable WindowContainerToken pipTaskToken) { - mPipTaskToken = pipTaskToken; - } - @Nullable private WindowContainerTransaction getExitPipViaExpandTransaction() { - if (mPipTaskToken == null) { + if (mPipTransitionState.mPipTaskToken == null) { return null; } WindowContainerTransaction wct = new WindowContainerTransaction(); // final expanded bounds to be inherited from the parent - wct.setBounds(mPipTaskToken, null); + wct.setBounds(mPipTransitionState.mPipTaskToken, null); // if we are hitting a multi-activity case // windowing mode change will reparent to original host task - wct.setWindowingMode(mPipTaskToken, WINDOWING_MODE_UNDEFINED); + wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED); return wct; } @@ -183,43 +148,12 @@ public class PipScheduler { /** * Animates resizing of the pinned stack given the duration. */ - public void scheduleAnimateResizePip(Rect toBounds, Consumer<Rect> onFinishResizeCallback) { - if (mPipTaskToken == null) { + public void scheduleAnimateResizePip(Rect toBounds) { + if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) { return; } WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mPipTaskToken, toBounds); - mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); - } - - void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName, - Rect destinationBounds, SurfaceControl overlay, Rect appBounds) { - mInSwipePipToHomeTransition = true; - mSwipePipToHomeOverlay = overlay; - mSwipePipToHomeAppBounds.set(appBounds); - if (overlay != null) { - // Shell transitions might use a root animation leash, which will be removed when - // the Recents transition is finished. Launcher attaches the overlay leash to this - // animation target leash; thus, we need to reparent it to the actual Task surface now. - // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP - // transition. - SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - mShellTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, tx); - tx.setLayer(overlay, Integer.MAX_VALUE); - tx.apply(); - } - } - - void setInSwipePipToHomeTransition(boolean inSwipePipToHome) { - mInSwipePipToHomeTransition = inSwipePipToHome; - } - - boolean isInSwipePipToHomeTransition() { - return mInSwipePipToHomeTransition; - } - - void onExitPip() { - mPipTaskToken = null; - mPinnedTaskLeash = null; + wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + mPipTransitionController.startResizeTransition(wct); } } 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 e829d4ef650e..12dce5bf70c0 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 @@ -34,6 +34,7 @@ import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.os.IBinder; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -43,6 +44,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.internal.util.Preconditions; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; @@ -54,23 +56,33 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; -import java.util.function.Consumer; - /** * Implementation of transitions for PiP on phone. */ -public class PipTransition extends PipTransitionController { +public class PipTransition extends PipTransitionController implements + PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = PipTransition.class.getSimpleName(); + private static final String PIP_TASK_TOKEN = "pip_task_token"; + private static final String PIP_TASK_LEASH = "pip_task_leash"; + /** * The fixed start delay in ms when fading out the content overlay from bounds animation. * The fadeout animation is guaranteed to start after the client has drawn under the new config. */ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 400; + // + // Dependencies + // + private final Context mContext; private final PipScheduler mPipScheduler; - @Nullable - private WindowContainerToken mPipTaskToken; + private final PipTransitionState mPipTransitionState; + + // + // Transition tokens + // + @Nullable private IBinder mEnterTransition; @Nullable @@ -78,7 +90,14 @@ public class PipTransition extends PipTransitionController { @Nullable private IBinder mResizeTransition; - private Consumer<Rect> mFinishResizeCallback; + // + // Internal state and relevant cached info + // + + @Nullable + private WindowContainerToken mPipTaskToken; + @Nullable + private SurfaceControl mPipLeash; public PipTransition( Context context, @@ -88,13 +107,16 @@ public class PipTransition extends PipTransitionController { PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, - PipScheduler pipScheduler) { + PipScheduler pipScheduler, + PipTransitionState pipTransitionState) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); mContext = context; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); + mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); } @Override @@ -104,6 +126,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition collection stage lifecycle hooks + // + @Override public void startExitTransition(int type, WindowContainerTransaction out, @Nullable Rect destinationBounds) { @@ -117,13 +143,11 @@ public class PipTransition extends PipTransitionController { } @Override - public void startResizeTransition(WindowContainerTransaction wct, - Consumer<Rect> onFinishResizeCallback) { + public void startResizeTransition(WindowContainerTransaction wct) { if (wct == null) { return; } mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this); - mFinishResizeCallback = onFinishResizeCallback; } @Nullable @@ -146,6 +170,10 @@ public class PipTransition extends PipTransitionController { } } + // + // Transition playing stage lifecycle hooks + // + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @@ -163,7 +191,19 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition == mEnterTransition || info.getType() == TRANSIT_PIP) { mEnterTransition = null; - if (mPipScheduler.isInSwipePipToHomeTransition()) { + // If we are in swipe PiP to Home transition we are ENTERING_PIP as a jumpcut transition + // is being carried out. + TransitionInfo.Change pipChange = getPipChange(info); + + // If there is no PiP change, exit this transition handler and potentially try others. + if (pipChange == null) return false; + + Bundle extra = new Bundle(); + extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer()); + extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash()); + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP, extra); + + if (mPipTransitionState.isInSwipePipToHomeTransition()) { // If this is the second transition as a part of swipe PiP to home cuj, // handle this transition as a special case with no-op animation. return handleSwipePipToHomeTransition(info, startTransaction, finishTransaction, @@ -179,9 +219,11 @@ public class PipTransition extends PipTransitionController { finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; + mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS); return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } @@ -191,6 +233,10 @@ public class PipTransition extends PipTransitionController { return false; } + // + // Animation schedulers and entry points + // + private boolean startResizeAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -236,11 +282,7 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipScheduler.setInSwipePipToHomeTransition(false); - mPipTaskToken = pipChange.getContainer(); - - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); SurfaceControl pipLeash = pipChange.getLeash(); PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; @@ -264,9 +306,9 @@ public class PipTransition extends PipTransitionController { } else { final float scaleX = (float) destinationBounds.width() / startBounds.width(); final float scaleY = (float) destinationBounds.height() / startBounds.height(); - final int overlaySize = PipContentOverlay.PipAppIconOverlay - .getOverlaySize(mPipScheduler.mSwipePipToHomeAppBounds, destinationBounds); - SurfaceControl overlayLeash = mPipScheduler.mSwipePipToHomeOverlay; + final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( + mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); + SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay(); startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top) .setScale(pipLeash, scaleX, scaleY) @@ -274,7 +316,7 @@ public class PipTransition extends PipTransitionController { .reparent(overlayLeash, pipLeash) .setLayer(overlayLeash, Integer.MAX_VALUE); - if (mPipTaskToken != null) { + if (pipTaskToken != null) { SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), this::onClientDrawAtTransitionEnd) @@ -282,7 +324,7 @@ public class PipTransition extends PipTransitionController { .setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f, (destinationBounds.height() - overlaySize) / 2f); - finishWct.setBoundsChangeTransaction(mPipTaskToken, tx); + finishWct.setBoundsChangeTransaction(pipTaskToken, tx); } } startTransaction.apply(); @@ -293,14 +335,6 @@ public class PipTransition extends PipTransitionController { return true; } - private void onClientDrawAtTransitionEnd() { - startOverlayFadeoutAnimation(); - } - - // - // Subroutines setting up and starting transitions' animations. - // - private void startOverlayFadeoutAnimation() { ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f); animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS); @@ -309,15 +343,17 @@ public class PipTransition extends PipTransitionController { public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.remove(mPipScheduler.mSwipePipToHomeOverlay); + tx.remove(mPipTransitionState.getSwipePipToHomeOverlay()); tx.apply(); - mPipScheduler.mSwipePipToHomeOverlay = null; + + // We have fully completed enter-PiP animation after the overlay is gone. + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); } }); animator.addUpdateListener(animation -> { float alpha = (float) animation.getAnimatedValue(); SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.setAlpha(mPipScheduler.mSwipePipToHomeOverlay, alpha).apply(); + tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply(); }); animator.start(); } @@ -330,10 +366,8 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); startTransaction.apply(); // TODO: b/275910498 Use a new implementation of the PiP animator here. @@ -349,10 +383,8 @@ public class PipTransition extends PipTransitionController { if (pipChange == null) { return false; } - mPipTaskToken = pipChange.getContainer(); - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); + WindowContainerToken pipTaskToken = pipChange.getContainer(); startTransaction.apply(); finishCallback.onTransitionFinished(null); @@ -366,7 +398,7 @@ public class PipTransition extends PipTransitionController { startTransaction.apply(); // TODO: b/275910498 Use a new implementation of the PiP animator here. finishCallback.onTransitionFinished(null); - onExitPip(); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); return true; } @@ -376,12 +408,20 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback) { startTransaction.apply(); finishCallback.onTransitionFinished(null); - onExitPip(); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); return true; } + /** + * TODO: b/275910498 Use a new implementation of the PiP animator here. + */ + private void startResizeAnimation(SurfaceControl leash, Rect startBounds, + Rect endBounds, int duration) { + mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); + } + // - // Utility methods for checking PiP-related transition info and requests. + // Various helpers to resolve transition requests and infos // @Nullable @@ -442,11 +482,11 @@ public class PipTransition extends PipTransitionController { } private boolean isRemovePipTransition(@NonNull TransitionInfo info) { - if (mPipTaskToken == null) { + if (mPipTransitionState.mPipTaskToken == null) { // PiP removal makes sense if enter-PiP has cached a valid pinned task token. return false; } - TransitionInfo.Change pipChange = info.getChange(mPipTaskToken); + TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.mPipTaskToken); if (pipChange == null) { // Search for the PiP change by token since the windowing mode might be FULLSCREEN now. return false; @@ -460,14 +500,43 @@ public class PipTransition extends PipTransitionController { return isPipMovedToBack || isPipClosed; } - /** - * TODO: b/275910498 Use a new implementation of the PiP animator here. - */ - private void startResizeAnimation(SurfaceControl leash, Rect startBounds, - Rect endBounds, int duration) {} + // + // Miscellaneous callbacks and listeners + // - private void onExitPip() { - mPipTaskToken = null; - mPipScheduler.onExitPip(); + private void onClientDrawAtTransitionEnd() { + if (mPipTransitionState.getSwipePipToHomeOverlay() != null) { + startOverlayFadeoutAnimation(); + } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) { + // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint, + // and then we get a signal on client finishing its draw after the transition + // has ended, then we have fully entered PiP. + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + } + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.ENTERING_PIP: + Preconditions.checkState(extra != null, + "No extra bundle for " + mPipTransitionState); + + mPipTransitionState.mPipTaskToken = extra.getParcelable( + PIP_TASK_TOKEN, WindowContainerToken.class); + mPipTransitionState.mPinnedTaskLeash = extra.getParcelable( + PIP_TASK_LEASH, SurfaceControl.class); + boolean hasValidTokenAndLeash = mPipTransitionState.mPipTaskToken != null + && mPipTransitionState.mPinnedTaskLeash != null; + + Preconditions.checkState(hasValidTokenAndLeash, + "Unexpected bundle for " + mPipTransitionState); + break; + case PipTransitionState.EXITED_PIP: + mPipTransitionState.mPipTaskToken = null; + mPipTransitionState.mPinnedTaskLeash = 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 new file mode 100644 index 000000000000..f7bc622b6195 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -0,0 +1,275 @@ +/* + * 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 android.annotation.IntDef; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * Contains the state relevant to carry out or probe the status of PiP transitions. + * + * <p>Existing and new PiP components can subscribe to PiP transition related state changes + * via <code>PipTransitionStateChangedListener</code>.</p> + * + * <p><code>PipTransitionState</code> users shouldn't rely on listener execution ordering. + * For example, if a class <code>Foo</code> wants to change some arbitrary state A that belongs + * to some other class <code>Bar</code>, a special care must be given when manipulating state A in + * <code>Foo#onPipTransitionStateChanged()</code>, since that's the responsibility of + * the class <code>Bar</code>.</p> + * + * <p>Hence, the recommended usage for classes who want to subscribe to + * <code>PipTransitionState</code> changes is to manipulate only their own internal state or + * <code>PipTransitionState</code> state.</p> + * + * <p>If there is some state that must be manipulated in another class <code>Bar</code>, it should + * just be moved to <code>PipTransitionState</code> and become a shared state + * between Foo and Bar.</p> + * + * <p>Moreover, <code>onPipTransitionStateChanged(oldState, newState, extra)</code> + * receives a <code>Bundle</code> extra object that can be optionally set via + * <code>setState(state, extra)</code>. This can be used to resolve extra information to update + * relevant internal or <code>PipTransitionState</code> state. However, each listener + * needs to check for whether the extra passed is correct for a particular state, + * and throw an <code>IllegalStateException</code> otherwise.</p> + */ +public class PipTransitionState { + public static final int UNDEFINED = 0; + + // State for Launcher animating the swipe PiP to home animation. + public static final int SWIPING_TO_PIP = 1; + + // State for Shell animating enter PiP or jump-cutting to PiP mode after Launcher animation. + public static final int ENTERING_PIP = 2; + + // State for app finishing drawing in PiP mode as a final step in enter PiP flow. + public static final int ENTERED_PIP = 3; + + // State for scheduling a transition to change PiP bounds. + public static final int CHANGING_PIP_BOUNDS = 4; + + // State for app potentially finishing drawing in new PiP bounds after resize is complete. + public static final int CHANGED_PIP_BOUNDS = 5; + + // State for starting exiting PiP. + public static final int EXITING_PIP = 6; + + // State for finishing exit PiP flow. + public static final int EXITED_PIP = 7; + + private static final int FIRST_CUSTOM_STATE = 1000; + + private int mPrevCustomState = FIRST_CUSTOM_STATE; + + @IntDef(prefix = { "TRANSITION_STATE_" }, value = { + UNDEFINED, + SWIPING_TO_PIP, + ENTERING_PIP, + ENTERED_PIP, + CHANGING_PIP_BOUNDS, + CHANGED_PIP_BOUNDS, + EXITING_PIP, + EXITED_PIP, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionState {} + + @TransitionState + private int mState; + + // + // Swipe up to enter PiP related state + // + + // true if Launcher has started swipe PiP to home animation + private boolean mInSwipePipToHomeTransition; + + // App bounds used when as a starting point to swipe PiP to home animation in Launcher; + // these are also used to calculate the app icon overlay buffer size. + @NonNull + private final Rect mSwipePipToHomeAppBounds = new Rect(); + + // + // Tokens and leashes + // + + // pinned PiP task's WC token + @Nullable + WindowContainerToken mPipTaskToken; + + // pinned PiP task's leash + @Nullable + SurfaceControl mPinnedTaskLeash; + + // Overlay leash potentially used during swipe PiP to home transition; + // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid. + @Nullable + private SurfaceControl mSwipePipToHomeOverlay; + + /** + * An interface to track state updates as we progress through PiP transitions. + */ + public interface PipTransitionStateChangedListener { + + /** Reports changes in PiP transition state. */ + void onPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState, @Nullable Bundle extra); + } + + private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>(); + + /** + * @return the state of PiP in the context of transitions. + */ + @TransitionState + public int getState() { + return mState; + } + + /** + * Sets the state of PiP in the context of transitions. + */ + public void setState(@TransitionState int state) { + setState(state, null /* extra */); + } + + /** + * Sets the state of PiP in the context of transitions + * + * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info. + */ + public void setState(@TransitionState int state, @Nullable Bundle extra) { + if (state == ENTERING_PIP || state == SWIPING_TO_PIP) { + // Whenever we are entering PiP caller must provide extra state to set as well. + Preconditions.checkArgument(extra != null && !extra.isEmpty(), + "No extra bundle for either ENTERING_PIP or SWIPING_TO_PIP state."); + } + if (mState != state) { + dispatchPipTransitionStateChanged(mState, state, extra); + mState = state; + } + } + + private void dispatchPipTransitionStateChanged(@TransitionState int oldState, + @TransitionState int newState, @Nullable Bundle extra) { + mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra)); + } + + /** + * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates. + */ + public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) { + if (mCallbacks.contains(listener)) { + return; + } + mCallbacks.add(listener); + } + + /** + * @return true if provided {@link PipTransitionStateChangedListener} + * is registered before removing it. + */ + public boolean removePipTransitionStateChangedListener( + PipTransitionStateChangedListener listener) { + return mCallbacks.remove(listener); + } + + /** + * @return true if we have fully entered PiP. + */ + public boolean isInPip() { + return mState > ENTERING_PIP && mState < EXITING_PIP; + } + + void setSwipePipToHomeState(@Nullable SurfaceControl overlayLeash, + @NonNull Rect appBounds) { + mInSwipePipToHomeTransition = true; + if (overlayLeash != null && !appBounds.isEmpty()) { + mSwipePipToHomeOverlay = overlayLeash; + mSwipePipToHomeAppBounds.set(appBounds); + } + } + + void resetSwipePipToHomeState() { + mInSwipePipToHomeTransition = false; + mSwipePipToHomeOverlay = null; + mSwipePipToHomeAppBounds.setEmpty(); + } + + /** + * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. + */ + public boolean isInSwipePipToHomeTransition() { + return mInSwipePipToHomeTransition; + } + + /** + * @return the overlay used during swipe PiP to home for invalid srcRectHints in auto-enter PiP; + * null if srcRectHint provided is valid. + */ + @Nullable + public SurfaceControl getSwipePipToHomeOverlay() { + return mSwipePipToHomeOverlay; + } + + /** + * @return app bounds used to calculate + */ + @NonNull + public Rect getSwipePipToHomeAppBounds() { + return mSwipePipToHomeAppBounds; + } + + /** + * @return a custom state solely for internal use by the caller. + */ + @TransitionState + public int getCustomState() { + return ++mPrevCustomState; + } + + private String stateToString() { + switch (mState) { + case UNDEFINED: return "undefined"; + case ENTERING_PIP: return "entering-pip"; + case ENTERED_PIP: return "entered-pip"; + case CHANGING_PIP_BOUNDS: return "changing-bounds"; + case CHANGED_PIP_BOUNDS: return "changed-bounds"; + case EXITING_PIP: return "exiting-pip"; + case EXITED_PIP: return "exited-pip"; + } + throw new IllegalStateException("Unknown state: " + mState); + } + + @Override + public String toString() { + return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)", + stateToString(), mInSwipePipToHomeTransition); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java new file mode 100644 index 000000000000..bd8ac379b86f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java @@ -0,0 +1,110 @@ +/* + * 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; + +import android.os.Bundle; +import android.os.Parcelable; +import android.testing.AndroidTestingRunner; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.pip.PhoneSizeSpecSource; +import com.android.wm.shell.pip2.phone.PipTransitionState; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit test against {@link PhoneSizeSpecSource}. + * + * This test mocks the PiP2 flag to be true. + */ +@RunWith(AndroidTestingRunner.class) +public class PipTransitionStateTest extends ShellTestCase { + private static final String EXTRA_ENTRY_KEY = "extra_entry_key"; + private PipTransitionState mPipTransitionState; + private PipTransitionState.PipTransitionStateChangedListener mStateChangedListener; + private Parcelable mEmptyParcelable; + + @Before + public void setUp() { + mPipTransitionState = new PipTransitionState(); + mPipTransitionState.setState(PipTransitionState.UNDEFINED); + mEmptyParcelable = new Bundle(); + } + + @Test + public void testEnteredState_withoutExtra() { + mStateChangedListener = (oldState, newState, extra) -> { + Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState); + Assert.assertNull(extra); + }; + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } + + @Test + public void testEnteredState_withExtra() { + mStateChangedListener = (oldState, newState, extra) -> { + Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState); + Assert.assertNotNull(extra); + Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY)); + }; + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP, extra); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } + + @Test(expected = IllegalArgumentException.class) + public void testEnteringState_withoutExtra() { + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP); + } + + @Test(expected = IllegalArgumentException.class) + public void testSwipingToPipState_withoutExtra() { + mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP); + } + + @Test + public void testCustomState_withExtra_thenEntered_withoutExtra() { + final int customState = mPipTransitionState.getCustomState(); + mStateChangedListener = (oldState, newState, extra) -> { + if (newState == customState) { + Assert.assertNotNull(extra); + Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY)); + return; + } else if (newState == PipTransitionState.ENTERED_PIP) { + Assert.assertNull(extra); + return; + } + Assert.fail("Neither custom not ENTERED_PIP state is received."); + }; + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener); + mPipTransitionState.setState(customState, extra); + mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); + mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); + } +} |