Merge "Move overview component logic from OverviewCommandHelper" into ub-launcher3-master
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1a485ed..9500a2f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -61,6 +61,9 @@
                 <category android:name="android.intent.category.MONKEY"/>
                 <category android:name="android.intent.category.LAUNCHER_APP" />
             </intent-filter>
+            <meta-data
+                android:name="com.android.launcher3.grid.control"
+                android:value="${packageName}.grid.control" />
         </activity>
 
     </application>
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
index 455c58c..5df8043 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
@@ -122,7 +122,11 @@
         }
 
         public Bitmap createPill(int width, int height) {
-            radius = height / 2f;
+            return createPill(width, height, height / 2f);
+        }
+
+        public Bitmap createPill(int width, int height, float r) {
+            radius = r;
 
             int centerX = Math.round(width / 2f + shadowBlur);
             int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index 8af310c..ab97344 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index e0c4e4b..8d62ab8 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -20,6 +20,8 @@
     <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <dimen name="task_corner_radius">8dp</dimen>
+    <!-- For screens without rounded corners -->
+    <dimen name="task_corner_radius_small">2dp</dimen>
     <dimen name="recents_page_spacing">10dp</dimen>
     <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
     <dimen name="quickscrub_adjacent_visible_width">20dp</dimen>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 7c47956..0c741a1 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -62,8 +62,4 @@
     <!-- Annotation shown on an app card in Recents, telling that the app has a usage limit set by
     the user, and a given time is left for it today [CHAR LIMIT=20] -->
     <string name="time_left_for_app"><xliff:g id="time" example="7 minutes">%1$s</xliff:g> left today</string>
-
-    <!-- Annotation shown on an app card in Recents, telling that the app is in a group that has a
-    usage limit set by the user, and a given time is left for the group today [CHAR LIMIT=20] -->
-    <string name="time_left_for_group"><xliff:g id="time" example="1 hour">%1$s</xliff:g> left for group</string>
 </resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 73a7c08..07a5b72 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -615,9 +615,12 @@
                 // Animate window corner radius from 100% to windowCornerRadius.
                 float windowCornerRadius = RecentsModel.INSTANCE.get(mLauncher)
                         .getWindowCornerRadius();
-                float circleRadius = iconWidth / 2f;
-                float windowRadius = Utilities.mapRange(easePercent, circleRadius,
-                        windowCornerRadius);
+                float windowRadius = 0;
+                if (RecentsModel.INSTANCE.get(mLauncher).supportsRoundedCornersOnWindows()) {
+                    float circleRadius = iconWidth / 2f;
+                    windowRadius = Utilities.mapRange(easePercent, circleRadius,
+                            windowCornerRadius);
+                }
 
                 // Animate the window crop so that it starts off as a square, and then reveals
                 // horizontally.
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index ff9d601..d295ac5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -21,7 +21,10 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.allapps.DiscoveryBounce.BOUNCE_MAX_COUNT;
+import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_COUNT;
 import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
+import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_COUNT;
 import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
 
 import android.animation.AnimatorSet;
@@ -136,7 +139,8 @@
                     LauncherState prevState = launcher.getStateManager().getLastState();
 
                     if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
-                            && finalState == ALL_APPS && prevState == NORMAL))) {
+                            && finalState == ALL_APPS && prevState == NORMAL) || BOUNCE_MAX_COUNT <=
+                            launcher.getSharedPrefs().getInt(HOME_BOUNCE_COUNT, 0))) {
                         launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
                         launcher.getStateManager().removeStateListener(this);
                     }
@@ -159,7 +163,8 @@
                 public void onStateTransitionComplete(LauncherState finalState) {
                     LauncherState prevState = launcher.getStateManager().getLastState();
 
-                    if (finalState == ALL_APPS && prevState == OVERVIEW) {
+                    if ((finalState == ALL_APPS && prevState == OVERVIEW) || BOUNCE_MAX_COUNT <=
+                            launcher.getSharedPrefs().getInt(SHELF_BOUNCE_COUNT, 0)) {
                         launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
                         launcher.getStateManager().removeStateListener(this);
                     }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 442b106..e61c00a 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -66,6 +66,7 @@
     private final TaskThumbnailCache mThumbnailCache;
 
     private float mWindowCornerRadius = -1;
+    private Boolean mSupportsRoundedCornersOnWindows;
 
     private RecentsModel(Context context) {
         mContext = context;
@@ -199,6 +200,26 @@
         return mWindowCornerRadius;
     }
 
+    public boolean supportsRoundedCornersOnWindows() {
+        if (mSupportsRoundedCornersOnWindows == null) {
+            if (mSystemUiProxy != null) {
+                try {
+                    mSupportsRoundedCornersOnWindows =
+                            mSystemUiProxy.supportsRoundedCornersOnWindows();
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Connection to ISystemUIProxy was lost, ignoring window corner "
+                            + "radius");
+                    return false;
+                }
+            } else {
+                Log.w(TAG, "ISystemUIProxy is null, ignoring window corner radius");
+                return false;
+            }
+        }
+
+        return mSupportsRoundedCornersOnWindows;
+    }
+
     public void onTrimMemory(int level) {
         if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
             mThumbnailCache.getHighResLoadingState().setVisible(false);
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
index 84033cb..31de683 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -91,6 +91,8 @@
     private final float mWindowCornerRadius;
     // Corner radius of windows when they're in overview mode.
     private final float mTaskCornerRadius;
+    // If windows can have real time rounded corners.
+    private final boolean mSupportsRoundedCornersOnWindows;
 
     // Corner radius currently applied to transformed window.
     private float mCurrentCornerRadius;
@@ -107,8 +109,12 @@
             (t, a1) -> a1;
 
     public ClipAnimationHelper(Context context) {
-        mTaskCornerRadius = context.getResources().getDimension(R.dimen.task_corner_radius);
-        mWindowCornerRadius  = RecentsModel.INSTANCE.get(context).getWindowCornerRadius();
+        mWindowCornerRadius = RecentsModel.INSTANCE.get(context).getWindowCornerRadius();
+        mSupportsRoundedCornersOnWindows = RecentsModel.INSTANCE.get(context)
+                .supportsRoundedCornersOnWindows();
+        int taskCornerRadiusRes = mSupportsRoundedCornersOnWindows ?
+                R.dimen.task_corner_radius : R.dimen.task_corner_radius_small;
+        mTaskCornerRadius = context.getResources().getDimension(taskCornerRadiusRes);
     }
 
     private void updateSourceStack(RemoteAnimationTargetCompat target) {
@@ -197,9 +203,10 @@
                     mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
                     mTmpMatrix.postTranslate(app.position.x, app.position.y);
                     mClipRectF.roundOut(crop);
-                    cornerRadius = Utilities.mapRange(params.progress, mWindowCornerRadius,
-                            mTaskCornerRadius);
-                    mCurrentCornerRadius = cornerRadius;
+                    if (mSupportsRoundedCornersOnWindows) {
+                        cornerRadius = Utilities.mapRange(params.progress, mWindowCornerRadius,
+                                mTaskCornerRadius);
+                    }
                 }
                 alpha = mTaskAlphaCallback.apply(app, params.targetAlpha);
             } else if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index aafd725..9ad750b 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -77,7 +77,6 @@
         Utilities.THREAD_POOL_EXECUTOR.execute(() -> {
             long appUsageLimitTimeMs = -1;
             long appRemainingTimeMs = -1;
-            boolean isGroupLimit = true;
 
             try {
                 final Method getAppUsageLimit = LauncherApps.class.getMethod(
@@ -95,8 +94,6 @@
                             invoke(usageLimit);
                     appRemainingTimeMs = (long) appUsageLimitClass.getMethod("getUsageRemaining").
                             invoke(usageLimit);
-                    isGroupLimit = (boolean) appUsageLimitClass.getMethod("isGroupLimit").
-                            invoke(usageLimit);
                 }
             } catch (Exception e) {
                 // Do nothing
@@ -104,14 +101,13 @@
 
             final long appUsageLimitTimeMsFinal = appUsageLimitTimeMs;
             final long appRemainingTimeMsFinal = appRemainingTimeMs;
-            final boolean isGroupLimitFinal = isGroupLimit;
 
             post(() -> {
                 if (appUsageLimitTimeMsFinal < 0) {
                     setVisibility(GONE);
                 } else {
                     setVisibility(VISIBLE);
-                    mText.setText(getText(appRemainingTimeMsFinal, isGroupLimitFinal));
+                    mText.setText(getText(appRemainingTimeMsFinal));
                     mImage.setImageResource(appRemainingTimeMsFinal > 0 ?
                             R.drawable.hourglass_top : R.drawable.hourglass_bottom);
                 }
@@ -119,9 +115,7 @@
                 callback.call(
                         appUsageLimitTimeMsFinal >= 0 && appRemainingTimeMsFinal <= 0 ? 0 : 1,
                         getContentDescriptionForTask(
-                                task, appUsageLimitTimeMsFinal,
-                                appRemainingTimeMsFinal,
-                                isGroupLimitFinal));
+                                task, appUsageLimitTimeMsFinal, appRemainingTimeMsFinal));
             });
         });
     }
@@ -185,12 +179,12 @@
                 duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
     }
 
-    private String getText(long remainingTime, boolean isGroupLimit) {
+    private String getText(long remainingTime) {
         final Resources resources = getResources();
         return (remainingTime <= 0) ?
                 resources.getString(R.string.app_in_grayscale) :
                 resources.getString(
-                        isGroupLimit ? R.string.time_left_for_group : R.string.time_left_for_app,
+                        R.string.time_left_for_app,
                         getShorterReadableDuration(Duration.ofMillis(remainingTime)));
     }
 
@@ -214,12 +208,12 @@
     }
 
     private String getContentDescriptionForTask(
-            Task task, long appUsageLimitTimeMs, long appRemainingTimeMs, boolean isGroupLimit) {
+            Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
         return appUsageLimitTimeMs >= 0 ?
                 getResources().getString(
                         R.string.task_contents_description_with_remaining_time,
                         task.titleDescription,
-                        getText(appRemainingTimeMs, isGroupLimit)) :
+                        getText(appRemainingTimeMs)) :
                 task.titleDescription;
     }
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index dafd5bb..6582df2 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -36,6 +36,7 @@
 import android.view.Display;
 import android.view.WindowManager;
 
+import com.android.launcher3.folder.FolderShape;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -285,6 +286,10 @@
                 !iconShapePath.equals(oldProfile.iconShapePath)) {
             changeFlags |= CHANGE_FLAG_ICON_PARAMS;
         }
+        if (!iconShapePath.equals(oldProfile.iconShapePath)) {
+            FolderShape.init(context);
+        }
+
         apply(context, changeFlags);
     }
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 9470635..a6b3a19 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
 
 import android.animation.LayoutTransition;
@@ -25,6 +26,7 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.TypedArray;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.provider.Settings;
@@ -121,6 +123,8 @@
 
     protected boolean mIsPageInTransition = false;
 
+    protected float mSpringOverScrollX;
+
     protected boolean mWasInOverscroll = false;
 
     protected int mUnboundedScrollX;
@@ -349,6 +353,11 @@
 
         boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0);
         boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX);
+
+        if (!isXBeforeFirstPage && !isXAfterLastPage) {
+            mSpringOverScrollX = 0;
+        }
+
         if (isXBeforeFirstPage) {
             super.scrollTo(mIsRtl ? mMaxScrollX : 0, y);
             if (mAllowOverScroll) {
@@ -988,12 +997,35 @@
         }
     }
 
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mScroller.isSpringing() && mSpringOverScrollX != 0) {
+            int saveCount = canvas.save();
+
+            canvas.translate(-mSpringOverScrollX, 0);
+            super.dispatchDraw(canvas);
+
+            canvas.restoreToCount(saveCount);
+        } else {
+            super.dispatchDraw(canvas);
+        }
+    }
+
     protected void dampedOverScroll(int amount) {
-        if (amount == 0) return;
+        mSpringOverScrollX = amount;
+        if (amount == 0) {
+            return;
+        }
 
         int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth());
+        mSpringOverScrollX = overScrollAmount;
+        if (mScroller.isSpringing()) {
+            invalidate();
+            return;
+        }
+
         if (amount < 0) {
-            super.scrollTo(overScrollAmount, getScrollY());
+            super.scrollTo(amount, getScrollY());
         } else {
             super.scrollTo(mMaxScrollX + overScrollAmount, getScrollY());
         }
@@ -1001,6 +1033,12 @@
     }
 
     protected void overScroll(int amount) {
+        mSpringOverScrollX = amount;
+        if (mScroller.isSpringing()) {
+            invalidate();
+            return;
+        }
+
         if (amount == 0) return;
 
         if (mFreeScroll && !mScroller.isFinished()) {
@@ -1372,7 +1410,12 @@
         // interpolator at zero, ie. 5. We use 4 to make it a little slower.
         duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
 
-        return snapToPage(whichPage, delta, duration);
+        if (QUICKSTEP_SPRINGS.get()) {
+            return snapToPage(whichPage, delta, duration, false, null,
+                    velocity * Math.signum(newX - getUnboundedScrollX()), true);
+        } else {
+            return snapToPage(whichPage, delta, duration);
+        }
     }
 
     public boolean snapToPage(int whichPage) {
@@ -1397,15 +1440,15 @@
 
         int newX = getScrollForPage(whichPage);
         final int delta = newX - getUnboundedScrollX();
-        return snapToPage(whichPage, delta, duration, immediate, interpolator);
+        return snapToPage(whichPage, delta, duration, immediate, interpolator, 0, false);
     }
 
     protected boolean snapToPage(int whichPage, int delta, int duration) {
-        return snapToPage(whichPage, delta, duration, false, null);
+        return snapToPage(whichPage, delta, duration, false, null, 0, false);
     }
 
     protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate,
-            TimeInterpolator interpolator) {
+            TimeInterpolator interpolator, float velocity, boolean spring) {
         if (mFirstLayout) {
             setCurrentPage(whichPage);
             return false;
@@ -1441,7 +1484,11 @@
             mScroller.setInterpolator(mDefaultInterpolator);
         }
 
-        mScroller.startScroll(getUnboundedScrollX(), delta, duration);
+        if (spring && QUICKSTEP_SPRINGS.get()) {
+            mScroller.startScrollSpring(getUnboundedScrollX(), delta, duration, velocity);
+        } else {
+            mScroller.startScroll(getUnboundedScrollX(), delta, duration);
+        }
 
         updatePageIndicator();
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index eb26961..3438a26 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -996,7 +996,7 @@
 
     @Override
     protected void overScroll(int amount) {
-        boolean shouldScrollOverlay = mLauncherOverlay != null &&
+        boolean shouldScrollOverlay = mLauncherOverlay != null && !mScroller.isSpringing() &&
                 ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl));
 
         boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlayScroll != 0 &&
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index 76b2565..7467119 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -25,6 +25,7 @@
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
 import android.app.ActivityManager;
+import android.content.SharedPreferences;
 import android.os.Handler;
 import android.view.MotionEvent;
 
@@ -43,6 +44,10 @@
 
     public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
     public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
+    public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
+    public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
+
+    public static final int BOUNCE_MAX_COUNT = 3;
 
     private final Launcher mLauncher;
     private final Animator mDiscoBounceAnimation;
@@ -137,6 +142,7 @@
             new Handler().postDelayed(() -> showForHomeIfNeeded(launcher, false), DELAY_MS);
             return;
         }
+        incrementHomeBounceCount(launcher);
 
         new DiscoveryBounce(launcher, 0).show(HOTSEAT);
     }
@@ -165,6 +171,7 @@
             // TODO: Move these checks to the top and call this method after invalidate handler.
             return;
         }
+        incrementShelfBounceCount(launcher);
 
         new DiscoveryBounce(launcher, (1 - OVERVIEW.getVerticalProgress(launcher)))
                 .show(PREDICTION);
@@ -197,4 +204,22 @@
                 PersonalWorkSlidingTabStrip.KEY_SHOWED_PEEK_WORK_TAB, false)
                 && UserManagerCompat.getInstance(launcher).hasWorkProfile();
     }
+
+    private static void incrementShelfBounceCount(Launcher launcher) {
+        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
+        int count = sharedPrefs.getInt(SHELF_BOUNCE_COUNT, 0);
+        if (count > BOUNCE_MAX_COUNT) {
+            return;
+        }
+        sharedPrefs.edit().putInt(SHELF_BOUNCE_COUNT, count + 1).apply();
+    }
+
+    private static void incrementHomeBounceCount(Launcher launcher) {
+        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
+        int count = sharedPrefs.getInt(HOME_BOUNCE_COUNT, 0);
+        if (count > BOUNCE_MAX_COUNT) {
+            return;
+        }
+        sharedPrefs.edit().putInt(HOME_BOUNCE_COUNT, count + 1).apply();
+    }
 }
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index fa93081..da99142 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -95,9 +95,8 @@
     public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
             "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
 
-    public static final ToggleableGlobalSettingsFlag ENABLE_TASK_STABILIZER
-            = new ToggleableGlobalSettingsFlag("ENABLE_TASK_STABILIZER", false,
-            "Stable task list across fast task switches");
+    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");
diff --git a/src/com/android/launcher3/folder/FolderShape.java b/src/com/android/launcher3/folder/FolderShape.java
index 4b06dda..61db6ff 100644
--- a/src/com/android/launcher3/folder/FolderShape.java
+++ b/src/com/android/launcher3/folder/FolderShape.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.folder;
 
-import static com.android.launcher3.Workspace.MAP_NO_RECURSE;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.FloatArrayEvaluator;
@@ -43,9 +41,6 @@
 import android.util.Xml;
 import android.view.ViewOutlineProvider;
 
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
@@ -358,7 +353,7 @@
         if (!Utilities.ATLEAST_OREO) {
             return;
         }
-        new MainThreadExecutor().execute(() -> pickShapeInBackground(context));
+        pickBestShape(context);
     }
 
     private static FolderShape getShapeDefinition(String type, float radius) {
@@ -410,7 +405,7 @@
     }
 
     @TargetApi(Build.VERSION_CODES.O)
-    protected static void pickShapeInBackground(Context context) {
+    protected static void pickBestShape(Context context) {
         // Pick any large size
         int size = 200;
 
@@ -447,25 +442,7 @@
         }
 
         if (closestShape != null) {
-            FolderShape shape = closestShape;
-            new MainThreadExecutor().execute(() -> updateFolderShape(shape));
-        }
-    }
-
-    private static void updateFolderShape(FolderShape shape) {
-        sInstance = shape;
-        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-        if (app == null) {
-            return;
-        }
-        Launcher launcher = (Launcher) app.getModel().getCallback();
-        if (launcher != null) {
-            launcher.getWorkspace().mapOverItems(MAP_NO_RECURSE, (i, v) -> {
-                if (v instanceof FolderIcon) {
-                    v.invalidate();
-                }
-                return false;
-            });
+            sInstance = closestShape;
         }
     }
 }
diff --git a/src/com/android/launcher3/util/OverScroller.java b/src/com/android/launcher3/util/OverScroller.java
index d697ece..fc8a138 100644
--- a/src/com/android/launcher3/util/OverScroller.java
+++ b/src/com/android/launcher3/util/OverScroller.java
@@ -26,6 +26,11 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
 /**
  * Based on {@link android.widget.OverScroller} supporting only 1-d scrolling and with more
  * customization options.
@@ -196,6 +201,9 @@
 
         switch (mMode) {
             case SCROLL_MODE:
+                if (isSpringing()) {
+                    return true;
+                }
                 long time = AnimationUtils.currentAnimationTimeMillis();
                 // Any scroller can be used for time, since they were started
                 // together in scroll mode. We use X here.
@@ -254,6 +262,22 @@
     }
 
     /**
+     * Start scrolling using a spring by providing a starting point and the distance to travel.
+     *
+     * @param start Starting scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param delta Distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param duration Duration of the scroll in milliseconds.
+     * @param velocity The starting velocity for the spring in px per ms.
+     */
+    public void startScrollSpring(int start, int delta, int duration, float velocity) {
+        mMode = SCROLL_MODE;
+        mScroller.mState = mScroller.SPRING;
+        mScroller.startScroll(start, delta, duration, velocity);
+    }
+
+    /**
      * Call this when you want to 'spring back' into a valid coordinate range.
      *
      * @param start Starting X coordinate
@@ -354,6 +378,10 @@
         return (int) (time - mScroller.mStartTime);
     }
 
+    public boolean isSpringing() {
+        return mScroller.mState == SplineOverScroller.SPRING && !isFinished();
+    }
+
     static class SplineOverScroller {
         // Initial position
         private int mStart;
@@ -397,6 +425,8 @@
         // Current state of the animation.
         private int mState = SPLINE;
 
+        private SpringAnimation mSpring;
+
         // Constant gravity value, used in the deceleration phase.
         private static final float GRAVITY = 2000.0f;
 
@@ -417,6 +447,20 @@
         private static final int SPLINE = 0;
         private static final int CUBIC = 1;
         private static final int BALLISTIC = 2;
+        private static final int SPRING = 3;
+
+        private static final FloatPropertyCompat<SplineOverScroller> SPRING_PROPERTY =
+                new FloatPropertyCompat<SplineOverScroller>("splineOverScrollerSpring") {
+                    @Override
+                    public float getValue(SplineOverScroller scroller) {
+                        return scroller.mCurrentPosition;
+                    }
+
+                    @Override
+                    public void setValue(SplineOverScroller scroller, float value) {
+                        scroller.mCurrentPosition = (int) value;
+                    }
+                };
 
         static {
             float x_min = 0.0f;
@@ -465,6 +509,9 @@
         }
 
         void updateScroll(float q) {
+            if (mState == SPRING) {
+                return;
+            }
             mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
         }
 
@@ -495,6 +542,10 @@
         }
 
         void startScroll(int start, int distance, int duration) {
+            startScroll(start, distance, duration, 0);
+        }
+
+        void startScroll(int start, int distance, int duration, float velocity) {
             mFinished = false;
 
             mCurrentPosition = mStart = start;
@@ -503,12 +554,31 @@
             mStartTime = AnimationUtils.currentAnimationTimeMillis();
             mDuration = duration;
 
+            if (mState == SPRING) {
+                if (mSpring != null) {
+                    mSpring.cancel();
+                }
+                mSpring = new SpringAnimation(this, SPRING_PROPERTY);
+
+                mSpring.setSpring(new SpringForce(mFinal)
+                        .setStiffness(SpringForce.STIFFNESS_LOW)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+                mSpring.setStartVelocity(velocity);
+                mSpring.animateToFinalPosition(mFinal);
+                mSpring.addEndListener((animation, canceled, value, velocity1) -> {
+                    finish();
+                    mState = SPLINE;
+                    mSpring = null;
+                });
+            }
             // Unused
             mDeceleration = 0.0f;
             mVelocity = 0;
         }
 
         void finish() {
+            if (mSpring != null && mSpring.isRunning()) mSpring.cancel();
+
             mCurrentPosition = mFinal;
             // Not reset since WebView relies on this value for fast fling.
             // TODO: restore when WebView uses the fast fling implemented in this class.
@@ -518,6 +588,9 @@
 
         void setFinalPosition(int position) {
             mFinal = position;
+            if (mState == SPRING && mSpring != null) {
+                mSpring.animateToFinalPosition(mFinal);
+            }
             mSplineDistance = mFinal - mStart;
             mFinished = false;
         }
@@ -722,6 +795,10 @@
          * reached.
          */
         boolean update() {
+            if (mState == SPRING) {
+                return mFinished;
+            }
+
             final long time = AnimationUtils.currentAnimationTimeMillis();
             final long currentTime = time - mStartTime;