Improve PIP enter transition w/ gesture nav (3/N)

Directly transition Activity to PiP mode in launcher if the Activity
claims auto-pip support. Video is taken by commenting out the
requirement of setting the auto-pip flag.

Note that we need app to actively push up-to-dated
PictureInPictureParams to the framework, otherwise we won't be get
- PictureInPictureParams on first entering
- Staled PictureInPictureParams if the aspect ratio is changed

Video: http://rcll/aaaaaabFQoRHlzixHdtY/abenIxLFI1pZzF2O8t4TbS
Bug: 143965596
Test: see demo videos
Change-Id: Iea9a6ff39a79431ac1afa14aea812c500b3ca3b2
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index ef7a8f6..cac1a81 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -45,6 +45,7 @@
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -92,8 +93,10 @@
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
@@ -229,6 +232,10 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
+    private static final long SWIPE_PIP_TO_HOME_DURATION = 425;
+    private SwipePipToHomeAnimator mSwipePipToHomeAnimator;
+    protected boolean mIsSwipingPipToHome;
+
     public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
@@ -1049,25 +1056,44 @@
 
         if (mGestureState.getEndTarget() == HOME) {
             mTaskViewSimulator.setDrawsBelowRecents(false);
-            HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
-            RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
-            windowAnim.addAnimatorListener(new AnimationSuccessListener() {
-                @Override
-                public void onAnimationSuccess(Animator animator) {
-                    if (mRecentsAnimationController == null) {
-                        // If the recents animation is interrupted, we still end the running
-                        // animation (not canceled) so this is still called. In that case, we can
-                        // skip doing any future work here for the current gesture.
-                        return;
-                    }
-                    // Finalize the state and notify of the change
-                    mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
-                }
-            });
             getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
-            windowAnim.start(mContext, velocityPxPerMs);
+            final RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
+                    ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                    : null;
+            HomeAnimationFactory homeAnimFactory = createHomeAnimationFactory(duration);
+            mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome()
+                    && runningTaskTarget != null
+                    && runningTaskTarget.pictureInPictureParams != null
+                    && runningTaskTarget.pictureInPictureParams.isAutoEnterEnabled()
+                    && runningTaskTarget.pictureInPictureParams.getSourceRectHint() != null;
+            if (mIsSwipingPipToHome) {
+                mSwipePipToHomeAnimator = getSwipePipToHomeAnimator(
+                        homeAnimFactory, runningTaskTarget);
+                mSwipePipToHomeAnimator.setDuration(SWIPE_PIP_TO_HOME_DURATION);
+                mSwipePipToHomeAnimator.setInterpolator(interpolator);
+                mSwipePipToHomeAnimator.setFloatValues(0f, 1f);
+                mSwipePipToHomeAnimator.start();
+                mRunningWindowAnim = RunningWindowAnim.wrap(mSwipePipToHomeAnimator);
+            } else {
+                mSwipePipToHomeAnimator = null;
+                RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
+                windowAnim.addAnimatorListener(new AnimationSuccessListener() {
+                    @Override
+                    public void onAnimationSuccess(Animator animator) {
+                        if (mRecentsAnimationController == null) {
+                            // If the recents animation is interrupted, we still end the running
+                            // animation (not canceled) so this is still called. In that case,
+                            // we can skip doing any future work here for the current gesture.
+                            return;
+                        }
+                        // Finalize the state and notify of the change
+                        mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+                    }
+                });
+                windowAnim.start(mContext, velocityPxPerMs);
+                mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+            }
             homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
-            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
             mLauncherTransitionController = null;
         } else {
             ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
@@ -1111,6 +1137,46 @@
         }
     }
 
+    private SwipePipToHomeAnimator getSwipePipToHomeAnimator(HomeAnimationFactory homeAnimFactory,
+            RemoteAnimationTargetCompat runningTaskTarget) {
+        // Directly animate the app to PiP (picture-in-picture) mode
+        final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask();
+        final RecentsOrientedState orientationState = mTaskViewSimulator.getOrientationState();
+        final Rect destinationBounds = SystemUiProxy.INSTANCE.get(mContext)
+                .startSwipePipToHome(taskInfo.topActivity, taskInfo.topActivityInfo,
+                        runningTaskTarget.pictureInPictureParams,
+                        orientationState.getRecentsActivityRotation(),
+                        mDp.hotseatBarSizePx);
+        final SwipePipToHomeAnimator swipePipToHomeAnimator = new SwipePipToHomeAnimator(
+                runningTaskTarget.taskId,
+                taskInfo.topActivity,
+                runningTaskTarget.leash.getSurfaceControl(),
+                runningTaskTarget.pictureInPictureParams.getSourceRectHint(),
+                taskInfo.configuration.windowConfiguration.getBounds(),
+                destinationBounds);
+        swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                // Ensure Launcher ends in NORMAL state, we intentionally skip other callbacks
+                // since they are not relevant in this swipe-pip-to-home case.
+                homeAnimFactory.createActivityAnimationToHome().dispatchOnStart();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mRecentsAnimationController == null) {
+                    // If the recents animation is interrupted, we still end the running
+                    // animation (not canceled) so this is still called. In that case, we can
+                    // skip doing any future work here for the current gesture.
+                    return;
+                }
+                // Finalize the state and notify of the change
+                mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+            }
+        });
+        return swipePipToHomeAnimator;
+    }
+
     private void computeRecentsScrollIfInvisible() {
         if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
             // Views typically don't compute scroll when invisible as an optimization,
@@ -1359,6 +1425,7 @@
             // If there are no targets or the animation not started, then there is nothing to finish
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
         } else {
+            maybeFinishSwipePipToHome();
             finishRecentsControllerToHome(
                     () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
         }
@@ -1366,6 +1433,22 @@
         doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView());
     }
 
+    /**
+     * Resets the {@link #mIsSwipingPipToHome} and notifies SysUI that transition is finished
+     * if applicable. This should happen before {@link #finishRecentsControllerToHome(Runnable)}.
+     */
+    private void maybeFinishSwipePipToHome() {
+        if (mIsSwipingPipToHome && mSwipePipToHomeAnimator != null) {
+            SystemUiProxy.INSTANCE.get(mContext).stopSwipePipToHome(
+                    mSwipePipToHomeAnimator.getComponentName(),
+                    mSwipePipToHomeAnimator.getDestinationBounds());
+            mRecentsAnimationController.setFinishTaskBounds(
+                    mSwipePipToHomeAnimator.getTaskId(),
+                    mSwipePipToHomeAnimator.getDestinationBounds());
+            mIsSwipingPipToHome = false;
+        }
+    }
+
     protected abstract void finishRecentsControllerToHome(Runnable callback);
 
     private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 4411455..842fb84 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -56,6 +56,7 @@
             final TaskView runningTaskView = mRecentsView.getRunningTaskView();
             final View workspaceView;
             if (runningTaskView != null
+                    && !mIsSwipingPipToHome
                     && runningTaskView.getTask().key.getComponent() != null) {
                 workspaceView = mActivity.getWorkspace().getFirstMatchForAppClose(
                         runningTaskView.getTask().key.getComponent().getPackageName(),
@@ -140,5 +141,10 @@
             new StaggeredWorkspaceAnim(mActivity, velocity,
                     true /* animateOverviewScrim */).start();
         }
+
+        @Override
+        public boolean supportSwipePipToHome() {
+            return true;
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 51f5e5d..20e2515 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -18,6 +18,8 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
+import android.graphics.Rect;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
@@ -156,6 +158,18 @@
     }
 
     /**
+     * Sets the final bounds on a Task. This is used by Launcher to notify the system that
+     * animating Activity to PiP has completed and the associated task surface should be updated
+     * accordingly. This should be called before `finish`
+     * @param taskId for which the leash should be updated
+     * @param destinationBounds bounds of the final PiP window
+     */
+    public void setFinishTaskBounds(int taskId, Rect destinationBounds) {
+        UI_HELPER_EXECUTOR.execute(
+                () -> mController.setFinishTaskBounds(taskId, destinationBounds));
+    }
+
+    /**
      * Enables the input consumer to start intercepting touches in the app window.
      */
     public void enableInputConsumer() {
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index b6eaa1c..18cd29d 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -159,6 +159,14 @@
         public void update(RectF currentRect, float progress, float radius) { }
 
         public void onCancel() { }
+
+        /**
+         * @return {@code true} if this factory supports animating an Activity to PiP window on
+         * swiping up to home.
+         */
+        public boolean supportSwipePipToHome() {
+            return false;
+        }
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
new file mode 100644
index 0000000..02b5dfe
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2020 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.quickstep.util;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.content.ComponentName;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An {@link Animator} that animates an Activity to PiP (picture-in-picture) window when
+ * swiping up (in gesture navigation mode). Note that this class is derived from
+ * {@link com.android.wm.shell.pip.PipAnimationController.PipTransitionAnimator}.
+ *
+ * TODO: consider sharing this class including the animator and leash operations between
+ * Launcher and SysUI. Also, there should be one source of truth for the corner radius of the
+ * PiP window, which would ideally be on SysUI side as well.
+ */
+public class SwipePipToHomeAnimator extends ValueAnimator implements
+        ValueAnimator.AnimatorUpdateListener {
+    private final int mTaskId;
+    private final ComponentName mComponentName;
+    private final SurfaceControl mLeash;
+    private final Rect mStartBounds = new Rect();
+    private final Rect mDestinationBounds = new Rect();
+    private final SurfaceTransactionHelper mSurfaceTransactionHelper;
+
+    /** for calculating the transform in {@link #onAnimationUpdate(ValueAnimator)} */
+    private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
+    private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
+    private final Rect mSourceHintRectInsets = new Rect();
+    private final Rect mSourceInsets = new Rect();
+
+    /**
+     * Flag to avoid the double-end problem since the leash would have been released
+     * after the first end call and any further operations upon it would lead to NPE.
+     */
+    private boolean mHasAnimationEnded;
+
+    public SwipePipToHomeAnimator(int taskId,
+            @NonNull ComponentName componentName,
+            @NonNull SurfaceControl leash,
+            @NonNull Rect sourceRectHint,
+            @NonNull Rect startBounds,
+            @NonNull Rect destinationBounds) {
+        mTaskId = taskId;
+        mComponentName = componentName;
+        mLeash = leash;
+        mStartBounds.set(startBounds);
+        mDestinationBounds.set(destinationBounds);
+        mSurfaceTransactionHelper = new SurfaceTransactionHelper();
+
+        mSourceHintRectInsets.set(sourceRectHint.left - startBounds.left,
+                sourceRectHint.top - startBounds.top,
+                startBounds.right - sourceRectHint.right,
+                startBounds.bottom - sourceRectHint.bottom);
+
+        addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                SwipePipToHomeAnimator.this.onAnimationEnd();
+            }
+        });
+        addUpdateListener(this);
+    }
+
+    @Override
+    public void onAnimationUpdate(ValueAnimator animator) {
+        if (mHasAnimationEnded) return;
+
+        final float fraction = animator.getAnimatedFraction();
+        final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds, mDestinationBounds);
+        final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
+                mSourceHintRectInsets);
+        final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+        mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mStartBounds, bounds, insets);
+        tx.setCornerRadius(mLeash, 0).apply();
+    }
+
+    public int getTaskId() {
+        return mTaskId;
+    }
+
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    public Rect getDestinationBounds() {
+        return mDestinationBounds;
+    }
+
+    private void onAnimationEnd() {
+        if (mHasAnimationEnded) return;
+
+        final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+        mSurfaceTransactionHelper.resetScale(tx, mLeash, mDestinationBounds);
+        mSurfaceTransactionHelper.crop(tx, mLeash, mDestinationBounds);
+        tx.setCornerRadius(mLeash, 0).apply();
+        mHasAnimationEnded = true;
+    }
+
+    /**
+     * Slim version of {@link com.android.wm.shell.pip.PipSurfaceTransactionHelper}
+     */
+    private static final class SurfaceTransactionHelper {
+        private final Matrix mTmpTransform = new Matrix();
+        private final float[] mTmpFloat9 = new float[9];
+        private final RectF mTmpSourceRectF = new RectF();
+        private final Rect mTmpDestinationRect = new Rect();
+
+        private void scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash,
+                Rect sourceBounds, Rect destinationBounds, Rect insets) {
+            mTmpSourceRectF.set(sourceBounds);
+            mTmpDestinationRect.set(sourceBounds);
+            mTmpDestinationRect.inset(insets);
+            // Scale by the shortest edge and offset such that the top/left of the scaled inset
+            // source rect aligns with the top/left of the destination bounds
+            final float scale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            final float left = destinationBounds.left - insets.left * scale;
+            final float top = destinationBounds.top - insets.top * scale;
+            mTmpTransform.setScale(scale, scale);
+            tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+                    .setWindowCrop(leash, mTmpDestinationRect)
+                    .setPosition(leash, left, top);
+        }
+
+        private void resetScale(SurfaceControl.Transaction tx, SurfaceControl leash,
+                Rect destinationBounds) {
+            tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9)
+                    .setPosition(leash, destinationBounds.left, destinationBounds.top);
+        }
+
+        private void crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+                Rect destinationBounds) {
+            tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+                    .setPosition(leash, destinationBounds.left, destinationBounds.top);
+        }
+    }
+}