/* * Copyright (C) 2021 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.pip.tv; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.transitTypeToString; import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP; import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP; import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP; import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.os.IBinder; import android.os.Trace; 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.annotation.FloatRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * PiP Transition for TV. */ public class TvPipTransition extends PipTransitionController { private static final String TAG = "TvPipTransition"; private static final float ZOOM_ANIMATION_SCALE_FACTOR = 0.97f; private final PipTransitionState mPipTransitionState; private final PipAnimationController mPipAnimationController; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final TvPipMenuController mTvPipMenuController; private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory mTransactionFactory; private final ThreadLocal mSfAnimationHandlerThreadLocal = ThreadLocal.withInitial(() -> { AnimationHandler handler = new AnimationHandler(); handler.setProvider(new SfVsyncFrameCallbackProvider()); return handler; }); private final long mEnterFadeOutDuration; private final long mEnterFadeInDuration; private final long mExitFadeOutDuration; private final long mExitFadeInDuration; @Nullable private Animator mCurrentAnimator; /** * The Task window that is currently in PIP windowing mode. */ @Nullable private WindowContainerToken mCurrentPipTaskToken; @Nullable private IBinder mPendingExitTransition; public TvPipTransition(Context context, @NonNull ShellInit shellInit, @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, TvPipBoundsState tvPipBoundsState, TvPipMenuController tvPipMenuController, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTransitionState pipTransitionState, PipAnimationController pipAnimationController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, PipDisplayLayoutState pipDisplayLayoutState) { super(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, tvPipMenuController, tvPipBoundsAlgorithm); mPipTransitionState = pipTransitionState; mPipAnimationController = pipAnimationController; mSurfaceTransactionHelper = pipSurfaceTransactionHelper; mTvPipMenuController = tvPipMenuController; mPipDisplayLayoutState = pipDisplayLayoutState; mTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); mEnterFadeOutDuration = context.getResources().getInteger( R.integer.config_tvPipEnterFadeOutDuration); mEnterFadeInDuration = context.getResources().getInteger( R.integer.config_tvPipEnterFadeInDuration); mExitFadeOutDuration = context.getResources().getInteger( R.integer.config_tvPipExitFadeOutDuration); mExitFadeInDuration = context.getResources().getInteger( R.integer.config_tvPipExitFadeInDuration); } @Override public void startExitTransition(int type, WindowContainerTransaction out, @Nullable Rect destinationBounds) { cancelAnimations(); mPendingExitTransition = mTransitions.startTransition(type, out, this); } @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (isCloseTransition(info)) { // PiP is closing (without reentering fullscreen activity) ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Starting close animation", TAG); cancelAnimations(); startCloseAnimation(info, startTransaction, finishTransaction, finishCallback); mCurrentPipTaskToken = null; return true; } else if (transition.equals(mPendingExitTransition)) { // PiP is exiting (reentering fullscreen activity) ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Starting exit animation", TAG); final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info); mPendingExitTransition = null; // PipTaskChange can be null if the PIP task has been detached, for example, when the // task contains multiple activities, the PIP will be moved to a new PIP task when // entering, and be moved back when exiting. In that case, the PIP task will be removed // immediately. final TaskInfo pipTaskInfo = currentPipTaskChange != null ? currentPipTaskChange.getTaskInfo() : mPipOrganizer.getTaskInfo(); if (pipTaskInfo == null) { throw new RuntimeException("Cannot find the pip task for exit-pip transition."); } final int type = info.getType(); switch (type) { case TRANSIT_EXIT_PIP -> { TransitionInfo.Change pipChange = currentPipTaskChange; SurfaceControl activitySc = null; if (mCurrentPipTaskToken == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG); } else if (pipChange == null) { // The pipTaskChange is null, this can happen if we are reparenting the // PIP activity back to its original Task. In that case, we should animate // the activity leash instead, which should be the change whose last parent // is the recorded PiP Task. for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (mCurrentPipTaskToken.equals(change.getLastParent())) { // Find the activity that is exiting PiP. pipChange = change; activitySc = change.getLeash(); break; } } } if (pipChange == null) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: No window of exiting PIP is found. Can't play expand " + "animation", TAG); removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction, finishCallback); return true; } final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info); final SurfaceControl pipLeash; if (activitySc != null) { // Use a local leash to animate activity in case the activity has // letterbox which may be broken by PiP animation, e.g. always end at 0,0 // in parent and unable to include letterbox area in crop bounds. final SurfaceControl activitySurface = pipChange.getLeash(); pipLeash = new SurfaceControl.Builder() .setName(activitySc + "_pip-leash") .setContainerLayer() .setHidden(false) .setParent(root.getLeash()) .setCallsite("TvPipTransition.startAnimation") .build(); startTransaction.reparent(activitySurface, pipLeash); // Put the activity at local position with offset in case it is letterboxed. final Point activityOffset = pipChange.getEndRelOffset(); startTransaction.setPosition(activitySc, activityOffset.x, activityOffset.y); } else { pipLeash = pipChange.getLeash(); startTransaction.reparent(pipLeash, root.getLeash()); } startTransaction.setLayer(pipLeash, Integer.MAX_VALUE); final Rect currentBounds = mPipBoundsState.getBounds(); final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds()); cancelAnimations(); startExitAnimation(pipTaskInfo, pipLeash, currentBounds, destinationBounds, startTransaction, finishTransaction, finishCallback); } // pass through here is intended case TRANSIT_TO_BACK, TRANSIT_REMOVE_PIP -> removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction, finishCallback ); default -> { return false; } } mCurrentPipTaskToken = null; return true; } else if (isEnteringPip(info)) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Starting enter animation", TAG); // Search for an Enter PiP transition TransitionInfo.Change enterPip = null; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change.getTaskInfo() != null && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { enterPip = change; } } if (enterPip == null) { throw new IllegalStateException("Trying to start PiP animation without a pip" + "participant"); } // Make sure other open changes are visible as entering PIP. Some may be hidden in // Transitions#setupStartState because the transition type is OPEN (such as auto-enter). for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change == enterPip) continue; if (TransitionUtil.isOpeningType(change.getMode())) { final SurfaceControl leash = change.getLeash(); startTransaction.show(leash).setAlpha(leash, 1.f); } } cancelAnimations(); startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback); return true; } return false; } /** * For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task. */ private void removePipImmediately(@NonNull TransitionInfo info, @NonNull TaskInfo taskInfo, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePipImmediately", TAG); cancelAnimations(); startTransaction.apply(); finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), mPipDisplayLayoutState.getDisplayBounds()); mTvPipMenuController.detach(); mPipOrganizer.onExitPipFinished(taskInfo); finishCallback.onTransitionFinished(/* wct= */ null); mPipTransitionState.setTransitionState(UNDEFINED); sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK); } private void startCloseAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { final TransitionInfo.Change pipTaskChange = findCurrentPipTaskChange(info); final SurfaceControl pipLeash = pipTaskChange.getLeash(); final List closeLeashes = new ArrayList<>(); for (TransitionInfo.Change change : info.getChanges()) { if (TransitionUtil.isClosingType(change.getMode()) && change != pipTaskChange) { closeLeashes.add(change.getLeash()); } } final Rect pipBounds = mPipBoundsState.getBounds(); mSurfaceTransactionHelper .resetScale(startTransaction, pipLeash, pipBounds) .crop(startTransaction, pipLeash, pipBounds) .shadow(startTransaction, pipLeash, false); final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction(); for (SurfaceControl leash : closeLeashes) { startTransaction.setShadowRadius(leash, 0f); } ValueAnimator closeFadeOutAnimator = createAnimator(); closeFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT); closeFadeOutAnimator.setDuration(mExitFadeOutDuration); closeFadeOutAnimator.addUpdateListener( animationUpdateListener(pipLeash).fadingOut().withMenu()); for (SurfaceControl leash : closeLeashes) { closeFadeOutAnimator.addUpdateListener(animationUpdateListener(leash).fadingOut()); } closeFadeOutAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(@NonNull Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: close animation: start", TAG); for (SurfaceControl leash : closeLeashes) { startTransaction.setShadowRadius(leash, 0f); } startTransaction.apply(); mPipTransitionState.setTransitionState(EXITING_PIP); sendOnPipTransitionStarted(TRANSITION_DIRECTION_REMOVE_STACK); } @Override public void onAnimationCancel(@NonNull Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: close animation: cancel", TAG); sendOnPipTransitionCancelled(TRANSITION_DIRECTION_REMOVE_STACK); } @Override public void onAnimationEnd(@NonNull Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: close animation: end", TAG); mTvPipMenuController.detach(); finishCallback.onTransitionFinished(null /* wct */); transaction.close(); mPipTransitionState.setTransitionState(UNDEFINED); sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK); mCurrentAnimator = null; } }); closeFadeOutAnimator.start(); mCurrentAnimator = closeFadeOutAnimator; } @Override public void startEnterAnimation(@NonNull TransitionInfo.Change pipChange, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { // Keep track of the PIP task mCurrentPipTaskToken = pipChange.getContainer(); final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo(); final SurfaceControl leash = pipChange.getLeash(); mTvPipMenuController.attach(leash); setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, taskInfo.topActivityInfo); final Rect pipBounds = mPipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas(); mPipBoundsState.setBounds(pipBounds); mTvPipMenuController.movePipMenu(null, pipBounds, 0f); final WindowContainerTransaction resizePipWct = new WindowContainerTransaction(); resizePipWct.setWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED); resizePipWct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED); resizePipWct.setBounds(taskInfo.token, pipBounds); mSurfaceTransactionHelper .resetScale(finishTransaction, leash, pipBounds) .crop(finishTransaction, leash, pipBounds) .shadow(finishTransaction, leash, false); final Rect currentBounds = pipChange.getStartAbsBounds(); final Rect fadeOutCurrentBounds = scaledRect(currentBounds, ZOOM_ANIMATION_SCALE_FACTOR); final ValueAnimator enterFadeOutAnimator = createAnimator(); enterFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT); enterFadeOutAnimator.setDuration(mEnterFadeOutDuration); enterFadeOutAnimator.addUpdateListener( animationUpdateListener(leash) .fadingOut() .animateBounds(currentBounds, fadeOutCurrentBounds, currentBounds)); enterFadeOutAnimator.addListener(new AnimatorListenerAdapter() { @SuppressLint("MissingPermission") @Override public void onAnimationEnd(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: enter fade out animation: end", TAG); SurfaceControl.Transaction tx = mTransactionFactory.getTransaction(); mSurfaceTransactionHelper .resetScale(tx, leash, pipBounds) .crop(tx, leash, pipBounds) .shadow(tx, leash, false); mShellTaskOrganizer.applyTransaction(resizePipWct); tx.apply(); } }); final ValueAnimator enterFadeInAnimator = createAnimator(); enterFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER); enterFadeInAnimator.setDuration(mEnterFadeInDuration); enterFadeInAnimator.addUpdateListener( animationUpdateListener(leash) .fadingIn() .withMenu() .atBounds(pipBounds)); final AnimatorSet animatorSet = new AnimatorSet(); animatorSet .play(enterFadeInAnimator) .after(500) .after(enterFadeOutAnimator); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: enter animation: start", TAG); startTransaction.apply(); mPipTransitionState.setTransitionState(ENTERING_PIP); sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); } @Override public void onAnimationCancel(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: enter animation: cancel", TAG); enterFadeInAnimator.setCurrentFraction(1f); sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP); } @Override public void onAnimationEnd(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: enter animation: end", TAG); WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); wct.setBounds(taskInfo.token, pipBounds); finishCallback.onTransitionFinished(wct); mPipTransitionState.setTransitionState(ENTERED_PIP); sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); mCurrentAnimator = null; } }); animatorSet.start(); mCurrentAnimator = animatorSet; } private void startExitAnimation(@NonNull TaskInfo taskInfo, SurfaceControl leash, Rect currentBounds, Rect destinationBounds, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { final Rect fadeInStartBounds = scaledRect(destinationBounds, ZOOM_ANIMATION_SCALE_FACTOR); final ValueAnimator exitFadeOutAnimator = createAnimator(); exitFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT); exitFadeOutAnimator.setDuration(mExitFadeOutDuration); exitFadeOutAnimator.addUpdateListener( animationUpdateListener(leash) .fadingOut() .withMenu() .atBounds(currentBounds)); exitFadeOutAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: exit fade out animation: end", TAG); startTransaction.apply(); mPipMenuController.detach(); } }); final ValueAnimator exitFadeInAnimator = createAnimator(); exitFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER); exitFadeInAnimator.setDuration(mExitFadeInDuration); exitFadeInAnimator.addUpdateListener( animationUpdateListener(leash) .fadingIn() .animateBounds(fadeInStartBounds, destinationBounds, destinationBounds)); final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playSequentially( exitFadeOutAnimator, exitFadeInAnimator ); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: exit animation: start", TAG); mPipTransitionState.setTransitionState(EXITING_PIP); sendOnPipTransitionStarted(TRANSITION_DIRECTION_LEAVE_PIP); } @Override public void onAnimationCancel(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: exit animation: cancel", TAG); sendOnPipTransitionCancelled(TRANSITION_DIRECTION_LEAVE_PIP); } @Override public void onAnimationEnd(Animator animation) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: exit animation: end", TAG); mPipOrganizer.onExitPipFinished(taskInfo); final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); wct.setBounds(taskInfo.token, destinationBounds); finishCallback.onTransitionFinished(wct); mPipTransitionState.setTransitionState(UNDEFINED); sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP); mCurrentAnimator = null; } }); animatorSet.start(); mCurrentAnimator = animatorSet; } @NonNull private ValueAnimator createAnimator() { final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); return animator; } @NonNull private TvPipTransitionAnimatorUpdateListener animationUpdateListener( @NonNull SurfaceControl leash) { return new TvPipTransitionAnimatorUpdateListener(leash, mTvPipMenuController, mTransactionFactory.getTransaction(), mSurfaceTransactionHelper); } @NonNull private static Rect scaledRect(@NonNull Rect rect, float scale) { final Rect out = new Rect(rect); out.inset((int) (rect.width() * (1 - scale) / 2), (int) (rect.height() * (1 - scale) / 2)); return out; } private boolean isCloseTransition(TransitionInfo info) { final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info); return currentPipTaskChange != null && info.getType() == TRANSIT_CLOSE; } @Nullable private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) { if (mCurrentPipTaskToken == null) { return null; } for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (mCurrentPipTaskToken.equals(change.getContainer())) { return change; } } return null; } /** * Whether we should handle the given {@link TransitionInfo} animation as entering PIP. */ private boolean isEnteringPip(@NonNull TransitionInfo info) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (isEnteringPip(change, info.getType())) return true; } return false; } /** * Whether a particular change is a window that is entering pip. */ @Override public boolean isEnteringPip(@NonNull TransitionInfo.Change change, @WindowManager.TransitionType int transitType) { if (change.getTaskInfo() != null && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED && !Objects.equals(change.getContainer(), mCurrentPipTaskToken)) { if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN || transitType == TRANSIT_CHANGE) { return true; } // Please file a bug to handle the unexpected transition type. android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + transitTypeToString(transitType), new Throwable()); } return false; } @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG); if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { mCurrentAnimator.end(); } } @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { if (requestHasPipEnter(request)) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: handle PiP enter request", TAG); WindowContainerTransaction wct = new WindowContainerTransaction(); augmentRequest(transition, request, wct); return wct; } else if (request.getType() == TRANSIT_TO_BACK && request.getTriggerTask() != null && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_PINNED) { // if we receive a TRANSIT_TO_BACK type of request while in PiP mPendingExitTransition = transition; // update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls mPipTransitionState.setTransitionState(EXITING_PIP); // return an empty WindowContainerTransaction so that we don't check other handlers return new WindowContainerTransaction(); } else { return null; } } @Override public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) { if (!requestHasPipEnter(request)) { throw new IllegalStateException("Called PiP augmentRequest when request has no PiP"); } outWCT.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED); } /** * Cancel any ongoing PiP transitions/animations. */ public void cancelAnimations() { if (mPipAnimationController.isAnimating()) { mPipAnimationController.getCurrentAnimator().cancel(); mPipAnimationController.resetAnimatorState(); } if (mCurrentAnimator != null) { mCurrentAnimator.cancel(); } } @Override public void end() { if (mCurrentAnimator != null) { mCurrentAnimator.end(); } } private static class TvPipTransitionAnimatorUpdateListener implements ValueAnimator.AnimatorUpdateListener { private final SurfaceControl mLeash; private final TvPipMenuController mTvPipMenuController; private final SurfaceControl.Transaction mTransaction; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final RectF mTmpRectF = new RectF(); private final Rect mTmpRect = new Rect(); private float mStartAlpha = ALPHA_NO_CHANGE; private float mEndAlpha = ALPHA_NO_CHANGE; @Nullable private Rect mStartBounds; @Nullable private Rect mEndBounds; private Rect mWindowContainerBounds; private boolean mShowMenu; TvPipTransitionAnimatorUpdateListener(@NonNull SurfaceControl leash, @NonNull TvPipMenuController tvPipMenuController, @NonNull SurfaceControl.Transaction transaction, @NonNull PipSurfaceTransactionHelper pipSurfaceTransactionHelper) { mLeash = leash; mTvPipMenuController = tvPipMenuController; mTransaction = transaction; mSurfaceTransactionHelper = pipSurfaceTransactionHelper; } public TvPipTransitionAnimatorUpdateListener animateAlpha( @FloatRange(from = 0.0, to = 1.0) float startAlpha, @FloatRange(from = 0.0, to = 1.0) float endAlpha) { mStartAlpha = startAlpha; mEndAlpha = endAlpha; return this; } public TvPipTransitionAnimatorUpdateListener animateBounds(@NonNull Rect startBounds, @NonNull Rect endBounds, @NonNull Rect windowContainerBounds) { mStartBounds = startBounds; mEndBounds = endBounds; mWindowContainerBounds = windowContainerBounds; return this; } public TvPipTransitionAnimatorUpdateListener atBounds(@NonNull Rect bounds) { return animateBounds(bounds, bounds, bounds); } public TvPipTransitionAnimatorUpdateListener fadingOut() { return animateAlpha(1f, 0f); } public TvPipTransitionAnimatorUpdateListener fadingIn() { return animateAlpha(0f, 1f); } public TvPipTransitionAnimatorUpdateListener withMenu() { mShowMenu = true; return this; } @Override public void onAnimationUpdate(@NonNull ValueAnimator animation) { final float fraction = animation.getAnimatedFraction(); final float alpha = lerp(mStartAlpha, mEndAlpha, fraction); if (mStartBounds != null && mEndBounds != null) { lerp(mStartBounds, mEndBounds, fraction, mTmpRectF); applyAnimatedValue(alpha, mTmpRectF); } else { applyAnimatedValue(alpha, null); } } private void applyAnimatedValue(float alpha, @Nullable RectF bounds) { Trace.beginSection("applyAnimatedValue"); final SurfaceControl.Transaction tx = mTransaction; Trace.beginSection("leash scale and alpha"); if (alpha != ALPHA_NO_CHANGE) { mSurfaceTransactionHelper.alpha(tx, mLeash, alpha); } if (bounds != null) { mSurfaceTransactionHelper.scale(tx, mLeash, mWindowContainerBounds, bounds); } mSurfaceTransactionHelper.shadow(tx, mLeash, false); tx.show(mLeash); Trace.endSection(); if (mShowMenu) { Trace.beginSection("movePipMenu"); if (bounds != null) { mTmpRect.set((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom); mTvPipMenuController.movePipMenu(tx, mTmpRect, alpha); } else { mTvPipMenuController.movePipMenu(tx, null, alpha); } Trace.endSection(); } else { mTvPipMenuController.movePipMenu(tx, null, 0f); } tx.apply(); Trace.endSection(); } private float lerp(float start, float end, float fraction) { return start * (1 - fraction) + end * fraction; } private void lerp(@NonNull Rect start, @NonNull Rect end, float fraction, @NonNull RectF out) { out.set( start.left * (1 - fraction) + end.left * fraction, start.top * (1 - fraction) + end.top * fraction, start.right * (1 - fraction) + end.right * fraction, start.bottom * (1 - fraction) + end.bottom * fraction); } } }