Merge "Fixing AOSP tests using TAPL" into ub-launcher3-master
diff --git a/Android.mk b/Android.mk
index cccf4e4..0477dab 100644
--- a/Android.mk
+++ b/Android.mk
@@ -182,7 +182,9 @@
 endif
 LOCAL_MODULE := Launcher3QuickStepLib
 LOCAL_PRIVILEGED_MODULE := true
-LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+    Launcher3CommonDepsLib \
+    SecondaryDisplayLauncherLib
 
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src) \
diff --git a/go/quickstep/res/.keep b/go/quickstep/res/.keep
deleted file mode 100644
index e69de29..0000000
--- a/go/quickstep/res/.keep
+++ /dev/null
diff --git a/go/quickstep/res/layout/icon_recents_root_view.xml b/go/quickstep/res/layout/icon_recents_root_view.xml
new file mode 100644
index 0000000..122fadf
--- /dev/null
+++ b/go/quickstep/res/layout/icon_recents_root_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<!-- TODO(114136250): Remove this temporary placeholder view for Go recents -->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:text="Stub!"
+    android:textSize="40sp"/>
\ No newline at end of file
diff --git a/go/quickstep/res/layout/overview_panel.xml b/go/quickstep/res/layout/overview_panel.xml
new file mode 100644
index 0000000..601edce
--- /dev/null
+++ b/go/quickstep/res/layout/overview_panel.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<fragment android:name="com.android.quickstep.IconRecentsFragment"
+          xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/low_ram_recents_fragment"
+          android:layout_width="match_parent"
+          android:layout_height="match_parent"/>
diff --git a/go/quickstep/src/.keep b/go/quickstep/src/.keep
deleted file mode 100644
index e69de29..0000000
--- a/go/quickstep/src/.keep
+++ /dev/null
diff --git a/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java b/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java
new file mode 100644
index 0000000..facf0d2
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+
+public class IconRecentsFragment extends Fragment {
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.icon_recents_root_view, container, false);
+    }
+}
\ No newline at end of file
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index 882af59..af1b353 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -253,19 +253,24 @@
         badge.draw(target);
     }
 
+    private Bitmap createIconBitmap(Drawable icon, float scale) {
+        return createIconBitmap(icon, scale, mIconBitmapSize);
+    }
+
     /**
+     * @param icon drawable that should be flattened to a bitmap
      * @param scale the scale to apply before drawing {@param icon} on the canvas
      */
-    private Bitmap createIconBitmap(Drawable icon, float scale) {
-        Bitmap bitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize,
-                Bitmap.Config.ARGB_8888);
+    public Bitmap createIconBitmap(Drawable icon, float scale, int size) {
+        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+
         mCanvas.setBitmap(bitmap);
         mOldBounds.set(icon.getBounds());
 
         if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
-            int offset = Math.max((int) Math.ceil(BLUR_FACTOR * mIconBitmapSize),
-                    Math.round(mIconBitmapSize * (1 - scale) / 2 ));
-            icon.setBounds(offset, offset, mIconBitmapSize - offset, mIconBitmapSize - offset);
+            int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
+                    Math.round(size * (1 - scale) / 2 ));
+            icon.setBounds(offset, offset, size - offset, size - offset);
             icon.draw(mCanvas);
         } else {
             if (icon instanceof BitmapDrawable) {
@@ -275,8 +280,8 @@
                     bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
                 }
             }
-            int width = mIconBitmapSize;
-            int height = mIconBitmapSize;
+            int width = size;
+            int height = size;
 
             int intrinsicWidth = icon.getIntrinsicWidth();
             int intrinsicHeight = icon.getIntrinsicHeight();
@@ -289,11 +294,11 @@
                     width = (int) (height * ratio);
                 }
             }
-            final int left = (mIconBitmapSize - width) / 2;
-            final int top = (mIconBitmapSize - height) / 2;
+            final int left = (size - width) / 2;
+            final int top = (size - height) / 2;
             icon.setBounds(left, top, left + width, top + height);
             mCanvas.save();
-            mCanvas.scale(scale, scale, mIconBitmapSize / 2, mIconBitmapSize / 2);
+            mCanvas.scale(scale, scale, size / 2, size / 2);
             icon.draw(mCanvas);
             mCanvas.restore();
 
diff --git a/quickstep/recents_ui_overrides/res/.keep b/quickstep/recents_ui_overrides/res/.keep
deleted file mode 100644
index e69de29..0000000
--- a/quickstep/recents_ui_overrides/res/.keep
+++ /dev/null
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/recents_ui_overrides/res/layout/overview_panel.xml
similarity index 100%
rename from quickstep/res/layout/overview_panel.xml
rename to quickstep/recents_ui_overrides/res/layout/overview_panel.xml
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index a1cee82..cb5152a 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -215,7 +215,8 @@
                         playIconAnimators(anim, v, windowTargetBounds, !isAllOpeningTargetTrs);
                         if (launcherClosing) {
                             Pair<AnimatorSet, Runnable> launcherContentAnimator =
-                                    getLauncherContentAnimator(true /* isAppOpening */);
+                                    getLauncherContentAnimator(true /* isAppOpening */,
+                                            new float[] {0, mContentTransY});
                             anim.play(launcherContentAnimator.first);
                             anim.addListener(new AnimatorListenerAdapter() {
                                 @Override
@@ -350,17 +351,16 @@
      *
      * @param isAppOpening True when this is called when an app is opening.
      *                     False when this is called when an app is closing.
+     * @param trans Array that contains the start and end translation values for the content.
      */
-    private Pair<AnimatorSet, Runnable> getLauncherContentAnimator(boolean isAppOpening) {
+    private Pair<AnimatorSet, Runnable> getLauncherContentAnimator(boolean isAppOpening,
+            float[] trans) {
         AnimatorSet launcherAnimator = new AnimatorSet();
         Runnable endListener;
 
         float[] alphas = isAppOpening
                 ? new float[] {1, 0}
                 : new float[] {0, 1};
-        float[] trans = isAppOpening
-                ? new float[] {0, mContentTransY}
-                : new float[] {-mContentTransY, 0};
 
         if (mLauncher.isInState(ALL_APPS)) {
             // All Apps in portrait mode is full screen, so we only animate AllAppsContainerView.
@@ -443,7 +443,16 @@
     private void playIconAnimators(AnimatorSet appOpenAnimator, View v, Rect windowTargetBounds,
             boolean toggleVisibility) {
         final boolean isBubbleTextView = v instanceof BubbleTextView;
-        mFloatingView = new View(mLauncher);
+        if (mFloatingView == null) {
+            mFloatingView = new View(mLauncher);
+        } else {
+            mFloatingView.setTranslationX(0);
+            mFloatingView.setTranslationY(0);
+            mFloatingView.setScaleX(1);
+            mFloatingView.setScaleY(1);
+            mFloatingView.setAlpha(1);
+            mFloatingView.setBackground(null);
+        }
         if (isBubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) {
             // Create a copy of the app icon
             mFloatingView.setBackground(DrawableFactory.INSTANCE.get(mLauncher)
@@ -481,19 +490,17 @@
                 : viewLocationLeft;
         LayoutParams lp = new LayoutParams(rect.width(), rect.height());
         lp.ignoreInsets = true;
-        lp.setMarginStart(viewLocationStart);
+        lp.leftMargin = viewLocationStart;
         lp.topMargin = viewLocationTop;
         mFloatingView.setLayoutParams(lp);
 
         // Set the properties here already to make sure they'are available when running the first
         // animation frame.
-        mFloatingView.setLeft(viewLocationLeft);
-        mFloatingView.setTop(viewLocationTop);
-        mFloatingView.setRight(viewLocationLeft + rect.width());
-        mFloatingView.setBottom(viewLocationTop + rect.height());
+        mFloatingView.layout(viewLocationLeft, viewLocationTop,
+                viewLocationLeft + rect.width(), viewLocationTop + rect.height());
 
         // Swap the two views in place.
-        ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView);
+        ((ViewGroup) mDragLayer.getParent()).getOverlay().add(mFloatingView);
         if (toggleVisibility) {
             v.setVisibility(View.INVISIBLE);
         }
@@ -562,7 +569,7 @@
                     ((BubbleTextView) v).setStayPressed(false);
                 }
                 v.setVisibility(View.VISIBLE);
-                ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
+                ((ViewGroup) mDragLayer.getParent()).getOverlay().remove(mFloatingView);
             }
         });
     }
@@ -681,10 +688,13 @@
             RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
             definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
                     WindowManagerWrapper.ACTIVITY_TYPE_STANDARD,
-                    new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(),
+                    new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(false /* fromUnlock */),
                             CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
 
-            // TODO: Transition for unlock to home TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER
+            definition.addRemoteAnimation(
+                    WindowManagerWrapper.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+                    new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(true /* fromUnlock */),
+                            CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
             new ActivityCompat(mLauncher).registerRemoteAnimations(definition);
         }
     }
@@ -697,7 +707,7 @@
      * @return Runner that plays when user goes to Launcher
      *         ie. pressing home, swiping up from nav bar.
      */
-    private RemoteAnimationRunnerCompat getWallpaperOpenRunner() {
+    private RemoteAnimationRunnerCompat getWallpaperOpenRunner(boolean fromUnlock) {
         return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) {
             @Override
             public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
@@ -723,7 +733,9 @@
 
                 if (anim == null) {
                     anim = new AnimatorSet();
-                    anim.play(getClosingWindowAnimators(targetCompats));
+                    anim.play(fromUnlock
+                            ? getUnlockWindowAnimator(targetCompats)
+                            : getClosingWindowAnimators(targetCompats));
 
                     // Normally, we run the launcher content animation when we are transitioning
                     // home, but if home is already visible, then we don't want to animate the
@@ -737,7 +749,21 @@
                             || mLauncher.isForceInvisible()) {
                         // Only register the content animation for cancellation when state changes
                         mLauncher.getStateManager().setCurrentAnimation(anim);
-                        createLauncherResumeAnimation(anim);
+                        if (fromUnlock) {
+                            Pair<AnimatorSet, Runnable> contentAnimator =
+                                    getLauncherContentAnimator(false /* isAppOpening */,
+                                            new float[] {mContentTransY, 0});
+                            contentAnimator.first.setStartDelay(0);
+                            anim.play(contentAnimator.first);
+                            anim.addListener(new AnimatorListenerAdapter() {
+                                @Override
+                                public void onAnimationEnd(Animator animation) {
+                                    contentAnimator.second.run();
+                                }
+                            });
+                        } else {
+                            createLauncherResumeAnimation(anim);
+                        }
                     }
                 }
 
@@ -748,6 +774,31 @@
     }
 
     /**
+     * Animator that controls the transformations of the windows when unlocking the device.
+     */
+    private Animator getUnlockWindowAnimator(RemoteAnimationTargetCompat[] targets) {
+        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
+                new SyncRtSurfaceTransactionApplierCompat(mDragLayer);
+        ValueAnimator unlockAnimator = ValueAnimator.ofFloat(0, 1);
+        unlockAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
+        float cornerRadius = RecentsModel.INSTANCE.get(mLauncher).getWindowCornerRadius();
+        unlockAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                SurfaceParams[] params = new SurfaceParams[targets.length];
+                for (int i = targets.length - 1; i >= 0; i--) {
+                    RemoteAnimationTargetCompat target = targets[i];
+                    params[i] = new SurfaceParams(target.leash, 1f, null,
+                            target.sourceContainerBounds,
+                            RemoteAnimationProvider.getLayer(target, MODE_OPENING), cornerRadius);
+                }
+                surfaceApplier.scheduleApply(params);
+            }
+        });
+        return unlockAnimator;
+    }
+
+    /**
      * Animator that controls the transformations of the windows the targets that are closing.
      */
     private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) {
@@ -801,7 +852,8 @@
     private void createLauncherResumeAnimation(AnimatorSet anim) {
         if (mLauncher.isInState(LauncherState.ALL_APPS)) {
             Pair<AnimatorSet, Runnable> contentAnimator =
-                    getLauncherContentAnimator(false /* isAppOpening */);
+                    getLauncherContentAnimator(false /* isAppOpening */,
+                            new float[] {-mContentTransY, 0});
             contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY);
             anim.play(contentAnimator.first);
             anim.addListener(new AnimatorListenerAdapter() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index 1906286..25e0af2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_QUICKSTEP_PREVIEW;
 import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
@@ -45,7 +46,11 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        AbstractFloatingView.closeAllOpenViewsExcept(launcher, TYPE_QUICKSTEP_PREVIEW);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            AbstractFloatingView.closeAllOpenViews(launcher);
+        } else {
+            AbstractFloatingView.closeAllOpenViewsExcept(launcher, TYPE_QUICKSTEP_PREVIEW);
+        }
         dispatchWindowStateChanged(launcher);
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 753f73a..50af4a1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -49,7 +50,7 @@
     private static final String TAG = "OverviewSwipeController";
 
     // Progress after which the transition is assumed to be a success in case user does not fling
-    private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+    public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
 
     protected final T mActivity;
     private final SwipeDetector mDetector;
@@ -231,6 +232,12 @@
             mFlingBlockCheck.onEvent();
         }
         mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier);
+
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
+                mRecentsView.redrawLiveTile(true);
+            }
+        }
         return true;
     }
 
@@ -267,6 +274,13 @@
         anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
         anim.setDuration(animationDuration);
         anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            anim.addUpdateListener(valueAnimator -> {
+                if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
+                    mRecentsView.redrawLiveTile(true);
+                }
+            });
+        }
         anim.start();
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index 4e79fed..0ef67c4 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -44,6 +44,8 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.UiThreadHelper.AsyncCommand;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
@@ -58,6 +60,9 @@
 
 public class UiFactory {
 
+    private static final AsyncCommand SET_SHELF_HEIGHT_CMD = (visible, height) ->
+            WindowManagerWrapper.getInstance().setShelfHeight(visible != 0, height);
+
     public static TouchController[] createTouchControllers(Launcher launcher) {
         boolean swipeUpEnabled = OverviewInteractionState.INSTANCE.get(launcher)
                 .isSwipeUpGestureEnabled();
@@ -175,10 +180,10 @@
         LauncherState state = launcher.getStateManager().getState();
         if (!OverviewInteractionState.INSTANCE.get(launcher).swipeGestureInitializing()) {
             DeviceProfile profile = launcher.getDeviceProfile();
-            WindowManagerWrapper.getInstance().setShelfHeight(
-                    (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
-                            && !profile.isVerticalBarLayout(),
-                    profile.hotseatBarSizePx);
+            boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
+                    && !profile.isVerticalBarLayout();
+            UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT_CMD,
+                    visible ? 1 : 0, profile.hotseatBarSizePx);
         }
 
         if (state == NORMAL) {
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 4646fd7f..8293083 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.FAST_OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
@@ -44,6 +43,8 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.view.View;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
@@ -53,9 +54,9 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.TestProtocol;
-import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.SpringObjectAnimator;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -77,9 +78,6 @@
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
 /**
  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
  */
@@ -150,11 +148,13 @@
      */
     int getContainerType();
 
+    boolean isInLiveTileMode();
+
     class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
 
         @Override
         public LayoutListener createLayoutListener(Launcher activity) {
-            return new LauncherLayoutListener(activity);
+            return LauncherLayoutListener.resetAndGet(activity);
         }
 
         @Override
@@ -295,8 +295,8 @@
 
             AnimatorSet anim = new AnimatorSet();
             if (!activity.getDeviceProfile().isVerticalBarLayout()) {
-                AllAppsTransitionController controller = activity.getAllAppsController();
-                ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(controller, ALL_APPS_PROGRESS,
+                Animator shiftAnim = new SpringObjectAnimator(activity.getAllAppsController(),
+                        activity.getAllAppsController().getShiftRange(),
                         fromState.getVerticalProgress(activity),
                         endState.getVerticalProgress(activity));
                 shiftAnim.setInterpolator(LINEAR);
@@ -442,6 +442,13 @@
             return launcher != null ? launcher.getStateManager().getState().containerType
                     : LauncherLogProto.ContainerType.APP;
         }
+
+        @Override
+        public boolean isInLiveTileMode() {
+            Launcher launcher = getCreatedActivity();
+            return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
+                    launcher.isStarted();
+        }
     }
 
     class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> {
@@ -627,6 +634,11 @@
         public int getContainerType() {
             return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER;
         }
+
+        @Override
+        public boolean isInLiveTileMode() {
+            return false;
+        }
     }
 
     interface LayoutListener {
diff --git a/quickstep/src/com/android/quickstep/LongSwipeHelper.java b/quickstep/src/com/android/quickstep/LongSwipeHelper.java
index 16214dd..80d37ae 100644
--- a/quickstep/src/com/android/quickstep/LongSwipeHelper.java
+++ b/quickstep/src/com/android/quickstep/LongSwipeHelper.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION;
 import static com.android.quickstep.WindowTransformSwipeHandler.MIN_OVERSHOOT_DURATION;
 
@@ -41,7 +42,6 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 /**
  * Utility class to handle long swipe from an app.
@@ -113,7 +113,7 @@
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
 
-            if (blockedFling && !toAllApps) {
+            if (blockedFling && !toAllApps && !QUICKSTEP_SPRINGS.get()) {
                 Interpolators.OvershootParams overshoot = new OvershootParams(currentFraction,
                         currentFraction, endProgress, velocityPxPerMs, (int) mMaxSwipeDistance);
                 duration = (overshoot.duration + duration);
@@ -145,7 +145,12 @@
         ValueAnimator animator = mAnimator.getAnimationPlayer();
         animator.setDuration(duration).setInterpolator(interpolator);
         animator.setFloatValues(currentFraction, endProgress);
-        animator.start();
+
+        if (QUICKSTEP_SPRINGS.get()) {
+            mAnimator.dispatchOnStartWithVelocity(endProgress, velocityPxPerMs);
+        } else {
+            animator.start();
+        }
     }
 
     private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) {
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 95be188..e27af2a 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -21,11 +21,9 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
-
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
-import static com.android.systemui.shared.system.ActivityManagerWrapper
-        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.annotation.TargetApi;
@@ -192,7 +190,7 @@
 
                 if (mPassedInitialSlop && mInteractionHandler != null) {
                     // Move
-                    mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
+                    dispatchMotion(ev, displacement - mStartDisplacement);
                 }
                 break;
             }
@@ -207,6 +205,14 @@
         }
     }
 
+    private void dispatchMotion(MotionEvent ev, float displacement) {
+        mInteractionHandler.updateDisplacement(displacement);
+        boolean isLandscape = isNavBarOnLeft() || isNavBarOnRight();
+        if (!isLandscape) {
+            mInteractionHandler.dispatchMotionEventToRecentsView(ev);
+        }
+    }
+
     private void notifyGestureStarted() {
         if (mInteractionHandler == null) {
             return;
@@ -297,15 +303,16 @@
      */
     private void finishTouchTracking(MotionEvent ev) {
         if (mPassedInitialSlop && mInteractionHandler != null) {
-            mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
+            dispatchMotion(ev, getDisplacement(ev) - mStartDisplacement);
 
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
 
-            float velocity = isNavBarOnRight() ? mVelocityTracker.getXVelocity(mActivePointerId)
-                    : isNavBarOnLeft() ? -mVelocityTracker.getXVelocity(mActivePointerId)
+            float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
+            float velocity = isNavBarOnRight() ? velocityX
+                    : isNavBarOnLeft() ? -velocityX
                             : mVelocityTracker.getYVelocity(mActivePointerId);
-            mInteractionHandler.onGestureEnded(velocity);
+            mInteractionHandler.onGestureEnded(velocity, velocityX);
         } else {
             // Since we start touch tracking on DOWN, we may reach this state without actually
             // starting the gesture. In that case, just cleanup immediately.
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
index da5c4fa..db0150e 100644
--- a/quickstep/src/com/android/quickstep/QuickScrubController.java
+++ b/quickstep/src/com/android/quickstep/QuickScrubController.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASK_STABILIZER;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -124,6 +125,12 @@
         mActivityControlHelper = controlHelper;
         mTouchInteractionLog = touchInteractionLog;
 
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (mRecentsView.getRunningTaskView() != null) {
+                mRecentsView.getRunningTaskView().setShowScreenshot(false);
+            }
+        }
+
         if (mIsQuickSwitch) {
             mShouldSwitchToNext = true;
             mPrevProgressDelta = 0;
@@ -342,6 +349,7 @@
         if (action != null) {
             action.run();
         }
+        mRecentsView.setEnableDrawingLiveTile(true);
     }
 
     public void onTaskRemoved(int taskId) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
index 2f3cb5f..042afea 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -97,8 +97,8 @@
     }
 
     /**
-     * @param onFinishComplete A callback that runs after the animation controller has finished
-     *                         on the background thread.
+     * @param onFinishComplete A callback that runs on the main thread after the animation
+     *                         controller has finished on the background thread.
      */
     public void finish(boolean toHome, Runnable onFinishComplete) {
         if (!toHome) {
@@ -128,7 +128,7 @@
             controller.finish(toHome);
 
             if (onFinishComplete != null) {
-                onFinishComplete.run();
+                mMainThreadExecutor.execute(onFinishComplete);
             }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 75ccba6..442b106 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -67,7 +67,6 @@
 
     private float mWindowCornerRadius = -1;
 
-
     private RecentsModel(Context context) {
         mContext = context;
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a42ee09..7a6b135 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -22,6 +22,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT;
 import static android.view.MotionEvent.ACTION_UP;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper
         .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
@@ -252,6 +253,11 @@
                 mOverviewCommandHelper.getActivityControlHelper().isResumed()) {
             return OverviewTouchConsumer.newInstance(
                     mOverviewCommandHelper.getActivityControlHelper(), false, mTouchInteractionLog);
+        } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
+                mOverviewCommandHelper.getActivityControlHelper().isInLiveTileMode()) {
+            return OverviewTouchConsumer.newInstance(
+                    mOverviewCommandHelper.getActivityControlHelper(), false, mTouchInteractionLog,
+                    false /* waitForWindowAvailable */);
         } else {
             if (tracker == null) {
                 tracker = VelocityTracker.obtain();
@@ -298,9 +304,11 @@
         private float mLastProgress = 0;
         private boolean mStartPending = false;
         private boolean mEndPending = false;
+        private boolean mWaitForWindowAvailable;
 
         OverviewTouchConsumer(ActivityControlHelper<T> activityHelper, T activity,
-                boolean startingInActivityBounds, TouchInteractionLog touchInteractionLog) {
+                boolean startingInActivityBounds, TouchInteractionLog touchInteractionLog,
+                boolean waitForWindowAvailable) {
             mActivityHelper = activityHelper;
             mActivity = activity;
             mTarget = activity.getDragLayer();
@@ -311,6 +319,8 @@
                     .getQuickScrubController();
             mTouchInteractionLog = touchInteractionLog;
             mTouchInteractionLog.setTouchConsumer(this);
+
+            mWaitForWindowAvailable = waitForWindowAvailable;
         }
 
         @Override
@@ -433,7 +443,11 @@
                 }
             };
 
-            mActivityHelper.executeOnWindowAvailable(mActivity, action);
+            if (mWaitForWindowAvailable) {
+                mActivityHelper.executeOnWindowAvailable(mActivity, action);
+            } else {
+                action.run();
+            }
         }
 
         @Override
@@ -461,12 +475,19 @@
 
         public static TouchConsumer newInstance(ActivityControlHelper activityHelper,
                 boolean startingInActivityBounds, TouchInteractionLog touchInteractionLog) {
+            return newInstance(activityHelper, startingInActivityBounds, touchInteractionLog,
+                    true /* waitForWindowAvailable */);
+        }
+
+        public static TouchConsumer newInstance(ActivityControlHelper activityHelper,
+                boolean startingInActivityBounds, TouchInteractionLog touchInteractionLog,
+                boolean waitForWindowAvailable) {
             BaseDraggingActivity activity = activityHelper.getCreatedActivity();
             if (activity == null) {
                 return TouchConsumer.NO_OP;
             }
             return new OverviewTouchConsumer(activityHelper, activity, startingInActivityBounds,
-                    touchInteractionLog);
+                    touchInteractionLog, waitForWindowAvailable);
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index e951750..fd4585b 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -22,6 +22,8 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION;
 import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_FROM_APP_START_DURATION;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
@@ -46,10 +48,12 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.WindowManager;
 import android.view.animation.Interpolator;
+
 import androidx.annotation.AnyThread;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
@@ -109,7 +113,7 @@
 
     // Interaction finish states
     private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5;
-    private static final int STATE_SCALED_CONTROLLER_APP = 1 << 6;
+    private static final int STATE_SCALED_CONTROLLER_LAST_TASK = 1 << 6;
 
     private static final int STATE_HANDLER_INVALIDATED = 1 << 7;
     private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8;
@@ -127,7 +131,8 @@
     private static final int STATE_SCREENSHOT_VIEW_SHOWN = 1 << 17;
 
     private static final int STATE_RESUME_LAST_TASK = 1 << 18;
-    private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 19;
+    private static final int STATE_START_NEW_TASK = 1 << 19;
+    private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 20;
 
 
     private static final int LAUNCHER_UI_STATES =
@@ -153,7 +158,7 @@
             "STATE_ACTIVITY_MULTIPLIER_COMPLETE",
             "STATE_APP_CONTROLLER_RECEIVED",
             "STATE_SCALED_CONTROLLER_RECENTS",
-            "STATE_SCALED_CONTROLLER_APP",
+            "STATE_SCALED_CONTROLLER_LAST_TASK",
             "STATE_HANDLER_INVALIDATED",
             "STATE_GESTURE_STARTED_QUICKSTEP",
             "STATE_GESTURE_STARTED_QUICKSCRUB",
@@ -166,6 +171,7 @@
             "STATE_SCREENSHOT_CAPTURED",
             "STATE_SCREENSHOT_VIEW_SHOWN",
             "STATE_RESUME_LAST_TASK",
+            "STATE_START_NEW_TASK",
             "STATE_ASSIST_DATA_RECEIVED",
     };
 
@@ -173,7 +179,7 @@
     public static final long MIN_SWIPE_DURATION = 80;
     public static final long MIN_OVERSHOOT_DURATION = 120;
 
-    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
+    public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
     private static final float SWIPE_DURATION_MULTIPLIER =
             Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
 
@@ -190,6 +196,7 @@
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
     // visible.
     private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+    private boolean mDispatchedDownEvent;
     // To avoid UI jump when gesture is started, we offset the animation by the threshold.
     private float mShiftAtGestureStart = 0;
 
@@ -298,10 +305,12 @@
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
                 this::sendRemoteAnimationsToAnimationFactory);
 
-        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP,
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_LAST_TASK,
                 this::resumeLastTaskForQuickstep);
         mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
+        mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_APP_CONTROLLER_RECEIVED,
+                this::startNewTask);
 
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE
@@ -327,7 +336,7 @@
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED
-                | STATE_SCALED_CONTROLLER_APP,
+                | STATE_SCALED_CONTROLLER_LAST_TASK,
                 this::notifyTransitionCancelled);
 
         mStateCallback.addCallback(QUICK_SCRUB_START_UI_STATE, this::onQuickScrubStartUi);
@@ -339,9 +348,11 @@
         mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter);
         mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart);
 
-        mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
-                | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
-                (b) -> mRecentsView.setRunningTaskHidden(!b));
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
+                            | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
+                    (b) -> mRecentsView.setRunningTaskHidden(!b));
+        }
     }
 
     private void executeOnUiThread(Runnable action) {
@@ -410,6 +421,13 @@
         SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, (applier) -> {
             mSyncTransactionApplier = applier;
         });
+        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+            if (!mBgLongSwipeMode) {
+                updateFinalShift();
+            }
+        });
+        mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper);
+        mRecentsView.setClipAnimationHelper(mClipAnimationHelper);
         mQuickScrubController = mRecentsView.getQuickScrubController();
         mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity);
 
@@ -462,6 +480,7 @@
     }
 
     private void setupRecentsViewUi() {
+        mRecentsView.setEnableDrawingLiveTile(false);
         mRecentsView.showTask(mRunningTaskId);
         mRecentsView.setRunningTaskHidden(true);
         mRecentsView.setRunningTaskIconScaledDown(true);
@@ -535,15 +554,39 @@
         } else {
             offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing)
                     + tempRect.rect.width();
-            float distanceToReachEdge = mDp.widthPx / 2 + tempRect.rect.width() / 2 +
-                    res.getDimensionPixelSize(R.dimen.recents_page_spacing);
-            float interpolation = Math.min(1, offsetX / distanceToReachEdge);
-            scale = TaskView.getCurveScaleForInterpolation(interpolation);
+            scale = getTaskCurveScaleForOffsetX(offsetX, tempRect.rect.width());
         }
         mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY,
                 QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR);
     }
 
+    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
+        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 +
+                mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
+        return TaskView.getCurveScaleForInterpolation(interpolation);
+    }
+
+    @WorkerThread
+    public void dispatchMotionEventToRecentsView(MotionEvent event) {
+        if (mRecentsView == null) {
+            return;
+        }
+        // Pass the motion events to RecentsView to allow scrolling during swipe up.
+        if (mDispatchedDownEvent) {
+            mRecentsView.dispatchTouchEvent(event);
+        } else {
+            // The first event we dispatch should be ACTION_DOWN.
+            mDispatchedDownEvent = true;
+            MotionEvent downEvent = MotionEvent.obtain(event);
+            downEvent.setAction(MotionEvent.ACTION_DOWN);
+            int flags = downEvent.getEdgeFlags();
+            downEvent.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR);
+            mRecentsView.dispatchTouchEvent(downEvent);
+            downEvent.recycle();
+        }
+    }
+
     @WorkerThread
     public void updateDisplacement(float displacement) {
         // We are moving in the negative x/y direction
@@ -588,11 +631,21 @@
 
         RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController();
         if (controller != null) {
+            float offsetX = 0;
+            if (mRecentsView != null && mInteractionType == INTERACTION_NORMAL) {
+                int startScroll = mRecentsView.getScrollForPage(mRecentsView.indexOfChild(
+                        mRecentsView.getRunningTaskView()));
+                offsetX = startScroll - mRecentsView.getScrollX();
+                offsetX *= mRecentsView.getScaleX();
+            }
+            float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
+                    mClipAnimationHelper.getTargetRect().width());
             SyncRtSurfaceTransactionApplierCompat syncTransactionApplier
                     = Looper.myLooper() == mMainThreadHandler.getLooper()
                             ? mSyncTransactionApplier
                             : null;
-            mTransformParams.setProgress(shift).setSyncTransactionApplier(syncTransactionApplier);
+            mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale)
+                    .setSyncTransactionApplier(syncTransactionApplier);
             mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
                     mTransformParams);
 
@@ -608,6 +661,9 @@
 
     private void updateFinalShiftUi() {
         if (mRecentsAnimationWrapper.getController() != null && mLayoutListener != null) {
+            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                mLayoutListener.open();
+            }
             mLayoutListener.update(mCurrentShift.value > 1, mUiLongSwipeMode,
                     mClipAnimationHelper.getCurrentRectWithInsets(),
                     mClipAnimationHelper.getCurrentCornerRadius());
@@ -621,10 +677,17 @@
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
             }
         }
-        // Update insets of the next previous task, as we might switch to it.
-        TaskView nextTaskView = mRecentsView == null ? null : mRecentsView.getNextTaskView();
-        if (mInteractionType == INTERACTION_NORMAL && nextTaskView != null) {
-            nextTaskView.setFullscreenProgress(1 - mCurrentShift.value);
+        // Update insets of the adjacent tasks, as we might switch to them.
+        int runningTaskIndex = mRecentsView == null ? -1 : mRecentsView.getRunningTaskIndex();
+        if (mInteractionType == INTERACTION_NORMAL && runningTaskIndex >= 0) {
+            TaskView nextTaskView = mRecentsView.getTaskViewAt(runningTaskIndex + 1);
+            TaskView prevTaskView = mRecentsView.getTaskViewAt(runningTaskIndex - 1);
+            if (nextTaskView != null) {
+                nextTaskView.setFullscreenProgress(1 - mCurrentShift.value);
+            }
+            if (prevTaskView != null) {
+                prevTaskView.setFullscreenProgress(1 - mCurrentShift.value);
+            }
         }
 
         if (mLauncherTransitionController == null || mLauncherTransitionController
@@ -714,7 +777,7 @@
     }
 
     @WorkerThread
-    public void onGestureEnded(float endVelocity) {
+    public void onGestureEnded(float endVelocity, float velocityX) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
@@ -723,9 +786,9 @@
         mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
 
         if (mBgLongSwipeMode) {
-            executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling));
+            executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling, velocityX));
         } else {
-            handleNormalGestureEnd(endVelocity, isFling);
+            handleNormalGestureEnd(endVelocity, isFling, velocityX);
         }
     }
 
@@ -735,23 +798,30 @@
         if (mLauncherTransitionController != null) {
             mLauncherTransitionController.getAnimationPlayer().end();
         }
-        // Hide the task view, if not already hidden
-        setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // Hide the task view, if not already hidden
+            setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
+        }
 
         return OverviewTouchConsumer.newInstance(mActivityControlHelper, true,
                 mTouchInteractionLog);
     }
 
-    private void handleNormalGestureEnd(float endVelocity, boolean isFling) {
+    private void handleNormalGestureEnd(float endVelocity, boolean isFling, float velocityX) {
         float velocityPxPerMs = endVelocity / 1000;
+        float velocityXPxPerMs = velocityX / 1000;
         long duration = MAX_SWIPE_DURATION;
         float currentShift = mCurrentShift.value;
         final boolean goingToHome;
         float endShift;
         final float startShift;
         Interpolator interpolator = DEACCEL;
+        final int nextPage = mRecentsView != null ? mRecentsView.getNextPage() : -1;
+        final int runningTaskIndex = mRecentsView != null ? mRecentsView.getRunningTaskIndex() : -1;
+        boolean goingToNewTask = mRecentsView != null && nextPage != runningTaskIndex;
+        final boolean reachedOverviewThreshold = currentShift >= MIN_PROGRESS_FOR_OVERVIEW;
         if (!isFling) {
-            goingToHome = currentShift >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted;
+            goingToHome = reachedOverviewThreshold && mGestureStarted;
             endShift = goingToHome ? 1 : 0;
             long expectedDuration = Math.abs(Math.round((endShift - currentShift)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
@@ -759,7 +829,9 @@
             startShift = currentShift;
             interpolator = goingToHome ? OVERSHOOT_1_2 : DEACCEL;
         } else {
-            goingToHome = endVelocity < 0;
+            // If user scrolled to a new task, only go to home (overview) if they already passed
+            // the overview threshold. Otherwise, we'll snap to the new task and launch it.
+            goingToHome = endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold);
             endShift = goingToHome ? 1 : 0;
             startShift = Utilities.boundToRange(currentShift - velocityPxPerMs
                     * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1);
@@ -786,9 +858,29 @@
         }
         if (goingToHome) {
             mRecentsAnimationWrapper.enableTouchProxy();
+        } else if (goingToNewTask) {
+            // We aren't goingToHome, and user scrolled/flung to a new task; snap to the closest
+            // task in that direction and launch it (in startNewTask()).
+            int taskToLaunch = runningTaskIndex + (nextPage > runningTaskIndex ? 1 : - 1);
+            if (taskToLaunch >= mRecentsView.getTaskViewCount()) {
+                // Scrolled to Clear all button, snap back to current task and resume it.
+                mRecentsView.snapToPage(runningTaskIndex, Math.toIntExact(duration));
+                goingToNewTask = false;
+            } else {
+                float distance = Math.abs(mRecentsView.getScrollForPage(taskToLaunch)
+                        - mRecentsView.getScrollX());
+                int durationX = (int) Math.abs(distance / velocityXPxPerMs);
+                if (durationX > MAX_SWIPE_DURATION) {
+                    durationX = Math.toIntExact(MAX_SWIPE_DURATION);
+                }
+                interpolator = Interpolators.scrollInterpolatorForVelocity(velocityXPxPerMs);
+                mRecentsView.snapToPage(taskToLaunch, durationX, interpolator);
+                duration = Math.max(duration, durationX);
+            }
         }
 
-        animateToProgress(startShift, endShift, duration, interpolator, goingToHome);
+        animateToProgress(startShift, endShift, duration, interpolator, goingToHome,
+                goingToNewTask, velocityPxPerMs);
     }
 
     private void doLogGesture(boolean toLauncher) {
@@ -813,23 +905,28 @@
     }
 
     /** Animates to the given progress, where 0 is the current app and 1 is overview. */
-    private void animateToProgress(float start, float end, long duration,
-            Interpolator interpolator, boolean goingToHome) {
+    private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
+            boolean goingToHome, boolean goingToNewTask, float velocityPxPerMs) {
         mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
-                interpolator, goingToHome));
+                interpolator, goingToHome, goingToNewTask, velocityPxPerMs));
     }
 
     private void animateToProgressInternal(float start, float end, long duration,
-            Interpolator interpolator, boolean goingToHome) {
+            Interpolator interpolator, boolean goingToHome, boolean goingToNewTask,
+            float velocityPxPerMs) {
         mIsGoingToHome = goingToHome;
         ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration);
         anim.setInterpolator(interpolator);
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
+                int recentsState = STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+                        | STATE_SCREENSHOT_VIEW_SHOWN;
                 setStateOnUiThread(mIsGoingToHome
-                        ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
-                        | STATE_SCREENSHOT_VIEW_SHOWN) : STATE_SCALED_CONTROLLER_APP);
+                        ? recentsState
+                        : goingToNewTask
+                            ? STATE_START_NEW_TASK
+                            : STATE_SCALED_CONTROLLER_LAST_TASK);
             }
         });
         anim.start();
@@ -854,7 +951,12 @@
                 mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress(
                         interpolator, adjustedStart, end));
                 mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration);
-                mLauncherTransitionController.getAnimationPlayer().start();
+
+                if (QUICKSTEP_SPRINGS.get()) {
+                    mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs);
+                } else {
+                    mLauncherTransitionController.getAnimationPlayer().start();
+                }
             }
         });
     }
@@ -872,6 +974,18 @@
         mTouchInteractionLog.finishRecentsAnimation(false);
     }
 
+    @UiThread
+    private void startNewTask() {
+        // Launch the task user scrolled to (mRecentsView.getNextPage()).
+        mRecentsAnimationWrapper.finish(true /* toHome */, () -> {
+            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false,
+                    result -> setStateOnUiThread(STATE_HANDLER_INVALIDATED),
+                    mMainThreadHandler);
+        });
+        mTouchInteractionLog.finishRecentsAnimation(false);
+        doLogGesture(false /* toLauncher */);
+    }
+
     public void reset() {
         if (mInteractionType != INTERACTION_QUICK_SCRUB) {
             // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be
@@ -889,6 +1003,10 @@
 
         mActivityInitListener.unregister();
         mTaskSnapshot = null;
+
+        if (mRecentsView != null) {
+            mRecentsView.setOnScrollChangeListener(null);
+        }
     }
 
     private void invalidateHandlerWithLauncher() {
@@ -913,57 +1031,66 @@
     }
 
     public void layoutListenerClosed() {
+        mRecentsView.setRunningTaskHidden(false);
         if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) {
             mLauncherTransitionController.setPlayFraction(1);
         }
-        mRecentsView.setRunningTaskHidden(false);
+        mRecentsView.setEnableDrawingLiveTile(true);
     }
 
     private void switchToScreenshot() {
-        boolean finishTransitionPosted = false;
-        RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController();
-        if (controller != null) {
-            // Update the screenshot of the task
-            if (mTaskSnapshot == null) {
-                mTaskSnapshot = controller.screenshotTask(mRunningTaskId);
-            }
-            TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot);
-            if (taskView != null) {
-                // Defer finishing the animation until the next launcher frame with the
-                // new thumbnail
-                finishTransitionPosted = new WindowCallbacksCompat(taskView) {
-
-                    // The number of frames to defer until we actually finish the animation
-                    private int mDeferFrameCount = 2;
-
-                    @Override
-                    public void onPostDraw(Canvas canvas) {
-                        if (mDeferFrameCount > 0) {
-                            mDeferFrameCount--;
-                            // Workaround, detach and reattach to invalidate the root node for
-                            // another draw
-                            detach();
-                            attach();
-                            taskView.invalidate();
-                            return;
-                        }
-
-                        setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-                        detach();
-                    }
-                }.attach();
-            }
-        }
-        if (!finishTransitionPosted) {
-            // If we haven't posted a draw callback, set the state immediately.
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        } else {
+            boolean finishTransitionPosted = false;
+            RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController();
+            if (controller != null) {
+                // Update the screenshot of the task
+                if (mTaskSnapshot == null) {
+                    mTaskSnapshot = controller.screenshotTask(mRunningTaskId);
+                }
+                TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot);
+                if (taskView != null) {
+                    // Defer finishing the animation until the next launcher frame with the
+                    // new thumbnail
+                    finishTransitionPosted = new WindowCallbacksCompat(taskView) {
+
+                        // The number of frames to defer until we actually finish the animation
+                        private int mDeferFrameCount = 2;
+
+                        @Override
+                        public void onPostDraw(Canvas canvas) {
+                            if (mDeferFrameCount > 0) {
+                                mDeferFrameCount--;
+                                // Workaround, detach and reattach to invalidate the root node for
+                                // another draw
+                                detach();
+                                attach();
+                                taskView.invalidate();
+                                return;
+                            }
+
+                            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+                            detach();
+                        }
+                    }.attach();
+                }
+            }
+            if (!finishTransitionPosted) {
+                // If we haven't posted a draw callback, set the state immediately.
+                setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+            }
         }
     }
 
     private void finishCurrentTransitionToHome() {
-        synchronized (mRecentsAnimationWrapper) {
-            mRecentsAnimationWrapper.finish(true /* toHome */,
-                    () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+        } else {
+            synchronized (mRecentsAnimationWrapper) {
+                mRecentsAnimationWrapper.finish(true /* toHome */,
+                        () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+            }
         }
         mTouchInteractionLog.finishRecentsAnimation(true);
     }
@@ -999,7 +1126,8 @@
         long duration = FeatureFlags.QUICK_SWITCH.get()
                 ? QUICK_SWITCH_FROM_APP_START_DURATION
                 : QUICK_SCRUB_FROM_APP_START_DURATION;
-        animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToHome */);
+        animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToHome */,
+                false /* goingToNewTask */, 1f);
     }
 
     private void onQuickScrubStartUi() {
@@ -1012,7 +1140,6 @@
             mLauncherTransitionController.getAnimationPlayer().end();
             mLauncherTransitionController = null;
         }
-        mLayoutListener.finish();
 
         mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false,
                 mTouchInteractionLog);
@@ -1025,6 +1152,7 @@
         if (mQuickScrubBlocked) {
             return;
         }
+        mLayoutListener.finish();
         mQuickScrubController.onFinishedTransitionToQuickScrub();
 
         mRecentsView.animateUpRunningTaskIconScale();
@@ -1149,13 +1277,15 @@
         mLongSwipeController = mActivityControlHelper.getLongSwipeController(
                 mActivity, mRunningTaskId);
         onLongSwipeDisplacementUpdated();
-        setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
+        }
     }
 
-    private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) {
+    private void onLongSwipeGestureFinishUi(float velocity, boolean isFling, float velocityX) {
         if (!mUiLongSwipeMode || mLongSwipeController == null) {
             mUiLongSwipeMode = false;
-            handleNormalGestureEnd(velocity, isFling);
+            handleNormalGestureEnd(velocity, isFling, velocityX);
             return;
         }
         mUiLongSwipeMode = false;
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
index 431517a..ea73e2c 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -73,6 +73,9 @@
     // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
     // app window coordinates.
     private final RectF mSourceWindowClipInsets = new RectF();
+    // The insets to be used for clipping the app window. For live tile, we don't transform the clip
+    // relative to the target rect.
+    private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();
 
     // The bounds of launcher (not including insets) in device coordinates
     public final Rect mHomeStackBounds = new Rect();
@@ -144,6 +147,7 @@
                 Math.max(scaledTargetRect.top, 0),
                 Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
                 Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
+        mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
         mSourceRect.set(scaledTargetRect);
     }
 
@@ -152,26 +156,32 @@
     }
 
     public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
-        RectF currentRect;
-        mTmpRectF.set(mTargetRect);
-        Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale);
-        float offsetYProgress = mOffsetYInterpolator.getInterpolation(params.progress);
-        float progress = mInterpolator.getInterpolation(params.progress);
-        currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
+        if (params.currentRect == null) {
+            RectF currentRect;
+            mTmpRectF.set(mTargetRect);
+            Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale * params.offsetScale);
+            float offsetYProgress = mOffsetYInterpolator.getInterpolation(params.progress);
+            float progress = mInterpolator.getInterpolation(params.progress);
+            currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
+            currentRect.offset(params.offsetX, 0);
 
-        synchronized (mTargetOffset) {
-            // Stay lined up with the center of the target, since it moves for quick scrub.
-            currentRect.offset(mTargetOffset.x * mOffsetScale * progress,
-                    mTargetOffset.y  * offsetYProgress);
+            synchronized (mTargetOffset) {
+                // Stay lined up with the center of the target, since it moves for quick scrub.
+                currentRect.offset(mTargetOffset.x * mOffsetScale * progress,
+                        mTargetOffset.y  * offsetYProgress);
+            }
+
+            final RectF sourceWindowClipInsets = params.forLiveTile
+                    ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
+            mClipRectF.left = sourceWindowClipInsets.left * progress;
+            mClipRectF.top = sourceWindowClipInsets.top * progress;
+            mClipRectF.right =
+                    mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
+            mClipRectF.bottom =
+                    mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
+            params.setCurrentRectAndTargetAlpha(currentRect, 1);
         }
 
-        mClipRectF.left = mSourceWindowClipInsets.left * progress;
-        mClipRectF.top = mSourceWindowClipInsets.top * progress;
-        mClipRectF.right =
-                mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress);
-        mClipRectF.bottom =
-                mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress);
-
         SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length];
         for (int i = 0; i < targetSet.unfilteredApps.length; i++) {
             RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i];
@@ -180,23 +190,17 @@
             float alpha = 1f;
             int layer;
             float cornerRadius = 0f;
-            float scale = currentRect.width() / crop.width();
+            float scale = params.currentRect.width() / crop.width();
             if (app.mode == targetSet.targetMode) {
                 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
-                    mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
+                    mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
                     mTmpMatrix.postTranslate(app.position.x, app.position.y);
                     mClipRectF.roundOut(crop);
-                    cornerRadius = Utilities.mapRange(progress, mWindowCornerRadius,
+                    cornerRadius = Utilities.mapRange(params.progress, mWindowCornerRadius,
                             mTaskCornerRadius);
                     mCurrentCornerRadius = cornerRadius;
                 }
-
-                if (app.isNotInRecents
-                        || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
-                    alpha = 1 - progress;
-                }
-
-                alpha = mTaskAlphaCallback.apply(app, alpha);
+                alpha = mTaskAlphaCallback.apply(app, params.targetAlpha);
                 layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
             } else {
                 crop = null;
@@ -209,7 +213,7 @@
                     cornerRadius / scale);
         }
         applySurfaceParams(params.syncTransactionApplier, surfaceParams);
-        return currentRect;
+        return params.currentRect;
     }
 
     public RectF getCurrentRectWithInsets() {
@@ -353,14 +357,48 @@
 
     public static class TransformParams {
         float progress;
+        float offsetX;
+        float offsetScale;
+        @Nullable RectF currentRect;
+        float targetAlpha;
+        boolean forLiveTile;
+
         SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
 
         public TransformParams() {
             progress = 0;
+            offsetX = 0;
+            offsetScale = 1;
+            currentRect = null;
+            targetAlpha = 0;
+            forLiveTile = false;
         }
 
         public TransformParams setProgress(float progress) {
             this.progress = progress;
+            this.currentRect = null;
+            return this;
+        }
+
+        public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
+            this.currentRect = currentRect;
+            this.targetAlpha = targetAlpha;
+            this.progress = 1;
+            return this;
+        }
+
+        public TransformParams setOffsetX(float offsetX) {
+            this.offsetX = offsetX;
+            return this;
+        }
+
+        public TransformParams setOffsetScale(float offsetScale) {
+            this.offsetScale = offsetScale;
+            return this;
+        }
+
+        public TransformParams setForLiveTile(boolean forLiveTile) {
+            this.forLiveTile = forLiveTile;
             return this;
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java b/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java
index 6ee305f..10283bf 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java
@@ -53,6 +53,7 @@
     private final RecentsView mParent;
     private final View mIconView;
     private final int[] mIconPos;
+    private final TaskView mTaskView;
 
     private final TaskThumbnailView mThumbnailView;
 
@@ -65,6 +66,7 @@
 
     public TaskViewDrawable(TaskView tv, RecentsView parent) {
         mParent = parent;
+        mTaskView = tv;
         mIconView = tv.getIconView();
         mIconPos = new int[2];
         mIconScale = mIconView.getScaleX();
@@ -139,4 +141,8 @@
     public int getOpacity() {
         return PixelFormat.TRANSLUCENT;
     }
+
+    public TaskView getTaskView() {
+        return mTaskView;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
index 8ec5361..e684889 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.views;
 
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 
 import android.graphics.Canvas;
@@ -39,23 +40,41 @@
 public class LauncherLayoutListener extends AbstractFloatingView
         implements Insettable, LayoutListener {
 
+    public static LauncherLayoutListener resetAndGet(Launcher launcher) {
+        LauncherRecentsView lrv = launcher.getOverviewPanel();
+        LauncherLayoutListener listener = lrv.mLauncherLayoutListener;
+        if (listener.isOpen()) {
+            listener.close(false);
+        }
+        listener.setHandler(null);
+        return listener;
+    }
+
     private final Launcher mLauncher;
     private final Paint mPaint = new Paint();
     private WindowTransformSwipeHandler mHandler;
     private RectF mCurrentRect;
     private float mCornerRadius;
 
-    public LauncherLayoutListener(Launcher launcher) {
+    private boolean mWillNotDraw;
+
+    /**
+     * package private
+     */
+    LauncherLayoutListener(Launcher launcher) {
         super(launcher, null);
         mLauncher = launcher;
         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
         setLayoutParams(new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+        mWillNotDraw = willNotDraw();
+        super.setWillNotDraw(false);
     }
 
     @Override
     public void update(boolean shouldFinish, boolean isLongSwipe, RectF currentRect,
-            float cornerRadius) {
-        if (shouldFinish) {
+                  float cornerRadius) {
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get() && shouldFinish) {
             finish();
             return;
         }
@@ -68,6 +87,12 @@
     }
 
     @Override
+    public void setWillNotDraw(boolean willNotDraw) {
+        // Prevent super call as that causes additional relayout.
+        mWillNotDraw = willNotDraw;
+    }
+
+    @Override
     public void setHandler(WindowTransformSwipeHandler handler) {
         mHandler = handler;
     }
@@ -124,6 +149,8 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
+        if (!mWillNotDraw) {
+            canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 697bb4f..7389d65 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -15,10 +15,12 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_QUICKSTEP_PREVIEW;
 import static com.android.launcher3.LauncherAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
@@ -32,6 +34,8 @@
 import android.view.View;
 import android.view.ViewDebug;
 
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
@@ -40,6 +44,7 @@
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
 import com.android.quickstep.util.LayoutUtils;
 
 /**
@@ -65,6 +70,9 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private float mTranslationYFactor;
 
+    private final TransformParams mTransformParams = new TransformParams();
+    final LauncherLayoutListener mLauncherLayoutListener;
+
     public LauncherRecentsView(Context context) {
         this(context, null);
     }
@@ -76,11 +84,17 @@
     public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         setContentAlpha(0);
+        mLauncherLayoutListener = new LauncherLayoutListener(BaseActivity.fromContext(context));
     }
 
     @Override
     protected void startHome() {
-        mActivity.getStateManager().goToState(NORMAL);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            takeScreenshotAndFinishRecentsAnimation(true,
+                    () -> mActivity.getStateManager().goToState(NORMAL));
+        } else {
+            mActivity.getStateManager().goToState(NORMAL);
+        }
     }
 
     @Override
@@ -92,6 +106,9 @@
     public void setTranslationYFactor(float translationFactor) {
         mTranslationYFactor = translationFactor;
         setTranslationY(computeTranslationYForFactor(mTranslationYFactor));
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            redrawLiveTile(false);
+        }
     }
 
     public float computeTranslationYForFactor(float translationYFactor) {
@@ -168,4 +185,45 @@
     public boolean shouldUseMultiWindowTaskSizeStrategy() {
         return mActivity.isInMultiWindowModeCompat();
     }
+
+    @Override
+    public void scrollTo(int x, int y) {
+        super.scrollTo(x, y);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
+            redrawLiveTile(true);
+        }
+    }
+
+    @Override
+    public void redrawLiveTile(boolean mightNeedToRefill) {
+        AbstractFloatingView layoutListener = AbstractFloatingView.getTopOpenViewWithType(
+                mActivity, TYPE_QUICKSTEP_PREVIEW);
+        if (layoutListener != null && layoutListener.isOpen()) {
+            return;
+        }
+        if (mRecentsAnimationWrapper == null || mClipAnimationHelper == null) {
+            return;
+        }
+        TaskView taskView = getRunningTaskView();
+        if (taskView != null) {
+            taskView.getThumbnail().getGlobalVisibleRect(mTempRect);
+            int offsetX = (int) (mTaskWidth * taskView.getScaleX() * getScaleX()
+                    - mTempRect.width());
+            int offsetY = (int) (mTaskHeight * taskView.getScaleY() * getScaleY()
+                    - mTempRect.height());
+            if (((mCurrentPage != 0) || mightNeedToRefill) && offsetX > 0) {
+                mTempRect.right += offsetX;
+            }
+            if (mightNeedToRefill && offsetY > 0) {
+                mTempRect.top -= offsetY;
+            }
+            mTempRectF.set(mTempRect);
+            mTransformParams.setCurrentRectAndTargetAlpha(mTempRectF, taskView.getAlpha())
+                    .setSyncTransactionApplier(mSyncTransactionApplier);
+            if (mRecentsAnimationWrapper.targetSet != null) {
+                mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
+                        mTransformParams);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 34b5748..4cfd8cb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -17,14 +17,17 @@
 package com.android.quickstep.views;
 
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.uioverrides.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.quickstep.util.ClipAnimationHelper.TransformParams;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
-
+import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.LayoutTransition;
@@ -40,6 +43,7 @@
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
@@ -60,11 +64,13 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ListView;
+
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -77,6 +83,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.OverviewCallbacks;
 import com.android.quickstep.QuickScrubController;
+import com.android.quickstep.RecentsAnimationWrapper;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
@@ -87,7 +94,10 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.WindowCallbacksCompat;
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
@@ -97,7 +107,8 @@
  */
 @TargetApi(Build.VERSION_CODES.P)
 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
-        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback {
+        TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+        InvariantDeviceProfile.OnIDPChangeListener {
 
     private static final String TAG = RecentsView.class.getSimpleName();
 
@@ -114,7 +125,14 @@
                 }
             };
 
-    private final Rect mTempRect = new Rect();
+    protected RecentsAnimationWrapper mRecentsAnimationWrapper;
+    protected ClipAnimationHelper mClipAnimationHelper;
+    protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier;
+    protected int mTaskWidth;
+    protected int mTaskHeight;
+    protected boolean mEnableDrawingLiveTile = false;
+    protected final Rect mTempRect = new Rect();
+    protected final RectF mTempRectF = new RectF();
 
     private static final int DISMISS_TASK_DURATION = 300;
     private static final int ADDITION_TASK_DURATION = 200;
@@ -136,6 +154,8 @@
     // Keeps track of the previously known visible tasks for purposes of loading/unloading task data
     private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray();
 
+    private final InvariantDeviceProfile mIdp;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -278,6 +298,8 @@
         mActivity = (T) BaseActivity.fromContext(context);
         mQuickScrubController = new QuickScrubController(mActivity, this);
         mModel = RecentsModel.INSTANCE.get(context);
+        mIdp = InvariantDeviceProfile.INSTANCE.get(context);
+
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
         mClearAllButton.setOnClickListener(this::dismissAllTasks);
@@ -320,12 +342,23 @@
     }
 
     @Override
+    public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
+        if ((changeFlags & CHANGE_FLAG_ICON_PARAMS) == 0) {
+            return;
+        }
+        mModel.getIconCache().clear();
+        reset();
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         updateTaskStackListenerState();
         mModel.getThumbnailCache().getHighResLoadingState().addCallback(this);
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
+        mSyncTransactionApplier = new SyncRtSurfaceTransactionApplierCompat(this);
+        mIdp.addOnChangeListener(this);
     }
 
     @Override
@@ -335,6 +368,8 @@
         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+        mSyncTransactionApplier = null;
+        mIdp.removeOnChangeListener(this);
     }
 
     @Override
@@ -421,7 +456,8 @@
                         final boolean clearAllButtonDeadZoneConsumed =
                                 mClearAllButton.getAlpha() == 1
                                         && mClearAllButtonDeadZoneRect.contains(x, y);
-                        if (!clearAllButtonDeadZoneConsumed
+                        final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+                        if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar
                                 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) {
                             mTouchDownToStartHome = true;
                         }
@@ -548,6 +584,8 @@
         mInsets.set(insets);
         DeviceProfile dp = mActivity.getDeviceProfile();
         getTaskSize(dp, mTempRect);
+        mTaskWidth = mTempRect.width();
+        mTaskHeight = mTempRect.height();
 
         // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub.
         mTempRect.top -= mTaskTopMargin;
@@ -681,11 +719,15 @@
     protected abstract void startHome();
 
     public void reset() {
+        setRunningTaskViewShowScreenshot(false);
         mRunningTaskId = -1;
         mRunningTaskTileHidden = false;
         mIgnoreResetTaskId = -1;
         mTaskListChangeId = -1;
 
+        mRecentsAnimationWrapper = null;
+        mClipAnimationHelper = null;
+
         unloadVisibleTaskData();
         setCurrentPage(0);
 
@@ -732,6 +774,11 @@
         return getTaskView(mRunningTaskId);
     }
 
+    public int getRunningTaskIndex() {
+        TaskView tv = getRunningTaskView();
+        return tv == null ? -1 : indexOfChild(tv);
+    }
+
     /**
      * Hides the tile associated with {@link #mRunningTaskId}
      */
@@ -752,17 +799,27 @@
 
         setRunningTaskIconScaledDown(false);
         setRunningTaskHidden(false);
+        setRunningTaskViewShowScreenshot(true);
         mRunningTaskId = runningTaskId;
+        setRunningTaskViewShowScreenshot(false);
         setRunningTaskIconScaledDown(runningTaskIconScaledDown);
         setRunningTaskHidden(runningTaskTileHidden);
 
-        TaskView tv = getRunningTaskView();
-        setCurrentPage(tv == null ? 0 : indexOfChild(tv));
+        setCurrentPage(getRunningTaskIndex());
 
         // Load the tasks (if the loading is already
         mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
     }
 
+    private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            TaskView runningTaskView = getRunningTaskView();
+            if (runningTaskView != null) {
+                runningTaskView.setShowScreenshot(showScreenshot);
+            }
+        }
+    }
+
     public void showNextTask() {
         TaskView runningTaskView = getRunningTaskView();
         if (runningTaskView == null) {
@@ -970,26 +1027,40 @@
         }
 
         mPendingAnimation = pendingAnimation;
-        mPendingAnimation.addEndListener((onEndListener) -> {
-           if (onEndListener.isSuccess) {
-               if (shouldRemoveTask) {
-                   removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
-               }
-               int pageToSnapTo = mCurrentPage;
-               if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount() - 1)) {
-                   pageToSnapTo -= 1;
-               }
-               removeView(taskView);
+        mPendingAnimation.addEndListener(new Consumer<PendingAnimation.OnEndListener>() {
+            @Override
+            public void accept(PendingAnimation.OnEndListener onEndListener) {
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
+                        taskView.isRunningTask() && onEndListener.isSuccess) {
+                    finishRecentsAnimation(true /* toHome */, () -> onEnd(onEndListener));
+                } else {
+                    onEnd(onEndListener);
+                }
+            }
 
-               if (getTaskViewCount() == 0) {
-                   removeView(mClearAllButton);
-                   startHome();
-               } else {
-                   snapToPageImmediately(pageToSnapTo);
-               }
-           }
-           resetTaskVisuals();
-           mPendingAnimation = null;
+            private void onEnd(PendingAnimation.OnEndListener onEndListener) {
+                if (onEndListener.isSuccess) {
+                    if (shouldRemoveTask) {
+                        removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
+                    }
+
+                    int pageToSnapTo = mCurrentPage;
+                    if (draggedIndex < pageToSnapTo ||
+                            pageToSnapTo == (getTaskViewCount() - 1)) {
+                        pageToSnapTo -= 1;
+                    }
+                    removeView(taskView);
+
+                    if (getTaskViewCount() == 0) {
+                        removeView(mClearAllButton);
+                        startHome();
+                    } else {
+                        snapToPageImmediately(pageToSnapTo);
+                    }
+                }
+                resetTaskVisuals();
+                mPendingAnimation = null;
+            }
         });
         return pendingAnimation;
     }
@@ -1337,20 +1408,38 @@
         ObjectAnimator drawableAnim =
                 ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0);
         drawableAnim.setInterpolator(LINEAR);
-        drawableAnim.addUpdateListener((animator) -> {
-            // Once we pass a certain threshold, update the sysui flags to match the target tasks'
-            // flags
-            mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW,
-                    animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD
-                            ? targetSysUiFlags
-                            : 0);
+        drawableAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            TransformParams mParams = new TransformParams();
 
-            // Passing the threshold from taskview to fullscreen app will vibrate
-            final boolean passed = animator.getAnimatedFraction() >= MIN_PROGRESS_FOR_OVERVIEW;
-            if (passed != passedOverviewThreshold[0]) {
-                passedOverviewThreshold[0] = passed;
-                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+            @Override
+            public void onAnimationUpdate(ValueAnimator animator) {
+                // Once we pass a certain threshold, update the sysui flags to match the target
+                // tasks' flags
+                mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW,
+                        animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD
+                                ? targetSysUiFlags
+                                : 0);
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                    if (mRecentsAnimationWrapper.targetSet != null
+                            && drawable.getTaskView().isRunningTask()) {
+                        mParams.setProgress(1 - animator.getAnimatedFraction())
+                                .setSyncTransactionApplier(mSyncTransactionApplier)
+                                .setForLiveTile(true);
+                        drawable.getClipAnimationHelper().applyTransform(
+                                mRecentsAnimationWrapper.targetSet, mParams);
+                    } else {
+                        redrawLiveTile(true);
+                    }
+                }
+
+                // Passing the threshold from taskview to fullscreen app will vibrate
+                final boolean passed = animator.getAnimatedFraction() >=
+                        SUCCESS_TRANSITION_PROGRESS;
+                if (passed != passedOverviewThreshold[0]) {
+                    passedOverviewThreshold[0] = passed;
+                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+                            HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                }
             }
         });
 
@@ -1449,4 +1538,74 @@
     protected boolean isPageOrderFlipped() {
         return true;
     }
+
+    public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) {
+        mEnableDrawingLiveTile = enableDrawingLiveTile;
+    }
+
+    public void redrawLiveTile(boolean mightNeedToRefill) { }
+
+    public void setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper) {
+        mRecentsAnimationWrapper = recentsAnimationWrapper;
+    }
+
+    public void setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper) {
+        mClipAnimationHelper = clipAnimationHelper;
+    }
+
+    public void finishRecentsAnimation(boolean toHome, Runnable onFinishComplete) {
+        if (mRecentsAnimationWrapper == null) {
+            if (onFinishComplete != null) {
+                onFinishComplete.run();
+            }
+            return;
+        }
+
+        mRecentsAnimationWrapper.finish(toHome, onFinishComplete);
+    }
+
+    public void takeScreenshotAndFinishRecentsAnimation(boolean toHome, Runnable onFinishComplete) {
+        if (mRecentsAnimationWrapper == null || getRunningTaskView() == null) {
+            if (onFinishComplete != null) {
+                onFinishComplete.run();
+            }
+            return;
+        }
+
+        RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController();
+        if (controller != null) {
+            // Update the screenshot of the task
+            ThumbnailData taskSnapshot = controller.screenshotTask(mRunningTaskId);
+            TaskView taskView = updateThumbnail(mRunningTaskId, taskSnapshot);
+            if (taskView != null) {
+                taskView.setShowScreenshot(true);
+                // Defer finishing the animation until the next launcher frame with the
+                // new thumbnail
+                new WindowCallbacksCompat(taskView) {
+
+                    // The number of frames to defer until we actually finish the animation
+                    private int mDeferFrameCount = 2;
+
+                    @Override
+                    public void onPostDraw(Canvas canvas) {
+                        if (mDeferFrameCount > 0) {
+                            mDeferFrameCount--;
+                            // Workaround, detach and reattach to invalidate the root node for
+                            // another draw
+                            detach();
+                            attach();
+                            taskView.invalidate();
+                            return;
+                        }
+
+                        detach();
+                        mRecentsAnimationWrapper.finish(toHome, () -> {
+                            onFinishComplete.run();
+                            mRunningTaskId = -1;
+                        });
+                    }
+                }.attach();
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 667165b..bea646a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.views;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -206,7 +207,13 @@
                 R.layout.task_view_menu_option, this, false);
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
-        menuOptionView.setOnClickListener(onClickListener);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            menuOptionView.setOnClickListener(
+                    view -> mTaskView.getRecentsView().takeScreenshotAndFinishRecentsAnimation(true,
+                            () -> onClickListener.onClick(view)));
+        } else {
+            menuOptionView.setOnClickListener(onClickListener);
+        }
         mOptionLayout.addView(menuOptionView);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index c2403a3..e5bed54 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.views;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.content.Context;
@@ -29,6 +30,8 @@
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.Shader;
 import android.util.AttributeSet;
@@ -76,6 +79,8 @@
     private final boolean mIsDarkTextTheme;
     private final Paint mPaint = new Paint();
     private final Paint mBackgroundPaint = new Paint();
+    private final Paint mClearPaint = new Paint();
+    private final Paint mDimmingPaintAfterClearing = new Paint();
 
     private final Matrix mMatrix = new Matrix();
 
@@ -105,6 +110,8 @@
         mOverlay = TaskOverlayFactory.get(context).createOverlay(this);
         mPaint.setFilterBitmap(true);
         mBackgroundPaint.setColor(Color.WHITE);
+        mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+        mDimmingPaintAfterClearing.setColor(Color.BLACK);
         mActivity = BaseActivity.fromContext(context);
         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
     }
@@ -213,6 +220,15 @@
 
     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
             float cornerRadius) {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
+                canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
+                canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
+                        mDimmingPaintAfterClearing);
+                return;
+            }
+        }
+
         // Draw the background in all cases, except when the thumbnail data is opaque
         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
                 || mThumbnailData == null;
@@ -233,8 +249,13 @@
         }
     }
 
+    protected TaskView getTaskView() {
+        return (TaskView) getParent();
+    }
+
     private void updateThumbnailPaintFilter() {
         int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
+        mDimmingPaintAfterClearing.setAlpha(255 - mul);
         if (mBitmapShader != null) {
             ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
             mPaint.setColorFilter(filter);
@@ -242,6 +263,7 @@
         } else {
             mPaint.setColorFilter(null);
             mPaint.setColor(Color.argb(255, mul, mul, mul));
+            mBackgroundPaint.setColorFilter(null);
         }
         invalidate();
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index bb6f514..456a022 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.BaseActivity.fromContext;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -46,9 +47,11 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.PendingAnimation;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
@@ -88,6 +91,7 @@
 
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
+    private static final long TASK_LAUNCH_ANIM_DURATION = 200;
 
     public static final Property<TaskView, Float> ZOOM_SCALE =
             new FloatProperty<TaskView>("zoomScale") {
@@ -119,7 +123,7 @@
             new FloatProperty<TaskView>("focusTransition") {
                 @Override
                 public void setValue(TaskView taskView, float v) {
-                    taskView.setIconAndDimTransitionProgress(v);
+                    taskView.setIconAndDimTransitionProgress(v, false /* invert */);
                 }
 
                 @Override
@@ -158,6 +162,8 @@
     private Animator mIconAndDimAnimator;
     private float mFocusTransitionProgress = 1;
 
+    private boolean mShowScreenshot;
+
     // The current background requests to load the task thumbnail and icon
     private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
     private TaskIconCache.IconLoadRequest mIconLoadRequest;
@@ -176,7 +182,15 @@
             if (getTask() == null) {
                 return;
             }
-            launchTask(true /* animate */);
+            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                if (isRunningTask()) {
+                    createLaunchAnimationForRunningTask().start();
+                } else {
+                    launchTask(true /* animate */);
+                }
+            } else {
+                launchTask(true /* animate */);
+            }
 
             fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss(
                     Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
@@ -219,6 +233,19 @@
         return mSnapshotView.getTaskOverlay();
     }
 
+    public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
+        final PendingAnimation pendingAnimation =
+                getRecentsView().createTaskLauncherAnimation(this, TASK_LAUNCH_ANIM_DURATION);
+        pendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN);
+        AnimatorPlaybackController currentAnimation = AnimatorPlaybackController
+                .wrap(pendingAnimation.anim, TASK_LAUNCH_ANIM_DURATION, null);
+        currentAnimation.setEndAction(() -> {
+            pendingAnimation.finish(true, Touch.SWIPE);
+            launchTask(false);
+        });
+        return currentAnimation;
+    }
+
     public void launchTask(boolean animate) {
         launchTask(animate, (result) -> {
             if (!result) {
@@ -229,6 +256,21 @@
 
     public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
             Handler resultCallbackHandler) {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (isRunningTask()) {
+                getRecentsView().finishRecentsAnimation(false,
+                        () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
+            } else {
+                getRecentsView().takeScreenshotAndFinishRecentsAnimation(true,
+                        () -> launchTaskInternal(animate, resultCallback, resultCallbackHandler));
+            }
+        } else {
+            launchTaskInternal(animate, resultCallback, resultCallbackHandler);
+        }
+    }
+
+    private void launchTaskInternal(boolean animate, Consumer<Boolean> resultCallback,
+            Handler resultCallbackHandler) {
         if (mTask != null) {
             final ActivityOptions opts;
             if (animate) {
@@ -316,11 +358,17 @@
         }
     }
 
-    private void setIconAndDimTransitionProgress(float progress) {
+    private void setIconAndDimTransitionProgress(float progress, boolean invert) {
+        if (invert) {
+            progress = 1 - progress;
+        }
         mFocusTransitionProgress = progress;
         mSnapshotView.setDimAlphaMultipler(progress);
-        float scale = FAST_OUT_SLOW_IN.getInterpolation(Utilities.boundToRange(
-                progress * DIM_ANIM_DURATION / SCALE_ICON_DURATION, 0, 1));
+        float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
+        float lowerClamp = invert ? 1f - iconScalePercentage : 0;
+        float upperClamp = invert ? 1 : iconScalePercentage;
+        float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp)
+                .getInterpolation(progress);
         mIconView.setScaleX(scale);
         mIconView.setScaleY(scale);
     }
@@ -341,10 +389,14 @@
     }
 
     protected void setIconScaleAndDim(float iconScale) {
+        setIconScaleAndDim(iconScale, false);
+    }
+
+    private void setIconScaleAndDim(float iconScale, boolean invert) {
         if (mIconAndDimAnimator != null) {
             mIconAndDimAnimator.cancel();
         }
-        setIconAndDimTransitionProgress(iconScale);
+        setIconAndDimTransitionProgress(iconScale, invert);
     }
 
     public void resetVisualProperties() {
@@ -501,7 +553,7 @@
         return super.performAccessibilityAction(action, arguments);
     }
 
-    private RecentsView getRecentsView() {
+    public RecentsView getRecentsView() {
         return (RecentsView) getParent();
     }
 
@@ -524,7 +576,8 @@
         }
         mFullscreenProgress = progress;
         boolean isFullscreen = mFullscreenProgress > 0;
-        mIconView.setVisibility(isFullscreen ? INVISIBLE : VISIBLE);
+        setIconScaleAndDim(progress, true /* invert */);
+        mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
         setClipChildren(!isFullscreen);
         setClipToPadding(!isFullscreen);
         getThumbnail().invalidate();
@@ -533,4 +586,19 @@
     public float getFullscreenProgress() {
         return mFullscreenProgress;
     }
+
+    public boolean isRunningTask() {
+        return this == getRecentsView().getRunningTaskView();
+    }
+
+    public void setShowScreenshot(boolean showScreenshot) {
+        mShowScreenshot = showScreenshot;
+    }
+
+    public boolean showScreenshot() {
+        if (!isRunningTask()) {
+            return true;
+        }
+        return mShowScreenshot;
+    }
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 5c842a5..3c0ef79 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -30,12 +30,16 @@
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
+import android.util.SparseArray;
+import android.util.TypedValue;
 import android.util.Xml;
 import android.view.Display;
 import android.view.WindowManager;
 
 import com.android.launcher3.util.ConfigMonitor;
+import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.Themes;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -44,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 public class InvariantDeviceProfile {
@@ -91,6 +96,8 @@
     public int fillResIconDpi;
     public float iconTextSize;
 
+    private SparseArray<TypedValue> mExtraAttrs;
+
     /**
      * Number of icons inside the hotseat area.
      */
@@ -122,6 +129,7 @@
         numHotseatIcons = p.numHotseatIcons;
         defaultLayoutId = p.defaultLayoutId;
         demoModeLayoutId = p.demoModeLayoutId;
+        mExtraAttrs = p.mExtraAttrs;
     }
 
     @TargetApi(23)
@@ -171,6 +179,8 @@
         demoModeLayoutId = closestProfile.demoModeLayoutId;
         numFolderRows = closestProfile.numFolderRows;
         numFolderColumns = closestProfile.numFolderColumns;
+        mExtraAttrs = closestProfile.extraAttrs;
+
         if (!closestProfile.name.equals(gridName)) {
             Utilities.getPrefs(context).edit()
                     .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
@@ -210,10 +220,19 @@
         }
     }
 
+    @Nullable
+    public TypedValue getAttrValue(int attr) {
+        return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
+    }
+
     public void addOnChangeListener(OnIDPChangeListener listener) {
         mChangeListeners.add(listener);
     }
 
+    public void removeOnChangeListener(OnIDPChangeListener listener) {
+        mChangeListeners.remove(listener);
+    }
+
     private void killProcess(Context context) {
         Log.e("ConfigMonitor", "restarting launcher");
         android.os.Process.killProcess(android.os.Process.myPid());
@@ -436,6 +455,8 @@
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
+        private final SparseArray<TypedValue> extraAttrs;
+
         GridOption(Context context, AttributeSet attrs) {
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.GridDisplayOption);
@@ -454,6 +475,9 @@
             numFolderColumns = a.getInt(
                     R.styleable.GridDisplayOption_numFolderColumns, numColumns);
             a.recycle();
+
+            extraAttrs = Themes.createValueMap(context, attrs,
+                    IntArray.wrap(R.styleable.GridDisplayOption));
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index ffbf34c..962c25b 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,7 +1,6 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
@@ -15,9 +14,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
 import android.util.Property;
-import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.DeviceProfile;
@@ -29,10 +26,15 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.SpringObjectAnimator;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.anim.SpringObjectAnimator.SpringProperty;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+
 /**
  * Handles AllApps view transition.
  * 1) Slides all apps view using direct manipulation
@@ -59,6 +61,53 @@
         }
     };
 
+    public static final FloatPropertyCompat<AllAppsTransitionController> ALL_APPS_PROGRESS_SPRING
+            = new FloatPropertyCompat<AllAppsTransitionController>("allAppsProgressSpring") {
+        @Override
+        public float getValue(AllAppsTransitionController controller) {
+            return controller.mProgress;
+        }
+
+        @Override
+        public void setValue(AllAppsTransitionController controller, float progress) {
+            controller.setProgress(progress);
+        }
+    };
+
+    /**
+     * Property that either sets the progress directly or animates the progress via a spring.
+     */
+    public static class AllAppsSpringProperty extends
+            SpringProperty<AllAppsTransitionController, Float> {
+
+        SpringAnimation mSpring;
+        boolean useSpring = false;
+
+        public AllAppsSpringProperty(SpringAnimation spring) {
+            super(Float.class, "allAppsSpringProperty");
+            mSpring = spring;
+        }
+
+        @Override
+        public Float get(AllAppsTransitionController controller) {
+            return controller.getProgress();
+        }
+
+        @Override
+        public void set(AllAppsTransitionController controller, Float progress) {
+            if (useSpring) {
+                mSpring.animateToFinalPosition(progress);
+            } else {
+                controller.setProgress(progress);
+            }
+        }
+
+        @Override
+        public void switchToSpring() {
+            useSpring = true;
+        }
+    }
+
     private AllAppsContainerView mAppsView;
     private ScrimView mScrimView;
 
@@ -174,8 +223,8 @@
         Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW
                 ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
                 : FAST_OUT_SLOW_IN;
-        ObjectAnimator anim =
-                ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, mProgress, targetProgress);
+        Animator anim = new SpringObjectAnimator(this, 1f / mShiftRange, mProgress,
+                targetProgress);
         anim.setDuration(config.duration);
         anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
         anim.addListener(getProgressAnimatorListener());
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 819c843..62f59e4 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.anim;
 
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
@@ -23,10 +24,16 @@
 import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
 
 /**
  * Helper class to control the playback of an {@link AnimatorSet}, with custom interpolators
@@ -37,6 +44,9 @@
  */
 public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
+    private static final String TAG = "AnimatorPlaybackCtrler";
+    private static boolean DEBUG = false;
+
     public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
         return wrap(anim, duration, null);
     }
@@ -60,6 +70,7 @@
     private final long mDuration;
 
     protected final AnimatorSet mAnim;
+    private Set<SpringAnimation> mSprings;
 
     protected float mCurrentFraction;
     private Runnable mEndAction;
@@ -67,6 +78,9 @@
     protected boolean mTargetCancelled = false;
     protected Runnable mOnCancelRunnable;
 
+    private OnAnimationEndDispatcher mEndListener;
+    private DynamicAnimation.OnAnimationEndListener mSpringEndListener;
+
     protected AnimatorPlaybackController(AnimatorSet anim, long duration,
             Runnable onCancelRunnable) {
         mAnim = anim;
@@ -75,7 +89,8 @@
 
         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
         mAnimationPlayer.setInterpolator(LINEAR);
-        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
+        mEndListener = new OnAnimationEndDispatcher();
+        mAnimationPlayer.addListener(mEndListener);
         mAnimationPlayer.addUpdateListener(this);
 
         mAnim.addListener(new AnimatorListenerAdapter() {
@@ -99,6 +114,15 @@
                 mTargetCancelled = false;
             }
         });
+
+        mSprings = new HashSet<>();
+        mSpringEndListener = (animation, canceled, value, velocity1) -> {
+            if (canceled) {
+                mEndListener.onAnimationCancel(mAnimationPlayer);
+            } else {
+                mEndListener.onAnimationEnd(mAnimationPlayer);
+            }
+        };
     }
 
     public AnimatorSet getTarget() {
@@ -180,6 +204,29 @@
         }
     }
 
+    /**
+     * Starts playback and sets the spring.
+     */
+    public void dispatchOnStartWithVelocity(float end, float velocity) {
+        if (!QUICKSTEP_SPRINGS.get()) {
+            dispatchOnStart();
+            return;
+        }
+
+        if (DEBUG) Log.d(TAG, "dispatchOnStartWithVelocity#end=" + end + ", velocity=" + velocity);
+
+        for (Animator a : mAnim.getChildAnimations()) {
+            if (a instanceof SpringObjectAnimator) {
+                if (DEBUG) Log.d(TAG, "Found springAnimator=" + a);
+                SpringObjectAnimator springAnimator = (SpringObjectAnimator) a;
+                mSprings.add(springAnimator.getSpring());
+                springAnimator.startSpring(end, velocity, mSpringEndListener);
+            }
+        }
+
+        dispatchOnStart();
+    }
+
     public void dispatchOnStart() {
         dispatchOnStartRecursively(mAnim);
     }
@@ -282,6 +329,18 @@
         }
     }
 
+    private boolean isAnySpringRunning() {
+        for (SpringAnimation spring : mSprings) {
+            if (spring.isRunning()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Only dispatches the on end actions once the animator and all springs have completed running.
+     */
     private class OnAnimationEndDispatcher extends AnimationSuccessListener {
 
         @Override
@@ -291,9 +350,12 @@
 
         @Override
         public void onAnimationSuccess(Animator animator) {
-            dispatchOnEndRecursively(mAnim);
-            if (mEndAction != null) {
-                mEndAction.run();
+            // We wait for the spring (if any) to finish running before completing the end callback.
+            if (mSprings.isEmpty() || !isAnySpringRunning()) {
+                dispatchOnEndRecursively(mAnim);
+                if (mEndAction != null) {
+                    mEndAction.run();
+                }
             }
         }
 
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
new file mode 100644
index 0000000..b2b931d
--- /dev/null
+++ b/src/com/android/launcher3/anim/SpringObjectAnimator.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2019 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.launcher3.anim;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.util.Log;
+import android.util.Property;
+
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.allapps.AllAppsTransitionController.AllAppsSpringProperty;
+
+import java.util.ArrayList;
+
+import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+/**
+ * This animator allows for an object's property to be be controlled by an {@link ObjectAnimator} or
+ * a {@link SpringAnimation}. It extends ValueAnimator so it can be used in an AnimatorSet.
+ */
+public class SpringObjectAnimator extends ValueAnimator {
+
+    private static final String TAG = "SpringObjectAnimator";
+    private static boolean DEBUG = false;
+
+    private AllAppsTransitionController mObject;
+    private ObjectAnimator mObjectAnimator;
+    private float[] mValues;
+
+    private SpringAnimation mSpring;
+    private AllAppsSpringProperty mProperty;
+
+    private ArrayList<AnimatorListener> mListeners;
+    private boolean mSpringEnded = false;
+    private boolean mAnimatorEnded = false;
+    private boolean mEnded = false;
+
+    private static final float SPRING_DAMPING_RATIO = 0.9f;
+    private static final float SPRING_STIFFNESS = 600f;
+
+    public SpringObjectAnimator(AllAppsTransitionController object, float minimumVisibleChange,
+            float... values) {
+        mObject = object;
+        mSpring = new SpringAnimation(object, AllAppsTransitionController.ALL_APPS_PROGRESS_SPRING);
+        mSpring.setMinimumVisibleChange(minimumVisibleChange);
+        mSpring.setSpring(new SpringForce(0)
+                .setDampingRatio(SPRING_DAMPING_RATIO)
+                .setStiffness(SPRING_STIFFNESS));
+        mSpring.setStartVelocity(0.01f);
+        mProperty = new AllAppsSpringProperty(mSpring);
+        mObjectAnimator = ObjectAnimator.ofFloat(object, mProperty, values);
+        mValues = values;
+        mListeners = new ArrayList<>();
+        setFloatValues(values);
+
+        mObjectAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mAnimatorEnded = false;
+                mEnded = false;
+                for (AnimatorListener l : mListeners) {
+                    l.onAnimationStart(animation);
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimatorEnded = true;
+                tryEnding();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                for (AnimatorListener l : mListeners) {
+                    l.onAnimationCancel(animation);
+                }
+                mSpring.animateToFinalPosition(mObject.getProgress());
+            }
+        });
+
+        mSpring.addUpdateListener((animation, value, velocity) -> mSpringEnded = false);
+        mSpring.addEndListener((animation, canceled, value, velocity) -> {
+            mSpringEnded = true;
+            tryEnding();
+        });
+    }
+
+    private void tryEnding() {
+        if (DEBUG) {
+            Log.d(TAG, "tryEnding#mAnimatorEnded=" + mAnimatorEnded + ", mSpringEnded="
+                    + mSpringEnded + ", mEnded=" + mEnded);
+        }
+
+        if (mAnimatorEnded && mSpringEnded && !mEnded) {
+            for (AnimatorListener l : mListeners) {
+                l.onAnimationEnd(this);
+            }
+            mEnded = true;
+        }
+    }
+
+    public SpringAnimation getSpring() {
+        return mSpring;
+    }
+
+    /**
+     * Initializes and sets up the spring to take over controlling the object.
+     */
+    void startSpring(float end, float velocity, OnAnimationEndListener endListener) {
+        // Cancel the spring so we can set new start velocity and final position. We need to remove
+        // the listener since the spring is not actually ending.
+        mSpring.removeEndListener(endListener);
+        mSpring.cancel();
+        mSpring.addEndListener(endListener);
+
+        mProperty.switchToSpring();
+
+        mSpring.setStartVelocity(velocity);
+        mSpring.animateToFinalPosition(end == 0 ? mValues[0] : mValues[1]);
+    }
+
+    @Override
+    public void addListener(AnimatorListener listener) {
+        mListeners.add(listener);
+    }
+
+    @Override
+    public ArrayList<AnimatorListener> getListeners() {
+        return mListeners;
+    }
+
+    @Override
+    public void removeAllListeners() {
+        mListeners.clear();
+    }
+
+    @Override
+    public void removeListener(AnimatorListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    public void addPauseListener(AnimatorPauseListener listener) {
+        mObjectAnimator.addPauseListener(listener);
+    }
+
+    @Override
+    public void cancel() {
+        mSpring.animateToFinalPosition(mObject.getProgress());
+        mObjectAnimator.cancel();
+    }
+
+    @Override
+    public void end() {
+        mObjectAnimator.end();
+    }
+
+    @Override
+    public long getDuration() {
+        return mObjectAnimator.getDuration();
+    }
+
+    @Override
+    public TimeInterpolator getInterpolator() {
+        return mObjectAnimator.getInterpolator();
+    }
+
+    @Override
+    public long getStartDelay() {
+        return mObjectAnimator.getStartDelay();
+    }
+
+    @Override
+    public long getTotalDuration() {
+        return mObjectAnimator.getTotalDuration();
+    }
+
+    @Override
+    public boolean isPaused() {
+        return mObjectAnimator.isPaused();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return mObjectAnimator.isRunning();
+    }
+
+    @Override
+    public boolean isStarted() {
+        return mObjectAnimator.isStarted();
+    }
+
+    @Override
+    public void pause() {
+        mObjectAnimator.pause();
+    }
+
+    @Override
+    public void removePauseListener(AnimatorPauseListener listener) {
+        mObjectAnimator.removePauseListener(listener);
+    }
+
+    @Override
+    public void resume() {
+        mObjectAnimator.resume();
+    }
+
+    @Override
+    public ValueAnimator setDuration(long duration) {
+        return mObjectAnimator.setDuration(duration);
+    }
+
+    @Override
+    public void setInterpolator(TimeInterpolator value) {
+        mObjectAnimator.setInterpolator(value);
+    }
+
+    @Override
+    public void setStartDelay(long startDelay) {
+        mObjectAnimator.setStartDelay(startDelay);
+    }
+
+    @Override
+    public void setTarget(Object target) {
+        mObjectAnimator.setTarget(target);
+    }
+
+    @Override
+    public void start() {
+        mObjectAnimator.start();
+    }
+
+    @Override
+    public void setCurrentFraction(float fraction) {
+        mObjectAnimator.setCurrentFraction(fraction);
+    }
+
+    @Override
+    public void setCurrentPlayTime(long playTime) {
+        mObjectAnimator.setCurrentPlayTime(playTime);
+    }
+
+    public static abstract class SpringProperty<T, V> extends Property<T, V> {
+
+        public SpringProperty(Class<V> type, String name) {
+            super(type, name);
+        }
+
+        abstract public void switchToSpring();
+    }
+
+}
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 03fdc64..1b1b152 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -98,6 +98,12 @@
     public static final TogglableFlag ENABLE_TASK_STABILIZER = new TogglableFlag(
             "ENABLE_TASK_STABILIZER", false, "Stable task list across fast task switches");
 
+    public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
+            false, "Enable springs for quickstep animations");
+
+    public static final TogglableFlag ENABLE_QUICKSTEP_LIVE_TILE = new TogglableFlag(
+            "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
+
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
         if (Utilities.IS_DEBUG_DEVICE) {
diff --git a/src/com/android/launcher3/folder/FolderShape.java b/src/com/android/launcher3/folder/FolderShape.java
index f7cdb77..4b06dda 100644
--- a/src/com/android/launcher3/folder/FolderShape.java
+++ b/src/com/android/launcher3/folder/FolderShape.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.Themes;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -387,6 +388,8 @@
 
             final int depth = parser.getDepth();
             int[] radiusAttr = new int[] {R.attr.folderIconRadius};
+            IntArray keysToIgnore = new IntArray(0);
+
             while (((type = parser.next()) != XmlPullParser.END_TAG ||
                     parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
 
@@ -396,7 +399,7 @@
                     FolderShape shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
                     a.recycle();
 
-                    shape.mAttrs = Themes.createValueMap(context, attrs);
+                    shape.mAttrs = Themes.createValueMap(context, attrs, keysToIgnore);
                     result.add(shape);
                 }
             }
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
index 66f9dbf..6fac31e2 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
@@ -16,16 +16,9 @@
 
 package com.android.launcher3.graphics;
 
-import static android.content.Intent.ACTION_SCREEN_OFF;
-import static android.content.Intent.ACTION_USER_PRESENT;
-
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.ObjectAnimator;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -96,20 +89,6 @@
                 }
             };
 
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            if (ACTION_SCREEN_OFF.equals(action)) {
-                mAnimateScrimOnNextDraw = true;
-            } else if (ACTION_USER_PRESENT.equals(action)) {
-                // ACTION_USER_PRESENT is sent after onStart/onResume. This covers the case where
-                // the user unlocked and the Launcher is not in the foreground.
-                mAnimateScrimOnNextDraw = false;
-            }
-        }
-    };
-
     private static final int DARK_SCRIM_COLOR = 0x55000000;
     private static final int MAX_HOTSEAT_SCRIM_ALPHA = 100;
     private static final int ALPHA_MASK_HEIGHT_DP = 500;
@@ -225,20 +204,11 @@
     public void onViewAttachedToWindow(View view) {
         mWallpaperColorInfo.addOnChangeListener(this);
         onExtractedColorsChanged(mWallpaperColorInfo);
-
-        if (mTopScrim != null) {
-            IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
-            filter.addAction(ACTION_USER_PRESENT); // When the device wakes up + keyguard is gone
-            mRoot.getContext().registerReceiver(mReceiver, filter);
-        }
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
         mWallpaperColorInfo.removeOnChangeListener(this);
-        if (mTopScrim != null) {
-            mRoot.getContext().unregisterReceiver(mReceiver);
-        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index 9c4a4ea..65103f6 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -29,6 +29,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.UiThreadHelper;
 
 /**
  * Utility class to manage launcher rotation
@@ -154,7 +155,7 @@
         }
         if (activityFlags != mLastActivityFlags) {
             mLastActivityFlags = activityFlags;
-            mActivity.setRequestedOrientation(activityFlags);
+            UiThreadHelper.setOrientationAsync(mActivity, activityFlags);
         }
     }
 
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index a7bd243..bb14328 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -45,6 +46,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -428,8 +430,8 @@
         maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f);
         updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
                 targetState, velocity, fling);
-        mCurrentAnimation.dispatchOnStart();
-        if (fling && targetState == LauncherState.ALL_APPS) {
+        mCurrentAnimation.dispatchOnStartWithVelocity(endProgress, velocity);
+        if (fling && targetState == LauncherState.ALL_APPS && !QUICKSTEP_SPRINGS.get()) {
             mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
         }
         anim.start();
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index e3ab1a5..675e2f4 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -112,16 +112,19 @@
      * Creates a map for attribute-name to value for all the values in {@param attrs} which can be
      * held in memory for later use.
      */
-    public static SparseArray<TypedValue> createValueMap(Context context, AttributeSet attrSet) {
+    public static SparseArray<TypedValue> createValueMap(Context context, AttributeSet attrSet,
+            IntArray keysToIgnore) {
         int count = attrSet.getAttributeCount();
-        int[] attrNames = new int[count];
+        IntArray attrNameArray = new IntArray(count);
         for (int i = 0; i < count; i++) {
-            attrNames[i] = attrSet.getAttributeNameResource(i);
+            attrNameArray.add(attrSet.getAttributeNameResource(i));
         }
+        attrNameArray.removeAllValues(keysToIgnore);
 
-        SparseArray<TypedValue> result = new SparseArray<>(count);
+        int[] attrNames = attrNameArray.toArray();
+        SparseArray<TypedValue> result = new SparseArray<>(attrNames.length);
         TypedArray ta = context.obtainStyledAttributes(attrSet, attrNames);
-        for (int i = 0; i < count; i++) {
+        for (int i = 0; i < attrNames.length; i++) {
             TypedValue tv = new TypedValue();
             ta.getValue(i, tv);
             result.put(attrNames[i], tv);
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index 27140a1..cc442f9 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.util;
 
+import android.app.Activity;
 import android.content.Context;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -33,6 +34,8 @@
     private static Handler sHandler;
 
     private static final int MSG_HIDE_KEYBOARD = 1;
+    private static final int MSG_SET_ORIENTATION = 2;
+    private static final int MSG_RUN_COMMAND = 3;
 
     public static Looper getBackgroundLooper() {
         if (sHandlerThread == null) {
@@ -55,6 +58,15 @@
         Message.obtain(getHandler(context), MSG_HIDE_KEYBOARD, token).sendToTarget();
     }
 
+    public static void setOrientationAsync(Activity activity, int orientation) {
+        Message.obtain(getHandler(activity), MSG_SET_ORIENTATION, orientation, 0, activity)
+                .sendToTarget();
+    }
+
+    public static void runAsyncCommand(Context context, AsyncCommand command, int arg1, int arg2) {
+        Message.obtain(getHandler(context), MSG_RUN_COMMAND, arg1, arg2, command).sendToTarget();
+    }
+
     private static class UiCallbacks implements Handler.Callback {
 
         private final InputMethodManager mIMM;
@@ -69,8 +81,19 @@
                 case MSG_HIDE_KEYBOARD:
                     mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0);
                     return true;
+                case MSG_SET_ORIENTATION:
+                    ((Activity) message.obj).setRequestedOrientation(message.arg1);
+                    return true;
+                case MSG_RUN_COMMAND:
+                    ((AsyncCommand) message.obj).execute(message.arg1, message.arg2);
+                    return true;
             }
             return false;
         }
     }
+
+    public interface AsyncCommand {
+
+        void execute(int arg1, int arg2);
+    }
 }