Merge "Decouple recents view from window while swiping up" into ub-launcher3-qt-dev
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index 35783b5..a033402 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -16,14 +16,15 @@
 package com.android.quickstep;
 
 import static android.view.View.TRANSLATION_Y;
-
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.SPRING_DAMPING_RATIO;
 import static com.android.launcher3.allapps.AllAppsTransitionController.SPRING_STIFFNESS;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -177,6 +178,8 @@
         return new AnimationFactory() {
             private Animator mShelfAnim;
             private ShelfAnimState mShelfState;
+            private Animator mAttachToWindowAnim;
+            private boolean mIsAttachedToWindow;
 
             @Override
             public void createActivityController(long transitionLength) {
@@ -221,6 +224,28 @@
                 mShelfAnim.setDuration(duration);
                 mShelfAnim.start();
             }
+
+            @Override
+            public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
+                if (mIsAttachedToWindow == attached && animate) {
+                    return;
+                }
+                mIsAttachedToWindow = attached;
+                if (mAttachToWindowAnim != null) {
+                    mAttachToWindowAnim.cancel();
+                }
+                mAttachToWindowAnim = ObjectAnimator.ofFloat(activity.getOverviewPanel(),
+                        RecentsView.CONTENT_ALPHA, attached ? 1 : 0);
+                mAttachToWindowAnim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mAttachToWindowAnim = null;
+                    }
+                });
+                mAttachToWindowAnim.setInterpolator(ACCEL_DEACCEL);
+                mAttachToWindowAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0);
+                mAttachToWindowAnim.start();
+            }
         };
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
index bb320f2..5dc641f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java
@@ -267,6 +267,7 @@
                         mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
                                 || isLikelyToStartNewTask);
                         mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
+                        mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
                     }
                 }
                 break;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 7ffd8d7..afc4fcb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -43,6 +43,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
@@ -111,6 +112,8 @@
         implements SwipeAnimationListener, OnApplyWindowInsetsListener {
     private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
 
+    private static final Rect TEMP_RECT = new Rect();
+
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
     private static int getFlagForIndex(int index, String name) {
@@ -162,22 +165,23 @@
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
 
     enum GestureEndTarget {
-        HOME(1, STATE_SCALED_CONTROLLER_HOME, true, false, ContainerType.WORKSPACE),
+        HOME(1, STATE_SCALED_CONTROLLER_HOME, true, false, ContainerType.WORKSPACE, false),
 
         RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
-                | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER),
+                | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER, true),
 
-        NEW_TASK(0, STATE_START_NEW_TASK, false, true, ContainerType.APP),
+        NEW_TASK(0, STATE_START_NEW_TASK, false, true, ContainerType.APP, true),
 
-        LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP);
+        LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP, false);
 
         GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued,
-                int containerType) {
+                int containerType, boolean recentsAttachedToAppWindow) {
             this.endShift = endShift;
             this.endState = endState;
             this.isLauncher = isLauncher;
             this.canBeContinued = canBeContinued;
             this.containerType = containerType;
+            this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
         }
 
         /** 0 is app, 1 is overview */
@@ -190,6 +194,8 @@
         public final boolean canBeContinued;
         /** Used to log where the user ended up after the gesture ends */
         public final int containerType;
+        /** Whether RecentsView should be attached to the window as we animate to this target */
+        public final boolean recentsAttachedToAppWindow;
     }
 
     public static final long MAX_SWIPE_DURATION = 350;
@@ -202,6 +208,7 @@
     private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
 
     private static final long SHELF_ANIM_DURATION = 120;
+    public static final long RECENTS_ATTACH_DURATION = 300;
 
     /**
      * Used as the page index for logging when we return to the last task at the end of the gesture.
@@ -254,6 +261,7 @@
     private int mLogAction = Touch.SWIPE;
     private int mLogDirection = Direction.UP;
     private PointF mDownPos;
+    private boolean mIsLikelyToStartNewTask;
 
     private final RecentsAnimationWrapper mRecentsAnimationWrapper;
 
@@ -437,6 +445,7 @@
                 mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
                         mWasLauncherAlreadyVisible, true,
                         this::onAnimatorPlaybackControllerCreated);
+                maybeUpdateRecentsAttachedState(false /* animate */);
             };
             if (mWasLauncherAlreadyVisible) {
                 // Launcher is visible, but might be about to stop. Thus, if we prepare recents
@@ -541,10 +550,65 @@
         setShelfState(isPaused ? PEEK : HIDE, FAST_OUT_SLOW_IN, SHELF_ANIM_DURATION);
     }
 
+    public void maybeUpdateRecentsAttachedState() {
+        maybeUpdateRecentsAttachedState(true /* animate */);
+    }
+
+    /**
+     * Determines whether to show or hide RecentsView. The window is always
+     * synchronized with its corresponding TaskView in RecentsView, so if
+     * RecentsView is shown, it will appear to be attached to the window.
+     *
+     * Note this method has no effect unless the navigation mode is NO_BUTTON.
+     */
+    private void maybeUpdateRecentsAttachedState(boolean animate) {
+        if (mMode != Mode.NO_BUTTON || mRecentsView == null) {
+            return;
+        }
+        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationWrapper.targetSet == null
+                ? null
+                : mRecentsAnimationWrapper.targetSet.findTask(mRunningTaskId);
+        final boolean recentsAttachedToAppWindow;
+        int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+        if (mContinuingLastGesture) {
+            recentsAttachedToAppWindow = true;
+            animate = false;
+        } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
+            // The window is going away so make sure recents is always visible in this case.
+            recentsAttachedToAppWindow = true;
+            animate = false;
+        } else {
+            if (mGestureEndTarget != null) {
+                recentsAttachedToAppWindow = mGestureEndTarget.recentsAttachedToAppWindow;
+            } else {
+                recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
+            }
+            if (animate) {
+                // Only animate if an adjacent task view is visible on screen.
+                TaskView adjacentTask1 = mRecentsView.getTaskViewAt(runningTaskIndex + 1);
+                TaskView adjacentTask2 = mRecentsView.getTaskViewAt(runningTaskIndex - 1);
+                animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT))
+                        || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT));
+            }
+        }
+        mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+    }
+
+    public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
+        if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
+            mIsLikelyToStartNewTask = isLikelyToStartNewTask;
+            maybeUpdateRecentsAttachedState();
+        }
+    }
+
     @UiThread
     public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
         mAnimationFactory.setShelfState(shelfState, interpolator, duration);
+        boolean wasShelfPeeking = mIsShelfPeeking;
         mIsShelfPeeking = shelfState == PEEK;
+        if (mIsShelfPeeking != wasShelfPeeking) {
+            maybeUpdateRecentsAttachedState();
+        }
         if (mRecentsView != null && shelfState.shouldPreformHaptic) {
             mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
@@ -872,6 +936,8 @@
             Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
         mGestureEndTarget = target;
 
+        maybeUpdateRecentsAttachedState();
+
         if (mGestureEndTarget == HOME) {
             HomeAnimationFactory homeAnimFactory;
             if (mActivity != null) {
@@ -905,8 +971,15 @@
             windowAnim.start(velocityPxPerMs);
             mLauncherTransitionController = null;
         } else {
-            Animator windowAnim = mCurrentShift.animateToValue(start, end);
+            ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
             windowAnim.setDuration(duration).setInterpolator(interpolator);
+            windowAnim.addUpdateListener(valueAnimator -> {
+                if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) {
+                    // Views typically don't compute scroll when invisible as an optimization,
+                    // but in our case we need to since the window offset depends on the scroll.
+                    mRecentsView.computeScroll();
+                }
+            });
             windowAnim.addListener(new AnimationSuccessListener() {
                 @Override
                 public void onAnimationSuccess(Animator animator) {
@@ -1180,10 +1253,14 @@
     }
 
     public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) {
-        if (!(app.isNotInRecents
-                || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) {
+        if (!isNotInRecents(app)) {
             return 0;
         }
         return expectedAlpha;
     }
+
+    private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
+        return app.isNotInRecents
+                || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index a71b7bb..17f88c9 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -27,6 +27,10 @@
 import android.view.View;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -37,10 +41,6 @@
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
 /**
  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
  */
@@ -122,6 +122,13 @@
 
         default void setShelfState(ShelfAnimState animState, Interpolator interpolator,
                 long duration) { }
+
+        /**
+         * @param attached Whether to show RecentsView alongside the app window. If false, recents
+         *                 will be hidden by some property we can animate, e.g. alpha.
+         * @param animate Whether to animate recents to/from its new attached state.
+         */
+        default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
     }
 
     interface HomeAnimationFactory {