[automerger skipped] Import translations. DO NOT MERGE am: 275e841c7b -s ours
am skip reason: subject contains skip directive

Change-Id: Ibe51fe1ef511ffcd2bc13503bf396f49da213220
diff --git a/Android.mk b/Android.mk
index 9d113d9..78ea02a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -23,12 +23,7 @@
 LOCAL_USE_AAPT2 := true
 LOCAL_AAPT2_ONLY := true
 LOCAL_MODULE_TAGS := optional
-
-ifneq (,$(wildcard frameworks/base))
-    LOCAL_STATIC_JAVA_LIBRARIES:= PluginCoreLib
-else
-    LOCAL_STATIC_JAVA_LIBRARIES:= libPluginCore
-endif
+LOCAL_STATIC_JAVA_LIBRARIES:= PluginCoreLib
 
 LOCAL_SRC_FILES := \
     $(call all-java-files-under, src_plugins)
@@ -151,11 +146,10 @@
 LOCAL_AAPT2_ONLY := true
 LOCAL_MODULE_TAGS := optional
 
+LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
 ifneq (,$(wildcard frameworks/base))
-  LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
-  LOCAL_STATIC_JAVA_LIBRARIES := libSharedSystemUI libLauncherProtos
   LOCAL_SDK_VERSION := system_current
   LOCAL_MIN_SDK_VERSION := 26
 endif
@@ -224,11 +218,10 @@
 LOCAL_USE_AAPT2 := true
 LOCAL_MODULE_TAGS := optional
 
+LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
 ifneq (,$(wildcard frameworks/base))
-  LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
-  LOCAL_STATIC_JAVA_LIBRARIES := libSharedSystemUI libLauncherProtos
   LOCAL_SDK_VERSION := system_current
   LOCAL_MIN_SDK_VERSION := 26
 endif
@@ -271,11 +264,10 @@
 LOCAL_USE_AAPT2 := true
 LOCAL_MODULE_TAGS := optional
 
+LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
 ifneq (,$(wildcard frameworks/base))
-  LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLib launcherprotosnano
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
-  LOCAL_STATIC_JAVA_LIBRARIES := libSharedSystemUI libLauncherProtos
   LOCAL_SDK_VERSION := system_current
   LOCAL_MIN_SDK_VERSION := 26
 endif
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 8db875b..555cc73 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -169,7 +169,7 @@
         <activity
             android:name="com.android.launcher3.settings.SettingsActivity"
             android:label="@string/settings_button_text"
-            android:theme="@android:style/Theme.DeviceDefault.Settings"
+            android:theme="@style/HomeSettingsTheme"
             android:autoRemoveFromRecents="true">
             <intent-filter>
                 <action android:name="android.intent.action.APPLICATION_PREFERENCES" />
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..f3db20e
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,2 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/go/quickstep/res/values/override.xml b/go/quickstep/res/values/override.xml
index 7636fb3..bb267a3 100644
--- a/go/quickstep/res/values/override.xml
+++ b/go/quickstep/res/values/override.xml
@@ -22,5 +22,7 @@
   <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
 
   <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
+
+  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
 </resources>
 
diff --git a/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 0fa3d86..216972c 100644
--- a/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,8 +15,8 @@
  */
 package com.android.quickstep;
 
-import static com.android.systemui.shared.system.ActivityManagerWrapper
-        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -25,7 +25,6 @@
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
@@ -43,7 +42,6 @@
     private final Context mContext;
     private final ActivityManagerWrapper mAM;
     private final RecentsModel mRecentsModel;
-    private final MainThreadExecutor mMainThreadExecutor;
     private final OverviewComponentObserver mOverviewComponentObserver;
 
     private long mLastToggleTime;
@@ -51,7 +49,6 @@
     public OverviewCommandHelper(Context context, OverviewComponentObserver observer) {
         mContext = context;
         mAM = ActivityManagerWrapper.getInstance();
-        mMainThreadExecutor = new MainThreadExecutor();
         mRecentsModel = RecentsModel.INSTANCE.get(mContext);
         mOverviewComponentObserver = observer;
     }
@@ -63,19 +60,19 @@
         }
 
         mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-        mMainThreadExecutor.execute(new RecentsActivityCommand<>());
+        MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
     }
 
     public void onOverviewShown(boolean triggeredFromAltTab) {
-        mMainThreadExecutor.execute(new ShowRecentsCommand());
+        MAIN_EXECUTOR.execute(new ShowRecentsCommand());
     }
 
     public void onOverviewHidden() {
-        mMainThreadExecutor.execute(new HideRecentsCommand());
+        MAIN_EXECUTOR.execute(new HideRecentsCommand());
     }
 
     public void onTip(int actionType, int viewType) {
-        mMainThreadExecutor.execute(() ->
+        MAIN_EXECUTOR.execute(() ->
                 UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
     }
 
@@ -161,7 +158,7 @@
             // Otherwise, start overview.
             mListener = mHelper.createActivityInitListener(provider::onActivityReady);
             mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
-                    provider, mContext, mMainThreadExecutor.getHandler(),
+                    provider, mContext, MAIN_EXECUTOR.getHandler(),
                     provider.getRecentsLaunchDuration());
         }
 
diff --git a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java b/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 577b175..19dd82f 100644
--- a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -34,8 +34,6 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.UiThreadHelper;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
@@ -139,9 +137,6 @@
         return sConnected;
     }
 
-    public static final LooperExecutor BACKGROUND_EXECUTOR =
-            new LooperExecutor(UiThreadHelper.getBackgroundLooper());
-
     private RecentsModel mRecentsModel;
     private OverviewComponentObserver mOverviewComponentObserver;
     private OverviewCommandHelper mOverviewCommandHelper;
diff --git a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java b/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
new file mode 100644
index 0000000..fb89013
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util;
+
+import com.android.launcher3.Launcher;
+
+/** Empty class, only exists so that l3goWithQuickstepIconRecentsDebug compiles. */
+public class ShelfPeekAnim {
+    public ShelfPeekAnim(Launcher launcher) {
+    }
+
+    public enum ShelfAnimState {
+    }
+
+    public boolean isPeeking() {
+        return false;
+    }
+}
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index b82f362..26c3313 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -16,9 +16,8 @@
 
 package com.android.launcher3.model;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 
 import java.lang.ref.WeakReference;
 
diff --git a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
index ee113df..42b1194 100644
--- a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -27,34 +27,23 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 
-import java.util.Collections;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
  */
 public class DeepShortcutManager {
-    private static DeepShortcutManager sInstance;
-    private static final Object sInstanceLock = new Object();
+
+    private static final DeepShortcutManager sInstance = new DeepShortcutManager();
 
     public static DeepShortcutManager getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                sInstance = new DeepShortcutManager(context.getApplicationContext());
-            }
-            return sInstance;
-        }
+        return sInstance;
     }
 
-    private DeepShortcutManager(Context context) {
-    }
+    private final QueryResult mFailure = new QueryResult();
 
-    public boolean wasLastCallSuccess() {
-        return false;
-    }
-
-    public void onShortcutsChanged(List<ShortcutInfo> shortcuts) {
-    }
+    private DeepShortcutManager() { }
 
     /**
      * Queries for the shortcuts with the package name and provided ids.
@@ -62,18 +51,18 @@
      * This method is intended to get the full details for shortcuts when they are added or updated,
      * because we only get "key" fields in onShortcutsChanged().
      */
-    public List<ShortcutInfo> queryForFullDetails(String packageName,
+    public QueryResult queryForFullDetails(String packageName,
             List<String> shortcutIds, UserHandle user) {
-        return Collections.emptyList();
+        return mFailure;
     }
 
     /**
      * Gets all the manifest and dynamic shortcuts associated with the given package and user,
      * to be displayed in the shortcuts container on long press.
      */
-    public List<ShortcutInfo> queryForShortcutsContainer(ComponentName activity,
+    public QueryResult queryForShortcutsContainer(ComponentName activity,
             UserHandle user) {
-        return Collections.emptyList();
+        return mFailure;
     }
 
     /**
@@ -103,20 +92,28 @@
      *
      * If packageName is null, returns all pinned shortcuts regardless of package.
      */
-    public List<ShortcutInfo> queryForPinnedShortcuts(String packageName, UserHandle user) {
-        return Collections.emptyList();
+    public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
+        return mFailure;
     }
 
-    public List<ShortcutInfo> queryForPinnedShortcuts(String packageName,
+    public QueryResult queryForPinnedShortcuts(String packageName,
             List<String> shortcutIds, UserHandle user) {
-        return Collections.emptyList();
+        return mFailure;
     }
 
-    public List<ShortcutInfo> queryForAllShortcuts(UserHandle user) {
-        return Collections.emptyList();
+    public QueryResult queryForAllShortcuts(UserHandle user) {
+        return mFailure;
     }
 
     public boolean hasHostPermission() {
         return false;
     }
+
+
+    public static class QueryResult extends ArrayList<ShortcutInfo> {
+
+        public boolean wasSuccess() {
+            return true;
+        }
+    }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index fc7d6b3..5c4f37c 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -22,6 +22,7 @@
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
+
 import androidx.annotation.NonNull;
 
 /**
@@ -35,6 +36,8 @@
     static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
     static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
 
+    private static final float ICON_BADGE_SCALE = 0.444f;
+
     private final Rect mOldBounds = new Rect();
     protected final Context mContext;
     private final Canvas mCanvas;
@@ -168,22 +171,19 @@
             mCanvas.setBitmap(null);
         }
 
-        final Bitmap result;
-        if (user != null && !Process.myUserHandle().equals(user)) {
+        if (isInstantApp) {
+            badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
+        }
+        if (user != null) {
             BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
             Drawable badged = mPm.getUserBadgedIcon(drawable, user);
             if (badged instanceof BitmapDrawable) {
-                result = ((BitmapDrawable) badged).getBitmap();
+                bitmap = ((BitmapDrawable) badged).getBitmap();
             } else {
-                result = createIconBitmap(badged, 1f);
+                bitmap = createIconBitmap(badged, 1f);
             }
-        } else if (isInstantApp) {
-            badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
-            result = bitmap;
-        } else {
-            result = bitmap;
         }
-        return BitmapInfo.fromBitmap(result, mDisableColorExtractor ? null : mColorExtractor);
+        return BitmapInfo.fromBitmap(bitmap, mDisableColorExtractor ? null : mColorExtractor);
     }
 
     public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
@@ -254,7 +254,7 @@
      * Adds the {@param badge} on top of {@param target} using the badge dimensions.
      */
     public void badgeWithDrawable(Canvas target, Drawable badge) {
-        int badgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size);
+        int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize);
         badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
                 mIconBitmapSize, mIconBitmapSize);
         badge.draw(target);
@@ -335,6 +335,13 @@
     }
 
     /**
+     * Returns the correct badge size given an icon size
+     */
+    public static int getBadgeSizeForIconSize(int iconSize) {
+        return (int) (ICON_BADGE_SCALE * iconSize);
+    }
+
+    /**
      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
      * This allows the badging to be done based on the action bitmap size rather than
      * the scaled bitmap size.
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 36d1c3e..a886c0a 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -267,9 +267,13 @@
             entry = new CacheEntry();
             cachingLogic.loadIcon(mContext, object, entry);
         }
+        // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
+        // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
+        // an empty entry.
+        if (entry.icon == null) return;
         entry.title = cachingLogic.getLabel(object);
         entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
-        mCache.put(key, entry);
+        if (cachingLogic.addToMemCache()) mCache.put(key, entry);
 
         ContentValues values = newContentValues(entry, entry.title.toString(),
                 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
@@ -308,20 +312,12 @@
             @NonNull ComponentName componentName, @NonNull UserHandle user,
             @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
             boolean usePackageIcon, boolean useLowResIcon) {
-        return cacheLocked(componentName, user, infoProvider, cachingLogic, usePackageIcon,
-                useLowResIcon, true);
-    }
-
-    protected <T> CacheEntry cacheLocked(
-            @NonNull ComponentName componentName, @NonNull UserHandle user,
-            @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
-            boolean usePackageIcon, boolean useLowResIcon, boolean addToMemCache) {
         assertWorkerThread();
         ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
         if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
             entry = new CacheEntry();
-            if (addToMemCache) {
+            if (cachingLogic.addToMemCache()) {
                 mCache.put(cacheKey, entry);
             }
 
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index 09f59b8..e40a9c2 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -41,4 +41,11 @@
     default String getKeywords(T object, LocaleList localeList) {
         return null;
     }
+
+    /**
+     * Returns true the object should be added to mem cache; otherwise returns false.
+     */
+    default boolean addToMemCache() {
+        return true;
+    }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
index 3c71bd0..8224966 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -171,8 +171,8 @@
                 long updateTime = c.getLong(indexLastUpdate);
                 int version = c.getInt(indexVersion);
                 T app = componentMap.remove(component);
-                if (version == info.versionCode && updateTime == info.lastUpdateTime &&
-                        TextUtils.equals(c.getString(systemStateIndex),
+                if (version == info.versionCode && updateTime == info.lastUpdateTime
+                        && TextUtils.equals(c.getString(systemStateIndex),
                                 mIconCache.getIconSystemState(info.packageName))) {
 
                     if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index 49fd436..055ade5 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -118,6 +118,7 @@
   APP_USAGE_SETTINGS = 18;
   BACK_GESTURE = 19;
   UNDO = 20;
+  DISMISS_PREDICTION = 21;
 }
 
 enum TipType {
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 5465480..826a275 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -88,6 +88,7 @@
         <activity android:name="com.android.quickstep.LockScreenRecentsActivity"
                   android:theme="@android:style/Theme.NoDisplay"
                   android:showOnLockScreen="true"
+                  android:taskAffinity="${packageName}.locktask"
                   android:directBootAware="true" />
 
     </application>
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index 1ddd3f5..ed3ba92 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -24,5 +24,7 @@
   <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
 
   <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
+
+  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherAppPredictionExtension</string>
 </resources>
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 7115943..114fd8e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -17,9 +17,19 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_TRANSLATE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
 import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
 
@@ -27,10 +37,17 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.quickstep.util.ClipAnimationHelper;
@@ -38,9 +55,6 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
  * A {@link QuickstepAppTransitionManagerImpl} that also implements recents transitions from
  * {@link RecentsView}.
@@ -50,6 +64,9 @@
     public static final int INDEX_SHELF_ANIM = 0;
     public static final int INDEX_RECENTS_FADE_ANIM = 1;
     public static final int INDEX_RECENTS_TRANSLATE_X_ANIM = 2;
+    public static final int INDEX_PAUSE_TO_OVERVIEW_ANIM = 3;
+
+    public static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
 
     public LauncherAppTransitionManagerImpl(Context context) {
         super(context);
@@ -138,14 +155,43 @@
 
     @Override
     public int getStateElementAnimationsCount() {
-        return 3;
+        return 4;
     }
 
     @Override
     public Animator createStateElementAnimation(int index, float... values) {
         switch (index) {
-            case INDEX_SHELF_ANIM:
-                return mLauncher.getAllAppsController().createSpringAnimation(values);
+            case INDEX_SHELF_ANIM: {
+                AllAppsTransitionController aatc = mLauncher.getAllAppsController();
+                Animator springAnim = aatc.createSpringAnimation(values);
+
+                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+                    // Translate hotseat with the shelf until reaching overview.
+                    float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
+                    ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mLauncher);
+                    float shiftRange = aatc.getShiftRange();
+                    if (values.length == 1) {
+                        values = new float[] {aatc.getProgress(), values[0]};
+                    }
+                    ValueAnimator hotseatAnim = ValueAnimator.ofFloat(values);
+                    hotseatAnim.addUpdateListener(anim -> {
+                        float progress = (Float) anim.getAnimatedValue();
+                        if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
+                            float hotseatShift = (progress - overviewProgress) * shiftRange;
+                            mLauncher.getHotseat().setTranslationY(hotseatShift + sat.translationY);
+                        }
+                    });
+                    hotseatAnim.setInterpolator(LINEAR);
+                    hotseatAnim.setDuration(springAnim.getDuration());
+
+                    AnimatorSet anim = new AnimatorSet();
+                    anim.play(hotseatAnim);
+                    anim.play(springAnim);
+                    return anim;
+                }
+
+                return springAnim;
+            }
             case INDEX_RECENTS_FADE_ANIM:
                 return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
@@ -155,6 +201,20 @@
                         .setStiffness(250)
                         .setValues(values)
                         .build(mLauncher);
+            case INDEX_PAUSE_TO_OVERVIEW_ANIM: {
+                AnimatorSetBuilder builder = new AnimatorSetBuilder();
+                builder.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
+                builder.setInterpolator(ANIM_ALL_APPS_FADE, DEACCEL_3);
+                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+                    builder.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
+                    builder.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
+                }
+                LauncherStateManager stateManager = mLauncher.getStateManager();
+                return stateManager.createAtomicAnimation(
+                        stateManager.getCurrentStableState(), OVERVIEW, builder,
+                        ANIM_ALL, ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW);
+            }
+
             default:
                 return super.createStateElementAnimation(index, values);
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 4ecc39c..65e69b6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -17,6 +17,8 @@
 
 import static android.content.pm.PackageManager.MATCH_INSTANT;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -30,8 +32,12 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
@@ -45,11 +51,6 @@
 import java.util.List;
 import java.util.Map;
 
-import androidx.annotation.MainThread;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
 /**
  * Utility class which loads and caches predicted items like instant apps and shortcuts, before
  * they can be displayed on the UI
@@ -77,7 +78,7 @@
 
     public DynamicItemCache(Context context, Runnable onUpdateCallback) {
         mContext = context;
-        mWorker = new Handler(LauncherModel.getWorkerLooper(), this::handleWorkerMessage);
+        mWorker = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
         mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
         mInstantAppResolver = InstantAppResolver.newInstance(context);
         mOnUpdateCallback = onUpdateCallback;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
index 24fc61b..4c7943b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,6 +16,7 @@
 package com.android.launcher3.appprediction;
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.prediction.AppPredictionContext;
@@ -34,19 +35,25 @@
 import android.util.Log;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
 import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.systemui.plugins.AppLaunchEventsPlugin;
+import com.android.systemui.plugins.PluginListener;
 
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public class PredictionAppTracker extends AppLaunchTracker {
+public class PredictionAppTracker extends AppLaunchTracker
+        implements PluginListener<AppLaunchEventsPlugin> {
 
     private static final String TAG = "PredictionAppTracker";
     private static final boolean DBG = false;
@@ -58,6 +65,7 @@
 
     protected final Context mContext;
     private final Handler mMessageHandler;
+    private final List<AppLaunchEventsPlugin> mAppLaunchEventsPluginsList;
 
     // Accessed only on worker thread
     private AppPredictor mHomeAppPredictor;
@@ -65,10 +73,14 @@
 
     public PredictionAppTracker(Context context) {
         mContext = context;
-        mMessageHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleMessage);
+        mMessageHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessage);
         InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
 
         mMessageHandler.sendEmptyMessage(MSG_INIT);
+
+        mAppLaunchEventsPluginsList = new ArrayList<>();
+        PluginManagerWrapper.INSTANCE.get(context)
+                .addPluginListener(this, AppLaunchEventsPlugin.class, true);
     }
 
     @UiThread
@@ -96,7 +108,7 @@
         AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
 
         if (apm == null) {
-          return null;
+            return null;
         }
 
         AppPredictor predictor = apm.createAppPredictionSession(
@@ -116,7 +128,7 @@
      */
     @WorkerThread
     @Nullable
-    public Bundle getAppPredictionContextExtras(Client client){
+    public Bundle getAppPredictionContextExtras(Client client) {
         return null;
     }
 
@@ -128,7 +140,7 @@
                 destroy();
 
                 // Initialize the clients
-                int count = InvariantDeviceProfile.INSTANCE.get(mContext).numColumns;
+                int count = InvariantDeviceProfile.INSTANCE.get(mContext).numAllAppsColumns;
                 mHomeAppPredictor = createPredictor(Client.HOME, count);
                 mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count);
                 return true;
@@ -167,37 +179,98 @@
         if (DBG) {
             Log.d(TAG, String.format("Sent immediate message to update %s", client));
         }
+
+        // Relay onReturnedToHome to every plugin.
+        mAppLaunchEventsPluginsList.forEach(AppLaunchEventsPlugin::onReturnedToHome);
     }
 
     @Override
     @UiThread
     public void onStartShortcut(String packageName, String shortcutId, UserHandle user,
-            String container) {
+                                String container) {
         // TODO: Use the full shortcut info
-        AppTarget target = new AppTarget
-                .Builder(new AppTargetId("shortcut:" + shortcutId), packageName, user)
-                    .setClassName(shortcutId)
-                    .build();
+        AppTarget target = new AppTarget.Builder(
+                new AppTargetId("shortcut:" + shortcutId), packageName, user)
+                .setClassName(shortcutId)
+                .build();
+
         sendLaunch(target, container);
+
+        // Relay onStartShortcut info to every connected plugin.
+        mAppLaunchEventsPluginsList
+                .forEach(plugin -> plugin.onStartShortcut(
+                        packageName,
+                        shortcutId,
+                        user,
+                        container != null ? container : CONTAINER_DEFAULT)
+        );
+
     }
 
     @Override
     @UiThread
     public void onStartApp(ComponentName cn, UserHandle user, String container) {
         if (cn != null) {
-            AppTarget target = new AppTarget
-                    .Builder(new AppTargetId("app:" + cn), cn.getPackageName(), user)
-                        .setClassName(cn.getClassName())
-                        .build();
+            AppTarget target = new AppTarget.Builder(
+                    new AppTargetId("app:" + cn), cn.getPackageName(), user)
+                    .setClassName(cn.getClassName())
+                    .build();
             sendLaunch(target, container);
+
+            // Relay onStartApp to every connected plugin.
+            mAppLaunchEventsPluginsList
+                    .forEach(plugin -> plugin.onStartApp(
+                            cn,
+                            user,
+                            container != null ? container : CONTAINER_DEFAULT)
+            );
         }
     }
 
+    @Override
+    @UiThread
+    public void onDismissApp(ComponentName cn, UserHandle user, String container) {
+        if (cn == null) return;
+        AppTarget target = new AppTarget.Builder(
+                new AppTargetId("app: " + cn), cn.getPackageName(), user)
+                .setClassName(cn.getClassName())
+                .build();
+        sendDismiss(target, container);
+
+        // Relay onDismissApp to every connected plugin.
+        mAppLaunchEventsPluginsList
+                .forEach(plugin -> plugin.onDismissApp(
+                        cn,
+                        user,
+                        container != null ? container : CONTAINER_DEFAULT)
+        );
+    }
+
+    @UiThread
+    private void sendEvent(AppTarget target, String container, int eventId) {
+        AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
+                .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
+                .build();
+        Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
+    }
+
     @UiThread
     private void sendLaunch(AppTarget target, String container) {
-        AppTargetEvent event = new AppTargetEvent.Builder(target, AppTargetEvent.ACTION_LAUNCH)
-                .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container)
-                .build();
-        Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget();
+        sendEvent(target, container, AppTargetEvent.ACTION_LAUNCH);
+    }
+
+    @UiThread
+    private void sendDismiss(AppTarget target, String container) {
+        sendEvent(target, container, AppTargetEvent.ACTION_DISMISS);
+    }
+
+    @Override
+    public void onPluginConnected(AppLaunchEventsPlugin appLaunchEventsPlugin, Context context) {
+        mAppLaunchEventsPluginsList.add(appLaunchEventsPlugin);
+    }
+
+    @Override
+    public void onPluginDisconnected(AppLaunchEventsPlugin appLaunchEventsPlugin) {
+        mAppLaunchEventsPluginsList.remove(appLaunchEventsPlugin);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
index 0c7ba9c..f82af62 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.allapps.AllAppsStore;
@@ -92,7 +93,7 @@
 
     private final Launcher mLauncher;
     private final PredictionUiStateManager mPredictionUiStateManager;
-    private final int mNumPredictedAppsPerRow;
+    private int mNumPredictedAppsPerRow;
 
     // The set of predicted app component names
     private final List<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
@@ -128,7 +129,7 @@
 
         mFocusHelper = new SimpleFocusIndicatorHelper(this);
 
-        mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numColumns;
+        mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numAllAppsColumns;
         mLauncher = Launcher.getLauncher(context);
         mLauncher.addOnDeviceProfileChangeListener(this);
 
@@ -226,6 +227,7 @@
 
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
+        mNumPredictedAppsPerRow = dp.inv.numAllAppsColumns;
         removeAllViews();
         applyPredictionApps();
     }
@@ -274,14 +276,14 @@
         boolean predictionsEnabled = predictionCount > 0;
         if (predictionsEnabled != mPredictionsEnabled) {
             mPredictionsEnabled = predictionsEnabled;
-            mLauncher.reapplyUi();
+            mLauncher.reapplyUi(false /* cancelCurrentAnimation */);
             updateVisibility();
         }
         mParent.onHeightUpdated();
     }
 
     private List<ItemInfoWithIcon> processPredictedAppComponents(List<ComponentKeyMapper> components) {
-        if (getAppsStore().getApps().isEmpty()) {
+        if (getAppsStore().getApps().length == 0) {
             // Apps have not been bound yet.
             return Collections.emptyList();
         }
@@ -290,7 +292,9 @@
         for (ComponentKeyMapper mapper : components) {
             ItemInfoWithIcon info = mapper.getApp(getAppsStore());
             if (info != null) {
-                predictedApps.add(info);
+                ItemInfoWithIcon predictedApp = info.clone();
+                predictedApp.container = LauncherSettings.Favorites.CONTAINER_PREDICTION;
+                predictedApps.add(predictedApp);
             } else {
                 if (FeatureFlags.IS_DOGFOOD_BUILD) {
                     Log.e(TAG, "Predicted app not found: " + mapper);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index 1a59770..9c66107 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -25,12 +25,16 @@
 import android.content.ComponentName;
 import android.content.Context;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.ItemInfo;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.Utilities;
@@ -39,12 +43,14 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.MainThreadInitializedObject;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.IntStream;
 
 /**
  * Handler responsible to updating the UI due to predicted apps changes. Operations:
@@ -322,6 +328,30 @@
         return mCurrentState;
     }
 
+    /**
+     * Fill in predicted_rank field based on app prediction.
+     * Only applicable when {@link ItemInfo#itemType} is one of the followings:
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
+     * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
+     */
+    public static void fillInPredictedRank(
+            @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
+        final PredictionUiStateManager manager = PredictionUiStateManager.INSTANCE.getNoCreate();
+        if (manager == null || itemInfo.getTargetComponent() == null || itemInfo.user == null
+                || (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
+                && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
+            return;
+        }
+        final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
+        final List<ComponentKeyMapper> predictedApps = manager.getCurrentState().apps;
+        IntStream.range(0, predictedApps.size())
+                .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
+                .findFirst()
+                .ifPresent((rank) -> target.predictedRank = rank);
+    }
+
     public static class PredictionState {
 
         public boolean isEnabled;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
index 596bc4f..cac170c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -28,17 +28,17 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
 import com.android.launcher3.util.TouchController;
@@ -145,7 +145,7 @@
         ArrayList<TouchController> list = new ArrayList<>();
         list.add(launcher.getDragController());
         if (mode == NO_BUTTON) {
-            list.add(new QuickSwitchTouchController(launcher));
+            list.add(new NoButtonQuickSwitchTouchController(launcher));
             list.add(new NavBarToHomeTouchController(launcher));
             list.add(new FlingAndHoldTouchController(launcher));
         } else {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 50cfac8..63ac528 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -19,7 +19,6 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.LayoutUtils;
@@ -69,8 +68,16 @@
         if (taskCount == 0) {
             return super.getOverviewScaleAndTranslation(launcher);
         }
-        TaskView dummyTask = recentsView.getTaskViewAt(Utilities.boundToRange(
-                recentsView.getCurrentPage(), 0, taskCount - 1));
+        TaskView dummyTask;
+        if (recentsView.getCurrentPage() >= 0) {
+            if (recentsView.getCurrentPage() <= taskCount - 1) {
+                dummyTask = recentsView.getCurrentPageTaskView();
+            } else {
+                dummyTask = recentsView.getTaskViewAt(taskCount - 1);
+            }
+        } else {
+            dummyTask = recentsView.getTaskViewAt(0);
+        }
         return recentsView.getTempClipAnimationHelper().updateForFullscreenOverview(dummyTask)
                 .getScaleAndTranslation();
     }
@@ -91,7 +98,7 @@
         if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
             // Translate hotseat offscreen if we show it in overview.
             ScaleAndTranslation scaleAndTranslation = super.getHotseatScaleAndTranslation(launcher);
-            scaleAndTranslation.translationY = LayoutUtils.getShelfTrackingDistance(launcher,
+            scaleAndTranslation.translationY += LayoutUtils.getShelfTrackingDistance(launcher,
                     launcher.getDeviceProfile());
             return scaleAndTranslation;
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 93d4de1..ed5dba1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
@@ -32,7 +33,6 @@
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
-import android.content.Context;
 import android.graphics.Rect;
 import android.view.View;
 
@@ -47,6 +47,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
@@ -91,8 +92,19 @@
     @Override
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
         if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
-            // If the hotseat icons are visible in overview, keep them in their normal position.
-            return super.getWorkspaceScaleAndTranslation(launcher);
+            DeviceProfile dp = launcher.getDeviceProfile();
+            if (dp.allAppsIconSizePx >= dp.iconSizePx) {
+                return new ScaleAndTranslation(1, 0, 0);
+            } else {
+                float scale = ((float) dp.allAppsIconSizePx) / dp.iconSizePx;
+                // Distance between the screen center (which is the pivotY for hotseat) and the
+                // bottom of the hotseat (which we want to preserve)
+                float distanceFromBottom = dp.heightPx / 2 - dp.hotseatBarBottomPaddingPx;
+                // On scaling, the bottom edge is moved closer to the pivotY. We move the
+                // hotseat back down so that the bottom edge's position is preserved.
+                float translationY = distanceFromBottom * (1 - scale);
+                return new ScaleAndTranslation(scale, 0, translationY);
+            }
         }
         return getWorkspaceScaleAndTranslation(launcher);
     }
@@ -160,15 +172,7 @@
     }
 
     public static float getDefaultSwipeHeight(Launcher launcher) {
-        return getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
-    }
-
-    public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) {
-        float swipeHeight = dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
-        if (SysUINavigationMode.getMode(context) == SysUINavigationMode.Mode.NO_BUTTON) {
-            swipeHeight -= dp.getInsets().bottom;
-        }
-        return swipeHeight;
+        return LayoutUtils.getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
     }
 
     @Override
@@ -202,6 +206,7 @@
             builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
             builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
             builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
+            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_7);
             builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
         }
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index ab346c0..3231f37 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -16,39 +16,37 @@
 
 package com.android.launcher3.uioverrides.touchcontrollers;
 
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_PAUSE_TO_OVERVIEW_ANIM;
 import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
-import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
 import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_PEEK_COMPONENT;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_HEADER_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_TRANSLATE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppTransitionManagerImpl;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.views.RecentsView;
@@ -77,7 +75,7 @@
 
     @Override
     protected long getAtomicDuration() {
-        return 300;
+        return LauncherAppTransitionManagerImpl.ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
     }
 
     @Override
@@ -105,8 +103,7 @@
                     }
                 });
                 mPeekAnim.start();
-                recentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
 
                 mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
                         peekDuration, 0);
@@ -131,16 +128,33 @@
             // Fade in prediction icons quickly, then rest of all apps after reaching overview.
             float progressToReachOverview = NORMAL.getVerticalProgress(mLauncher)
                     - OVERVIEW.getVerticalProgress(mLauncher);
-            builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(ACCEL,
-                    0, ALL_APPS_CONTENT_FADE_THRESHOLD));
-            builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(LINEAR,
-                    progressToReachOverview, 1));
+            builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(
+                    ACCEL,
+                    0,
+                    ALL_APPS_CONTENT_FADE_THRESHOLD));
+            builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
+                    ACCEL,
+                    progressToReachOverview,
+                    progressToReachOverview + ALL_APPS_CONTENT_FADE_THRESHOLD));
 
             // Get workspace out of the way quickly, to prepare for potential pause.
             builder.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL_3);
             builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, DEACCEL_3);
             builder.setInterpolator(ANIM_WORKSPACE_FADE, DEACCEL_3);
             return builder;
+        } else if (fromState == ALL_APPS && toState == NORMAL) {
+            AnimatorSetBuilder builder = new AnimatorSetBuilder();
+            // Keep all apps/predictions opaque until the very end of the transition.
+            float progressToReachOverview = OVERVIEW.getVerticalProgress(mLauncher);
+            builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(
+                    DEACCEL,
+                    progressToReachOverview - ALL_APPS_CONTENT_FADE_THRESHOLD,
+                    progressToReachOverview));
+            builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(
+                    DEACCEL,
+                    1 - ALL_APPS_CONTENT_FADE_THRESHOLD,
+                    1));
+            return builder;
         }
         return super.getAnimatorSetBuilderForStates(fromState, toState);
     }
@@ -155,20 +169,14 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
         if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
             if (mPeekAnim != null) {
                 mPeekAnim.cancel();
             }
 
-            AnimatorSetBuilder builder = new AnimatorSetBuilder();
-            builder.setInterpolator(ANIM_VERTICAL_PROGRESS, OVERSHOOT_1_2);
-            if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-                builder.setInterpolator(ANIM_HOTSEAT_SCALE, OVERSHOOT_1_2);
-                builder.setInterpolator(ANIM_HOTSEAT_TRANSLATE, OVERSHOOT_1_2);
-            }
-            AnimatorSet overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
-                    NORMAL, OVERVIEW, builder, ANIM_ALL, ATOMIC_DURATION);
+            Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
+                    INDEX_PAUSE_TO_OVERVIEW_ANIM);
             overviewAnim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -177,7 +185,12 @@
             });
             overviewAnim.start();
         } else {
-            super.onDragEnd(velocity, fling);
+            super.onDragEnd(velocity);
+        }
+
+        View searchView = mLauncher.getAppsView().getSearchView();
+        if (searchView instanceof FeedbackHandler) {
+            ((FeedbackHandler) searchView).resetFeedback();
         }
         mMotionPauseDetector.clear();
     }
@@ -205,4 +218,16 @@
             builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
         }
     }
+
+    /**
+     * Interface for views with feedback animation requiring reset
+     */
+    public interface FeedbackHandler {
+
+        /**
+         * reset searchWidget feedback
+         */
+        void resetFeedback();
+    }
+
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index d66af1a..ef50c7b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -43,21 +44,25 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.config.BaseFlags;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
  */
-public class NavBarToHomeTouchController implements TouchController, SwipeDetector.Listener {
+public class NavBarToHomeTouchController implements TouchController,
+        SingleAxisSwipeDetector.Listener {
 
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
 
     private final Launcher mLauncher;
-    private final SwipeDetector mSwipeDetector;
+    private final SingleAxisSwipeDetector mSwipeDetector;
     private final float mPullbackDistance;
 
     private boolean mNoIntercept;
@@ -67,7 +72,8 @@
 
     public NavBarToHomeTouchController(Launcher launcher) {
         mLauncher = launcher;
-        mSwipeDetector = new SwipeDetector(mLauncher, this, SwipeDetector.VERTICAL);
+        mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this,
+                SingleAxisSwipeDetector.VERTICAL);
         mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
     }
 
@@ -79,7 +85,8 @@
             if (mNoIntercept) {
                 return false;
             }
-            mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+            mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_POSITIVE,
+                    false /* ignoreSlop */);
         }
 
         if (mNoIntercept) {
@@ -101,6 +108,10 @@
         if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
             return true;
         }
+        if (BaseFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+                && AssistantUtilities.isExcludedAssistantRunning()) {
+            return true;
+        }
         return false;
     }
 
@@ -173,7 +184,8 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
+        boolean fling = mSwipeDetector.isFling(velocity);
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
         float progress = mCurrentAnimation.getProgressFraction();
         float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
@@ -190,6 +202,8 @@
                 AbstractFloatingView.closeAllOpenViews(mLauncher);
                 logStateChange(topOpenView.getLogContainerType(), logAction);
             }
+            ActivityManagerWrapper.getInstance()
+                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
         } else {
             // Quickly return to the state we came from (we didn't move far).
             ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
new file mode 100644
index 0000000..76374af
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -0,0 +1,473 @@
+/*
+ * 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.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_PAUSE_TO_OVERVIEW_ANIM;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.QUICK_SWITCH;
+import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
+import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_5;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
+import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.CANCEL;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.LauncherStateManager.AnimationConfig;
+import com.android.launcher3.QuickstepAppTransitionManagerImpl;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.BothAxesSwipeDetector;
+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;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.views.LauncherRecentsView;
+
+/**
+ * Handles quick switching to a recent task from the home screen. To give as much flexibility to
+ * the user as possible, also handles swipe up and hold to go to overview and swiping back home.
+ */
+public class NoButtonQuickSwitchTouchController implements TouchController,
+        BothAxesSwipeDetector.Listener, MotionPauseDetector.OnMotionPauseListener {
+
+    /** The minimum progress of the scale/translationY animation until drag end. */
+    private static final float Y_ANIM_MIN_PROGRESS = 0.15f;
+    private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_5;
+    private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
+    private static final Interpolator SCALE_DOWN_INTERPOLATOR = DEACCEL;
+
+    private final Launcher mLauncher;
+    private final BothAxesSwipeDetector mSwipeDetector;
+    private final float mXRange;
+    private final float mYRange;
+    private final MotionPauseDetector mMotionPauseDetector;
+    private final float mMotionPauseMinDisplacement;
+
+    private boolean mNoIntercept;
+    private LauncherState mStartState;
+
+    private ShelfPeekAnim mShelfPeekAnim;
+    private boolean mIsHomeScreenVisible = true;
+
+    // As we drag, we control 3 animations: one to get non-overview components out of the way,
+    // and the other two to set overview properties based on x and y progress.
+    private AnimatorPlaybackController mNonOverviewAnim;
+    private AnimatorPlaybackController mXOverviewAnim;
+    private AnimatorPlaybackController mYOverviewAnim;
+
+    public NoButtonQuickSwitchTouchController(Launcher launcher) {
+        mLauncher = launcher;
+        mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
+        mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
+        mYRange = LayoutUtils.getShelfTrackingDistance(mLauncher, mLauncher.getDeviceProfile());
+        mMotionPauseDetector = new MotionPauseDetector(mLauncher);
+        mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
+                R.dimen.motion_pause_detector_min_displacement_from_app);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mNoIntercept = !canInterceptTouch(ev);
+            if (mNoIntercept) {
+                return false;
+            }
+
+            // Only detect horizontal swipe for intercept, then we will allow swipe up as well.
+            mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT,
+                    false /* ignoreSlopWhenSettling */);
+        }
+
+        if (mNoIntercept) {
+            return false;
+        }
+
+        onControllerTouchEvent(ev);
+        return mSwipeDetector.isDraggingOrSettling();
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return mSwipeDetector.onTouchEvent(ev);
+    }
+
+    private boolean canInterceptTouch(MotionEvent ev) {
+        if (!mLauncher.isInState(LauncherState.NORMAL)) {
+            return false;
+        }
+        if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
+            return false;
+        }
+        int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags();
+        if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void onDragStart(boolean start) {
+        mMotionPauseDetector.clear();
+        if (start) {
+            mShelfPeekAnim = ((QuickstepAppTransitionManagerImpl) mLauncher
+                    .getAppTransitionManager()).getShelfPeekAnim();
+
+            mStartState = mLauncher.getStateManager().getState();
+
+            mMotionPauseDetector.setOnMotionPauseListener(this);
+
+            // We have detected horizontal drag start, now allow swipe up as well.
+            mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
+                    false /* ignoreSlopWhenSettling */);
+
+            setupAnimators();
+        }
+    }
+
+    @Override
+    public void onMotionPauseChanged(boolean isPaused) {
+        ShelfAnimState shelfState = isPaused ? PEEK : HIDE;
+        if (shelfState == PEEK) {
+            // Some shelf elements (e.g. qsb) were hidden, but we need them visible when peeking.
+            AnimatorSetBuilder builder = new AnimatorSetBuilder();
+            AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
+            allAppsController.setAlphas(NORMAL.getVisibleElements(mLauncher),
+                    new AnimationConfig(), builder);
+            builder.build().setDuration(0).start();
+
+            if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+                // Hotseat was hidden, but we need it visible when peeking.
+                mLauncher.getHotseat().setAlpha(1);
+            }
+        }
+        mShelfPeekAnim.setShelfState(shelfState, ShelfPeekAnim.INTERPOLATOR,
+                ShelfPeekAnim.DURATION);
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+    }
+
+    private void setupAnimators() {
+        // Animate the non-overview components (e.g. workspace, shelf) out of the way.
+        AnimatorSetBuilder nonOverviewBuilder = new AnimatorSetBuilder();
+        nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
+        nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
+        nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, TRANSLATE_OUT_INTERPOLATOR);
+        nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
+        updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder, ANIM_ALL);
+        mNonOverviewAnim.dispatchOnStart();
+
+        setupOverviewAnimators();
+    }
+
+    /** Create state animation to control non-overview components. */
+    private void updateNonOverviewAnim(LauncherState toState, AnimatorSetBuilder builder,
+            @LauncherStateManager.AnimationComponents int animComponents) {
+        builder.addFlag(FLAG_DONT_ANIMATE_OVERVIEW);
+        long accuracy = (long) (Math.max(mXRange, mYRange) * 2);
+        mNonOverviewAnim = mLauncher.getStateManager().createAnimationToNewWorkspace(toState,
+                builder, accuracy, this::clearState, animComponents);
+    }
+
+    private void setupOverviewAnimators() {
+        final LauncherState fromState = QUICK_SWITCH;
+        final LauncherState toState = OVERVIEW;
+        LauncherState.ScaleAndTranslation fromScaleAndTranslation = fromState
+                .getOverviewScaleAndTranslation(mLauncher);
+        LauncherState.ScaleAndTranslation toScaleAndTranslation = toState
+                .getOverviewScaleAndTranslation(mLauncher);
+        // Update RecentView's translationX to have it start offscreen.
+        LauncherRecentsView recentsView = mLauncher.getOverviewPanel();
+        float startScale = Utilities.mapRange(
+                SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
+                fromScaleAndTranslation.scale,
+                toScaleAndTranslation.scale);
+        fromScaleAndTranslation.translationX = recentsView.getOffscreenTranslationX(startScale);
+
+        // Set RecentView's initial properties.
+        recentsView.setScaleX(fromScaleAndTranslation.scale);
+        recentsView.setScaleY(fromScaleAndTranslation.scale);
+        recentsView.setTranslationX(fromScaleAndTranslation.translationX);
+        recentsView.setTranslationY(fromScaleAndTranslation.translationY);
+        recentsView.setContentAlpha(1);
+
+        // As we drag right, animate the following properties:
+        //   - RecentsView translationX
+        //   - OverviewScrim
+        AnimatorSet xOverviewAnim = new AnimatorSet();
+        xOverviewAnim.play(ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_X,
+                toScaleAndTranslation.translationX));
+        xOverviewAnim.play(ObjectAnimator.ofFloat(
+                mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
+                toState.getOverviewScrimAlpha(mLauncher)));
+        long xAccuracy = (long) (mXRange * 2);
+        xOverviewAnim.setDuration(xAccuracy);
+        mXOverviewAnim = AnimatorPlaybackController.wrap(xOverviewAnim, xAccuracy);
+        mXOverviewAnim.dispatchOnStart();
+
+        // As we drag up, animate the following properties:
+        //   - RecentsView translationY
+        //   - RecentsView scale
+        //   - RecentsView fullscreenProgress
+        AnimatorSet yAnimation = new AnimatorSet();
+        Animator translateYAnim = ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_Y,
+                toScaleAndTranslation.translationY);
+        Animator scaleAnim = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
+                toScaleAndTranslation.scale);
+        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS,
+                fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress());
+        scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
+        fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
+        yAnimation.play(translateYAnim);
+        yAnimation.play(scaleAnim);
+        yAnimation.play(fullscreenProgressAnim);
+        long yAccuracy = (long) (mYRange * 2);
+        yAnimation.setDuration(yAccuracy);
+        mYOverviewAnim = AnimatorPlaybackController.wrap(yAnimation, yAccuracy);
+        mYOverviewAnim.dispatchOnStart();
+    }
+
+    @Override
+    public boolean onDrag(PointF displacement, MotionEvent ev) {
+        float xProgress = Math.max(0, displacement.x) / mXRange;
+        float yProgress = Math.max(0, -displacement.y) / mYRange;
+        yProgress = Utilities.mapRange(yProgress, Y_ANIM_MIN_PROGRESS, 1f);
+
+        boolean wasHomeScreenVisible = mIsHomeScreenVisible;
+        if (wasHomeScreenVisible && mNonOverviewAnim != null) {
+            mNonOverviewAnim.setPlayFraction(xProgress);
+        }
+        mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress)
+                <= 1 - ALPHA_CUTOFF_THRESHOLD;
+
+        if (wasHomeScreenVisible && !mIsHomeScreenVisible) {
+            // Get the shelf all the way offscreen so it pops up when we decide to peek it.
+            mShelfPeekAnim.setShelfState(HIDE, LINEAR, 0);
+        }
+
+        // Only allow motion pause if the home screen is invisible, since some
+        // home screen elements will appear in the shelf on motion pause.
+        mMotionPauseDetector.setDisallowPause(mIsHomeScreenVisible
+                || -displacement.y < mMotionPauseMinDisplacement);
+        mMotionPauseDetector.addPosition(displacement.y, ev.getEventTime());
+
+        if (mIsHomeScreenVisible) {
+            // Cancel the shelf anim so it doesn't clobber mNonOverviewAnim.
+            mShelfPeekAnim.setShelfState(CANCEL, LINEAR, 0);
+        }
+
+        if (mXOverviewAnim != null) {
+            mXOverviewAnim.setPlayFraction(xProgress);
+        }
+        if (mYOverviewAnim != null) {
+            mYOverviewAnim.setPlayFraction(yProgress);
+        }
+        return true;
+    }
+
+    @Override
+    public void onDragEnd(PointF velocity) {
+        boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
+        boolean verticalFling = mSwipeDetector.isFling(velocity.y);
+        boolean noFling = !horizontalFling && !verticalFling;
+        int logAction = noFling ? Touch.SWIPE : Touch.FLING;
+        if (mMotionPauseDetector.isPaused() && noFling) {
+            cancelAnimations();
+
+            Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
+                    INDEX_PAUSE_TO_OVERVIEW_ANIM);
+            overviewAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    onAnimationToStateCompleted(OVERVIEW, logAction);
+                }
+            });
+            overviewAnim.start();
+            return;
+        }
+
+        final LauncherState targetState;
+        if (horizontalFling && verticalFling) {
+            if (velocity.x < 0) {
+                // Flinging left and up or down both go back home.
+                targetState = NORMAL;
+            } else {
+                if (velocity.y > 0) {
+                    // Flinging right and down goes to quick switch.
+                    targetState = QUICK_SWITCH;
+                } else {
+                    // Flinging up and right could go either home or to quick switch.
+                    // Determine the target based on the higher velocity.
+                    targetState = Math.abs(velocity.x) > Math.abs(velocity.y)
+                        ? QUICK_SWITCH : NORMAL;
+                }
+            }
+        } else if (horizontalFling) {
+            targetState = velocity.x > 0 ? QUICK_SWITCH : NORMAL;
+        } else if (verticalFling) {
+            targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL;
+        } else {
+            // If user isn't flinging, just snap to the closest state based on x progress.
+            boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
+            targetState = passedHorizontalThreshold ? QUICK_SWITCH : NORMAL;
+        }
+
+        // Animate the various components to the target state.
+
+        float xProgress = mXOverviewAnim.getProgressFraction();
+        float startXProgress = Utilities.boundToRange(xProgress
+                + velocity.x * getSingleFrameMs(mLauncher) / mXRange, 0f, 1f);
+        final float endXProgress = targetState == NORMAL ? 0 : 1;
+        long xDuration = BaseSwipeDetector.calculateDuration(velocity.x,
+                Math.abs(endXProgress - startXProgress));
+        ValueAnimator xOverviewAnim = mXOverviewAnim.getAnimationPlayer();
+        xOverviewAnim.setFloatValues(startXProgress, endXProgress);
+        xOverviewAnim.setDuration(xDuration)
+                .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
+        mXOverviewAnim.dispatchOnStartWithVelocity(endXProgress, velocity.x);
+
+        boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
+
+        float yProgress = mYOverviewAnim.getProgressFraction();
+        float startYProgress = Utilities.boundToRange(yProgress
+                - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, 1f);
+        final float endYProgress;
+        if (flingUpToNormal) {
+            endYProgress = 1;
+        } else if (targetState == NORMAL) {
+            // Keep overview at its current scale/translationY as it slides off the screen.
+            endYProgress = startYProgress;
+        } else {
+            endYProgress = 0;
+        }
+        long yDuration = BaseSwipeDetector.calculateDuration(velocity.y,
+                Math.abs(endYProgress - startYProgress));
+        ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
+        yOverviewAnim.setFloatValues(startYProgress, endYProgress);
+        yOverviewAnim.setDuration(yDuration);
+        mYOverviewAnim.dispatchOnStartWithVelocity(endYProgress, velocity.y);
+
+        ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
+        if (flingUpToNormal && !mIsHomeScreenVisible) {
+            // We are flinging to home while workspace is invisible, run the same staggered
+            // animation as from an app.
+            // Update mNonOverviewAnim to do nothing so it doesn't interfere.
+            updateNonOverviewAnim(targetState, new AnimatorSetBuilder(), 0 /* animComponents */);
+            nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
+
+            new StaggeredWorkspaceAnim(mLauncher, velocity.y, false /* animateOverviewScrim */)
+                    .start();
+        } else {
+            boolean canceled = targetState == NORMAL;
+            if (canceled) {
+                // Let the state manager know that the animation didn't go to the target state,
+                // but don't clean up yet (we already clean up when the animation completes).
+                mNonOverviewAnim.dispatchOnCancelWithoutCancelRunnable();
+            }
+            float startProgress = mNonOverviewAnim.getProgressFraction();
+            float endProgress = canceled ? 0 : 1;
+            nonOverviewAnim.setFloatValues(startProgress, endProgress);
+            mNonOverviewAnim.dispatchOnStartWithVelocity(endProgress,
+                    horizontalFling ? velocity.x : velocity.y);
+        }
+
+        nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
+        mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState, logAction));
+
+        cancelAnimations();
+        xOverviewAnim.start();
+        yOverviewAnim.start();
+        nonOverviewAnim.start();
+    }
+
+    private void onAnimationToStateCompleted(LauncherState targetState, int logAction) {
+        mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
+                getDirectionForLog(), mSwipeDetector.getDownX(), mSwipeDetector.getDownY(),
+                LauncherLogProto.ContainerType.NAVBAR,
+                mStartState.containerType,
+                targetState.containerType,
+                mLauncher.getWorkspace().getCurrentPage());
+        mLauncher.getStateManager().goToState(targetState, false, this::clearState);
+    }
+
+    private int getDirectionForLog() {
+        return Utilities.isRtl(mLauncher.getResources()) ? Direction.LEFT : Direction.RIGHT;
+    }
+
+    private void cancelAnimations() {
+        if (mNonOverviewAnim != null) {
+            mNonOverviewAnim.getAnimationPlayer().cancel();
+        }
+        if (mXOverviewAnim != null) {
+            mXOverviewAnim.getAnimationPlayer().cancel();
+        }
+        if (mYOverviewAnim != null) {
+            mYOverviewAnim.getAnimationPlayer().cancel();
+        }
+        mShelfPeekAnim.setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
+        mMotionPauseDetector.clear();
+    }
+
+    private void clearState() {
+        cancelAnimations();
+        mNonOverviewAnim = null;
+        mXOverviewAnim = null;
+        mYOverviewAnim = null;
+        mIsHomeScreenVisible = true;
+        mSwipeDetector.finishedScrolling();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 20a2487..03862db 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -47,9 +47,9 @@
      * @return true if we should intercept the motion event
      */
     boolean canInterceptTouch(MotionEvent ev) {
-        if (mRecentsView.getChildCount() > 0) {
+        if (mRecentsView.getTaskViewCount() > 0) {
             // Allow swiping up in the gap between the hotseat and overview.
-            return ev.getY() >= mRecentsView.getChildAt(0).getBottom();
+            return ev.getY() >= mRecentsView.getTaskViewAt(0).getBottom();
         } else {
             // If there are no tasks, we only intercept if we're below the hotseat height.
             return isTouchOverHotseat(mLauncher, ev);
@@ -63,7 +63,7 @@
      * @return true if going back should take the user to the currently running task
      */
     boolean shouldSwipeDownReturnToApp() {
-        TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getNextPage());
+        TaskView taskView = mRecentsView.getNextPageTaskView();
         return taskView != null && mRecentsView.shouldSwipeDownLaunchApp();
     }
 
@@ -76,7 +76,7 @@
      */
     PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
         mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
-        TaskView taskView = mRecentsView.getTaskViewAt(mRecentsView.getCurrentPage());
+        TaskView taskView = mRecentsView.getCurrentPageTaskView();
         if (taskView == null) {
             throw new IllegalStateException("There is no task view to animate to.");
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index eb571f6..14216ff 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.view.MotionEvent;
@@ -42,7 +43,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.quickstep.OverviewInteractionState;
@@ -50,7 +51,7 @@
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Handles quick switching to a recent task from the home screen.
@@ -60,10 +61,10 @@
     private @Nullable TaskView mTaskToLaunch;
 
     public QuickSwitchTouchController(Launcher launcher) {
-        this(launcher, SwipeDetector.HORIZONTAL);
+        this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
     }
 
-    protected QuickSwitchTouchController(Launcher l, SwipeDetector.Direction dir) {
+    protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
         super(l, dir);
     }
 
@@ -95,6 +96,8 @@
         super.onDragStart(start);
         mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
         mTaskToLaunch = mLauncher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
+        ActivityManagerWrapper.getInstance()
+            .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 00e4f58..ad02de1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -19,6 +19,9 @@
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
@@ -32,7 +35,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.launcher3.util.PendingAnimation;
@@ -46,15 +50,14 @@
  * Touch controller for handling task view card swipes
  */
 public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
-        extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener {
-
-    private static final String TAG = "OverviewSwipeController";
+        extends AnimatorListenerAdapter implements TouchController,
+        SingleAxisSwipeDetector.Listener {
 
     // Progress after which the transition is assumed to be a success in case user does not fling
     public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
 
     protected final T mActivity;
-    private final SwipeDetector mDetector;
+    private final SingleAxisSwipeDetector mDetector;
     private final RecentsView mRecentsView;
     private final int[] mTempCords = new int[2];
 
@@ -74,7 +77,7 @@
     public TaskViewTouchController(T activity) {
         mActivity = activity;
         mRecentsView = activity.getOverviewPanel();
-        mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL);
+        mDetector = new SingleAxisSwipeDetector(activity, this, SingleAxisSwipeDetector.VERTICAL);
     }
 
     private boolean canInterceptTouch() {
@@ -113,7 +116,7 @@
             int directionsToDetectScroll = 0;
             boolean ignoreSlopWhenSettling = false;
             if (mCurrentAnimation != null) {
-                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                directionsToDetectScroll = DIRECTION_BOTH;
                 ignoreSlopWhenSettling = true;
             } else {
                 mTaskBeingDragged = null;
@@ -126,12 +129,12 @@
                         if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
                             // Don't allow swipe down to open if we don't support swipe up
                             // to enter overview.
-                            directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                            directionsToDetectScroll = DIRECTION_POSITIVE;
                         } else {
                             // The task can be dragged up to dismiss it,
                             // and down to open if it's the current page.
                             directionsToDetectScroll = i == mRecentsView.getCurrentPage()
-                                    ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE;
+                                    ? DIRECTION_BOTH : DIRECTION_POSITIVE;
                         }
                         break;
                     }
@@ -165,8 +168,8 @@
             return;
         }
         int scrollDirections = mDetector.getScrollDirections();
-        if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0)
-                || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) {
+        if (goingUp && ((scrollDirections & DIRECTION_POSITIVE) == 0)
+                || !goingUp && ((scrollDirections & DIRECTION_NEGATIVE) == 0)) {
             // Trying to re-init in an unsupported direction.
             return;
         }
@@ -243,7 +246,8 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
+        boolean fling = mDetector.isFling(velocity);
         final boolean goingToEnd;
         final int logAction;
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -260,7 +264,7 @@
             logAction = Touch.SWIPE;
             goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
         }
-        long animationDuration = SwipeDetector.calculateDuration(
+        long animationDuration = BaseSwipeDetector.calculateDuration(
                 velocity, goingToEnd ? (1 - progress) : progress);
         if (blockedFling && !goingToEnd) {
             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
index f1e4041..0ed5291 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
@@ -17,12 +17,12 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 
 public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController {
 
     public TransposedQuickSwitchTouchController(Launcher launcher) {
-        super(launcher, SwipeDetector.VERTICAL);
+        super(launcher, SingleAxisSwipeDetector.VERTICAL);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index d627a7f..5cce53e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -15,16 +15,13 @@
  */
 package com.android.quickstep;
 
-import static android.os.VibrationEffect.EFFECT_CLICK;
-import static android.os.VibrationEffect.createPredefined;
-
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
-import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
-import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 
 import android.animation.Animator;
@@ -39,14 +36,12 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.provider.Settings;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.UiThread;
+
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -54,8 +49,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
@@ -75,8 +70,6 @@
 
 import java.util.function.Consumer;
 
-import androidx.annotation.UiThread;
-
 /**
  * Base class for swipe up handler with some utility methods
  */
@@ -107,7 +100,6 @@
     protected final ClipAnimationHelper mClipAnimationHelper;
     protected final TransformParams mTransformParams = new TransformParams();
 
-    private final Vibrator mVibrator;
     protected final Mode mMode;
 
     // Shift in the range of [0, 1].
@@ -126,7 +118,7 @@
 
     protected Runnable mGestureEndCallback;
 
-    protected final Handler mMainThreadHandler = MAIN_THREAD_EXECUTOR.getHandler();
+    protected final Handler mMainThreadHandler = MAIN_EXECUTOR.getHandler();
     protected MultiStateCallback mStateCallback;
 
     protected boolean mCanceled;
@@ -148,7 +140,6 @@
 
         mClipAnimationHelper = new ClipAnimationHelper(context);
         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-        mVibrator = context.getSystemService(Vibrator.class);
         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
                 .getDeviceProfile(mContext));
     }
@@ -162,19 +153,7 @@
     }
 
     protected void performHapticFeedback() {
-        if (!mVibrator.hasVibrator()) {
-            return;
-        }
-        if (Settings.System.getInt(
-                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) {
-            return;
-        }
-
-        VibrationEffect effect = createPredefined(EFFECT_CLICK);
-        if (effect == null) {
-            return;
-        }
-        BACKGROUND_EXECUTOR.execute(() -> mVibrator.vibrate(effect));
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
     public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
@@ -229,10 +208,10 @@
         // Launch the task user scrolled to (mRecentsView.getNextPage()).
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // We finish recents animation inside launchTask() when live tile is enabled.
-            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false /* animate */,
+            mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
                     true /* freezeTaskList */);
         } else {
-            int taskId = mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).getTask().key.id;
+            int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
             mFinishingRecentsAnimationForNewTaskId = taskId;
             mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
                 if (!mCanceled) {
@@ -275,7 +254,7 @@
             overviewStackBounds = getStackBounds(dp);
         }
         dp.updateInsets(targetSet.homeContentInsets);
-        dp.updateIsSeascape(mContext.getSystemService(WindowManager.class));
+        dp.updateIsSeascape(mContext);
         if (runningTaskTarget != null) {
             mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index 36eb8a1..c0be9ec 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -24,7 +24,6 @@
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
@@ -53,15 +52,15 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherInitListenerEx;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
@@ -167,18 +166,8 @@
 
             @Override
             public void playAtomicAnimation(float velocity) {
-                // Setup workspace with 0 duration to prepare for our staggered animation.
-                LauncherStateManager stateManager = activity.getStateManager();
-                AnimatorSetBuilder builder = new AnimatorSetBuilder();
-                // setRecentsAttachedToAppWindow() will animate recents out.
-                builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
-                stateManager.createAtomicAnimation(BACKGROUND_APP, NORMAL, builder, ANIM_ALL, 0);
-                builder.build().start();
-
-                // Stop scrolling so that it doesn't interfere with the translation offscreen.
-                recentsView.getScroller().forceFinished(true);
-
-                new StaggeredWorkspaceAnim(activity, workspaceView, velocity).start();
+                new StaggeredWorkspaceAnim(activity, velocity, true /* animateOverviewScrim */)
+                        .start();
             }
         };
     }
@@ -201,7 +190,9 @@
         activity.getAppsView().reset(false /* animate */);
 
         return new AnimationFactory() {
-            private ShelfAnimState mShelfState;
+            private final ShelfPeekAnim mShelfAnim =
+                    ((QuickstepAppTransitionManagerImpl) activity.getAppTransitionManager())
+                            .getShelfPeekAnim();
             private boolean mIsAttachedToWindow;
 
             @Override
@@ -230,30 +221,7 @@
             @Override
             public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
                     long duration) {
-                if (mShelfState == shelfState) {
-                    return;
-                }
-                mShelfState = shelfState;
-                activity.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
-                if (mShelfState == ShelfAnimState.CANCEL) {
-                    return;
-                }
-                float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(activity);
-                float shelfOverviewProgress = OVERVIEW.getVerticalProgress(activity);
-                // Peek based on default overview progress so we can see hotseat if we're showing
-                // that instead of predictions in overview.
-                float defaultOverviewProgress = OverviewState.getDefaultVerticalProgress(activity);
-                float shelfPeekingProgress = shelfHiddenProgress
-                        - (shelfHiddenProgress - defaultOverviewProgress) * 0.25f;
-                float toProgress = mShelfState == ShelfAnimState.HIDE
-                        ? shelfHiddenProgress
-                        : mShelfState == ShelfAnimState.PEEK
-                                ? shelfPeekingProgress
-                                : shelfOverviewProgress;
-                Animator shelfAnim = activity.getStateManager()
-                        .createStateElementAnimation(INDEX_SHELF_ANIM, toProgress);
-                shelfAnim.setInterpolator(interpolator);
-                shelfAnim.setDuration(duration).start();
+                mShelfAnim.setShelfState(shelfState, interpolator, duration);
             }
 
             @Override
@@ -268,22 +236,12 @@
                         INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
 
                 int runningTaskIndex = recentsView.getRunningTaskIndex();
-                if (runningTaskIndex == 0) {
+                if (runningTaskIndex == recentsView.getTaskViewStartIndex()) {
                     // If we are on the first task (we haven't quick switched), translate recents in
                     // from the side. Calculate the start translation based on current scale/scroll.
                     float currScale = recentsView.getScaleX();
                     float scrollOffsetX = recentsView.getScrollOffset();
-
-                    float offscreenX = NORMAL.getOverviewScaleAndTranslation(activity).translationX;
-                    // The first task is hidden, so offset by its width.
-                    int firstTaskWidth = recentsView.getTaskViewAt(0).getWidth();
-                    offscreenX -= (firstTaskWidth + recentsView.getPageSpacing()) * currScale;
-                    // Offset since scale pushes tasks outwards.
-                    offscreenX += firstTaskWidth * (currScale - 1) / 2;
-                    offscreenX = Math.max(0, offscreenX);
-                    if (recentsView.isRtl()) {
-                        offscreenX = -offscreenX;
-                    }
+                    float offscreenX = recentsView.getOffscreenTranslationX(currScale);
 
                     float fromTranslationX = attached ? offscreenX - scrollOffsetX : 0;
                     float toTranslationX = attached ? 0 : offscreenX - scrollOffsetX;
@@ -351,8 +309,7 @@
     private void playScaleDownAnim(AnimatorSet anim, Launcher launcher, LauncherState fromState,
             LauncherState endState) {
         RecentsView recentsView = launcher.getOverviewPanel();
-        TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage());
-        if (v == null) {
+        if (recentsView.getCurrentPageTaskView() == null) {
             return;
         }
 
@@ -380,7 +337,7 @@
             // recents as a whole needs to translate further to keep up with the app window.
             TaskView runningTaskView = recentsView.getRunningTaskView();
             if (runningTaskView == null) {
-                runningTaskView = recentsView.getTaskViewAt(recentsView.getCurrentPage());
+                runningTaskView = recentsView.getCurrentPageTaskView();
                 if (runningTaskView == null) {
                     // There are no task views in LockTask mode when Overview is enabled.
                     return;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index 222ef97..a8d402e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,8 +15,8 @@
  */
 package com.android.quickstep;
 
-import static com.android.systemui.shared.system.ActivityManagerWrapper
-        .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -29,7 +29,6 @@
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -49,7 +48,6 @@
     private final Context mContext;
     private final ActivityManagerWrapper mAM;
     private final RecentsModel mRecentsModel;
-    private final MainThreadExecutor mMainThreadExecutor;
     private final OverviewComponentObserver mOverviewComponentObserver;
 
     private long mLastToggleTime;
@@ -57,7 +55,6 @@
     public OverviewCommandHelper(Context context, OverviewComponentObserver observer) {
         mContext = context;
         mAM = ActivityManagerWrapper.getInstance();
-        mMainThreadExecutor = new MainThreadExecutor();
         mRecentsModel = RecentsModel.INSTANCE.get(mContext);
         mOverviewComponentObserver = observer;
     }
@@ -69,19 +66,19 @@
         }
 
         mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-        mMainThreadExecutor.execute(new RecentsActivityCommand<>());
+        MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
     }
 
     public void onOverviewShown(boolean triggeredFromAltTab) {
-        mMainThreadExecutor.execute(new ShowRecentsCommand(triggeredFromAltTab));
+        MAIN_EXECUTOR.execute(new ShowRecentsCommand(triggeredFromAltTab));
     }
 
     public void onOverviewHidden() {
-        mMainThreadExecutor.execute(new HideRecentsCommand());
+        MAIN_EXECUTOR.execute(new HideRecentsCommand());
     }
 
     public void onTip(int actionType, int viewType) {
-        mMainThreadExecutor.execute(() ->
+        MAIN_EXECUTOR.execute(() ->
                 UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
     }
 
@@ -112,7 +109,7 @@
                 TaskView taskView = rv.getNextTaskView();
                 if (taskView == null) {
                     if (rv.getTaskViewCount() > 0) {
-                        taskView = (TaskView) rv.getPageAt(0);
+                        taskView = rv.getTaskViewAt(0);
                         taskView.requestFocus();
                     } else {
                         rv.requestFocus();
@@ -180,7 +177,7 @@
             // Otherwise, start overview.
             mListener = mHelper.createActivityInitListener(this::onActivityReady);
             mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
-                    this::createWindowAnimation, mContext, mMainThreadExecutor.getHandler(),
+                    this::createWindowAnimation, mContext, MAIN_EXECUTOR.getHandler(),
                     mAnimationProvider.getRecentsLaunchDuration());
         }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 2864c46..3c78dd8 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -1,12 +1,12 @@
 package com.android.quickstep;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.Context;
 import android.os.Bundle;
 
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
@@ -24,7 +24,7 @@
         switch (method) {
             case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
                 final float swipeHeight =
-                        OverviewState.getDefaultSwipeHeight(mContext, mDeviceProfile);
+                        LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
                 return response;
             }
@@ -46,7 +46,7 @@
 
             case TestProtocol.REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN: {
                 try {
-                    final int leftMargin = new MainThreadExecutor().submit(() ->
+                    final int leftMargin = MAIN_EXECUTOR.submit(() ->
                             mLauncher.<RecentsView>getOverviewPanel().getLeftGestureMargin()).get();
                     response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, leftMargin);
                 } catch (ExecutionException e) {
@@ -59,7 +59,7 @@
 
             case TestProtocol.REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN: {
                 try {
-                    final int rightMargin = new MainThreadExecutor().submit(() ->
+                    final int rightMargin = MAIN_EXECUTOR.submit(() ->
                             mLauncher.<RecentsView>getOverviewPanel().getRightGestureMargin()).
                             get();
                     response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, rightMargin);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index f08ae4a..9bdc98b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -136,6 +136,12 @@
     }
 
     @Override
+    public void returnToHomescreen() {
+        super.returnToHomescreen();
+        // TODO(b/137318995) This should go home, but doing so removes freeform windows
+    }
+
+    @Override
     public ActivityOptions getActivityLaunchOptions(final View v) {
         if (!(v instanceof TaskView)) {
             return null;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
index e51ba63..c4d3fa0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -19,6 +19,8 @@
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
 
+import android.os.SystemClock;
+import android.util.Log;
 import android.view.InputEvent;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -38,6 +40,8 @@
  */
 public class RecentsAnimationWrapper {
 
+    private static final String TAG = "RecentsAnimationWrapper";
+
     // A list of callbacks to run when we receive the recents animation target. There are different
     // than the state callbacks as these run on the current worker thread.
     private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
@@ -125,6 +129,7 @@
             boolean sendUserLeaveHint) {
         SwipeAnimationTargetSet controller = targetSet;
         targetSet = null;
+        disableInputProxy();
         if (controller != null) {
             controller.finishController(toRecents, onFinishComplete, sendUserLeaveHint);
         }
@@ -153,6 +158,16 @@
         mInputConsumerController.setInputListener(this::onInputConsumerEvent);
     }
 
+    private void disableInputProxy() {
+        if (mInputConsumer != null && mTouchInProgress) {
+            long now = SystemClock.uptimeMillis();
+            MotionEvent dummyCancel = MotionEvent.obtain(now,  now, ACTION_CANCEL, 0, 0, 0);
+            mInputConsumer.onMotionEvent(dummyCancel);
+            dummyCancel.recycle();
+        }
+        mInputConsumerController.setInputListener(null);
+    }
+
     private boolean onInputConsumerEvent(InputEvent ev) {
         if (ev instanceof MotionEvent) {
             onInputConsumerMotionEvent((MotionEvent) ev);
@@ -168,6 +183,18 @@
 
     private boolean onInputConsumerMotionEvent(MotionEvent ev) {
         int action = ev.getAction();
+
+        // Just to be safe, verify that ACTION_DOWN comes before any other action,
+        // and ignore any ACTION_DOWN after the first one (though that should not happen).
+        if (!mTouchInProgress && action != ACTION_DOWN) {
+            Log.w(TAG, "Received non-down motion before down motion: " + action);
+            return false;
+        }
+        if (mTouchInProgress && action == ACTION_DOWN) {
+            Log.w(TAG, "Received down motion while touch was already in progress");
+            return false;
+        }
+
         if (action == ACTION_DOWN) {
             mTouchInProgress = true;
             if (mInputConsumer == null) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
index c55f656..8783ee3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
@@ -15,7 +15,7 @@
  */
 package com.android.quickstep;
 
-import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.util.Log;
 
@@ -25,6 +25,7 @@
 import com.android.quickstep.util.RecentsAnimationListenerSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+
 import java.io.PrintWriter;
 
 /**
@@ -44,6 +45,7 @@
     public boolean goingToLauncher;
     public boolean recentsAnimationFinishInterrupted;
     public int nextRunningTaskId = -1;
+    private int mLogId;
 
     public void setOverviewComponentObserver(OverviewComponentObserver observer) {
         mOverviewComponentObserver = observer;
@@ -77,7 +79,7 @@
             mRecentsAnimationListener.removeListener(this);
             mRecentsAnimationListener.cancelListener();
             if (mLastAnimationRunning && mLastAnimationTarget != null) {
-                Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(),
+                Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(),
                         finishAnimation
                                 ? mLastAnimationTarget::finishAnimation
                                 : mLastAnimationTarget::cancelAnimation);
@@ -155,5 +157,10 @@
         pw.println(prefix + "nextRunningTaskId=" + nextRunningTaskId);
         pw.println(prefix + "lastAnimationCancelled=" + mLastAnimationCancelled);
         pw.println(prefix + "lastAnimationRunning=" + mLastAnimationRunning);
+        pw.println(prefix + "logTraceId=" + mLogId);
+    }
+
+    public void setLogTraceId(int logId) {
+        this.mLogId = logId;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
index fd45923..1af0db0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
@@ -36,8 +36,6 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
@@ -267,12 +265,16 @@
 
         @Override
         protected ActivityOptions makeLaunchOptions(Activity activity) {
-            return ActivityOptionsCompat.makeFreeformOptions();
+            ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
+            // Arbitrary bounds only because freeform is in dev mode right now
+            Rect r = new Rect(50, 50, 200, 200);
+            activityOptions.setLaunchBounds(r);
+            return activityOptions;
         }
 
         @Override
         protected boolean onActivityStarted(BaseDraggingActivity activity) {
-            Launcher.getLauncher(activity).getStateManager().goToState(LauncherState.NORMAL);
+            activity.returnToHomescreen();
             return true;
         }
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 86ba855..f321826 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -23,6 +23,8 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.FAKE_LANDSCAPE_UI;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
@@ -40,7 +42,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.Service;
-import android.app.TaskInfo;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -50,8 +51,6 @@
 import android.graphics.Point;
 import android.graphics.RectF;
 import android.graphics.Region;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -61,29 +60,26 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Choreographer;
-import android.view.Display;
 import android.view.InputEvent;
 import android.view.MotionEvent;
 import android.view.Surface;
-import android.view.WindowManager;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.config.BaseFlags;
 import com.android.launcher3.logging.EventLogArray;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
@@ -96,6 +92,7 @@
 import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
+import com.android.quickstep.util.AssistantUtilities;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -107,7 +104,6 @@
 import com.android.systemui.shared.system.RecentsAnimationListener;
 import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
 
-import com.android.systemui.shared.system.TaskInfoCompat;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -136,11 +132,14 @@
  */
 @TargetApi(Build.VERSION_CODES.Q)
 public class TouchInteractionService extends Service implements
-        NavigationModeChangeListener, DisplayListener {
+        NavigationModeChangeListener, DefaultDisplay.DisplayInfoChangeListener {
 
-    public static final MainThreadExecutor MAIN_THREAD_EXECUTOR = new MainThreadExecutor();
-    public static final LooperExecutor BACKGROUND_EXECUTOR =
-            new LooperExecutor(UiThreadHelper.getBackgroundLooper());
+    /**
+     * NOTE: This value should be kept same as
+     * ActivityTaskManagerService#INTENT_EXTRA_LOG_TRACE_ID in platform
+     */
+    public static final String INTENT_EXTRA_LOG_TRACE_ID = "INTENT_EXTRA_LOG_TRACE_ID";
+
 
     public static final EventLogArray TOUCH_INTERACTION_LOG =
             new EventLogArray("touch_interaction_log", 40);
@@ -161,9 +160,9 @@
         public void onInitialize(Bundle bundle) {
             mISystemUiProxy = ISystemUiProxy.Stub
                     .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
-            MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor);
-            MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::onSystemUiProxySet);
-            MAIN_THREAD_EXECUTOR.execute(() -> preloadOverview(true /* fromInit */));
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor);
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::onSystemUiProxySet);
+            MAIN_EXECUTOR.execute(() -> preloadOverview(true /* fromInit */));
             sIsInitialized = true;
         }
 
@@ -198,7 +197,7 @@
         @Override
         public void onAssistantVisibilityChanged(float visibility) {
             mLastAssistantVisibility = visibility;
-            MAIN_THREAD_EXECUTOR.execute(
+            MAIN_EXECUTOR.execute(
                     TouchInteractionService.this::onAssistantVisibilityChanged);
         }
 
@@ -214,13 +213,13 @@
                     isButton, gestureSwipeLeft, activityControl.getContainerType());
 
             if (completed && !isButton && shouldNotifyBackGesture()) {
-                BACKGROUND_EXECUTOR.execute(TouchInteractionService.this::tryNotifyBackGesture);
+                UI_HELPER_EXECUTOR.execute(TouchInteractionService.this::tryNotifyBackGesture);
             }
         }
 
         public void onSystemUiStateChanged(int stateFlags) {
             mSystemUiStateFlags = stateFlags;
-            MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::onSystemUiFlagsChanged);
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::onSystemUiFlagsChanged);
         }
 
         /** Deprecated methods **/
@@ -244,6 +243,7 @@
     private static boolean sConnected = false;
     private static boolean sIsInitialized = false;
     private static final SwipeSharedState sSwipeSharedState = new SwipeSharedState();
+    private int mLogId;
 
     public static boolean isConnected() {
         return sConnected;
@@ -323,8 +323,7 @@
             registerReceiver(mUserUnlockedReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
         }
 
-        mDefaultDisplayId = getSystemService(WindowManager.class).getDefaultDisplay()
-                .getDisplayId();
+        mDefaultDisplayId = DefaultDisplay.INSTANCE.get(this).getInfo().id;
         String blockingActivity = getString(R.string.gesture_blocking_activity);
         mGestureBlockingActivity = TextUtils.isEmpty(blockingActivity) ? null :
                 ComponentName.unflattenFromString(blockingActivity);
@@ -391,9 +390,8 @@
             return;
         }
 
-        Display defaultDisplay = getSystemService(WindowManager.class).getDefaultDisplay();
-        Point realSize = new Point();
-        defaultDisplay.getRealSize(realSize);
+        DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(this).getInfo();
+        Point realSize = new Point(displayInfo.realSize);
         mSwipeTouchRegion.set(0, 0, realSize.x, realSize.y);
         if (mMode == Mode.NO_BUTTON) {
             int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
@@ -415,7 +413,7 @@
         } else {
             mAssistantLeftRegion.setEmpty();
             mAssistantRightRegion.setEmpty();
-            switch (defaultDisplay.getRotation()) {
+            switch (displayInfo.rotation) {
                 case Surface.ROTATION_90:
                     mSwipeTouchRegion.left = mSwipeTouchRegion.right
                             - getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
@@ -438,10 +436,9 @@
         }
         if (mMode.hasGestures != newMode.hasGestures) {
             if (newMode.hasGestures) {
-                getSystemService(DisplayManager.class).registerDisplayListener(
-                        this, MAIN_THREAD_EXECUTOR.getHandler());
+                DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
             } else {
-                getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+                DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
             }
         }
         mMode = newMode;
@@ -457,14 +454,8 @@
     }
 
     @Override
-    public void onDisplayAdded(int i) { }
-
-    @Override
-    public void onDisplayRemoved(int i) { }
-
-    @Override
-    public void onDisplayChanged(int displayId) {
-        if (displayId != mDefaultDisplayId) {
+    public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+        if (info.id != mDefaultDisplayId) {
             return;
         }
 
@@ -529,7 +520,7 @@
         }
         disposeEventHandlers();
         if (mMode.hasGestures) {
-            getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+            DefaultDisplay.INSTANCE.get(this).removeChangeListener(this);
         }
 
         sConnected = false;
@@ -554,9 +545,12 @@
             Log.e(TAG, "Unknown event " + ev);
             return;
         }
+
         MotionEvent event = (MotionEvent) ev;
-        TOUCH_INTERACTION_LOG.addLog("onMotionEvent", event.getActionMasked());
         if (event.getAction() == ACTION_DOWN) {
+            mLogId = TOUCH_INTERACTION_LOG.generateAndSetLogId();
+            sSwipeSharedState.setLogTraceId(mLogId);
+
             if (mSwipeTouchRegion.contains(event.getX(), event.getY())) {
                 boolean useSharedState = mConsumer.useSharedSwipeState();
                 mConsumer.onConsumerAboutToBeSwitched();
@@ -576,6 +570,8 @@
                 mUncheckedConsumer = InputConsumer.NO_OP;
             }
         }
+
+        TOUCH_INTERACTION_LOG.addLog("onMotionEvent", event.getActionMasked());
         mUncheckedConsumer.onMotionEvent(event);
     }
 
@@ -653,16 +649,14 @@
                 mOverviewComponentObserver.getActivityControlHelper();
 
         boolean forceOverviewInputConsumer = false;
-        if (isExcludedAssistant(runningTaskInfo)) {
+        if (AssistantUtilities.isExcludedAssistant(runningTaskInfo)) {
             // In the case where we are in the excluded assistant state, ignore it and treat the
             // running activity as the task behind the assistant
-            runningTaskInfo = mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT);
-            if (!ActivityManagerWrapper.isHomeTask(runningTaskInfo)) {
-                final ComponentName homeComponent =
-                    mOverviewComponentObserver.getHomeIntent().getComponent();
-                forceOverviewInputConsumer =
-                    runningTaskInfo.baseIntent.getComponent().equals(homeComponent);
-            }
+            runningTaskInfo = mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT /* ignoreActivityType */);
+            ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
+            ComponentName runningComponent = runningTaskInfo.baseIntent.getComponent();
+            forceOverviewInputConsumer =
+                runningComponent != null && runningComponent.equals(homeComponent);
         }
 
         if (runningTaskInfo == null && !sSwipeSharedState.goingToLauncher
@@ -676,9 +670,9 @@
             return createOtherActivityInputConsumer(event, info);
         } else if (sSwipeSharedState.goingToLauncher || activityControl.isResumed()
                 || forceOverviewInputConsumer) {
-            return createOverviewInputConsumer(event);
+            return createOverviewInputConsumer(event, forceOverviewInputConsumer);
         } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityControl.isInLiveTileMode()) {
-            return createOverviewInputConsumer(event);
+            return createOverviewInputConsumer(event, forceOverviewInputConsumer);
         } else if (mGestureBlockingActivity != null && runningTaskInfo != null
                 && mGestureBlockingActivity.equals(runningTaskInfo.topActivity)) {
             return mResetGestureInputConsumer;
@@ -687,12 +681,6 @@
         }
     }
 
-    private boolean isExcludedAssistant(TaskInfo info) {
-        return info != null
-                && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
-                && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
-    }
-
     private boolean disableHorizontalSwipe(MotionEvent event) {
         // mExclusionRegion can change on binder thread, use a local instance here.
         Region exclusionRegion = mExclusionRegion;
@@ -718,19 +706,20 @@
         return new OtherActivityInputConsumer(this, runningTaskInfo,
                 shouldDefer, mOverviewCallbacks, this::onConsumerInactive,
                 sSwipeSharedState, mInputMonitorCompat, mSwipeTouchRegion,
-                disableHorizontalSwipe(event), factory);
+                disableHorizontalSwipe(event), factory, mLogId);
     }
 
     private InputConsumer createDeviceLockedInputConsumer(RunningTaskInfo taskInfo) {
         if (mMode == Mode.NO_BUTTON && taskInfo != null) {
             return new DeviceLockedInputConsumer(this, sSwipeSharedState, mInputMonitorCompat,
-                    mSwipeTouchRegion, taskInfo.taskId);
+                    mSwipeTouchRegion, taskInfo.taskId, mLogId);
         } else {
             return mResetGestureInputConsumer;
         }
     }
 
-    public InputConsumer createOverviewInputConsumer(MotionEvent event) {
+    public InputConsumer createOverviewInputConsumer(MotionEvent event,
+            boolean forceOverviewInputConsumer) {
         final ActivityControlHelper activityControl =
                 mOverviewComponentObserver.getActivityControlHelper();
         BaseDraggingActivity activity = activityControl.getCreatedActivity();
@@ -738,7 +727,10 @@
             return mResetGestureInputConsumer;
         }
 
-        if (activity.getRootView().hasWindowFocus() || sSwipeSharedState.goingToLauncher) {
+        if (activity.getRootView().hasWindowFocus()
+                || sSwipeSharedState.goingToLauncher
+                || (BaseFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+                    && forceOverviewInputConsumer)) {
             return new OverviewInputConsumer(activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
@@ -788,7 +780,8 @@
         }
 
         // Pass null animation handler to indicate this start is preload.
-        startRecentsActivityAsync(mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState(), null);
+        startRecentsActivityAsync(mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState(),
+                null);
     }
 
     @Override
@@ -897,7 +890,7 @@
     }
 
     public static void startRecentsActivityAsync(Intent intent, RecentsAnimationListener listener) {
-        BACKGROUND_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
+        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
                 .startRecentsActivity(intent, null, listener, null, null));
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index a3bd348..2fa4feb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -26,14 +26,14 @@
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE;
-import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME;
 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK;
 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK;
 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
+import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
@@ -74,13 +74,14 @@
 import com.android.launcher3.util.RaceConditionTracker;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.ActivityControlHelper.AnimationFactory;
-import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState;
 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.inputconsumers.InputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ClipAnimationHelper.TargetAlphaProvider;
 import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
@@ -192,7 +193,6 @@
             Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
     private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
 
-    private static final long SHELF_ANIM_DURATION = 240;
     public static final long RECENTS_ATTACH_DURATION = 300;
 
     /**
@@ -206,8 +206,6 @@
     private boolean mIsShelfPeeking;
 
     private boolean mContinuingLastGesture;
-    // To avoid UI jump when gesture is started, we offset the animation by the threshold.
-    private float mShiftAtGestureStart = 0;
 
     private ThumbnailData mTaskSnapshot;
 
@@ -442,7 +440,7 @@
 
     @Override
     public void onMotionPauseChanged(boolean isPaused) {
-        setShelfState(isPaused ? PEEK : HIDE, OVERSHOOT_1_2, SHELF_ANIM_DURATION);
+        setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
     }
 
     public void maybeUpdateRecentsAttachedState() {
@@ -479,8 +477,8 @@
             recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
             if (animate) {
                 // Only animate if an adjacent task view is visible on screen.
-                TaskView adjacentTask1 = mRecentsView.getTaskViewAt(runningTaskIndex + 1);
-                TaskView adjacentTask2 = mRecentsView.getTaskViewAt(runningTaskIndex - 1);
+                TaskView adjacentTask1 = mRecentsView.getNextTaskView();
+                TaskView adjacentTask2 = mRecentsView.getPreviousTaskView();
                 float prevTranslationX = mRecentsView.getTranslationX();
                 mRecentsView.setTranslationX(0);
                 animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT))
@@ -580,9 +578,7 @@
         // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
         // anyway. The controller mimics the drag length factor by applying it to its interpolators.
         float progress = mCurrentShift.value / mDragLengthFactor;
-        mLauncherTransitionController.setPlayFraction(
-                progress <= mShiftAtGestureStart || mShiftAtGestureStart >= 1
-                        ? 0 : (progress - mShiftAtGestureStart) / (1 - mShiftAtGestureStart));
+        mLauncherTransitionController.setPlayFraction(progress);
     }
 
     /**
@@ -590,8 +586,7 @@
      */
     private void updateSysUiFlags(float windowProgress) {
         if (mRecentsView != null) {
-            TaskView centermostTask = mRecentsView.getTaskViewAt(mRecentsView
-                    .getPageNearestToCenterOfScreen());
+            TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
             int centermostTaskFlags = centermostTask == null ? 0
                     : centermostTask.getThumbnail().getSysUiStatusNavFlags();
             boolean useHomeScreenFlags = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -623,7 +618,6 @@
     @Override
     public void onGestureStarted() {
         notifyGestureStartedAsync();
-        mShiftAtGestureStart = mCurrentShift.value;
         setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
     }
@@ -923,7 +917,15 @@
             windowAnim.addListener(new AnimationSuccessListener() {
                 @Override
                 public void onAnimationSuccess(Animator animator) {
-                    setStateOnUiThread(target.endState);
+                    if (target == NEW_TASK && mRecentsView != null
+                            && mRecentsView.getNextPage() == mRecentsView.getRunningTaskIndex()) {
+                        // We are about to launch the current running task, so use LAST_TASK state
+                        // instead of NEW_TASK. This could happen, for example, if our scroll is
+                        // aborted after we determined the target to be NEW_TASK.
+                        setStateOnUiThread(LAST_TASK.endState);
+                    } else {
+                        setStateOnUiThread(target.endState);
+                    }
                 }
             });
             windowAnim.start();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 2e9c0a3..7f1aae5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -176,7 +176,7 @@
     protected void applyLoadPlan(ArrayList<Task> tasks) {
         // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
-        // track the index of the next task appropriately, as it we are switching on any other app.
+        // track the index of the next task appropriately, as if we are switching on any other app.
         if (mRunningTaskInfo != null && mRunningTaskInfo.taskId == mRunningTaskId) {
             // Check if the task list has running task
             boolean found = false;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 3d763ab..b24c788 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -22,8 +22,9 @@
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.TouchInteractionService.INTENT_EXTRA_LOG_TRACE_ID;
 import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -35,10 +36,10 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
-import android.view.WindowManager;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.LockScreenRecentsActivity;
 import com.android.quickstep.MultiStateCallback;
 import com.android.quickstep.SwipeSharedState;
@@ -76,6 +77,7 @@
 
     private final PointF mTouchDown = new PointF();
     private final ClipAnimationHelper mClipAnimationHelper;
+    private int mLogId;
     private final ClipAnimationHelper.TransformParams mTransformParams;
     private final Point mDisplaySize;
     private final MultiStateCallback mStateCallback;
@@ -90,19 +92,20 @@
     private SwipeAnimationTargetSet mTargetSet;
 
     public DeviceLockedInputConsumer(Context context, SwipeSharedState swipeSharedState,
-            InputMonitorCompat inputMonitorCompat, RectF swipeTouchRegion, int runningTaskId) {
+            InputMonitorCompat inputMonitorCompat, RectF swipeTouchRegion, int runningTaskId,
+            int logId) {
         mContext = context;
         mTouchSlopSquared = squaredTouchSlop(context);
         mSwipeSharedState = swipeSharedState;
         mClipAnimationHelper = new ClipAnimationHelper(context);
+        mLogId = logId;
         mTransformParams = new ClipAnimationHelper.TransformParams();
         mInputMonitorCompat = inputMonitorCompat;
         mSwipeTouchRegion = swipeTouchRegion;
         mRunningTaskId = runningTaskId;
 
         // Do not use DeviceProfile as the user data might be locked
-        mDisplaySize = new Point();
-        context.getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(mDisplaySize);
+        mDisplaySize = DefaultDisplay.INSTANCE.get(context).getInfo().realSize;
 
         // Init states
         mStateCallback = new MultiStateCallback(STATE_NAMES);
@@ -205,7 +208,8 @@
         Intent intent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_DEFAULT)
                 .setComponent(new ComponentName(mContext, LockScreenRecentsActivity.class))
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+                .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId);
 
         mInputMonitorCompat.pilferPointers();
         startRecentsActivityAsync(intent, newListenerSet);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
index 6ec1da0..e0ff8af 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
@@ -50,7 +50,6 @@
 import com.android.quickstep.util.ObjectWrapper;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 86766d9..e41880d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -22,11 +22,11 @@
 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.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
+import static com.android.quickstep.TouchInteractionService.INTENT_EXTRA_LOG_TRACE_ID;
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -35,6 +35,7 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.Intent;
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.os.Build;
@@ -43,12 +44,13 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
-
+import androidx.annotation.UiThread;
 import com.android.launcher3.R;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RaceConditionTracker;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.BaseSwipeUpHandler;
+import com.android.quickstep.BaseSwipeUpHandler.Factory;
 import com.android.quickstep.OverviewCallbacks;
 import com.android.quickstep.SwipeSharedState;
 import com.android.quickstep.SysUINavigationMode;
@@ -59,11 +61,8 @@
 import com.android.quickstep.util.RecentsAnimationListenerSet;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
-
 import java.util.function.Consumer;
 
-import androidx.annotation.UiThread;
-
 /**
  * Input consumer for handling events originating from an activity other than Launcher
  */
@@ -119,14 +118,16 @@
         ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
                 true /* restoreHomeStackPosition */);
     };
+    private int mLogId;
 
     public OtherActivityInputConsumer(Context base, RunningTaskInfo runningTaskInfo,
             boolean isDeferredDownTarget, OverviewCallbacks overviewCallbacks,
             Consumer<OtherActivityInputConsumer> onCompleteCallback,
             SwipeSharedState swipeSharedState, InputMonitorCompat inputMonitorCompat,
             RectF swipeTouchRegion, boolean disableHorizontalSwipe,
-            BaseSwipeUpHandler.Factory handlerFactory) {
+            Factory handlerFactory, int logId) {
         super(base);
+        mLogId = logId;
 
         mMainThreadHandler = new Handler(Looper.getMainLooper());
         mRunningTask = runningTaskInfo;
@@ -341,7 +342,9 @@
             RecentsAnimationListenerSet newListenerSet =
                     mSwipeSharedState.newRecentsAnimationListenerSet();
             newListenerSet.addListener(handler);
-            startRecentsActivityAsync(handler.getLaunchIntent(), newListenerSet);
+            Intent intent = handler.getLaunchIntent();
+            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId);
+            startRecentsActivityAsync(intent, newListenerSet);
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 05cbb78..f40d552 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -22,9 +22,9 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.PointF;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -34,12 +34,9 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogUtils;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.quickstep.OverviewCallbacks;
 import com.android.quickstep.util.NavBarPosition;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public class OverviewWithoutFocusInputConsumer implements InputConsumer {
@@ -148,9 +145,9 @@
         }
 
         if (triggerQuickstep) {
-            OverviewCallbacks.get(mContext).closeAllWindows();
-            ActivityManagerWrapper.getInstance()
-                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+            mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+                    .addCategory(Intent.CATEGORY_HOME)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
             TOUCH_INTERACTION_LOG.addLog("startQuickstep");
             BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
             int pageIndex = -1; // This number doesn't reflect workspace page index.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
new file mode 100644
index 0000000..b251f9e
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 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.logging;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+
+/**
+ * This class handles AOSP MetricsLogger function calls and logging around
+ * quickstep interactions and app launches.
+ */
+@SuppressWarnings("unused")
+public class UserEventDispatcherAppPredictionExtension extends UserEventDispatcherExtension {
+
+    public static final int ALL_APPS_PREDICTION_TIPS = 2;
+
+    private static final String TAG = "UserEventDispatcher";
+
+    public UserEventDispatcherAppPredictionExtension(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void onFillInLogContainerData(
+            @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target,
+            @NonNull LauncherLogProto.Target targetParent) {
+        PredictionUiStateManager.fillInPredictedRank(itemInfo, target);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
index cae273a..fa3be9c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.views.RecentsView;
@@ -50,8 +51,6 @@
 import com.android.systemui.shared.system.TransactionCompat;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
-import java.util.function.BiFunction;
-
 /**
  * Utility class to handle window clip animation
  */
@@ -213,6 +212,11 @@
                         }
                         mCurrentCornerRadius = cornerRadius;
                     }
+                    // Fade out Assistant overlay.
+                    if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
+                            && app.isNotInRecents) {
+                        alpha = 1 - Interpolators.DEACCEL_2_5.getInterpolation(progress);
+                    }
                 } else if (targetSet.hasRecents) {
                     // If home has a different target then recents, reverse anim the
                     // home target.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
index 3ce341d..bbb318a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
@@ -21,9 +21,9 @@
 
 import android.content.Context;
 import android.view.Surface;
-import android.view.WindowManager;
 
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.SysUINavigationMode;
 
 /**
@@ -36,8 +36,7 @@
 
     public NavBarPosition(Context context) {
         mMode = SysUINavigationMode.getMode(context);
-        mDisplayRotation = context.getSystemService(WindowManager.class)
-                .getDefaultDisplay().getRotation();
+        mDisplayRotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
     }
 
     public boolean isRightEdge() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
index 14083dd..b1999d7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
@@ -15,11 +15,13 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.graphics.Rect;
 import android.util.ArraySet;
 
+import androidx.annotation.UiThread;
+
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Preconditions;
 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
@@ -31,8 +33,6 @@
 import java.util.Set;
 import java.util.function.Consumer;
 
-import androidx.annotation.UiThread;
-
 /**
  * Wrapper around {@link RecentsAnimationListener} which delegates callbacks to multiple listeners
  * on the main thread
@@ -82,7 +82,7 @@
         if (mCancelled) {
             targetSet.cancelAnimation();
         } else {
-            Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
+            Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
                 for (SwipeAnimationListener listener : getListeners()) {
                     listener.onRecentsAnimationStart(targetSet);
                 }
@@ -92,14 +92,14 @@
 
     @Override
     public final void onAnimationCanceled(ThumbnailData thumbnailData) {
-        Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
+        Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
             for (SwipeAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationCanceled();
             }
         });
         // TODO: handle the transition better instead of simply using a transition delay.
         if (thumbnailData != null) {
-            MAIN_THREAD_EXECUTOR.getHandler().postDelayed(() -> mController.cleanupScreenshot(),
+            MAIN_EXECUTOR.getHandler().postDelayed(() -> mController.cleanupScreenshot(),
                     TRANSITION_DELAY);
         }
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
new file mode 100644
index 0000000..83bc416
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
@@ -0,0 +1,105 @@
+/*
+ * 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.util;
+
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_SHELF_ANIM;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.uioverrides.states.OverviewState;
+
+/**
+ * Animates the shelf between states HIDE, PEEK, and OVERVIEW.
+ */
+
+public class ShelfPeekAnim {
+
+    public static final Interpolator INTERPOLATOR = OVERSHOOT_1_2;
+    public static final long DURATION = 240;
+
+    private final Launcher mLauncher;
+
+    private ShelfAnimState mShelfState;
+    private boolean mIsPeeking;
+
+    public ShelfPeekAnim(Launcher launcher) {
+        mLauncher = launcher;
+    }
+
+    /**
+     * Animates to the given state, canceling the previous animation if it was still running.
+     */
+    public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
+        if (mShelfState == shelfState) {
+            return;
+        }
+        mLauncher.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
+        mShelfState = shelfState;
+        mIsPeeking = mShelfState == ShelfAnimState.PEEK || mShelfState == ShelfAnimState.HIDE;
+        if (mShelfState == ShelfAnimState.CANCEL) {
+            return;
+        }
+        float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(mLauncher);
+        float shelfOverviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
+        // Peek based on default overview progress so we can see hotseat if we're showing
+        // that instead of predictions in overview.
+        float defaultOverviewProgress = OverviewState.getDefaultVerticalProgress(mLauncher);
+        float shelfPeekingProgress = shelfHiddenProgress
+                - (shelfHiddenProgress - defaultOverviewProgress) * 0.25f;
+        float toProgress = mShelfState == ShelfAnimState.HIDE
+                ? shelfHiddenProgress
+                : mShelfState == ShelfAnimState.PEEK
+                        ? shelfPeekingProgress
+                        : shelfOverviewProgress;
+        Animator shelfAnim = mLauncher.getStateManager()
+                .createStateElementAnimation(INDEX_SHELF_ANIM, toProgress);
+        shelfAnim.setInterpolator(interpolator);
+        shelfAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mShelfState = ShelfAnimState.CANCEL;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                mIsPeeking = mShelfState == ShelfAnimState.PEEK;
+            }
+        });
+        shelfAnim.setDuration(duration).start();
+    }
+
+    /** @return Whether the shelf is currently peeking or animating to or from peeking. */
+    public boolean isPeeking() {
+        return mIsPeeking;
+    }
+
+    /** The various shelf states we can animate to. */
+    public enum ShelfAnimState {
+        HIDE(true), PEEK(true), OVERVIEW(false), CANCEL(false);
+
+        ShelfAnimState(boolean shouldPreformHaptic) {
+            this.shouldPreformHaptic = shouldPreformHaptic;
+        }
+
+        public final boolean shouldPreformHaptic;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 1aa5365..958ef7d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 
 import android.animation.Animator;
@@ -27,13 +28,11 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
@@ -41,9 +40,8 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.SpringObjectAnimator;
-import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.views.IconLabelDotView;
+import com.android.quickstep.views.RecentsView;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -66,18 +64,12 @@
     private final float mVelocity;
     private final float mSpringTransY;
 
-    // The original view of the {@link FloatingIconView}.
-    private final View mOriginalView;
-
     private final List<Animator> mAnimators = new ArrayList<>();
 
-    /**
-     * @param floatingViewOriginalView The FloatingIconView's original view.
-     */
-    public StaggeredWorkspaceAnim(Launcher launcher, @Nullable View floatingViewOriginalView,
-            float velocity) {
+    public StaggeredWorkspaceAnim(Launcher launcher, float velocity, boolean animateOverviewScrim) {
+        prepareToAnimate(launcher);
+
         mVelocity = velocity;
-        mOriginalView = floatingViewOriginalView;
 
         // Scale the translationY based on the initial velocity to better sync the workspace items
         // with the floating view.
@@ -133,8 +125,10 @@
             addStaggeredAnimationForView(qsb, grid.inv.numRows + 2, totalRows);
         }
 
-        addScrimAnimationForState(launcher, BACKGROUND_APP, 0);
-        addScrimAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+        if (animateOverviewScrim) {
+            addScrimAnimationForState(launcher, BACKGROUND_APP, 0);
+            addScrimAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+        }
 
         AnimatorListener resetClipListener = new AnimatorListenerAdapter() {
             int numAnimations = mAnimators.size();
@@ -161,6 +155,21 @@
     }
 
     /**
+     * Setup workspace with 0 duration to prepare for our staggered animation.
+     */
+    private void prepareToAnimate(Launcher launcher) {
+        LauncherStateManager stateManager = launcher.getStateManager();
+        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+        // setRecentsAttachedToAppWindow() will animate recents out.
+        builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
+        stateManager.createAtomicAnimation(BACKGROUND_APP, NORMAL, builder, ANIM_ALL, 0);
+        builder.build().start();
+
+        // Stop scrolling so that it doesn't interfere with the translation offscreen.
+        launcher.<RecentsView>getOverviewPanel().getScroller().forceFinished(true);
+    }
+
+    /**
      * Starts the animation.
      */
     public void start() {
@@ -192,35 +201,12 @@
         springTransY.setStartDelay(startDelay);
         mAnimators.add(springTransY);
 
-        ObjectAnimator alpha = getAlphaAnimator(v, startDelay);
-        if (v == mOriginalView) {
-            // For IconLabelDotViews, we just want the label to fade in.
-            // Icon, badge, and dots will animate in separately (controlled via FloatingIconView)
-            if (v instanceof IconLabelDotView) {
-                alpha.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        IconLabelDotView view = (IconLabelDotView) v;
-                        view.setIconVisible(false);
-                        view.setForceHideDot(true);
-                    }
-                });
-            } else {
-                return;
-            }
-        }
-
         v.setAlpha(0);
-        mAnimators.add(alpha);
-    }
-
-    private ObjectAnimator getAlphaAnimator(View v, long startDelay) {
         ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
         alpha.setInterpolator(LINEAR);
         alpha.setDuration(ALPHA_DURATION_MS);
         alpha.setStartDelay(startDelay);
-        return alpha;
-
+        mAnimators.add(alpha);
     }
 
     private void addScrimAnimationForState(Launcher launcher, LauncherState state, long duration) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
index 381c27a..3619d3a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
@@ -15,8 +15,8 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
-import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.graphics.Rect;
@@ -68,25 +68,25 @@
 
     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
         mOnFinishListener.accept(this);
-        BACKGROUND_EXECUTOR.execute(() -> {
+        UI_HELPER_EXECUTOR.execute(() -> {
             controller.setInputConsumerEnabled(false);
             controller.finish(toRecents, sendUserLeaveHint);
 
             if (callback != null) {
-                MAIN_THREAD_EXECUTOR.execute(callback);
+                MAIN_EXECUTOR.execute(callback);
             }
         });
     }
 
     public void enableInputConsumer() {
-        BACKGROUND_EXECUTOR.submit(() -> {
+        UI_HELPER_EXECUTOR.submit(() -> {
             controller.hideCurrentInputMethod();
             controller.setInputConsumerEnabled(true);
         });
     }
 
     public void setWindowThresholdCrossed(boolean thresholdCrossed) {
-        BACKGROUND_EXECUTOR.execute(() -> {
+        UI_HELPER_EXECUTOR.execute(() -> {
             controller.setAnimationTargetsBehindSystemBars(!thresholdCrossed);
             if (mShouldMinimizeSplitScreen && thresholdCrossed) {
                 // NOTE: As a workaround for conflicting animations (Launcher animating the task
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 7fac813..b06d4bc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
 
 import static com.android.launcher3.Utilities.prefixTextWithIcon;
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.ActivityOptions;
@@ -41,7 +42,6 @@
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.systemui.shared.recents.model.Task;
 
@@ -117,7 +117,7 @@
             return;
         }
 
-        Utilities.THREAD_POOL_EXECUTOR.execute(() -> {
+        THREAD_POOL_EXECUTOR.execute(() -> {
             final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
                     task.getTopComponent().getPackageName(),
                     UserHandle.of(task.key.userId));
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 03441c8..c2cb720 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -35,6 +35,7 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
+import android.widget.FrameLayout;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Hotseat;
@@ -45,11 +46,14 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.util.ClipAnimationHelper;
 import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
 import com.android.quickstep.util.LayoutUtils;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.RecentsExtraCard;
 
 /**
  * {@link RecentsView} used in Launcher activity
@@ -57,8 +61,29 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class LauncherRecentsView extends RecentsView<Launcher> implements StateListener {
 
+    private static final Rect sTempRect = new Rect();
+
     private final TransformParams mTransformParams = new TransformParams();
 
+    private RecentsExtraCard mRecentsExtraCardPlugin;
+    private RecentsExtraViewContainer mRecentsExtraViewContainer;
+    private PluginListener<RecentsExtraCard> mRecentsExtraCardPluginListener =
+            new PluginListener<RecentsExtraCard>() {
+        @Override
+        public void onPluginConnected(RecentsExtraCard recentsExtraCard, Context context) {
+            createRecentsExtraCard();
+            mRecentsExtraCardPlugin = recentsExtraCard;
+            mRecentsExtraCardPlugin.setupView(context, mRecentsExtraViewContainer, mActivity);
+        }
+
+        @Override
+        public void onPluginDisconnected(RecentsExtraCard plugin) {
+            removeView(mRecentsExtraViewContainer);
+            mRecentsExtraCardPlugin = null;
+            mRecentsExtraViewContainer = null;
+        }
+    };
+
     public LauncherRecentsView(Context context) {
         this(context, null);
     }
@@ -144,6 +169,25 @@
         LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
     }
 
+    /**
+     * @return The translationX to apply to this view so that the first task is just offscreen.
+     */
+    public float getOffscreenTranslationX(float recentsScale) {
+        float offscreenX = NORMAL.getOverviewScaleAndTranslation(mActivity).translationX;
+        // Offset since scale pushes tasks outwards.
+        getTaskSize(sTempRect);
+        int taskWidth = sTempRect.width();
+        offscreenX += taskWidth * (recentsScale - 1) / 2;
+        if (mRunningTaskTileHidden) {
+            // The first task is hidden, so offset by its width.
+            offscreenX -= (taskWidth + getPageSpacing()) * recentsScale;
+        }
+        if (isRtl()) {
+            offscreenX = -offscreenX;
+        }
+        return offscreenX;
+    }
+
     @Override
     protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
@@ -264,4 +308,66 @@
         }
         return super.shouldStealTouchFromSiblingsBelow(ev);
     }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        PluginManagerWrapper.INSTANCE.get(getContext())
+                .addPluginListener(mRecentsExtraCardPluginListener, RecentsExtraCard.class);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(
+                mRecentsExtraCardPluginListener);
+    }
+
+    @Override
+    protected int computeMinScrollX() {
+        if (canComputeScrollX() && !mIsRtl) {
+            return computeScrollX();
+        }
+        return super.computeMinScrollX();
+    }
+
+    @Override
+    protected int computeMaxScrollX() {
+        if (canComputeScrollX() && mIsRtl) {
+            return computeScrollX();
+        }
+        return super.computeMaxScrollX();
+    }
+
+    private boolean canComputeScrollX() {
+        return mRecentsExtraCardPlugin != null && getTaskViewCount() > 0
+                && !mDisallowScrollToClearAll;
+    }
+
+    private int computeScrollX() {
+        int scrollIndex = getTaskViewStartIndex() - 1;
+        while (scrollIndex >= 0 && getChildAt(scrollIndex) instanceof RecentsExtraViewContainer
+                && ((RecentsExtraViewContainer) getChildAt(scrollIndex)).isScrollable()) {
+            scrollIndex--;
+        }
+        return getScrollForPage(scrollIndex + 1);
+    }
+
+    private void createRecentsExtraCard() {
+        mRecentsExtraViewContainer = new RecentsExtraViewContainer(getContext());
+        FrameLayout.LayoutParams helpCardParams =
+                new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+                        FrameLayout.LayoutParams.MATCH_PARENT);
+        mRecentsExtraViewContainer.setLayoutParams(helpCardParams);
+        mRecentsExtraViewContainer.setScrollable(true);
+        addView(mRecentsExtraViewContainer, 0);
+    }
+
+    @Override
+    public void resetTaskVisuals() {
+        super.resetTaskVisuals();
+        if (mRecentsExtraViewContainer != null) {
+            mRecentsExtraViewContainer.setAlpha(mContentAlpha);
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java
new file mode 100644
index 0000000..1ea6d4a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsExtraViewContainer.java
@@ -0,0 +1,54 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Empty view to house recents overview extra card
+ */
+public class RecentsExtraViewContainer extends FrameLayout implements RecentsView.PageCallbacks {
+
+    private boolean mScrollable = false;
+
+    public RecentsExtraViewContainer(Context context) {
+        super(context);
+    }
+
+    public RecentsExtraViewContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public RecentsExtraViewContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    /**
+     * Determine whether the view should be scrolled to in the recents overview, similar to the
+     * taskviews.
+     * @return true if viewed should be scrolled to, false if not
+     */
+    public boolean isScrollable() {
+        return mScrollable;
+    }
+
+    public void setScrollable(boolean scrollable) {
+        this.mScrollable = scrollable;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index d98ef42..6ad3cc6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -35,9 +35,9 @@
 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
-import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -187,7 +187,7 @@
     private final ViewPool<TaskView> mTaskViewPool;
 
     private boolean mDwbToastShown;
-    private boolean mDisallowScrollToClearAll;
+    protected boolean mDisallowScrollToClearAll;
     private boolean mOverlayEnabled;
     private boolean mFreezeViewVisibility;
 
@@ -228,7 +228,7 @@
                 return;
             }
 
-            BACKGROUND_EXECUTOR.execute(() -> {
+            UI_HELPER_EXECUTOR.execute(() -> {
                 TaskView taskView = getTaskView(taskId);
                 if (taskView == null) {
                     return;
@@ -272,7 +272,7 @@
 
     // Only valid until the launcher state changes to NORMAL
     protected int mRunningTaskId = -1;
-    private boolean mRunningTaskTileHidden;
+    protected boolean mRunningTaskTileHidden;
     private Task mTmpRunningTask;
 
     private boolean mRunningTaskIconScaledDown = false;
@@ -289,7 +289,7 @@
     private LayoutTransition mLayoutTransition;
 
     @ViewDebug.ExportedProperty(category = "launcher")
-    private float mContentAlpha = 1;
+    protected float mContentAlpha = 1;
     @ViewDebug.ExportedProperty(category = "launcher")
     protected float mFullscreenProgress = 0;
 
@@ -306,6 +306,9 @@
     private Layout mEmptyTextLayout;
     private LiveTileOverlay mLiveTileOverlay;
 
+    // Keeps track of the index where the first TaskView should be
+    private int mTaskViewStartIndex = 0;
+
     private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener =
             (inMultiWindowMode) -> {
         if (!inMultiWindowMode && mOverviewStateEnabled) {
@@ -329,7 +332,6 @@
         mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
                 .inflate(R.layout.overview_clear_all_button, this, false);
         mClearAllButton.setOnClickListener(this::dismissAllTasks);
-
         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
                 10 /* initial size */);
 
@@ -430,7 +432,7 @@
         super.onViewRemoved(child);
 
         // Clear the task data for the removed child if it was visible
-        if (child != mClearAllButton) {
+        if (child instanceof TaskView) {
             TaskView taskView = (TaskView) child;
             mHasVisibleTaskData.delete(taskView.getTask().key.id);
             mTaskViewPool.recycle(taskView);
@@ -444,7 +446,7 @@
 
     public TaskView getTaskView(int taskId) {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = (TaskView) getChildAt(i);
+            TaskView tv = getTaskViewAt(i);
             if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) {
                 return tv;
             }
@@ -536,28 +538,26 @@
         }
 
         if (tasks == null || tasks.isEmpty()) {
-            removeAllViews();
+            removeTasksViewsAndClearAllButton();
             onTaskStackUpdated();
             return;
         }
 
-        int oldChildCount = getChildCount();
-
         // Unload existing visible task data
         unloadVisibleTaskData();
 
-        TaskView ignoreRestTaskView =
+        TaskView ignoreResetTaskView =
                 mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId);
 
         final int requiredTaskCount = tasks.size();
         if (getTaskViewCount() != requiredTaskCount) {
-            if (oldChildCount > 0) {
+            if (indexOfChild(mClearAllButton) != -1) {
                 removeView(mClearAllButton);
             }
-            for (int i = getChildCount(); i < requiredTaskCount; i++) {
+            for (int i = getTaskViewCount(); i < requiredTaskCount; i++) {
                 addView(mTaskViewPool.getView());
             }
-            while (getChildCount() > requiredTaskCount) {
+            while (getTaskViewCount() > requiredTaskCount) {
                 removeView(getChildAt(getChildCount() - 1));
             }
             if (requiredTaskCount > 0) {
@@ -567,17 +567,23 @@
 
         // Rebind and reset all task views
         for (int i = requiredTaskCount - 1; i >= 0; i--) {
-            final int pageIndex = requiredTaskCount - i - 1;
+            final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex;
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
             taskView.bind(task);
         }
-        TaskView runningTaskView = getRunningTaskView();
-        if (runningTaskView != null) {
-            setCurrentPage(indexOfChild(runningTaskView));
+
+        if (mNextPage == INVALID_PAGE) {
+            // Set the current page to the running task, but not if settling on new task.
+            TaskView runningTaskView = getRunningTaskView();
+            if (runningTaskView != null) {
+                setCurrentPage(indexOfChild(runningTaskView));
+            } else if (getTaskViewCount() > 0) {
+                setCurrentPage(indexOfChild(getTaskViewAt(0)));
+            }
         }
 
-        if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreRestTaskView) {
+        if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreResetTaskView) {
             // If the taskView mapping is changing, do not preserve the visuals. Since we are
             // mostly preserving the first task, and new taskViews are added to the end, it should
             // generally map to the same task.
@@ -588,17 +594,28 @@
         updateEnabledOverlays();
     }
 
+    private void removeTasksViewsAndClearAllButton() {
+        for (int i = getTaskViewCount() - 1; i >= 0; i--) {
+            removeView(getTaskViewAt(i));
+        }
+        if (indexOfChild(mClearAllButton) != -1) {
+            removeView(mClearAllButton);
+        }
+    }
+
     public int getTaskViewCount() {
-        // Account for the clear all button.
-        int childCount = getChildCount();
-        return childCount == 0 ? 0 : childCount - 1;
+        int taskViewCount = getChildCount() - mTaskViewStartIndex;
+        if (indexOfChild(mClearAllButton) != -1) {
+            taskViewCount--;
+        }
+        return taskViewCount;
     }
 
     protected void onTaskStackUpdated() { }
 
     public void resetTaskVisuals() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
-            TaskView taskView = (TaskView) getChildAt(i);
+            TaskView taskView = getTaskViewAt(i);
             if (mIgnoreResetTaskId != taskView.getTask().key.id) {
                 taskView.resetVisualProperties();
                 taskView.setStableAlpha(mContentAlpha);
@@ -704,7 +721,7 @@
     }
 
     /**
-     * Iterates through all thet asks, and loads the associated task data for newly visible tasks,
+     * Iterates through all the tasks, and loads the associated task data for newly visible tasks,
      * and unloads the associated task data for tasks that are no longer visible.
      */
     public void loadVisibleTaskData() {
@@ -715,15 +732,16 @@
         }
 
         int centerPageIndex = getPageNearestToCenterOfScreen();
-        int numChildren = getTaskViewCount();
+        int numChildren = getChildCount();
         int lower = Math.max(0, centerPageIndex - 2);
         int upper = Math.min(centerPageIndex + 2, numChildren - 1);
 
         // Update the task data for the in/visible children
-        for (int i = 0; i < numChildren; i++) {
-            TaskView taskView = (TaskView) getChildAt(i);
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            TaskView taskView = getTaskViewAt(i);
             Task task = taskView.getTask();
-            boolean visible = lower <= i && i <= upper;
+            int index = indexOfChild(taskView);
+            boolean visible = lower <= index && index <= upper;
             if (visible) {
                 if (task == mTmpRunningTask) {
                     // Skip loading if this is the task that we are animating into
@@ -798,6 +816,10 @@
         return tv == null ? -1 : indexOfChild(tv);
     }
 
+    public int getTaskViewStartIndex() {
+        return mTaskViewStartIndex;
+    }
+
     /**
      * Reloads the view if anything in recents changed.
      */
@@ -853,10 +875,10 @@
      */
     public void showCurrentTask(int runningTaskId) {
         if (getTaskView(runningTaskId) == null) {
-            boolean wasEmpty = getChildCount() == 0;
+            boolean wasEmpty = getTaskViewCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
-            addView(taskView, 0);
+            addView(taskView, mTaskViewStartIndex);
             if (wasEmpty) {
                 addView(mClearAllButton);
             }
@@ -922,14 +944,13 @@
         if (runningTaskView == null) {
             // Launch the first task
             if (getTaskViewCount() > 0) {
-                getTaskViewAt(0).launchTask(true /* animate */);
+                getTaskViewAt(0).launchTask(true);
             }
         } else {
-            TaskView nextTaskView = getNextTaskView();
-            if (nextTaskView != null) {
-                nextTaskView.launchTask(true /* animate */);
+            if (getNextTaskView() != null) {
+                getNextTaskView().launchTask(true);
             } else {
-                runningTaskView.launchTask(true /* animate */);
+                runningTaskView.launchTask(true);
             }
         }
     }
@@ -1193,7 +1214,7 @@
 
         int count = getTaskViewCount();
         for (int i = 0; i < count; i++) {
-            addDismissedTaskAnimations(getChildAt(i), anim, duration);
+            addDismissedTaskAnimations(getTaskViewAt(i), anim, duration);
         }
 
         mPendingAnimation = pendingAnimation;
@@ -1201,7 +1222,7 @@
             if (onEndListener.isSuccess) {
                 // Remove all the task views now
                 ActivityManagerWrapper.getInstance().removeAllRecentTasks();
-                removeAllViews();
+                removeTasksViewsAndClearAllButton();
                 startHome();
             }
             mPendingAnimation = null;
@@ -1348,26 +1369,50 @@
         child.setAlpha(mContentAlpha);
     }
 
-    /**
-     * @return The most recent task that is older than the currently running task. If there is
-     * currently no running task or there is no task older than it, then return null.
-     */
     @Nullable
     public TaskView getNextTaskView() {
-        TaskView runningTaskView = getRunningTaskView();
-        if (runningTaskView == null) {
-            return null;
-        }
-        return getTaskViewAt(indexOfChild(runningTaskView) + 1);
+        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() + 1);
     }
 
+    @Nullable
+    public TaskView getPreviousTaskView() {
+        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1);
+    }
+
+    @Nullable
+    public TaskView getCurrentPageTaskView() {
+        return getTaskViewAtByAbsoluteIndex(getCurrentPage());
+    }
+
+    @Nullable
+    public TaskView getNextPageTaskView() {
+        return getTaskViewAtByAbsoluteIndex(getNextPage());
+    }
+
+    @Nullable
+    public TaskView getTaskViewNearestToCenterOfScreen() {
+        return getTaskViewAtByAbsoluteIndex(getPageNearestToCenterOfScreen());
+    }
+
+    /**
+     * Returns null instead of indexOutOfBoundsError when index is not in range
+     */
+    @Nullable
     public TaskView getTaskViewAt(int index) {
-        View child = getChildAt(index);
-        return child == mClearAllButton ? null : (TaskView) child;
+        return getTaskViewAtByAbsoluteIndex(index + mTaskViewStartIndex);
+    }
+
+    @Nullable
+    private TaskView getTaskViewAtByAbsoluteIndex(int index) {
+        if (index < getChildCount() && index >= 0) {
+            View child = getChildAt(index);
+            return child instanceof TaskView ? (TaskView) child : null;
+        }
+        return null;
     }
 
     public void updateEmptyMessage() {
-        boolean isEmpty = getChildCount() == 0;
+        boolean isEmpty = getTaskViewCount() == 0;
         boolean hasSizeChanged = mLastMeasureSize.x != getWidth()
                 || mLastMeasureSize.y != getHeight();
         if (isEmpty == mShowEmptyMessage && !hasSizeChanged) {
@@ -1501,7 +1546,7 @@
             throw new IllegalStateException("Another pending animation is still running");
         }
 
-        int count = getChildCount();
+        int count = getTaskViewCount();
         if (count == 0) {
             return new PendingAnimation(new AnimatorSet());
         }
@@ -1674,18 +1719,38 @@
 
     @Override
     protected int computeMinScrollX() {
-        if (mIsRtl && mDisallowScrollToClearAll) {
-            // We aren't showing the clear all button, so use the leftmost task as the min scroll.
-            return getScrollForPage(getTaskViewCount() - 1);
+        if (getTaskViewCount() > 0) {
+            if (mDisallowScrollToClearAll) {
+                // We aren't showing the clear all button,
+                // so use the leftmost task as the min scroll.
+                if (mIsRtl) {
+                    return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)));
+                }
+                return getScrollForPage(mTaskViewStartIndex);
+            }
+            if (mIsRtl) {
+                return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1);
+            }
+            return getScrollForPage(mTaskViewStartIndex);
         }
         return super.computeMinScrollX();
     }
 
     @Override
     protected int computeMaxScrollX() {
-        if (!mIsRtl && mDisallowScrollToClearAll) {
-            // We aren't showing the clear all button, so use the rightmost task as the max scroll.
-            return getScrollForPage(getTaskViewCount() - 1);
+        if (getTaskViewCount() > 0) {
+            if (mDisallowScrollToClearAll) {
+                // We aren't showing the clear all button,
+                // so use the rightmost task as the min scroll.
+                if (mIsRtl) {
+                    return getScrollForPage(mTaskViewStartIndex);
+                }
+                return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)));
+            }
+            if (mIsRtl) {
+                return getScrollForPage(mTaskViewStartIndex);
+            }
+            return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1);
         }
         return super.computeMaxScrollX();
     }
@@ -1737,7 +1802,7 @@
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            ((TaskView) getChildAt(i)).setOverlayEnabled(i == overlayEnabledPage);
+            getTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
         }
     }
 
@@ -1757,4 +1822,25 @@
         final WindowInsets insets = getRootWindowInsets();
         return Math.max(insets.getSystemGestureInsets().right, insets.getSystemWindowInsetRight());
     }
+
+    @Override
+    public void addView(View child, int index) {
+        super.addView(child, index);
+        if (isExtraCardView(child, index)) {
+            mTaskViewStartIndex++;
+        }
+    }
+
+    @Override
+    public void removeView(View view) {
+        if (isExtraCardView(view, indexOfChild(view))) {
+            mTaskViewStartIndex--;
+        }
+        super.removeView(view);
+    }
+
+    private boolean isExtraCardView(View view, int index) {
+        return !(view instanceof TaskView) && !(view instanceof ClearAllButton)
+                && index <= mTaskViewStartIndex;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 7f1e898..044292a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -39,24 +39,28 @@
 import android.util.FloatProperty;
 import android.util.Property;
 import android.view.View;
+import android.view.ViewGroup;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
 import com.android.quickstep.util.TaskCornerRadius;
+import com.android.systemui.plugins.OverviewScreenshotActions;
+import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
 /**
  * A task in the Recents view.
  */
-public class TaskThumbnailView extends View {
+public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> {
 
     private final static ColorMatrix COLOR_MATRIX = new ColorMatrix();
     private final static ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
@@ -100,6 +104,7 @@
 
     private boolean mOverlayEnabled;
     private boolean mRotated;
+    private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
 
     public TaskThumbnailView(Context context) {
         this(context, null);
@@ -147,6 +152,11 @@
             mPaint.setShader(null);
             mOverlay.reset();
         }
+
+        if (mOverviewScreenshotActionsPlugin != null) {
+            mOverviewScreenshotActionsPlugin
+                .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity);
+        }
         updateThumbnailPaintFilter();
     }
 
@@ -211,6 +221,33 @@
         canvas.restore();
     }
 
+    @Override
+    public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions,
+            Context context) {
+        mOverviewScreenshotActionsPlugin = overviewScreenshotActions;
+        mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
+    }
+
+    @Override
+    public void onPluginDisconnected(OverviewScreenshotActions plugin) {
+        if (mOverviewScreenshotActionsPlugin != null) {
+            mOverviewScreenshotActionsPlugin = null;
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        PluginManagerWrapper.INSTANCE.get(getContext())
+            .addPluginListener(this, OverviewScreenshotActions.class);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
+    }
+
     public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
         // Don't show insets in multi window mode.
         return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 5c4d6d8..292eaaa 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -21,8 +21,6 @@
     <!-- Activity which blocks home gesture -->
     <string name="gesture_blocking_activity" translatable="false"></string>
 
-    <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
-
     <string name="stats_log_manager_class" translatable="false">com.android.quickstep.logging.StatsLogCompatManager</string>
 
     <string name="test_information_handler_class" translatable="false">com.android.quickstep.QuickstepTestInformationHandler</string>
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 991408c..a91410c 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -57,6 +57,9 @@
 import android.util.Pair;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.Interpolators;
@@ -69,6 +72,7 @@
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -80,9 +84,6 @@
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
  * {@link LauncherAppTransitionManager} with Quickstep-specific app transitions for launching from
  * home and/or all-apps.
@@ -150,6 +151,8 @@
 
     private RemoteAnimationProvider mRemoteAnimationProvider;
 
+    private final ShelfPeekAnim mShelfPeekAnim;
+
     private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
         @Override
         public void onAnimationStart(Animator animation) {
@@ -177,6 +180,12 @@
 
         mLauncher.addOnDeviceProfileChangeListener(this);
         registerRemoteAnimations();
+
+        mShelfPeekAnim = new ShelfPeekAnim(mLauncher);
+    }
+
+    public ShelfPeekAnim getShelfPeekAnim() {
+        return mShelfPeekAnim;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
index 910fa0d..7beb9db 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java
@@ -14,16 +14,17 @@
 
 package com.android.launcher3.uioverrides.plugins;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.content.Context;
 import android.os.Looper;
 
-import com.android.launcher3.LauncherModel;
 import com.android.systemui.shared.plugins.PluginInitializer;
 
 public class PluginInitializerImpl implements PluginInitializer {
     @Override
     public Looper getBgLooper() {
-        return LauncherModel.getWorkerLooper();
+        return MODEL_EXECUTOR.getLooper();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
index bb72315..39b0f8d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
@@ -11,7 +11,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.quickstep.RecentsModel;
@@ -24,7 +24,7 @@
     private static final String TAG = "LandscapeEdgeSwipeCtrl";
 
     public LandscapeEdgeSwipeController(Launcher l) {
-        super(l, SwipeDetector.HORIZONTAL);
+        super(l, SingleAxisSwipeDetector.HORIZONTAL);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index b81edfa..db6a40f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -43,7 +43,7 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -79,7 +79,7 @@
     private boolean mFinishFastOnSecondTouch;
 
     public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) {
-        super(l, SwipeDetector.VERTICAL);
+        super(l, SingleAxisSwipeDetector.VERTICAL);
         mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
         mAllowDragToOverview = allowDragToOverview;
     }
@@ -177,6 +177,20 @@
         return builder;
     }
 
+    private AnimatorSetBuilder getNormalToAllAppsAnimation() {
+        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+        builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
+                0, ALL_APPS_CONTENT_FADE_THRESHOLD));
+        return builder;
+    }
+
+    private AnimatorSetBuilder getAllAppsToNormalAnimation() {
+        AnimatorSetBuilder builder = new AnimatorSetBuilder();
+        builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
+                1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
+        return builder;
+    }
+
     @Override
     protected AnimatorSetBuilder getAnimatorSetBuilderForStates(LauncherState fromState,
             LauncherState toState) {
@@ -187,6 +201,10 @@
             builder = getOverviewToAllAppsAnimation();
         } else if (fromState == ALL_APPS && toState == OVERVIEW) {
             builder = getAllAppsToOverviewAnimation();
+        } else if (fromState == NORMAL && toState == ALL_APPS) {
+            builder = getNormalToAllAppsAnimation();
+        } else if (fromState == ALL_APPS && toState == NORMAL) {
+            builder = getAllAppsToNormalAnimation();
         }
         return builder;
     }
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 5c9c7d4..110cc23 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.function.BiPredicate;
@@ -109,16 +110,6 @@
 
     interface AnimationFactory {
 
-        enum ShelfAnimState {
-            HIDE(true), PEEK(true), OVERVIEW(false), CANCEL(false);
-
-            ShelfAnimState(boolean shouldPreformHaptic) {
-                this.shouldPreformHaptic = shouldPreformHaptic;
-            }
-
-            public final boolean shouldPreformHaptic;
-        }
-
         default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { }
 
         void createActivityController(long transitionLength);
@@ -127,8 +118,8 @@
 
         default void onTransitionCancelled() { }
 
-        default void setShelfState(ShelfAnimState animState, Interpolator interpolator,
-                long duration) { }
+        default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
+                Interpolator interpolator, long duration) { }
 
         /**
          * @param attached Whether to show RecentsView alongside the app window. If false, recents
diff --git a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
index c840132..1ac7ed4 100644
--- a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
@@ -157,6 +157,6 @@
     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
         super.dump(prefix, fd, writer, args);
         writer.println(prefix + "Misc:");
-        dumpMisc(writer);
+        dumpMisc(prefix + "\t", writer);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/OverviewInteractionState.java b/quickstep/src/com/android/quickstep/OverviewInteractionState.java
index 78b48d7..858c3b6 100644
--- a/quickstep/src/com/android/quickstep/OverviewInteractionState.java
+++ b/quickstep/src/com/android/quickstep/OverviewInteractionState.java
@@ -15,20 +15,21 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.Context;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
 import android.util.Log;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.UiThreadHelper;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
-import androidx.annotation.WorkerThread;
-
 /**
  * Sets alpha for the back button
  */
@@ -62,7 +63,7 @@
         // because of its high send frequency and data may be very different than the previous value
         // For example, send back alpha on uihandler to avoid flickering when setting its visibility
         mUiHandler = new Handler(this::handleUiMessage);
-        mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage);
+        mBgHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleBgMessage);
 
         onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(context)
                 .addModeChangeListener(this::onNavigationModeChanged));
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index e41dba9..10f9feb 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -16,19 +16,22 @@
 
 package com.android.quickstep;
 
-import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
-import android.content.Context;
 import android.os.Build;
 import android.os.Process;
 import android.util.SparseBooleanArray;
-import com.android.launcher3.MainThreadExecutor;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.util.LooperExecutor;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.KeyguardManagerCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -41,7 +44,8 @@
 public class RecentTasksList extends TaskStackChangeListener {
 
     private final KeyguardManagerCompat mKeyguardManager;
-    private final MainThreadExecutor mMainThreadExecutor;
+    private final LooperExecutor mMainThreadExecutor;
+    private final ActivityManagerWrapper mActivityManagerWrapper;
 
     // The list change id, increments as the task list changes in the system
     private int mChangeId;
@@ -52,11 +56,13 @@
 
     ArrayList<Task> mTasks = new ArrayList<>();
 
-    public RecentTasksList(Context context) {
-        mMainThreadExecutor = new MainThreadExecutor();
-        mKeyguardManager = new KeyguardManagerCompat(context);
+    public RecentTasksList(LooperExecutor mainThreadExecutor,
+            KeyguardManagerCompat keyguardManager, ActivityManagerWrapper activityManagerWrapper) {
+        mMainThreadExecutor = mainThreadExecutor;
+        mKeyguardManager = keyguardManager;
         mChangeId = 1;
-        ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
+        mActivityManagerWrapper = activityManagerWrapper;
+        mActivityManagerWrapper.registerTaskStackListener(this);
     }
 
     /**
@@ -64,7 +70,7 @@
      */
     public void getTaskKeys(int numTasks, Consumer<ArrayList<Task>> callback) {
         // Kick off task loading in the background
-        BACKGROUND_EXECUTOR.execute(() -> {
+        UI_HELPER_EXECUTOR.execute(() -> {
             ArrayList<Task> tasks = loadTasksInBackground(numTasks, true /* loadKeysOnly */);
             mMainThreadExecutor.execute(() -> callback.accept(tasks));
         });
@@ -86,12 +92,12 @@
         if (mLastLoadedId == mChangeId && (!mLastLoadHadKeysOnly || loadKeysOnly)) {
             // The list is up to date, send the callback on the next frame,
             // so that requestID can be returned first.
-            mMainThreadExecutor.getHandler().post(resultCallback);
+            mMainThreadExecutor.post(resultCallback);
             return requestLoadId;
         }
 
         // Kick off task loading in the background
-        BACKGROUND_EXECUTOR.execute(() -> {
+        UI_HELPER_EXECUTOR.execute(() -> {
             ArrayList<Task> tasks = loadTasksInBackground(Integer.MAX_VALUE, loadKeysOnly);
 
             mMainThreadExecutor.execute(() -> {
@@ -136,12 +142,13 @@
     /**
      * Loads and creates a list of all the recent tasks.
      */
-    private ArrayList<Task> loadTasksInBackground(int numTasks,
+    @VisibleForTesting
+    ArrayList<Task> loadTasksInBackground(int numTasks,
             boolean loadKeysOnly) {
         int currentUserId = Process.myUserHandle().getIdentifier();
         ArrayList<Task> allTasks = new ArrayList<>();
         List<ActivityManager.RecentTaskInfo> rawTasks =
-                ActivityManagerWrapper.getInstance().getRecentTasks(numTasks, currentUserId);
+                mActivityManagerWrapper.getRecentTasks(numTasks, currentUserId);
         // The raw tasks are given in most-recent to least-recent order, we need to reverse it
         Collections.reverse(rawTasks);
 
diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
index f9d2f11..4d1d9ef 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
@@ -22,7 +24,6 @@
 import android.os.Bundle;
 import android.os.Handler;
 
-import com.android.launcher3.MainThreadExecutor;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 
@@ -92,14 +93,10 @@
     private static class Scheduler implements Runnable {
 
         private WeakReference<RecentsActivityTracker> mPendingTracker = new WeakReference<>(null);
-        private MainThreadExecutor mMainThreadExecutor;
 
         public synchronized void schedule(RecentsActivityTracker tracker) {
             mPendingTracker = new WeakReference<>(tracker);
-            if (mMainThreadExecutor == null) {
-                mMainThreadExecutor = new MainThreadExecutor();
-            }
-            mMainThreadExecutor.execute(this);
+            MAIN_EXECUTOR.execute(this);
         }
 
         @Override
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index dfab434..2e59ed5 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -15,6 +15,10 @@
  */
 package com.android.quickstep;
 
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 
 import android.annotation.TargetApi;
@@ -22,16 +26,20 @@
 import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.os.Build;
-import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.LauncherAppsCompat.OnAppsChangedCallbackCompat;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.KeyguardManagerCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 
 import java.util.ArrayList;
@@ -61,13 +69,14 @@
 
     private RecentsModel(Context context) {
         mContext = context;
-        HandlerThread loaderThread = new HandlerThread("TaskThumbnailIconCache",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        loaderThread.start();
-        mTaskList = new RecentTasksList(context);
-        mIconCache = new TaskIconCache(context, loaderThread.getLooper());
-        mThumbnailCache = new TaskThumbnailCache(context, loaderThread.getLooper());
+        Looper looper =
+                createAndStartNewLooper("TaskThumbnailIconCache", THREAD_PRIORITY_BACKGROUND);
+        mTaskList = new RecentTasksList(MAIN_EXECUTOR,
+                new KeyguardManagerCompat(context), ActivityManagerWrapper.getInstance());
+        mIconCache = new TaskIconCache(context, looper);
+        mThumbnailCache = new TaskThumbnailCache(context, looper);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
+        setupPackageListener();
     }
 
     public TaskIconCache getIconCache() {
@@ -166,6 +175,7 @@
     public void onTaskRemoved(int taskId) {
         Task.TaskKey dummyKey = new Task.TaskKey(taskId, 0, null, null, 0, 0);
         mThumbnailCache.remove(dummyKey);
+        mIconCache.onTaskRemoved(dummyKey);
     }
 
     public void setSystemUiProxy(ISystemUiProxy systemUiProxy) {
@@ -200,6 +210,21 @@
         }
     }
 
+    private void setupPackageListener() {
+        LauncherAppsCompat.getInstance(mContext)
+                .addOnAppsChangedCallback(new OnAppsChangedCallbackCompat() {
+                    @Override
+                    public void onPackageRemoved(String packageName, UserHandle user) {
+                        mIconCache.invalidatePackage(packageName);
+                    }
+
+                    @Override
+                    public void onPackageChanged(String packageName, UserHandle user) {
+                        mIconCache.invalidatePackage(packageName);
+                    }
+                });
+    }
+
     public void addThumbnailChangeListener(TaskThumbnailChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 07af9b3..289a129 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.uioverrides.RecentsUiFactory.GO_LOW_RAM_RECENTS_ENABLED;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -27,26 +28,25 @@
 import android.util.LruCache;
 import android.view.accessibility.AccessibilityManager;
 
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.cache.HandlerRunnable;
-import com.android.launcher3.uioverrides.RecentsUiFactory;
 import com.android.launcher3.util.Preconditions;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
+import java.util.Map;
 import java.util.function.Consumer;
 
 /**
  * Manages the caching of task icons and related data.
- * TODO: This class should later be merged into IconCache.
+ * TODO(b/138944598): This class should later be merged into IconCache.
  */
 public class TaskIconCache {
 
     private final Handler mBackgroundHandler;
-    private final MainThreadExecutor mMainThreadExecutor;
     private final AccessibilityManager mAccessibilityManager;
 
     private final NormalizedIconLoader mIconLoader;
@@ -67,7 +67,6 @@
 
     public TaskIconCache(Context context, Looper backgroundLooper) {
         mBackgroundHandler = new Handler(backgroundLooper);
-        mMainThreadExecutor = new MainThreadExecutor();
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
 
         Resources res = context.getResources();
@@ -103,7 +102,7 @@
                     // We don't call back to the provided callback in this case
                     return;
                 }
-                mMainThreadExecutor.execute(() -> {
+                MAIN_EXECUTOR.execute(() -> {
                     task.icon = icon;
                     task.titleDescription = contentDescription;
                     callback.accept(task);
@@ -149,6 +148,21 @@
         return label;
     }
 
+
+    void onTaskRemoved(TaskKey taskKey) {
+        mIconCache.remove(taskKey);
+    }
+
+    void invalidatePackage(String packageName) {
+        // TODO(b/138944598): Merge this class into IconCache so we can do this at the base level
+        Map<ComponentName, ActivityInfo> activityInfoCache = mActivityInfoCache.snapshot();
+        for (ComponentName cn : activityInfoCache.keySet()) {
+            if (cn.getPackageName().equals(packageName)) {
+                mActivityInfoCache.remove(cn);
+            }
+        }
+    }
+
     public static abstract class IconLoadRequest extends HandlerRunnable {
         IconLoadRequest(Handler handler) {
             super(handler, null);
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 57c5a27..3b50c26 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -15,12 +15,14 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Handler;
 import android.os.Looper;
-import com.android.launcher3.MainThreadExecutor;
+
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.cache.HandlerRunnable;
@@ -30,13 +32,13 @@
 import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+
 import java.util.ArrayList;
 import java.util.function.Consumer;
 
 public class TaskThumbnailCache {
 
     private final Handler mBackgroundHandler;
-    private final MainThreadExecutor mMainThreadExecutor;
 
     private final int mCacheSize;
     private final ThumbnailCache mCache;
@@ -94,7 +96,6 @@
 
     public TaskThumbnailCache(Context context, Looper backgroundLooper) {
         mBackgroundHandler = new Handler(backgroundLooper);
-        mMainThreadExecutor = new MainThreadExecutor();
         mHighResLoadingState = new HighResLoadingState(context);
 
         Resources res = context.getResources();
@@ -168,7 +169,7 @@
                     // We don't call back to the provided callback in this case
                     return;
                 }
-                mMainThreadExecutor.execute(() -> {
+                MAIN_EXECUTOR.execute(() -> {
                     mCache.put(key, thumbnail);
                     callback.accept(thumbnail);
                     onEnd();
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index bf3cd8a..8e5ed1a 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -29,12 +29,16 @@
 import android.stats.launcher.nano.Launcher;
 import android.stats.launcher.nano.LauncherExtension;
 import android.stats.launcher.nano.LauncherTarget;
+import android.util.Log;
 import android.view.View;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogUtils;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.util.ComponentKey;
 import com.android.systemui.shared.system.StatsLogCompat;
 import com.google.protobuf.nano.MessageNano;
@@ -50,6 +54,8 @@
 public class StatsLogCompatManager extends StatsLogManager {
 
     private static final int SUPPORTED_TARGET_DEPTH = 2;
+    private static final String TAG = "StatsLogCompatManager";
+    private static final boolean DEBUG = false;
 
     public StatsLogCompatManager(Context context) { }
 
@@ -59,6 +65,9 @@
         ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
         int srcState = mStateProvider.getCurrentState();
         fillInLauncherExtension(v, ext);
+        if (ext.srcTarget[0] != null) {
+            ext.srcTarget[0].item = LauncherTarget.APP_ICON;
+        }
         StatsLogCompat.write(LAUNCH_APP, srcState, BACKGROUND /* dstState */,
                 MessageNano.toByteArray(ext), true);
     }
@@ -95,28 +104,132 @@
     }
 
     public static boolean fillInLauncherExtension(View v, LauncherExtension extension) {
+        if (DEBUG) {
+            Log.d(TAG, "fillInLauncherExtension");
+        }
+
         StatsLogUtils.LogContainerProvider provider = StatsLogUtils.getLaunchProviderRecursive(v);
         if (v == null || !(v.getTag() instanceof ItemInfo) || provider == null) {
+            if (DEBUG) {
+                Log.d(TAG, "View or provider is null, or view doesn't have an ItemInfo tag.");
+            }
+
             return false;
         }
         ItemInfo itemInfo = (ItemInfo) v.getTag();
         Target child = new Target();
         Target parent = new Target();
         provider.fillInLogContainerData(v, itemInfo, child, parent);
+        extension.srcTarget[0] = new LauncherTarget();
+        extension.srcTarget[1] = new LauncherTarget();
         copy(child, extension.srcTarget[0]);
         copy(parent, extension.srcTarget[1]);
         return true;
     }
 
     public static boolean fillInLauncherExtensionWithPageId(LauncherExtension ext, int pageId) {
+        if (DEBUG) {
+            Log.d(TAG, "fillInLauncherExtensionWithPageId, pageId = " + pageId);
+        }
+
         Target target = new Target();
         target.pageIndex = pageId;
+        ext.srcTarget[0] = new LauncherTarget();
         copy(target, ext.srcTarget[0]);
         return true;
     }
 
     private static void copy(Target src, LauncherTarget dst) {
-        // fill in
+        if (DEBUG) {
+            Log.d(TAG, "copy target information from clearcut Target to LauncherTarget.");
+        }
+
+        // Fill in type
+        switch (src.type) {
+            case Target.Type.ITEM:
+                dst.type = LauncherTarget.ITEM_TYPE;
+                break;
+            case Target.Type.CONTROL:
+                dst.type = LauncherTarget.CONTROL_TYPE;
+                break;
+            case Target.Type.CONTAINER:
+                dst.type = LauncherTarget.CONTAINER_TYPE;
+                break;
+            default:
+                dst.type = LauncherTarget.NONE;
+                break;
+        }
+
+        // Fill in item
+        switch (src.itemType) {
+            case ItemType.APP_ICON:
+                dst.item = LauncherTarget.APP_ICON;
+                break;
+            case ItemType.SHORTCUT:
+                dst.item = LauncherTarget.SHORTCUT;
+                break;
+            case ItemType.WIDGET:
+                dst.item = LauncherTarget.WIDGET;
+                break;
+            case ItemType.FOLDER_ICON:
+                dst.item = LauncherTarget.FOLDER_ICON;
+                break;
+            case ItemType.DEEPSHORTCUT:
+                dst.item = LauncherTarget.DEEPSHORTCUT;
+                break;
+            case ItemType.SEARCHBOX:
+                dst.item = LauncherTarget.SEARCHBOX;
+                break;
+            case ItemType.EDITTEXT:
+                dst.item = LauncherTarget.EDITTEXT;
+                break;
+            case ItemType.NOTIFICATION:
+                dst.item = LauncherTarget.NOTIFICATION;
+                break;
+            case ItemType.TASK:
+                dst.item = LauncherTarget.TASK;
+                break;
+            default:
+                dst.item = LauncherTarget.DEFAULT_ITEM;
+                break;
+        }
+
+        // Fill in container
+        switch (src.containerType) {
+            case ContainerType.HOTSEAT:
+                dst.container = LauncherTarget.HOTSEAT;
+                break;
+            case ContainerType.FOLDER:
+                dst.container = LauncherTarget.FOLDER;
+                break;
+            case ContainerType.PREDICTION:
+                dst.container = LauncherTarget.PREDICTION;
+                break;
+            case ContainerType.SEARCHRESULT:
+                dst.container = LauncherTarget.SEARCHRESULT;
+                break;
+            default:
+                dst.container = LauncherTarget.DEFAULT_CONTAINER;
+                break;
+        }
+
+        // Fill in control
+        switch (src.controlType) {
+            case ControlType.UNINSTALL_TARGET:
+                dst.control = LauncherTarget.UNINSTALL;
+                break;
+            case ControlType.REMOVE_TARGET:
+                dst.control = LauncherTarget.REMOVE;
+                break;
+            default:
+                dst.control = LauncherTarget.DEFAULT_CONTROL;
+                break;
+        }
+
+        // Fill in other fields
+        dst.pageId = src.pageIndex;
+        dst.gridX = src.gridX;
+        dst.gridY = src.gridY;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
index 4a11601..9ca7f23 100644
--- a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
+++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java
@@ -20,10 +20,10 @@
 
 import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CANCEL_TARGET;
-import static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE;
 import static com.android.systemui.shared.system.LauncherEventUtil.DISMISS;
 import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_QUICK_SCRUB_ONBOARDING_TIP;
 import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_SWIPE_UP_ONBOARDING_TIP;
+import static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE;
 
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
diff --git a/quickstep/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/src/com/android/quickstep/util/AssistantUtilities.java
new file mode 100644
index 0000000..552db1f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistantUtilities.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util;
+
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
+
+import android.annotation.TargetApi;
+import android.app.TaskInfo;
+import android.content.Intent;
+import android.os.Build;
+
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskInfoCompat;
+
+/**
+ * Utility class for interacting with the Assistant.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public final class AssistantUtilities {
+
+    /** Returns true if an Assistant activity that is excluded from recents is running. */
+    public static boolean isExcludedAssistantRunning() {
+        return isExcludedAssistant(ActivityManagerWrapper.getInstance().getRunningTask());
+    }
+
+    /** Returns true if the given task holds an Assistant activity that is excluded from recents. */
+    public static boolean isExcludedAssistant(TaskInfo info) {
+        return info != null
+            && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
+            && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+    }
+
+    private AssistantUtilities() {}
+}
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 050bdff..2e118b4 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -26,7 +26,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.quickstep.SysUINavigationMode;
 
 import java.lang.annotation.Retention;
 
@@ -39,12 +39,27 @@
     @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE})
     private @interface MultiWindowStrategy {}
 
+    /**
+     * The height for the swipe up motion
+     */
+    public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) {
+        float swipeHeight = dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
+        if (SysUINavigationMode.getMode(context) == SysUINavigationMode.Mode.NO_BUTTON) {
+            swipeHeight -= dp.getInsets().bottom;
+        }
+        return swipeHeight;
+    }
+
     public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) {
         float extraSpace;
         if (dp.isVerticalBarLayout()) {
             extraSpace = 0;
         } else {
-            extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx;
+            Resources res = context.getResources();
+
+            extraSpace = getDefaultSwipeHeight(context, dp) + dp.verticalDragHandleSizePx
+                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
+                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
         }
         calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect);
     }
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index dc6b56e..3320dae 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.QUICK_SWITCH;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -35,15 +36,18 @@
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.util.ShelfPeekAnim;
 
 /**
  * Scrim used for all-apps and shelf in Overview
@@ -161,9 +165,9 @@
                 mMidProgress =  OVERVIEW.getVerticalProgress(mLauncher);
                 Rect hotseatPadding = dp.getHotseatLayoutPadding();
                 int hotseatSize = dp.hotseatBarSizePx + dp.getInsets().bottom
-                        - hotseatPadding.bottom - hotseatPadding.top;
+                        + hotseatPadding.bottom + hotseatPadding.top;
                 float dragHandleTop =
-                        Math.min(hotseatSize, OverviewState.getDefaultSwipeHeight(context, dp));
+                        Math.min(hotseatSize, LayoutUtils.getDefaultSwipeHeight(context, dp));
                 mDragHandleProgress =  1 - (dragHandleTop / mShiftRange);
             }
             mTopOffset = dp.getInsets().top - mShelfOffset;
@@ -193,8 +197,12 @@
         if (mProgress >= 1) {
             mRemainingScreenColor = 0;
             mShelfColor = 0;
+            ShelfPeekAnim shelfPeekAnim = ((QuickstepAppTransitionManagerImpl)
+                    mLauncher.getAppTransitionManager()).getShelfPeekAnim();
+            LauncherState state = mLauncher.getStateManager().getState();
             if (mSysUINavigationMode == Mode.NO_BUTTON
-                    && mLauncher.getStateManager().getState() == BACKGROUND_APP) {
+                    && (state == BACKGROUND_APP || state == QUICK_SWITCH)
+                    && shelfPeekAnim.isPeeking()) {
                 // Show the shelf background when peeking during swipe up.
                 mShelfColor = setColorAlphaBound(mEndScrim, mMidAlpha);
             }
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
index d0956d1..7801775 100644
--- a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
+++ b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
@@ -150,10 +150,10 @@
             List<AppTarget> targets = new ArrayList<>(activities.length);
             for (LauncherActivityInfo info : activities) {
                 ComponentName cn = info.getComponentName();
-                AppTarget target =
-                        new AppTarget.Builder(new AppTargetId("app:" + cn), cn.getPackageName(), info.getUser())
-                            .setClassName(cn.getClassName())
-                            .build();
+                AppTarget target = new AppTarget.Builder(
+                        new AppTargetId("app:" + cn), cn.getPackageName(), info.getUser())
+                        .setClassName(cn.getClassName())
+                        .build();
                 targets.add(target);
             }
             mCallback.onTargetsAvailable(targets);
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
new file mode 100644
index 0000000..34eb7f8
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 static junit.framework.TestCase.assertNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.util.LooperExecutor;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.KeyguardManagerCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+public class RecentTasksListTest {
+
+    private ActivityManagerWrapper mockActivityManagerWrapper;
+
+    // Class under test
+    private RecentTasksList mRecentTasksList;
+
+    @Before
+    public void setup() {
+        LooperExecutor mockMainThreadExecutor = mock(LooperExecutor.class);
+        KeyguardManagerCompat mockKeyguardManagerCompat = mock(KeyguardManagerCompat.class);
+        mockActivityManagerWrapper = mock(ActivityManagerWrapper.class);
+        mRecentTasksList = new RecentTasksList(mockMainThreadExecutor, mockKeyguardManagerCompat,
+                mockActivityManagerWrapper);
+    }
+
+    @Test
+    public void onTaskRemoved_reloadsAllTasks() {
+        mRecentTasksList.onTaskRemoved(0);
+        verify(mockActivityManagerWrapper, times(1))
+                .getRecentTasks(anyInt(), anyInt());
+    }
+
+    @Test
+    public void onTaskStackChanged_doesNotFetchTasks() {
+        mRecentTasksList.onTaskStackChanged();
+        verify(mockActivityManagerWrapper, times(0))
+                .getRecentTasks(anyInt(), anyInt());
+    }
+
+    @Test
+    public void loadTasksInBackground_onlyKeys_noValidTaskDescription() {
+        ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo();
+        when(mockActivityManagerWrapper.getRecentTasks(anyInt(), anyInt()))
+                .thenReturn(Collections.singletonList(recentTaskInfo));
+
+        List<Task> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, true);
+
+        assertEquals(1, taskList.size());
+        assertNull(taskList.get(0).taskDescription.getLabel());
+    }
+
+    @Test
+    public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() {
+        String taskDescription = "Wheeee!";
+        ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo();
+        recentTaskInfo.taskDescription = new ActivityManager.TaskDescription(taskDescription);
+        when(mockActivityManagerWrapper.getRecentTasks(anyInt(), anyInt()))
+                .thenReturn(Collections.singletonList(recentTaskInfo));
+
+        List<Task> taskList = mRecentTasksList.loadTasksInBackground(Integer.MAX_VALUE, false);
+
+        assertEquals(1, taskList.size());
+        assertEquals(taskDescription, taskList.get(0).taskDescription.getLabel());
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 885fdbf..8cd3bb6 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -17,6 +17,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -34,6 +35,7 @@
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.tapl.AllAppsFromOverview;
 import com.android.launcher3.tapl.Background;
+import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
@@ -210,16 +212,21 @@
     @PortraitLandscape
     public void testBackground() throws Exception {
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        final Background background = getAndAssertBackground();
+
+        assertNotNull("Background.switchToOverview() returned null", background.switchToOverview());
+        assertTrue("Launcher internal state didn't switch to Overview",
+                isInState(LauncherState.OVERVIEW));
+    }
+
+    private Background getAndAssertBackground() {
         final Background background = mLauncher.getBackground();
         assertNotNull("Launcher.getBackground() returned null", background);
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
                         + "one",
                 isInBackground(launcher)));
-
-        assertNotNull("Background.switchToOverview() returned null", background.switchToOverview());
-        assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+        return background;
     }
 
     @Test
@@ -237,4 +244,47 @@
         assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
         assertNotNull("getHome returned null", mLauncher.getWorkspace());
     }
+
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testQuickSwitchFromApp() throws Exception {
+        startTestActivity(2);
+        startTestActivity(3);
+        startTestActivity(4);
+
+        Background background = getAndAssertBackground();
+        background.quickSwitchToPreviousApp();
+        assertTrue("The first app we should have quick switched to is not running",
+                isTestActivityRunning(3));
+
+        background = getAndAssertBackground();
+        background.quickSwitchToPreviousApp();
+        if (mLauncher.getNavigationModel() == NavigationModel.THREE_BUTTON) {
+            // 3-button mode toggles between 2 apps, rather than going back further.
+            assertTrue("Second quick switch should have returned to the first app.",
+                    isTestActivityRunning(4));
+        } else {
+            assertTrue("The second app we should have quick switched to is not running",
+                    isTestActivityRunning(2));
+        }
+        getAndAssertBackground();
+    }
+
+    private boolean isTestActivityRunning(int activityNumber) {
+        return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName())
+                        .text("TestActivity" + activityNumber)),
+                DEFAULT_UI_TIMEOUT);
+    }
+
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testQuickSwitchFromHome() throws Exception {
+        startTestActivity(2);
+        mLauncher.pressHome().quickSwitchToPreviousApp();
+        assertTrue("The most recent task is not running after quick switching from home",
+                isTestActivityRunning(2));
+        getAndAssertBackground();
+    }
 }
diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml
index c156e11..32a5419 100644
--- a/res/layout/folder_application.xml
+++ b/res/layout/folder_application.xml
@@ -20,4 +20,5 @@
     style="@style/BaseIcon"
     android:textColor="?attr/folderTextColor"
     android:includeFontPadding="false"
+    android:hapticFeedbackEnabled="false"
     launcher:iconDisplay="folder" />
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index ca59afa..ba16dd3 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Werk"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Program is nie geïnstalleer nie."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Program is nie beskikbaar nie"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index d942e9e..0396df6 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ስራ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"መተግበሪያ አልተጫነም።"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"መተግበሪያ አይገኝም"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index a80ecb0..2fbdae1 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"العمل"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"لم يتم تثبيت التطبيق."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"التطبيق ليس متاحًا"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 2984603..221921f 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"কৰ্মস্থান"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"এপটো ইনষ্টল কৰা নহ\'ল।"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"এপটো নাই"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 5528a19..7c1ce84 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"İş"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Tətbiq quraşdırılmayıb."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Tətbiq əlçatmazdır"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 883003c..e6fe3bd 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacija nije instalirana."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacija nije dostupna"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index b4cf913..2ccd34f 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Працоўная"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Праграма не ўсталявана."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Праграма недаступная"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 408f205..f7d1d0f 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Работа"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Приложението не е инсталирано."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Приложението не е налично"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index a67674c..0a72632 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"কাজ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"অ্যাপ্লিকেশান ইনস্টল করা নেই৷"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"অ্যাপ্লিকেশান অনুপলব্ধ"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 5650457..65ad91e 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Posao"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacija nije instalirana."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacija nije dostupna"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 50f92da..4ef9ec3 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Feina"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"L\'aplicació no s\'ha instal·lat."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"L\'aplicació no està disponible."</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 692b57d..57c5072 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Práce"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikace není nainstalována."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikace není k dispozici."</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index dc17516..9097ffd 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Arbejde"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Appen er ikke installeret."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Appen er ikke tilgængelig"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index a345bab..1602a53 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Arbeit"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App ist nicht installiert."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App nicht verfügbar"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 2363e61..d80e905 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Εργασία"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Η εφαρμογή δεν έχει εγκατασταθεί."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Η εφαρμογή δεν είναι διαθέσιμη"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 0c48b5d..7adc218 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App isn\'t installed."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App isn\'t available"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 0c48b5d..7adc218 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App isn\'t installed."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App isn\'t available"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 0c48b5d..7adc218 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App isn\'t installed."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App isn\'t available"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 0c48b5d..7adc218 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App isn\'t installed."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App isn\'t available"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 3c65f54..c741482 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‎‏‏‏‏‏‎‎‏‎‏‏‎‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‎‏‎‎‏‏‎Launcher3‎‏‎‎‏‎"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‎‏‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‏‏‎‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‎Work‎‏‎‎‏‎"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‏‏‏‎‏‏‎‎‏‎‏‎‏‎‏‏‏‎‏‎‎App isn\'t installed.‎‏‎‎‏‎"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‏‏‏‎‏‎‎‏‎‎‎‎‎‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‏‎‏‏‏‏‏‎‏‏‎App isn\'t available‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index d10c84b..960ef93 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabajo"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"No se instaló la aplicación."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"La aplicación no está disponible."</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 09b1239..01f60e4 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabajo"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"La aplicación no está instalada."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"La aplicación no está disponible"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 1e470e1..ff121fa 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Töö"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Rakendus pole installitud."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Rakendus ei ole saadaval"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 9ba46c1..10aebe7 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Lana"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikazioa instalatu gabe dago."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Ez dago erabilgarri aplikazioa"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 926cdb9..913ff48 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"کاری"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"برنامه نصب نشده است."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"برنامه در دسترس نیست"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index f87441f..e0930b6 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Työ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Sovellusta ei ole asennettu."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Sovellus ei ole käytettävissä"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 5ac514d..50d6a06 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Lanceur3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Travail"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"L\'application n\'est pas installée."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Application indisponible"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index c04c951..65db47e 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Android Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"L\'application n\'est pas installée."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Application indisponible"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index e115a72..ca5ba3c 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Traballo"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"A aplicación non está instalada"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"A aplicación non está dispoñible"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 9fed480..da9da38 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"કાર્યાલય"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ઍપ્લિકેશન ઇન્સ્ટોલ થઈ નથી."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ઍપ્લિકેશન ઉપલબ્ધ નથી"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 3b0432e..8fa02c0 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"कार्यस्‍थल"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ऐप्‍लिकेशन इंस्‍टॉल नहीं है."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ऐप्लिकेशन उपलब्ध नहीं है"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 1caa27f..b738bc0 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Posao"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacija nije instalirana."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacija nije dostupna"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 3327590..7e26ca4 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Munka"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Az alkalmazás nincs telepítve."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Az alkalmazás nem érhető el"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index c81d55a..93404e6 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Աշխատանքային"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Ծրագիրը տեղադրված չէ:"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Հավելվածը հասանելի չէ"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 211f735..92c7acc 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Kantor"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikasi tidak dipasang."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikasi tidak tersedia"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index d66ca32..9d26c8c 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Vinna"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Forritið er ekki uppsett."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Forritið er ekki í boði"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 8c4e3c5..aaceb53 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Lavoro"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App non installata."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App non disponibile"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index c25e879..3525699 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"עבודה"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"האפליקציה לא מותקנת."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"האפליקציה אינה זמינה"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 354f020..dad4879 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"仕事用"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"このアプリはインストールされていません。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"このアプリは使用できません"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index e9afef9..b348afb 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"სამუშაო"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"აპი არ არის დაყენებული."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"აპი მიუწვდომელია"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 1cd9045..bd25ac9 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Жұмыс"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Қолданба орнатылмаған."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Қолданба қол жетімді емес"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index f4a328d..9738042 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ការងារ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"មិន​បាន​ដំឡើង​កម្មវិធី។"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"មិន​មាន​កម្មវិធី"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 14c8300..67ea6a8 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ಕೆಲಸ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ಅಪ್ಲಿಕೇಶನ್‌ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ಅಪ್ಲಿಕೇಶನ್ ಲಭ್ಯವಿಲ್ಲ"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 58b65da..e2767f9 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"업무"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"앱이 설치되지 않았습니다."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"앱을 사용할 수 없음"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 046a662..d52446c 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Жумуш"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Колдонмо орнотулган эмес."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Колдонмо жеткиликтүү эмес"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index afe7664..e764347 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ວຽກ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ແອັບຯບໍ່ໄດ້ຖືກຕິດຕັ້ງ."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ແອັບຯ​ໃຊ້​ບໍ່​ໄດ້"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 6571582..7f93ac8 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Darbas"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Programa neįdiegta."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Programa nepasiekiama"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 3da0fbb..13d88fe 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Darbs"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Lietotne nav instalēta."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Lietotne nav pieejama."</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 49df044..458fb73 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Стартер3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Работа"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Апликацијата не е инсталирана."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Апликацијата не е достапна"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 4362e7c..a118368 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"ലോഞ്ചർ3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ഔദ്യോഗികം"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"അപ്ലിക്കേഷൻ ഇൻസ്‌റ്റാളുചെ‌യ്‌തിട്ടില്ല."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"അപ്ലിക്കേഷൻ ലഭ്യമല്ല"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index ab02ac6..1b0e753 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Ажил"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Апп суугаагүй байна."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Апп-г ашиглах боломжгүй"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 40a3a1e..f71bbd9 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"कार्य"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"अ‍ॅप इंस्टॉल केलेला नाही."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"अ‍ॅप उपलब्ध नाही"</string>
@@ -47,7 +46,7 @@
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"आवडीच्या ट्रे मध्ये आणखी जागा नाही"</string>
     <string name="all_apps_button_label" msgid="8130441508702294465">"अ‍ॅप्स सूची"</string>
     <string name="all_apps_button_personal_label" msgid="1315764287305224468">"वैयक्तिक अ‍ॅप्स सूची"</string>
-    <string name="all_apps_button_work_label" msgid="7270707118948892488">"कामाच्या ठिकाणी वापरली जाणाऱ्या ॲप्सची सूची"</string>
+    <string name="all_apps_button_work_label" msgid="7270707118948892488">"कामाच्या ठिकाणी वापरली जाणाऱ्या अॅप्सची सूची"</string>
     <string name="all_apps_home_button_label" msgid="252062713717058851">"होम"</string>
     <string name="remove_drop_target_label" msgid="7812859488053230776">"काढा"</string>
     <string name="uninstall_drop_target_label" msgid="4722034217958379417">"अनइंस्टॉल करा"</string>
@@ -93,7 +92,7 @@
     <string name="title_change_settings" msgid="1376365968844349552">"सेटिंग्ज बदला"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"सूचना बिंदू दाखवा"</string>
     <string name="auto_add_shortcuts_label" msgid="8222286205987725611">"होम स्क्रीनवर आयकन जोडा"</string>
-    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"नवीन ॲप्ससाठी"</string>
+    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"नवीन अॅप्ससाठी"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"अज्ञात"</string>
     <string name="abandoned_clean_this" msgid="7610119707847920412">"काढा"</string>
     <string name="abandoned_search" msgid="891119232568284442">"शोधा"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 7d05412..492e6b6 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Kerja"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Apl tidak dipasang."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Apl tidak tersedia"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index fd1f7d9..78856f6 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"ဖွင့်တင်စက်၃"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"အလုပ်"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"အက်ပ်မထည့်သွင်းထားပါ"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"အက်ပ်လက်လှမ်း မမှီပါ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 2257367..356ed03 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Jobb"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Appen er ikke installert."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Appen er ikke tilgjengelig"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 1e7aee9..58d61f9 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"कार्य"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"अनुप्रयोग स्थापित छैन।"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"अनुप्रयोग उपलब्ध छैन"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index ec30d8c..c2dfb71 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Werk"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"App is niet geïnstalleerd."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"App is niet beschikbaar"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 0b3fb99..37bd2d5 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"ଲଞ୍ଚର୍3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"କାମ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ଆପ୍‌ ଇନଷ୍ଟଲ୍‌ ହୋଇନାହିଁ"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ଆପ୍‌ ଉପଲବ୍ଧ ନାହିଁ"</string>
@@ -86,7 +85,7 @@
     <string name="allow_rotation_title" msgid="7728578836261442095">"ହୋମ୍‌ ସ୍କ୍ରୀନ୍ ବୁଲାଇବା ଅନୁମତି ଦିଅନ୍ତୁ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ଯେତେବେଳେ ଫୋନକୁ ବୁଲାଯାଇଥାଏ"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"ବିଜ୍ଞପ୍ତି ଡଟ୍ସ"</string>
-    <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ"</string>
+    <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ କରନ୍ତୁ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="title_missing_notification_access" msgid="7503287056163941064">"ବିଜ୍ଞପ୍ତି ଆକ୍ସେସ୍‌ ଆବଶ୍ୟକ ଅଟେ"</string>
     <string name="msg_missing_notification_access" msgid="281113995110910548">"ବିଜ୍ଞପ୍ତି ବିନ୍ଦୁ ଦେଖାଇବାକୁ, <xliff:g id="NAME">%1$s</xliff:g> ପାଇଁ ଆପ୍‌ ବିଜ୍ଞପ୍ତି ଅନ୍‌ କରନ୍ତୁ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index d4cd5be..24686c4 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"ਦਫ਼ਤਰ"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ਐਪ ਇੰਸਟੌਲ ਨਹੀਂ ਕੀਤਾ ਹੋਇਆ ਹੈ।"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ਐਪ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 1cf0905..5d9b50e 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Praca"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacja nie jest zainstalowana."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacja niedostępna"</string>
@@ -130,7 +129,7 @@
     <string name="widget_resized" msgid="9130327887929620">"Szerokość i wysokość widżetu zmieniła się na <xliff:g id="NUMBER_0">%1$s</xliff:g> x <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
     <string name="action_deep_shortcut" msgid="2864038805849372848">"Skróty"</string>
     <string name="shortcuts_menu_with_notifications_description" msgid="2676582286544232849">"Skróty i powiadomienia"</string>
-    <string name="action_dismiss_notification" msgid="5909461085055959187">"Zamknij"</string>
+    <string name="action_dismiss_notification" msgid="5909461085055959187">"Odrzuć"</string>
     <string name="notification_dismissed" msgid="6002233469409822874">"Powiadomienie odrzucone"</string>
     <string name="all_apps_personal_tab" msgid="4190252696685155002">"Osobiste"</string>
     <string name="all_apps_work_tab" msgid="4884822796154055118">"Praca"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 071b71e..e0d2c74 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabalho"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"A aplicação não está instalada."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"A aplicação não está disponível"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index e88ff18..bb4834f 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Tela de início 3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabalho"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"O app não está instalado."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"O app não está disponível"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 1251d7e..f8a61f1 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplicația nu este instalată."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplicația nu este disponibilă"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 23b00d0..1cc699f 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Работа"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Приложение удалено"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Приложение недоступно"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index ef99a59..2f9dc42 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"කාර්යාලය"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"යෙදුම ස්ථාපනය කර නැත."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"යෙදුම නොතිබේ"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 340a128..5bbf7c3 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Pracovné"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikácia nie je nainštalovaná."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikácia nie je k dispozícii"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 044a4b4..417151b 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Služba"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacija ni nameščena."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacija ni na voljo"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index e25c1c1..7f2567c 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Nisësi3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Puna"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Aplikacioni nuk është i instaluar."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Aplikacioni nuk mundësohet"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 4e52592..5e2c954 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Апликација није инсталирана."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Апликација није доступна"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index e7400a6..7df507b 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Arbete"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Appen är inte installerad."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Appen är inte tillgänglig"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 6f33c4f..445e382 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Kazini"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Programu haijasakinishwa."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Programu haipatikani"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index e73cb89..799b6b8 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"பணியிடம்"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ஆப்ஸ் நிறுவப்படவில்லை."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ஆப்ஸ் இல்லை"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 0d99e2b..1f8d3b8 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"కార్యాలయం"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"యాప్ ఇన్‌స్టాల్ చేయబడలేదు."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"యాప్ అందుబాటులో లేదు"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 03c02ad..5ab9b86 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"งาน"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ไม่ได้ติดตั้งแอป"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"แอปไม่พร้อมใช้งาน"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 0df94c7..de4863a 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Trabaho"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Hindi naka-install ang app."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Hindi available ang app"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 56f8447..1b2b3d1 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"İş"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Uygulama yüklü değil."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Uygulama kullanılamıyor"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 13ba701..622cdcc 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Робоча папка"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Додаток видалено."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Додаток недоступний"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 4f77670..74b5eaa 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"دفتری"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"ایپ انسٹال نہیں ہے۔"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ایپ دستیاب نہیں ہے"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 69084d7..dac1ac9 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Ishga oid"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Ilova o‘rnatilmadi."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Ilova mavjud emas"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index f3519a5..4dc58b9 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Trình chạy 3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Ứng dụng chưa được cài đặt."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Ứng dụng không có sẵn"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 9804af1..0478627 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Work"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"未安装该应用。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"应用不可用"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index e737744..ed53c32 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"工作"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"尚未安裝應用程式。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"目前無法使用這個應用程式"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index e971b69..14f2e06 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"公司"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"應用程式未安裝。"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"應用程式目前無法使用"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 83178fe..b937764 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -20,7 +20,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Isiqalisi3"</string>
-    <string name="folder_name" msgid="7371454440695724752"></string>
     <string name="work_folder_name" msgid="3753320833950115786">"Umsebenzi"</string>
     <string name="activity_not_found" msgid="8071924732094499514">"Uhlelo lokusebenza alufakiwe."</string>
     <string name="activity_not_available" msgid="7456344436509528827">"Uhlelo lokusebenza alutholakali"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 69b8c8a..de17eb7 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -115,7 +115,6 @@
         <attr name="numFolderColumns" format="integer" />
         <!-- numHotseatIcons defaults to numColumns, if not specified -->
         <attr name="numHotseatIcons" format="integer" />
-
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
     </declare-styleable>
@@ -152,21 +151,6 @@
         <attr name="canThumbDetach" format="boolean" />
     </declare-styleable>
 
-    <declare-styleable name="CustomAppWidgetProviderInfo">
-        <attr name="providerId" format="integer" />
-
-        <attr name="android:label" />
-        <attr name="android:initialLayout" />
-        <attr name="android:icon" />
-        <attr name="android:previewImage" />
-        <attr name="android:resizeMode" />
-
-        <attr name="numRows" />
-        <attr name="numColumns" />
-        <attr name="numMinRows" format="integer" />
-        <attr name="numMinColumns" format="integer" />
-    </declare-styleable>
-
     <declare-styleable name="PreviewFragment">
         <attr name="android:name" />
         <attr name="android:id" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 13e096c..9d9c2e8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -23,8 +23,6 @@
 
     <!-- Application name -->
     <string name="app_name">Launcher3</string>
-    <!-- Default folder name -->
-    <string name="folder_name"></string>
     <!-- Work folder name -->
     <string name="work_folder_name">Work</string>
     <!-- Displayed when user selects a shortcut for an app that was uninstalled [CHAR_LIMIT=none]-->
@@ -105,6 +103,9 @@
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
 
+    <!-- Label for install dismiss prediction. -->
+    <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
+
     <!-- Permissions: -->
     <skip />
     <!-- Permission short label -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 339aef5..80c791c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -130,6 +130,10 @@
         <item name="widgetsTheme">@style/WidgetContainerTheme</item>
     </style>
 
+    <style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+    </style>
+
     <!--
     Theme overrides to element on homescreen, i.e., which are drawn on top on wallpaper.
     Various foreground colors are overridden to be workspaceTextColor so that they are properly
diff --git a/res/xml/custom_widgets.xml b/res/xml/custom_widgets.xml
deleted file mode 100644
index 4b54386..0000000
--- a/res/xml/custom_widgets.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2017 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.
--->
-
-<widgets>
-    <!-- Sample widget definition
-        <widget
-            android:label="My custom widget"
-            android:initialLayout="@layout/sample_widget_layout"
-            android:icon="@drawable/ic_launcher_home"
-            android:resizeMode="horizontal|vertical"
-            launcher:numRows="2"
-            launcher:numColumns="3"
-            launcher:numMinRows="1"
-            launcher:numMinColumns="2"
-            launcher:providerId="1" />
-    -->
-</widgets>
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
index ab39274..bc936b7 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
@@ -15,14 +15,13 @@
 import android.os.Process;
 import android.os.UserHandle;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.AppFilter;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.icons.IconCache;
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
new file mode 100644
index 0000000..c08e198
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/**
+ * Robolectric unit tests for {@link IntArray}
+ */
+@RunWith(RobolectricTestRunner.class)
+public class IntArrayTest {
+
+    @Test
+    public void concatAndParseString() {
+        int[] array = new int[] {0, 2, 3, 9};
+        String concat = IntArray.wrap(array).toConcatString();
+
+        int[] parsed = IntArray.fromConcatString(concat).toArray();
+        assertThat(array).isEqualTo(parsed);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
index f846de5..8513353 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
@@ -21,7 +21,6 @@
 import org.junit.runner.RunWith;
 
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 65f9d6b..af2cdc3 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
@@ -30,6 +29,7 @@
 import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
 
 import androidx.annotation.IntDef;
@@ -86,7 +86,7 @@
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
-            | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE;
+            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE;
 
     // Usually we show the back button when a floating view is open. Instead, hide for these types.
     public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
@@ -171,7 +171,7 @@
                 targetInfo.first, TYPE_WINDOW_STATE_CHANGED, targetInfo.second);
 
         if (mIsOpen) {
-            sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
+            performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
         }
         ActivityContext.lookupContext(getContext()).getDragLayer()
                 .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index d884049..c8e7619 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -29,11 +29,19 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageManagerHelper;
 
+import java.util.Comparator;
+
 /**
  * Represents an app in AllAppsView.
  */
 public class AppInfo extends ItemInfoWithIcon {
 
+    public static AppInfo[] EMPTY_ARRAY = new AppInfo[0];
+    public static Comparator<AppInfo> COMPONENT_KEY_COMPARATOR = (a, b) -> {
+        int uc = a.user.hashCode() - b.user.hashCode();
+        return uc != 0 ? uc : a.componentName.compareTo(b.componentName);
+    };
+
     /**
      * The intent used to start the application.
      */
@@ -41,6 +49,9 @@
 
     public ComponentName componentName;
 
+    // Section name used for indexing.
+    public String sectionName = "";
+
     public AppInfo() {
         itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
     }
@@ -74,6 +85,8 @@
         componentName = info.componentName;
         title = Utilities.trim(info.title);
         intent = new Intent(info.intent);
+        user = info.user;
+        runtimeStatusFlags = info.runtimeStatusFlags;
     }
 
     @Override
@@ -116,4 +129,9 @@
             info.runtimeStatusFlags |= FLAG_ADAPTIVE_ICON;
         }
     }
+
+    @Override
+    public AppInfo clone() {
+        return new AppInfo(this);
+    }
 }
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 9724869..ac43967 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -38,11 +38,15 @@
 import android.util.Patterns;
 import android.util.Xml;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherProvider.SqlArguments;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Thunk;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -72,7 +76,7 @@
 
     static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
             LayoutParserCallback callback) {
-        Pair<String, Resources> customizationApkInfo = Utilities.findSystemApk(
+        Pair<String, Resources> customizationApkInfo = PackageManagerHelper.findSystemApk(
                 ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
         if (customizationApkInfo == null) {
             return null;
@@ -83,7 +87,7 @@
 
         // Try with grid size and hotseat count
         String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
-            grid.numColumns, grid.numRows, grid.numHotseatIcons);
+                grid.numColumns, grid.numRows, grid.numHotseatIcons);
         int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
 
         // Try with only grid size
@@ -91,7 +95,7 @@
             Log.d(TAG, "Formatted layout: " + layoutName
                     + " not found. Trying layout without hosteat");
             layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
-                grid.numColumns, grid.numRows);
+                    grid.numColumns, grid.numRows);
             layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
         }
 
@@ -116,6 +120,7 @@
     private static final String TAG_AUTO_INSTALL = "autoinstall";
     private static final String TAG_FOLDER = "folder";
     private static final String TAG_APPWIDGET = "appwidget";
+    protected static final String TAG_SEARCH_WIDGET = "searchwidget";
     private static final String TAG_SHORTCUT = "shortcut";
     private static final String TAG_EXTRA = "extra";
 
@@ -147,8 +152,10 @@
     private static final String HOTSEAT_CONTAINER_NAME =
             Favorites.containerToString(Favorites.CONTAINER_HOTSEAT);
 
-    @Thunk final Context mContext;
-    @Thunk final AppWidgetHost mAppWidgetHost;
+    @Thunk
+    final Context mContext;
+    @Thunk
+    final AppWidgetHost mAppWidgetHost;
     protected final LayoutParserCallback mCallback;
 
     protected final PackageManager mPackageManager;
@@ -160,7 +167,8 @@
     private final int mColumnCount;
 
     private final int[] mTemp = new int[2];
-    @Thunk final ContentValues mValues;
+    @Thunk
+    final ContentValues mValues;
     protected final String mRootTag;
 
     protected SQLiteDatabase mDb;
@@ -244,7 +252,7 @@
      */
     protected int parseAndAddNode(
             XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
-        throws XmlPullParserException, IOException {
+            throws XmlPullParserException, IOException {
 
         if (TAG_INCLUDE.equals(parser.getName())) {
             final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
@@ -315,6 +323,7 @@
         parsers.put(TAG_AUTO_INSTALL, new AutoInstallParser());
         parsers.put(TAG_FOLDER, new FolderParser());
         parsers.put(TAG_APPWIDGET, new PendingWidgetParser());
+        parsers.put(TAG_SEARCH_WIDGET, new SearchWidgetParser());
         parsers.put(TAG_SHORTCUT, new ShortcutParser(mSourceRes));
         return parsers;
     }
@@ -347,15 +356,15 @@
                         info = mPackageManager.getActivityInfo(cn, 0);
                     } catch (PackageManager.NameNotFoundException nnfe) {
                         String[] packages = mPackageManager.currentToCanonicalPackageNames(
-                                new String[] { packageName });
+                                new String[]{packageName});
                         cn = new ComponentName(packages[0], className);
                         info = mPackageManager.getActivityInfo(cn, 0);
                     }
                     final Intent intent = new Intent(Intent.ACTION_MAIN, null)
-                        .addCategory(Intent.CATEGORY_LAUNCHER)
-                        .setComponent(cn)
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
-                                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                            .addCategory(Intent.CATEGORY_LAUNCHER)
+                            .setComponent(cn)
+                            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
 
                     return addShortcut(info.loadLabel(mPackageManager).toString(),
                             intent, Favorites.ITEM_TYPE_APPLICATION);
@@ -393,10 +402,10 @@
 
             mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON);
             final Intent intent = new Intent(Intent.ACTION_MAIN, null)
-                .addCategory(Intent.CATEGORY_LAUNCHER)
-                .setComponent(new ComponentName(packageName, className))
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
-                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                    .addCategory(Intent.CATEGORY_LAUNCHER)
+                    .setComponent(new ComponentName(packageName, className))
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
             return addShortcut(mContext.getString(R.string.package_state_unknown), intent,
                     Favorites.ITEM_TYPE_APPLICATION);
         }
@@ -444,7 +453,7 @@
             mValues.put(Favorites.ICON_RESOURCE, mIconRes.getResourceName(iconId));
 
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
-                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+                    Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
             return addShortcut(mSourceRes.getString(titleResId),
                     intent, Favorites.ITEM_TYPE_SHORTCUT);
         }
@@ -469,12 +478,22 @@
      */
     protected class PendingWidgetParser implements TagParser {
 
-        @Override
-        public int parseAndAdd(XmlPullParser parser)
-                throws XmlPullParserException, IOException {
+        @Nullable
+        public ComponentName getComponentName(XmlPullParser parser) {
             final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
             final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
             if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) {
+                return null;
+            }
+            return new ComponentName(packageName, className);
+        }
+
+
+        @Override
+        public int parseAndAdd(XmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            ComponentName cn = getComponentName(parser);
+            if (cn == null) {
                 if (LOGD) Log.d(TAG, "Skipping invalid <appwidget> with no component");
                 return -1;
             }
@@ -505,16 +524,15 @@
                     throw new RuntimeException("Widgets can contain only extras");
                 }
             }
-
-            return verifyAndInsert(new ComponentName(packageName, className), extras);
+            return verifyAndInsert(cn, extras);
         }
 
         protected int verifyAndInsert(ComponentName cn, Bundle extras) {
             mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString());
             mValues.put(Favorites.RESTORED,
-                    LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
-                            LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
-                            LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
+                    LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
+                            | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
+                            | LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
             mValues.put(Favorites._ID, mCallback.generateNewItemId());
             if (!extras.isEmpty()) {
                 mValues.put(Favorites.INTENT, new Intent().putExtras(extras).toUri(0));
@@ -529,6 +547,23 @@
         }
     }
 
+    protected class SearchWidgetParser extends PendingWidgetParser {
+        @Override
+        @Nullable
+        public ComponentName getComponentName(XmlPullParser parser) {
+            return QsbContainerView.getSearchComponentName(mContext);
+        }
+
+        @Override
+        protected int verifyAndInsert(ComponentName cn, Bundle extras) {
+            mValues.put(Favorites.OPTIONS, LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET);
+            int flags = mValues.getAsInteger(Favorites.RESTORED)
+                    | WorkspaceItemInfo.FLAG_RESTORE_STARTED;
+            mValues.put(Favorites.RESTORED, flags);
+            return super.verifyAndInsert(cn, extras);
+        }
+    }
+
     protected class FolderParser implements TagParser {
         private final ArrayMap<String, TagParser> mFolderElements;
 
@@ -548,7 +583,7 @@
             if (titleResId != 0) {
                 title = mSourceRes.getString(titleResId);
             } else {
-                title = mContext.getResources().getString(R.string.folder_name);
+                title = "";
             }
 
             mValues.put(Favorites.TITLE, title);
@@ -680,7 +715,8 @@
         int insertAndCheck(SQLiteDatabase db, ContentValues values);
     }
 
-    @Thunk static void copyInteger(ContentValues from, ContentValues to, String key) {
+    @Thunk
+    static void copyInteger(ContentValues from, ContentValues to, String key) {
         to.put(key, from.getAsInteger(key));
     }
 }
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 6455056..b28077f 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -27,6 +27,8 @@
 import android.content.res.Configuration;
 import android.view.ContextThemeWrapper;
 
+import androidx.annotation.IntDef;
+
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogUtils;
@@ -44,8 +46,6 @@
 import java.lang.annotation.Retention;
 import java.util.ArrayList;
 
-import androidx.annotation.IntDef;
-
 public abstract class BaseActivity extends Activity
         implements UserEventDelegate, LogStateProvider, ActivityContext {
 
@@ -265,12 +265,13 @@
         }
     }
 
-    protected void dumpMisc(PrintWriter writer) {
-        writer.println(" deviceProfile isTransposed=" + getDeviceProfile().isVerticalBarLayout());
-        writer.println(" orientation=" + getResources().getConfiguration().orientation);
-        writer.println(" mSystemUiController: " + mSystemUiController);
-        writer.println(" mActivityFlags: " + mActivityFlags);
-        writer.println(" mForceInvisible: " + mForceInvisible);
+    protected void dumpMisc(String prefix, PrintWriter writer) {
+        writer.println(prefix + "deviceProfile isTransposed="
+                + getDeviceProfile().isVerticalBarLayout());
+        writer.println(prefix + "orientation=" + getResources().getConfiguration().orientation);
+        writer.println(prefix + "mSystemUiController: " + mSystemUiController);
+        writer.println(prefix + "mActivityFlags: " + mActivityFlags);
+        writer.println(prefix + "mForceInvisible: " + mForceInvisible);
     }
 
     public static <T extends BaseActivity> T fromContext(Context context) {
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index f0b3afd..994ba65 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -30,17 +30,17 @@
 import android.view.View;
 import android.widget.Toast;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.uioverrides.DisplayRotationListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Themes;
 
-import androidx.annotation.Nullable;
-
 /**
  * Extension of BaseActivity allowing support for drag-n-drop
  */
@@ -120,6 +120,10 @@
 
     public abstract View getRootView();
 
+    public void returnToHomescreen() {
+        // no-op
+    }
+
     public Rect getViewBounds(View v) {
         int[] pos = new int[2];
         v.getLocationOnScreen(pos);
@@ -135,7 +139,7 @@
 
     public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item,
             @Nullable String sourceContainer) {
-        if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) {
+        if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) {
             Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return false;
         }
@@ -237,14 +241,14 @@
     protected void onDeviceProfileInitiated() {
         if (mDeviceProfile.isVerticalBarLayout()) {
             mRotationListener.enable();
-            mDeviceProfile.updateIsSeascape(getWindowManager());
+            mDeviceProfile.updateIsSeascape(this);
         } else {
             mRotationListener.disable();
         }
     }
 
     private void onDeviceRotationChanged() {
-        if (mDeviceProfile.updateIsSeascape(getWindowManager())) {
+        if (mDeviceProfile.updateIsSeascape(this)) {
             reapplyUi();
         }
     }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index b113249..7adb6a4 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -104,6 +104,8 @@
     private Drawable mIcon;
     private final boolean mCenterVertically;
 
+    private final int mDisplay;
+
     private final CheckLongPressHelper mLongPressHelper;
     private final StylusEventHelper mStylusEventHelper;
     private final float mSlop;
@@ -133,6 +135,9 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisableRelayout = false;
 
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private final boolean mIgnorePaddingTouch;
+
     private IconLoadRequest mIconLoadRequest;
 
     public BubbleTextView(Context context) {
@@ -152,26 +157,32 @@
                 R.styleable.BubbleTextView, defStyle, 0);
         mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
 
-        int display = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
+        mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
         final int defaultIconSize;
-        if (display == DISPLAY_WORKSPACE) {
+        if (mDisplay == DISPLAY_WORKSPACE) {
             DeviceProfile grid = mActivity.getWallpaperDeviceProfile();
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
-        } else if (display == DISPLAY_ALL_APPS) {
+            mIgnorePaddingTouch = true;
+        } else if (mDisplay == DISPLAY_ALL_APPS) {
             DeviceProfile grid = mActivity.getDeviceProfile();
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
             defaultIconSize = grid.allAppsIconSizePx;
-        } else if (display == DISPLAY_FOLDER) {
+            mIgnorePaddingTouch = true;
+        } else if (mDisplay == DISPLAY_FOLDER) {
             DeviceProfile grid = mActivity.getDeviceProfile();
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
             setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
             defaultIconSize = grid.folderChildIconSizePx;
+            mIgnorePaddingTouch = true;
         } else {
+            // widget_selection or shortcut_popup
             defaultIconSize = mActivity.getDeviceProfile().iconSizePx;
+            mIgnorePaddingTouch = false;
         }
+
         mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false);
 
         mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride,
@@ -319,6 +330,15 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
+        // ignore events if they happen in padding area
+        if (event.getAction() == MotionEvent.ACTION_DOWN && mIgnorePaddingTouch
+                && (event.getY() < getPaddingTop()
+                || event.getX() < getPaddingLeft()
+                || event.getY() > getHeight() - getPaddingBottom()
+                || event.getX() > getWidth() - getPaddingRight())) {
+            return false;
+        }
+
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
         boolean result = super.onTouchEvent(event);
@@ -564,7 +584,11 @@
             mDotInfo = mActivity.getDotInfoForItem(itemInfo);
             boolean isDotted = mDotInfo != null;
             float newDotScale = isDotted ? 1f : 0;
-            mDotRenderer = mActivity.getDeviceProfile().mDotRenderer;
+            if (mDisplay == DISPLAY_ALL_APPS) {
+                mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps;
+            } else {
+                mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace;
+            }
             if (wasDotted || isDotted) {
                 // Animate when a dot is first added or when it is removed.
                 if (animate && (wasDotted ^ isDotted) && isShown()) {
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 09fb244..976ccd5 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -2702,6 +2702,14 @@
             }
         }
 
+        /**
+         * Sets the position to the provided point
+         */
+        public void setXY(Point point) {
+            cellX = point.x;
+            cellY = point.y;
+        }
+
         public String toString() {
             return "(" + this.cellX + ", " + this.cellY + ")";
         }
diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java
index 75297f6..af85594 100644
--- a/src/com/android/launcher3/DefaultLayoutParser.java
+++ b/src/com/android/launcher3/DefaultLayoutParser.java
@@ -14,13 +14,16 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
+
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.util.Thunk;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.List;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
 
 /**
  * Implements the layout parser with rules for internal layouts and partner layouts.
@@ -55,7 +58,8 @@
         return getFolderElementsMap(mSourceRes);
     }
 
-    @Thunk ArrayMap<String, TagParser> getFolderElementsMap(Resources res) {
+    @Thunk
+    ArrayMap<String, TagParser> getFolderElementsMap(Resources res) {
         ArrayMap<String, TagParser> parsers = new ArrayMap<>();
         parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
         parsers.put(TAG_SHORTCUT, new UriShortcutParser(res));
@@ -67,6 +71,7 @@
         ArrayMap<String, TagParser> parsers = new ArrayMap<>();
         parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
         parsers.put(TAG_APPWIDGET, new AppWidgetParser());
+        parsers.put(TAG_SEARCH_WIDGET, new SearchWidgetParser());
         parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
         parsers.put(TAG_RESOLVE, new ResolveParser());
         parsers.put(TAG_FOLDER, new MyFolderParser());
@@ -229,7 +234,8 @@
     /**
      * A parser which adds a folder whose contents come from partner apk.
      */
-    @Thunk class PartnerFolderParser implements TagParser {
+    @Thunk
+    class PartnerFolderParser implements TagParser {
 
         @Override
         public int parseAndAdd(XmlPullParser parser) throws XmlPullParserException,
@@ -255,7 +261,8 @@
     /**
      * An extension of FolderParser which allows adding items from a different xml.
      */
-    @Thunk class MyFolderParser extends FolderParser {
+    @Thunk
+    class MyFolderParser extends FolderParser {
 
         @Override
         public int parseAndAdd(XmlPullParser parser) throws XmlPullParserException,
@@ -281,7 +288,7 @@
                 mPackageManager.getReceiverInfo(cn, 0);
             } catch (Exception e) {
                 String[] packages = mPackageManager.currentToCanonicalPackageNames(
-                        new String[] { cn.getPackageName() });
+                        new String[]{cn.getPackageName()});
                 cn = new ComponentName(packages[0], cn.getClassName());
                 try {
                     mPackageManager.getReceiverInfo(cn, 0);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 883e8c6..bc6fa6e 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -24,16 +24,20 @@
 import android.graphics.Rect;
 import android.util.DisplayMetrics;
 import android.view.Surface;
-import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
+import com.android.launcher3.util.DefaultDisplay;
 
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
+    // IDP with no grid override values.
+    @Nullable private final InvariantDeviceProfile originalIdp;
 
     // Device properties
     public final boolean isTablet;
@@ -111,6 +115,7 @@
 
     // All apps
     public int allAppsCellHeightPx;
+    public int allAppsCellWidthPx;
     public int allAppsIconSizePx;
     public int allAppsIconDrawablePaddingPx;
     public float allAppsIconTextSizePx;
@@ -129,13 +134,15 @@
     private boolean mIsSeascape;
 
     // Notification dots
-    public DotRenderer mDotRenderer;
+    public DotRenderer mDotRendererWorkSpace;
+    public DotRenderer mDotRendererAllApps;
 
     public DeviceProfile(Context context, InvariantDeviceProfile inv,
-            Point minSize, Point maxSize,
+            InvariantDeviceProfile originalIDP, Point minSize, Point maxSize,
             int width, int height, boolean isLandscape, boolean isMultiWindowMode) {
 
         this.inv = inv;
+        this.originalIdp = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
 
@@ -227,17 +234,33 @@
             // Recalculate the available dimensions using the new hotseat size.
             updateAvailableDimensions(dm, res);
         }
+
+        if (originalIDP != null) {
+            // Grid size change should not affect All Apps UI, so we use the original profile
+            // measurements here.
+            DeviceProfile originalProfile = isLandscape
+                    ? originalIDP.landscapeProfile
+                    : originalIDP.portraitProfile;
+            allAppsIconSizePx = originalProfile.iconSizePx;
+            allAppsIconTextSizePx = originalProfile.iconTextSizePx;
+            allAppsCellHeightPx = originalProfile.allAppsCellHeightPx;
+            allAppsIconDrawablePaddingPx = originalProfile.iconDrawablePaddingOriginalPx;
+            allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
+        }
         updateWorkspacePadding();
 
         // This is done last, after iconSizePx is calculated above.
-        mDotRenderer = new DotRenderer(iconSizePx, IconShape.getShapePath(),
+        mDotRendererWorkSpace = new DotRenderer(iconSizePx, IconShape.getShapePath(),
                 IconShape.DEFAULT_PATH_SIZE);
+        mDotRendererAllApps = iconSizePx == allAppsIconSizePx ? mDotRendererWorkSpace :
+                new DotRenderer(allAppsIconSizePx, IconShape.getShapePath(),
+                        IconShape.DEFAULT_PATH_SIZE);
     }
 
     public DeviceProfile copy(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
-        return new DeviceProfile(context, inv, size, size, widthPx, heightPx, isLandscape,
-                isMultiWindowMode);
+        return new DeviceProfile(context, inv, originalIdp, size, size, widthPx, heightPx,
+                isLandscape, isMultiWindowMode);
     }
 
     public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
@@ -248,8 +271,8 @@
         // In multi-window mode, we can have widthPx = availableWidthPx
         // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles'
         // widthPx and heightPx values where it's needed.
-        DeviceProfile profile = new DeviceProfile(context, inv, mwSize, mwSize, mwSize.x, mwSize.y,
-                isLandscape, true);
+        DeviceProfile profile = new DeviceProfile(context, inv, originalIdp, mwSize, mwSize,
+                mwSize.x, mwSize.y, isLandscape, true);
 
         // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
         float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
@@ -307,11 +330,16 @@
         updateAvailableFolderCellDimensions(dm, res);
     }
 
+    /**
+     * Updating the iconSize affects many aspects of the launcher layout, such as: iconSizePx,
+     * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
+     * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
+     */
     private void updateIconSize(float scale, Resources res, DisplayMetrics dm) {
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
-        float invIconSizePx = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
-        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizePx, dm) * scale));
+        float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
+        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, dm) * scale));
         iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
@@ -328,13 +356,13 @@
         }
         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
-        // All apps
-        allAppsIconTextSizePx = iconTextSizePx;
         allAppsIconSizePx = iconSizePx;
+        allAppsIconTextSizePx = iconTextSizePx;
         allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
         allAppsCellHeightPx = getCellSize().y;
+        allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
 
-        if (isVerticalLayout) {
+        if (isVerticalBarLayout()) {
             // Always hide the Workspace text with vertical bar layout.
             adjustToHideWorkspaceLabels();
         }
@@ -375,14 +403,15 @@
         Point totalWorkspacePadding = getTotalWorkspacePadding();
 
         // Check if the icons fit within the available height.
-        float usedHeight = folderCellHeightPx * inv.numFolderRows + folderBottomPanelSize;
-        int maxHeight = availableHeightPx - totalWorkspacePadding.y - folderMargin;
-        float scaleY = maxHeight / usedHeight;
+        float contentUsedHeight = folderCellHeightPx * inv.numFolderRows;
+        int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
+                - folderMargin;
+        float scaleY = contentMaxHeight / contentUsedHeight;
 
         // Check if the icons fit within the available width.
-        float usedWidth = folderCellWidthPx * inv.numFolderColumns;
-        int maxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
-        float scaleX = maxWidth / usedWidth;
+        float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns;
+        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
+        float scaleX = contentMaxWidth / contentUsedWidth;
 
         float scale = Math.min(scaleX, scaleY);
         if (scale < 1f) {
@@ -419,14 +448,18 @@
     }
 
     public Point getCellSize() {
+        return getCellSize(inv.numColumns, inv.numRows);
+    }
+
+    private Point getCellSize(int numColumns, int numRows) {
         Point result = new Point();
         // Since we are only concerned with the overall padding, layout direction does
         // not matter.
         Point padding = getTotalWorkspacePadding();
         result.x = calculateCellWidth(availableWidthPx - padding.x
-                - cellLayoutPaddingLeftRightPx * 2, inv.numColumns);
+                - cellLayoutPaddingLeftRightPx * 2, numColumns);
         result.y = calculateCellHeight(availableHeightPx - padding.y
-                - cellLayoutBottomPaddingPx, inv.numRows);
+                - cellLayoutBottomPaddingPx, numRows);
         return result;
     }
 
@@ -542,11 +575,19 @@
     }
 
     /**
+     * Returns true when the number of workspace columns and all apps columns differs.
+     */
+    private boolean allAppsHasDifferentNumColumns() {
+        return inv.numAllAppsColumns != inv.numColumns;
+    }
+
+    /**
      * Updates orientation information and returns true if it has changed from the previous value.
      */
-    public boolean updateIsSeascape(WindowManager wm) {
+    public boolean updateIsSeascape(Context context) {
         if (isVerticalBarLayout()) {
-            boolean isSeascape = wm.getDefaultDisplay().getRotation() == Surface.ROTATION_270;
+            boolean isSeascape = DefaultDisplay.INSTANCE.get(context).getInfo().rotation
+                    == Surface.ROTATION_270;
             if (mIsSeascape != isSeascape) {
                 mIsSeascape = isSeascape;
                 return true;
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
index b747d62..e2b7b68 100644
--- a/src/com/android/launcher3/FolderInfo.java
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -50,9 +50,9 @@
     /**
      * The apps and shortcuts
      */
-    public ArrayList<WorkspaceItemInfo> contents = new ArrayList<WorkspaceItemInfo>();
+    public ArrayList<WorkspaceItemInfo> contents = new ArrayList<>();
 
-    ArrayList<FolderListener> listeners = new ArrayList<FolderListener>();
+    private ArrayList<FolderListener> mListeners = new ArrayList<>();
 
     public FolderInfo() {
         itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
@@ -72,10 +72,10 @@
      * Add an app or shortcut for a specified rank.
      */
     public void add(WorkspaceItemInfo item, int rank, boolean animate) {
-        rank = Utilities.boundToRange(rank, 0, contents.size());
+        rank = Utilities.boundToRange(rank, 0, contents.size() + 1);
         contents.add(rank, item);
-        for (int i = 0; i < listeners.size(); i++) {
-            listeners.get(i).onAdd(item, rank);
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onAdd(item, rank);
         }
         itemsChanged(animate);
     }
@@ -87,53 +87,37 @@
      */
     public void remove(WorkspaceItemInfo item, boolean animate) {
         contents.remove(item);
-        for (int i = 0; i < listeners.size(); i++) {
-            listeners.get(i).onRemove(item);
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onRemove(item);
         }
         itemsChanged(animate);
     }
 
-    public void setTitle(CharSequence title) {
-        this.title = title;
-        for (int i = 0; i < listeners.size(); i++) {
-            listeners.get(i).onTitleChanged(title);
-        }
-    }
-
     @Override
     public void onAddToDatabase(ContentWriter writer) {
         super.onAddToDatabase(writer);
         writer.put(LauncherSettings.Favorites.TITLE, title)
                 .put(LauncherSettings.Favorites.OPTIONS, options);
-
     }
 
     public void addListener(FolderListener listener) {
-        listeners.add(listener);
+        mListeners.add(listener);
     }
 
     public void removeListener(FolderListener listener) {
-        listeners.remove(listener);
+        mListeners.remove(listener);
     }
 
     public void itemsChanged(boolean animate) {
-        for (int i = 0; i < listeners.size(); i++) {
-            listeners.get(i).onItemsChanged(animate);
-        }
-    }
-
-    public void prepareAutoUpdate() {
-        for (int i = 0; i < listeners.size(); i++) {
-            listeners.get(i).prepareAutoUpdate();
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onItemsChanged(animate);
         }
     }
 
     public interface FolderListener {
         public void onAdd(WorkspaceItemInfo item, int rank);
         public void onRemove(WorkspaceItemInfo item);
-        public void onTitleChanged(CharSequence title);
         public void onItemsChanged(boolean animate);
-        public void prepareAutoUpdate();
     }
 
     public boolean hasOption(int optionFlag) {
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 00acdcd..03ee707 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -105,8 +105,7 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        // Don't let if follow through to workspace
-        return true;
+        return event.getY() > getCellHeight();
     }
 
     @Override
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index f19c602..fe91602 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.BroadcastReceiver;
@@ -29,8 +31,6 @@
 import android.content.pm.ShortcutInfo;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.os.Handler;
-import android.os.Message;
 import android.os.Parcelable;
 import android.os.Process;
 import android.os.UserHandle;
@@ -39,6 +39,9 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.icons.BitmapInfo;
@@ -65,9 +68,6 @@
 
 public class InstallShortcutReceiver extends BroadcastReceiver {
 
-    private static final int MSG_ADD_TO_QUEUE = 1;
-    private static final int MSG_FLUSH_QUEUE = 2;
-
     public static final int FLAG_ACTIVITY_PAUSED = 1;
     public static final int FLAG_LOADER_RUNNING = 2;
     public static final int FLAG_DRAG_AND_DROP = 4;
@@ -100,66 +100,57 @@
     public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
     public static final int NEW_SHORTCUT_STAGGER_DELAY = 85;
 
-    private static final Handler sHandler = new Handler(LauncherModel.getWorkerLooper()) {
+    @WorkerThread
+    private static void addToQueue(Context context, PendingInstallShortcutInfo info) {
+        String encoded = info.encodeToString();
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        Set<String> strings = prefs.getStringSet(APPS_PENDING_INSTALL, null);
+        strings = (strings != null) ? new HashSet<>(strings) : new HashSet<>(1);
+        strings.add(encoded);
+        prefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).apply();
+    }
 
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_ADD_TO_QUEUE: {
-                    Pair<Context, PendingInstallShortcutInfo> pair =
-                            (Pair<Context, PendingInstallShortcutInfo>) msg.obj;
-                    String encoded = pair.second.encodeToString();
-                    SharedPreferences prefs = Utilities.getPrefs(pair.first);
-                    Set<String> strings = prefs.getStringSet(APPS_PENDING_INSTALL, null);
-                    strings = (strings != null) ? new HashSet<>(strings) : new HashSet<String>(1);
-                    strings.add(encoded);
-                    prefs.edit().putStringSet(APPS_PENDING_INSTALL, strings).apply();
-                    return;
-                }
-                case MSG_FLUSH_QUEUE: {
-                    Context context = (Context) msg.obj;
-                    LauncherModel model = LauncherAppState.getInstance(context).getModel();
-                    if (model.getCallback() == null) {
-                        // Launcher not loaded
-                        return;
-                    }
-
-                    ArrayList<Pair<ItemInfo, Object>> installQueue = new ArrayList<>();
-                    SharedPreferences prefs = Utilities.getPrefs(context);
-                    Set<String> strings = prefs.getStringSet(APPS_PENDING_INSTALL, null);
-                    if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings);
-                    if (strings == null) {
-                        return;
-                    }
-
-                    LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
-                    for (String encoded : strings) {
-                        PendingInstallShortcutInfo info = decode(encoded, context);
-                        if (info == null) {
-                            continue;
-                        }
-
-                        String pkg = getIntentPackage(info.launchIntent);
-                        if (!TextUtils.isEmpty(pkg)
-                                && !launcherApps.isPackageEnabledForProfile(pkg, info.user)
-                                && !info.isActivity) {
-                            if (DBG) Log.d(TAG, "Ignoring shortcut for absent package: "
-                                    + info.launchIntent);
-                            continue;
-                        }
-
-                        // Generate a shortcut info to add into the model
-                        installQueue.add(info.getItemInfo());
-                    }
-                    prefs.edit().remove(APPS_PENDING_INSTALL).apply();
-                    if (!installQueue.isEmpty()) {
-                        model.addAndBindAddedWorkspaceItems(installQueue);
-                    }
-                    return;
-                }
-            }
+    @WorkerThread
+    private static void flushQueueInBackground(Context context) {
+        LauncherModel model = LauncherAppState.getInstance(context).getModel();
+        if (model.getCallback() == null) {
+            // Launcher not loaded
+            return;
         }
-    };
+
+        ArrayList<Pair<ItemInfo, Object>> installQueue = new ArrayList<>();
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        Set<String> strings = prefs.getStringSet(APPS_PENDING_INSTALL, null);
+        if (DBG) Log.d(TAG, "Getting and clearing APPS_PENDING_INSTALL: " + strings);
+        if (strings == null) {
+            return;
+        }
+
+        LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
+        for (String encoded : strings) {
+            PendingInstallShortcutInfo info = decode(encoded, context);
+            if (info == null) {
+                continue;
+            }
+
+            String pkg = getIntentPackage(info.launchIntent);
+            if (!TextUtils.isEmpty(pkg)
+                    && !launcherApps.isPackageEnabledForProfile(pkg, info.user)
+                    && !info.isActivity) {
+                if (DBG) {
+                    Log.d(TAG, "Ignoring shortcut for absent package: " + info.launchIntent);
+                }
+                continue;
+            }
+
+            // Generate a shortcut info to add into the model
+            installQueue.add(info.getItemInfo());
+        }
+        prefs.edit().remove(APPS_PENDING_INSTALL).apply();
+        if (!installQueue.isEmpty()) {
+            model.addAndBindAddedWorkspaceItems(installQueue);
+        }
+    }
 
     public static void removeFromInstallQueue(Context context, HashSet<String> packageNames,
             UserHandle user) {
@@ -250,11 +241,6 @@
         return info == null ? null : (WorkspaceItemInfo) info.getItemInfo().first;
     }
 
-    public static WorkspaceItemInfo fromActivityInfo(LauncherActivityInfo info, Context context) {
-        return (WorkspaceItemInfo)
-                new PendingInstallShortcutInfo(info, context).getItemInfo().first;
-    }
-
     public static void queueShortcut(ShortcutInfo info, Context context) {
         queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context);
     }
@@ -291,7 +277,7 @@
 
     private static void queuePendingShortcutInfo(PendingInstallShortcutInfo info, Context context) {
         // Queue the item up for adding if launcher has not loaded properly yet
-        Message.obtain(sHandler, MSG_ADD_TO_QUEUE, Pair.create(context, info)).sendToTarget();
+        MODEL_EXECUTOR.post(() -> addToQueue(context, info));
         flushInstallQueue(context);
     }
 
@@ -307,7 +293,7 @@
         if (sInstallQueueDisabledFlags != 0) {
             return;
         }
-        Message.obtain(sHandler, MSG_FLUSH_QUEUE, context.getApplicationContext()).sendToTarget();
+        MODEL_EXECUTOR.post(() -> flushQueueInBackground(context));
     }
 
     /**
@@ -330,10 +316,10 @@
     private static class PendingInstallShortcutInfo {
 
         final boolean isActivity;
-        final ShortcutInfo shortcutInfo;
-        final AppWidgetProviderInfo providerInfo;
+        @Nullable final ShortcutInfo shortcutInfo;
+        @Nullable final AppWidgetProviderInfo providerInfo;
 
-        final Intent data;
+        @Nullable final Intent data;
         final Context mContext;
         final Intent launchIntent;
         final String label;
@@ -363,7 +349,12 @@
             shortcutInfo = null;
             providerInfo = null;
 
-            data = null;
+            String packageName = info.getComponentName().getPackageName();
+            data = new Intent();
+            data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
+                    new ComponentName(packageName, "")).setPackage(packageName));
+            data.putExtra(Intent.EXTRA_SHORTCUT_NAME, info.getLabel());
+
             user = info.getUser();
             mContext = context;
 
@@ -457,21 +448,26 @@
                 // This name is only used for comparisons and notifications, so fall back to activity
                 // name if not supplied
                 String name = ensureValidName(mContext, launchIntent, label).toString();
-                Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
-                Intent.ShortcutIconResource iconResource =
-                    data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+                Bitmap icon = data == null ? null
+                        : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+                Intent.ShortcutIconResource iconResource = data == null ? null
+                    : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
 
                 // Only encode the parameters which are supported by the API.
                 JSONStringer json = new JSONStringer()
                     .object()
                     .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
                     .key(NAME_KEY).value(name)
+                    .key(USER_HANDLE_KEY).value(
+                            UserManagerCompat.getInstance(mContext).getSerialNumberForUser(user))
                     .key(APP_SHORTCUT_TYPE_KEY).value(isActivity);
                 if (icon != null) {
                     byte[] iconByteArray = GraphicsUtils.flattenBitmap(icon);
-                    json = json.key(ICON_KEY).value(
-                            Base64.encodeToString(
-                                    iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
+                    if (iconByteArray != null) {
+                        json = json.key(ICON_KEY).value(
+                                Base64.encodeToString(
+                                        iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
+                    }
                 }
                 if (iconResource != null) {
                     json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName);
@@ -487,7 +483,7 @@
 
         public Pair<ItemInfo, Object> getItemInfo() {
             if (isActivity) {
-                WorkspaceItemInfo si = createWorkspaceItemInfo(data,
+                WorkspaceItemInfo si = createWorkspaceItemInfo(data, user,
                         LauncherAppState.getInstance(mContext));
                 si.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
                 si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
@@ -512,7 +508,7 @@
                 return Pair.create(widgetInfo, providerInfo);
             } else {
                 WorkspaceItemInfo itemInfo =
-                        createWorkspaceItemInfo(data, LauncherAppState.getInstance(mContext));
+                        createWorkspaceItemInfo(data, user, LauncherAppState.getInstance(mContext));
                 return Pair.create(itemInfo, null);
             }
         }
@@ -617,7 +613,7 @@
             // Already an activity target
             return original;
         }
-        if (!Utilities.isLauncherAppTarget(original.launchIntent)) {
+        if (!PackageManagerHelper.isLauncherAppTarget(original.launchIntent)) {
             return original;
         }
 
@@ -630,7 +626,8 @@
         return new PendingInstallShortcutInfo(info, original.mContext);
     }
 
-    private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, LauncherAppState app) {
+    private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, UserHandle user,
+            LauncherAppState app) {
         if (data == null) {
             Log.e(TAG, "Can't construct WorkspaceItemInfo with null data");
             return null;
@@ -647,10 +644,7 @@
         }
 
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
-
-        // Only support intents for current user for now. Intents sent from other
-        // users wouldn't get here without intent forwarding anyway.
-        info.user = Process.myUserHandle();
+        info.user = user;
 
         BitmapInfo iconInfo = null;
         LauncherIcons li = LauncherIcons.obtain(app.getContext());
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index bde87cb..d66ba73 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,6 +18,8 @@
 
 import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.settings.SettingsActivity.GRID_OPTIONS_PREFERENCE_KEY;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
 import android.annotation.TargetApi;
@@ -39,11 +41,13 @@
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.util.Xml;
-import android.view.Display;
-import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.util.ConfigMonitor;
+import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Themes;
@@ -54,9 +58,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
+import java.util.Comparator;
 
 public class InvariantDeviceProfile {
 
@@ -102,6 +104,8 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public float iconTextSize;
+    public float allAppsIconSize;
+    public float allAppsIconTextSize;
 
     private SparseArray<TypedValue> mExtraAttrs;
 
@@ -110,6 +114,11 @@
      */
     public int numHotseatIcons;
 
+    /**
+     * Number of columns in the all apps list.
+     */
+    public int numAllAppsColumns;
+
     public int defaultLayoutId;
     int demoModeLayoutId;
 
@@ -136,6 +145,9 @@
         landscapeIconSize = p.landscapeIconSize;
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
+        numAllAppsColumns = p.numAllAppsColumns;
+        allAppsIconSize = p.allAppsIconSize;
+        allAppsIconTextSize = p.allAppsIconTextSize;
         defaultLayoutId = p.defaultLayoutId;
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
@@ -144,7 +156,10 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
-        initGrid(context, Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null));
+        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
+                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
+                : null;
+        initGrid(context, gridName);
         mConfigMonitor = new ConfigMonitor(context,
                 APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
         mOverlayMonitor = new OverlayMonitor(context);
@@ -172,63 +187,81 @@
     }
 
     private String initGrid(Context context, String gridName) {
-        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
-        Display display = wm.getDefaultDisplay();
-        DisplayMetrics dm = new DisplayMetrics();
-        display.getMetrics(dm);
+        DefaultDisplay.Info displayInfo = DefaultDisplay.INSTANCE.get(context).getInfo();
 
-        Point smallestSize = new Point();
-        Point largestSize = new Point();
-        display.getCurrentSizeRange(smallestSize, largestSize);
+        Point smallestSize = new Point(displayInfo.smallestSize);
+        Point largestSize = new Point(displayInfo.largestSize);
 
-        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
         // This guarantees that width < height
-        float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm);
-        float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm);
-        // Sort the profiles based on the closeness to the device size
-        Collections.sort(allOptions, (a, b) ->
-                Float.compare(dist(minWidthDps, minHeightDps, a.minWidthDps, a.minHeightDps),
-                        dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps)));
-        DisplayOption interpolatedDisplayOption =
-                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
+        float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
+                displayInfo.metrics);
+        float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
+                displayInfo.metrics);
 
-        GridOption closestProfile = allOptions.get(0).grid;
-        numRows = closestProfile.numRows;
-        numColumns = closestProfile.numColumns;
-        numHotseatIcons = closestProfile.numHotseatIcons;
-        defaultLayoutId = closestProfile.defaultLayoutId;
-        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();
-        }
-
-        iconSize = interpolatedDisplayOption.iconSize;
-        iconShapePath = getIconShapePath(context);
-        landscapeIconSize = interpolatedDisplayOption.landscapeIconSize;
-        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, dm);
-        iconTextSize = interpolatedDisplayOption.iconTextSize;
-        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
-
-        // If the partner customization apk contains any grid overrides, apply them
-        // Supported overrides: numRows, numColumns, iconSize
-        applyPartnerDeviceProfileOverrides(context, dm);
-
-        Point realSize = new Point();
-        display.getRealSize(realSize);
+        Point realSize = new Point(displayInfo.realSize);
         // The real size never changes. smallSide and largeSide will remain the
         // same in any orientation.
         int smallSide = Math.min(realSize.x, realSize.y);
         int largeSide = Math.max(realSize.x, realSize.y);
 
-        landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
-                largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */);
-        portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
-                smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */);
+        // We want a list of all options as well as the list of filtered options. This allows us
+        // to have a consistent UI for areas that the grid size change should not affect
+        // ie. All Apps should be consistent between grid sizes.
+        ArrayList<DisplayOption> allOptions = new ArrayList<>();
+        ArrayList<DisplayOption> filteredOptions = new ArrayList<>();
+        getPredefinedDeviceProfiles(context, gridName, filteredOptions, allOptions);
+
+        if (allOptions.isEmpty() && filteredOptions.isEmpty()) {
+            throw new RuntimeException("No display option with canBeDefault=true");
+        }
+
+        // Sort the profiles based on the closeness to the device size
+        Comparator<DisplayOption> comparator = (a, b) -> Float.compare(dist(minWidthDps,
+                minHeightDps, a.minWidthDps, a.minHeightDps),
+                dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps));
+
+        // Calculate the device profiles as if there is no grid override.
+        Collections.sort(allOptions, comparator);
+        DisplayOption interpolatedDisplayOption =
+                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
+        initGridOption(context, allOptions, interpolatedDisplayOption, displayInfo.metrics);
+
+        // Create IDP with no grid override values.
+        InvariantDeviceProfile originalIDP = new InvariantDeviceProfile(this);
+        originalIDP.landscapeProfile = new DeviceProfile(context, this, null, smallestSize,
+                largestSize, largeSide, smallSide, true /* isLandscape */,
+                false /* isMultiWindowMode */);
+        originalIDP.portraitProfile = new DeviceProfile(context, this, null, smallestSize,
+                largestSize, smallSide, largeSide, false /* isLandscape */,
+                false /* isMultiWindowMode */);
+
+        if (filteredOptions.isEmpty()) {
+            filteredOptions = allOptions;
+
+            landscapeProfile = originalIDP.landscapeProfile;
+            portraitProfile = originalIDP.portraitProfile;
+        } else {
+            Collections.sort(filteredOptions, comparator);
+            interpolatedDisplayOption =
+                    invDistWeightedInterpolate(minWidthDps, minHeightDps, filteredOptions);
+
+            initGridOption(context, filteredOptions, interpolatedDisplayOption,
+                    displayInfo.metrics);
+            numAllAppsColumns = originalIDP.numAllAppsColumns;
+
+            landscapeProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
+                    largestSize, largeSide, smallSide, true /* isLandscape */,
+                    false /* isMultiWindowMode */);
+            portraitProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
+                    largestSize, smallSide, largeSide, false /* isLandscape */,
+                    false /* isMultiWindowMode */);
+        }
+
+        GridOption closestProfile = filteredOptions.get(0).grid;
+        if (!closestProfile.name.equals(gridName)) {
+            Utilities.getPrefs(context).edit()
+                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
+        }
 
         // We need to ensure that there is enough extra space in the wallpaper
         // for the intended parallax effects
@@ -246,6 +279,33 @@
         return closestProfile.name;
     }
 
+    private void initGridOption(Context context, ArrayList<DisplayOption> options,
+            DisplayOption displayOption, DisplayMetrics metrics) {
+        GridOption closestProfile = options.get(0).grid;
+        numRows = closestProfile.numRows;
+        numColumns = closestProfile.numColumns;
+        numHotseatIcons = closestProfile.numHotseatIcons;
+        defaultLayoutId = closestProfile.defaultLayoutId;
+        demoModeLayoutId = closestProfile.demoModeLayoutId;
+        numFolderRows = closestProfile.numFolderRows;
+        numFolderColumns = closestProfile.numFolderColumns;
+        numAllAppsColumns = numColumns;
+
+        mExtraAttrs = closestProfile.extraAttrs;
+
+        iconSize = displayOption.iconSize;
+        iconShapePath = getIconShapePath(context);
+        landscapeIconSize = displayOption.landscapeIconSize;
+        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
+        iconTextSize = displayOption.iconTextSize;
+        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
+
+        // If the partner customization apk contains any grid overrides, apply them
+        // Supported overrides: numRows, numColumns, iconSize
+        applyPartnerDeviceProfileOverrides(context, metrics);
+    }
+
+
     @Nullable
     public TypedValue getAttrValue(int attr) {
         return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
@@ -280,7 +340,7 @@
     public void setCurrentGrid(Context context, String gridName) {
         Context appContext = context.getApplicationContext();
         Utilities.getPrefs(appContext).edit().putString(KEY_IDP_GRID_NAME, gridName).apply();
-        new MainThreadExecutor().execute(() -> onConfigChanged(appContext));
+        MAIN_EXECUTOR.execute(() -> onConfigChanged(appContext));
     }
 
     private void onConfigChanged(Context context) {
@@ -288,9 +348,10 @@
         InvariantDeviceProfile oldProfile = new InvariantDeviceProfile(this);
 
         // Re-init grid
-        // TODO(b/131867841): We pass in null here so that we can calculate the closest profile
-        // without the bias of the grid name.
-        initGrid(context, null);
+        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
+                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
+                : null;
+        initGrid(context, gridName);
 
         int changeFlags = 0;
         if (numRows != oldProfile.numRows ||
@@ -322,7 +383,13 @@
         }
     }
 
-    static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, String gridName) {
+    /**
+     * @param gridName The current grid name.
+     * @param filteredOptionsOut List filled with all the filtered options based on gridName.
+     * @param allOptionsOut List filled with all the options that can be the default option.
+     */
+    static void getPredefinedDeviceProfiles(Context context, String gridName,
+            ArrayList<DisplayOption> filteredOptionsOut, ArrayList<DisplayOption> allOptionsOut) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
@@ -349,26 +416,19 @@
             throw new RuntimeException(e);
         }
 
-        ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
         if (!TextUtils.isEmpty(gridName)) {
             for (DisplayOption option : profiles) {
                 if (gridName.equals(option.grid.name)) {
-                    filteredProfiles.add(option);
+                    filteredOptionsOut.add(option);
                 }
             }
         }
-        if (filteredProfiles.isEmpty()) {
-            // No grid found, use the default options
-            for (DisplayOption option : profiles) {
-                if (option.canBeDefault) {
-                    filteredProfiles.add(option);
-                }
+
+        for (DisplayOption option : profiles) {
+            if (option.canBeDefault) {
+                allOptionsOut.add(option);
             }
         }
-        if (filteredProfiles.isEmpty()) {
-            throw new RuntimeException("No display option with canBeDefault=true");
-        }
-        return filteredProfiles;
     }
 
     private int getLauncherIconDensity(int requiredSize) {
@@ -514,6 +574,7 @@
                     R.styleable.GridDisplayOption_numFolderRows, numRows);
             numFolderColumns = a.getInt(
                     R.styleable.GridDisplayOption_numFolderColumns, numColumns);
+
             a.recycle();
 
             extraAttrs = Themes.createValueMap(context, attrs,
@@ -530,8 +591,8 @@
         private final boolean canBeDefault;
 
         private float iconSize;
-        private float landscapeIconSize;
         private float iconTextSize;
+        private float landscapeIconSize;
 
         DisplayOption(GridOption grid, Context context, AttributeSet attrs) {
             this.grid = grid;
@@ -549,6 +610,7 @@
             landscapeIconSize = a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize,
                     iconSize);
             iconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0);
+
             a.recycle();
         }
 
diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java
index e29f927..1550bb0 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/ItemInfoWithIcon.java
@@ -27,6 +27,8 @@
  */
 public abstract class ItemInfoWithIcon extends ItemInfo {
 
+    public static final String TAG = "ItemInfoDebug";
+
     /**
      * A bitmap version of the application icon.
      */
@@ -126,4 +128,8 @@
         iconColor = info.color;
     }
 
+    /**
+     * @return a copy of this
+     */
+    public abstract ItemInfoWithIcon clone();
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 257f0df..8086045 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -16,10 +16,11 @@
 
 package com.android.launcher3;
 
-import static android.content.pm.ActivityInfo.CONFIG_LOCALE;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.ALL_APPS;
@@ -77,6 +78,9 @@
 import android.view.animation.OvershootInterpolator;
 import android.widget.Toast;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -91,9 +95,9 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.folder.FolderIconPreviewVerifier;
+import com.android.launcher3.folder.FolderNameProvider;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.keyboard.CustomActionsPopup;
@@ -103,14 +107,14 @@
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.logging.UserEventDispatcher.UserEventDelegate;
 import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.states.InternalStateHandler;
 import com.android.launcher3.states.RotationHelper;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -145,7 +149,7 @@
 import com.android.launcher3.widget.WidgetHostViewLoader;
 import com.android.launcher3.widget.WidgetListRowEntry;
 import com.android.launcher3.widget.WidgetsFullSheet;
-import com.android.launcher3.widget.custom.CustomWidgetParser;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -156,14 +160,11 @@
 import java.util.List;
 import java.util.function.Predicate;
 
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
 /**
  * Default launcher application.
  */
 public class Launcher extends BaseDraggingActivity implements LauncherExterns,
-        LauncherModel.Callbacks, LauncherProviderChangeListener, UserEventDelegate,
+        Callbacks, LauncherProviderChangeListener, UserEventDelegate,
         InvariantDeviceProfile.OnIDPChangeListener {
     public static final String TAG = "Launcher";
     static final boolean LOGD = false;
@@ -220,9 +221,11 @@
     private LauncherAppTransitionManager mAppTransitionManager;
     private Configuration mOldConfig;
 
-    @Thunk Workspace mWorkspace;
+    @Thunk
+    Workspace mWorkspace;
     private View mLauncherView;
-    @Thunk DragLayer mDragLayer;
+    @Thunk
+    DragLayer mDragLayer;
     private DragController mDragController;
 
     private AppWidgetManagerCompat mAppWidgetManager;
@@ -230,21 +233,25 @@
 
     private final int[] mTmpAddItemCellCoordinates = new int[2];
 
-    @Thunk Hotseat mHotseat;
+    @Thunk
+    Hotseat mHotseat;
 
     private DropTargetBar mDropTargetBar;
 
     // Main container view for the all apps screen.
-    @Thunk AllAppsContainerView mAppsView;
+    @Thunk
+    AllAppsContainerView mAppsView;
     AllAppsTransitionController mAllAppsController;
 
     // Scrim view for the all apps and overview state.
-    @Thunk ScrimView mScrimView;
+    @Thunk
+    ScrimView mScrimView;
 
     // UI and state for the overview panel
     private View mOverviewPanel;
 
-    @Thunk boolean mWorkspaceLoading = true;
+    @Thunk
+    boolean mWorkspaceLoading = true;
 
     private ArrayList<OnResumeCallback> mOnResumeCallbacks = new ArrayList<>();
 
@@ -326,8 +333,8 @@
         UiFactory.onCreate(this);
 
         mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
-
-        mAppWidgetHost = new LauncherAppWidgetHost(this);
+        mAppWidgetHost = new LauncherAppWidgetHost(this,
+                appWidgetId -> getWorkspace().removeWidget(appWidgetId));
         mAppWidgetHost.startListening();
 
         mLauncherView = LayoutInflater.from(this).inflate(R.layout.launcher, null);
@@ -389,7 +396,8 @@
         RaceConditionTracker.onEvent(ON_CREATE_EVT, EXIT);
         mStateManager.addStateListener(new LauncherStateManager.StateListener() {
             @Override
-            public void onStateTransitionStart(LauncherState toState) {}
+            public void onStateTransitionStart(LauncherState toState) {
+            }
 
             @Override
             public void onStateTransitionComplete(LauncherState finalState) {
@@ -419,10 +427,6 @@
     public void onConfigurationChanged(Configuration newConfig) {
         int diff = newConfig.diff(mOldConfig);
 
-        if ((diff & CONFIG_LOCALE) != 0) {
-            Folder.setLocaleDependentFields(getResources(), true /* force */);
-        }
-
         if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) {
             onIdpChanged(mDeviceProfile.inv);
         }
@@ -442,12 +446,16 @@
 
     @Override
     public void reapplyUi() {
+        reapplyUi(true /* cancelCurrentAnimation */);
+    }
+
+    public void reapplyUi(boolean cancelCurrentAnimation) {
         if (supportsFakeLandscapeUI()) {
             mRotationMode = mStableDeviceProfile == null
                     ? RotationMode.NORMAL : UiFactory.getRotationMode(mDeviceProfile);
         }
         getRootView().dispatchInsets();
-        getStateManager().reapplyState(true /* cancelCurrentAnimation */);
+        getStateManager().reapplyState(cancelCurrentAnimation);
     }
 
     @Override
@@ -497,6 +505,8 @@
         // Load configuration-specific DeviceProfile
         mDeviceProfile = idp.getDeviceProfile(this);
         if (isInMultiWindowMode()) {
+            // Note: Calls to getSize() can't rely on our cached DefaultDisplay since it can return
+            // the app window size
             Display display = getWindowManager().getDefaultDisplay();
             Point mwSize = new Point();
             display.getSize(mwSize);
@@ -548,6 +558,10 @@
         return mStateManager;
     }
 
+    public FolderNameProvider getFolderNameProvider() {
+        return new FolderNameProvider();
+    }
+
     @Override
     public <T extends View> T findViewById(int id) {
         return mLauncherView.findViewById(id);
@@ -604,10 +618,9 @@
         if (info.container >= 0) {
             View folderIcon = getWorkspace().getHomescreenIconByItemId(info.container);
             if (folderIcon instanceof FolderIcon && folderIcon.getTag() instanceof FolderInfo) {
-                FolderIconPreviewVerifier verifier =
-                        new FolderIconPreviewVerifier(getDeviceProfile().inv);
-                verifier.setFolderInfo((FolderInfo) folderIcon.getTag());
-                if (verifier.isItemInPreview(info.rank)) {
+                if (new FolderGridOrganizer(getDeviceProfile().inv)
+                        .setFolderInfo((FolderInfo) folderIcon.getTag())
+                        .isItemInPreview(info.rank)) {
                     folderIcon.invalidate();
                 }
             }
@@ -647,7 +660,8 @@
                             .getLauncherAppWidgetInfo(widgetId);
                     if (provider != null) {
                         new WidgetAddFlowHandler(provider)
-                                .startConfigActivity(this, widgetInfo, REQUEST_RECONFIGURE_APPWIDGET);
+                                .startConfigActivity(this, widgetInfo,
+                                        REQUEST_RECONFIGURE_APPWIDGET);
                     }
                 }
                 break;
@@ -835,7 +849,8 @@
         }
     }
 
-    @Thunk void completeTwoStageWidgetDrop(
+    @Thunk
+    void completeTwoStageWidgetDrop(
             final int resultCode, final int appWidgetId, final PendingRequestArgs requestArgs) {
         CellLayout cellLayout = mWorkspace.getScreenWithId(requestArgs.screenId);
         Runnable onCompleteRunnable = null;
@@ -932,15 +947,14 @@
         int containerType = mStateManager.getState().containerType;
         if (containerType == ContainerType.WORKSPACE && mWorkspace != null) {
             getUserEventDispatcher().logActionCommand(command,
-                containerType, -1, mWorkspace.isOverlayShown() ? -1 : 0);
+                    containerType, -1, mWorkspace.isOverlayShown() ? -1 : 0);
         } else {
             getUserEventDispatcher().logActionCommand(command, containerType, -1);
         }
 
     }
 
-    protected void onStateSet(LauncherState state) {
-        getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
+    public void onStateSetStart(LauncherState state) {
         if (mDeferredResumePending) {
             handleDeferredResume();
         }
@@ -949,6 +963,12 @@
         }
     }
 
+    public void onStateSetEnd(LauncherState state) {
+        getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
+        getWorkspace().setClipChildren(!state.disablePageClipping);
+        finishAutoCancelActionMode();
+    }
+
     @Override
     protected void onResume() {
         RaceConditionTracker.onEvent(ON_RESUME_EVT, ENTER);
@@ -1061,7 +1081,8 @@
             mStateManager.goToState(state, false /* animated */);
         }
 
-        PendingRequestArgs requestArgs = savedState.getParcelable(RUNTIME_STATE_PENDING_REQUEST_ARGS);
+        PendingRequestArgs requestArgs = savedState.getParcelable(
+                RUNTIME_STATE_PENDING_REQUEST_ARGS);
         if (requestArgs != null) {
             setWaitingForResult(requestArgs);
         }
@@ -1131,8 +1152,7 @@
      * Creates a view representing a shortcut inflated from the specified resource.
      *
      * @param parent The group the shortcut belongs to.
-     * @param info The data structure describing the shortcut.
-     *
+     * @param info   The data structure describing the shortcut.
      * @return A View inflated from layoutResId.
      */
     public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
@@ -1234,7 +1254,8 @@
      *
      * @param appWidgetId The app widget id
      */
-    @Thunk void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
+    @Thunk
+    void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
             AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
 
         if (appWidgetInfo == null) {
@@ -1352,7 +1373,9 @@
         return mSharedPrefs;
     }
 
-    public int getOrientation() { return mOldConfig.orientation; }
+    public int getOrientation() {
+        return mOldConfig.orientation;
+    }
 
     @Override
     protected void onNewIntent(Intent intent) {
@@ -1436,9 +1459,10 @@
             outState.remove(RUNTIME_STATE_WIDGET_PANEL);
         }
 
-        // We close any open folders and shortcut containers since they will not be re-opened,
+        // We close any open folders and shortcut containers that are not safe for rebind,
         // and we need to make sure this state is reflected.
-        AbstractFloatingView.closeAllOpenViews(this, false);
+        AbstractFloatingView.closeOpenViews(this, false, TYPE_ALL & ~TYPE_REBIND_SAFE);
+        finishAutoCancelActionMode();
 
         if (mPendingRequestArgs != null) {
             outState.putParcelable(RUNTIME_STATE_PENDING_REQUEST_ARGS, mPendingRequestArgs);
@@ -1575,7 +1599,8 @@
 
     void addAppWidgetImpl(int appWidgetId, ItemInfo info,
             AppWidgetHostView boundWidget, WidgetAddFlowHandler addFlowHandler, int delay) {
-        if (!addFlowHandler.startConfigActivity(this, appWidgetId, info, REQUEST_CREATE_APPWIDGET)) {
+        if (!addFlowHandler.startConfigActivity(this, appWidgetId, info,
+                REQUEST_CREATE_APPWIDGET)) {
             // If the configuration flow was not started, add the widget
 
             Runnable onComplete = new Runnable() {
@@ -1585,7 +1610,8 @@
                     mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                 }
             };
-            completeAddAppWidget(appWidgetId, info, boundWidget, addFlowHandler.getProviderInfo(this));
+            completeAddAppWidget(appWidgetId, info, boundWidget,
+                    addFlowHandler.getProviderInfo(this));
             mWorkspace.removeExtraEmptyScreenDelayed(true, onComplete, delay, false);
         }
     }
@@ -1611,7 +1637,7 @@
                 break;
             default:
                 throw new IllegalStateException("Unknown item type: " + info.itemType);
-            }
+        }
     }
 
     /**
@@ -1647,10 +1673,9 @@
         } else {
             // In this case, we either need to start an activity to get permission to bind
             // the widget, or we need to start an activity to configure the widget, or both.
-            if (FeatureFlags.ENABLE_CUSTOM_WIDGETS &&
-                    info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET) {
-                appWidgetId = CustomWidgetParser.getWidgetIdForCustomProvider(
-                        this, info.componentName);
+            if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET) {
+                appWidgetId = CustomWidgetManager.INSTANCE.get(this).getWidgetIdForCustomProvider(
+                        info.componentName);
             } else {
                 appWidgetId = getAppWidgetHost().allocateAppWidgetId();
             }
@@ -1669,7 +1694,7 @@
     FolderIcon addFolder(CellLayout layout, int container, final int screenId, int cellX,
             int cellY) {
         final FolderInfo folderInfo = new FolderInfo();
-        folderInfo.title = getText(R.string.folder_name);
+        folderInfo.title = "";
 
         // Update the model
         getModelWriter().addItemToDatabase(folderInfo, container, screenId, cellX, cellY);
@@ -1723,8 +1748,6 @@
         return true;
     }
 
-
-
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         return (event.getKeyCode() == KeyEvent.KEYCODE_HOME) || super.dispatchKeyEvent(event);
@@ -1788,7 +1811,7 @@
 
     @Override
     public int getCurrentState() {
-        if(mStateManager.getState() == LauncherState.ALL_APPS) {
+        if (mStateManager.getState() == LauncherState.ALL_APPS) {
             return StatsLogUtils.LAUNCHER_STATE_ALLAPPS;
         } else if (mStateManager.getState() == OVERVIEW) {
             return StatsLogUtils.LAUNCHER_STATE_OVERVIEW;
@@ -1923,8 +1946,7 @@
         // Floating panels (except the full widget sheet) are associated with individual icons. If
         // we are starting a fresh bind, close all such panels as all the icons are about
         // to go away.
-        AbstractFloatingView.closeOpenViews(this, true,
-                AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
+        AbstractFloatingView.closeOpenViews(this, true, TYPE_ALL & ~TYPE_REBIND_SAFE);
 
         setWorkspaceLoading(true);
 
@@ -2052,7 +2074,7 @@
                     throw new RuntimeException("Invalid Item Type");
             }
 
-             /*
+            /*
              * Remove colliding items.
              */
             if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
@@ -2124,6 +2146,14 @@
     }
 
     private View inflateAppWidget(LauncherAppWidgetInfo item) {
+        if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) {
+            item.providerName = QsbContainerView.getSearchComponentName(this);
+            if (item.providerName == null) {
+                getModelWriter().deleteItemFromDatabase(item);
+                return null;
+            }
+        }
+
         if (mIsSafeModeEnabled) {
             PendingAppWidgetHostView view =
                     new PendingAppWidgetHostView(this, item, mIconCache, true);
@@ -2170,7 +2200,8 @@
                     pendingInfo.spanY = item.spanY;
                     pendingInfo.minSpanX = item.minSpanX;
                     pendingInfo.minSpanY = item.minSpanY;
-                    Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo);
+                    Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(this,
+                            pendingInfo);
 
                     boolean isDirectConfig =
                             item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
@@ -2324,6 +2355,11 @@
         // override the previous page so we don't log the page switch.
         mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
 
+        // Cache one page worth of icons
+        getViewCache().setCacheSize(R.layout.folder_application,
+                mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
+        getViewCache().setCacheSize(R.layout.folder_page, 2);
+
         TraceHelper.endSection("finishBindingItems");
     }
 
@@ -2345,7 +2381,7 @@
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    public void bindAllApplications(ArrayList<AppInfo> apps) {
+    public void bindAllApplications(AppInfo[] apps) {
         mAppsView.getAppsStore().setApps(apps);
     }
 
@@ -2358,16 +2394,6 @@
         mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
     }
 
-    /**
-     * A package was updated.
-     *
-     * Implementation of the method from LauncherModel.Callbacks.
-     */
-    @Override
-    public void bindAppsAddedOrUpdated(ArrayList<AppInfo> apps) {
-        mAppsView.getAppsStore().addOrUpdateApps(apps);
-    }
-
     @Override
     public void bindPromiseAppProgressUpdated(PromiseAppInfo app) {
         mAppsView.getAppsStore().updatePromiseAppProgress(app);
@@ -2415,11 +2441,6 @@
     }
 
     @Override
-    public void bindAppInfosRemoved(final ArrayList<AppInfo> appInfos) {
-        mAppsView.getAppsStore().removeApps(appInfos);
-    }
-
-    @Override
     public void bindAllWidgets(final ArrayList<WidgetListRowEntry> allWidgets) {
         mPopupDataProvider.setAllWidgets(allWidgets);
     }
@@ -2464,14 +2485,16 @@
         }
 
         writer.println(prefix + "Misc:");
-        writer.print(prefix + "\tmWorkspaceLoading=" + mWorkspaceLoading);
-        writer.print(" mPendingRequestArgs=" + mPendingRequestArgs);
-        writer.println(" mPendingActivityResult=" + mPendingActivityResult);
-        writer.println(" mRotationHelper: " + mRotationHelper);
+        dumpMisc(prefix + "\t", writer);
+        writer.println(prefix + "\tmWorkspaceLoading=" + mWorkspaceLoading);
+        writer.println(prefix + "\tmPendingRequestArgs=" + mPendingRequestArgs
+                + " mPendingActivityResult=" + mPendingActivityResult);
+        writer.println(prefix + "\tmRotationHelper: " + mRotationHelper);
+        writer.println(prefix + "\tmAppWidgetHost.isListening: " + mAppWidgetHost.isListening());
+
         // Extra logging for b/116853349
         mDragLayer.dump(prefix, writer);
         mStateManager.dump(prefix, writer);
-        dumpMisc(writer);
 
         try {
             FileLog.flushAll(writer);
@@ -2532,8 +2555,8 @@
                     if (focusedView instanceof BubbleTextView
                             && focusedView.getTag() instanceof ItemInfo
                             && mAccessibilityDelegate.performAction(focusedView,
-                                    (ItemInfo) focusedView.getTag(),
-                                    LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
+                            (ItemInfo) focusedView.getTag(),
+                            LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
                         PopupContainerWithArrow.getOpen(this).requestFocus();
                         return true;
                     }
@@ -2578,6 +2601,12 @@
         return (Launcher) fromContext(context);
     }
 
+    @Override
+    public void returnToHomescreen() {
+        super.returnToHomescreen();
+        getStateManager().goToState(LauncherState.NORMAL);
+    }
+
     /**
      * Just a wrapper around the type cast to allow easier tracking of calls.
      */
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index b4a2216..d70abc2 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 public class LauncherAppState {
 
@@ -149,6 +150,8 @@
     LauncherModel setLauncher(Launcher launcher) {
         getLocalProvider(mContext).setLauncherProviderChangeListener(launcher);
         mModel.initialize(launcher);
+        CustomWidgetManager.INSTANCE.get(launcher)
+                .setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
         return mModel;
     }
 
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
index 7f5ac52..1215d43 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -27,14 +27,15 @@
 import android.content.Intent;
 import android.os.Handler;
 import android.util.SparseArray;
-import android.view.LayoutInflater;
 import android.widget.Toast;
 
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.widget.DeferredAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
+import java.util.function.IntConsumer;
 
 
 /**
@@ -56,9 +57,17 @@
     private final Context mContext;
     private int mFlags = FLAG_RESUMED;
 
+    private IntConsumer mAppWidgetRemovedCallback = null;
+
     public LauncherAppWidgetHost(Context context) {
+        this(context, null);
+    }
+
+    public LauncherAppWidgetHost(Context context,
+            IntConsumer appWidgetRemovedCallback) {
         super(context, APPWIDGET_HOST_ID);
         mContext = context;
+        mAppWidgetRemovedCallback = appWidgetRemovedCallback;
     }
 
     @Override
@@ -105,6 +114,10 @@
         super.stopListening();
     }
 
+    public boolean isListening() {
+        return (mFlags & FLAG_LISTENING) != 0;
+    }
+
     /**
      * Updates the resumed state of the host.
      * When a host is not resumed, it defers calls to startListening until host is resumed again.
@@ -180,10 +193,8 @@
             LauncherAppWidgetProviderInfo appWidget) {
         if (appWidget.isCustomWidget()) {
             LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
-            LayoutInflater inflater = (LayoutInflater)
-                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            inflater.inflate(appWidget.initialLayout, lahv);
             lahv.setAppWidget(0, appWidget);
+            CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
             return lahv;
         } else if ((mFlags & FLAG_LISTENING) == 0) {
             DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
@@ -207,7 +218,7 @@
                 }
                 view.setAppWidget(appWidgetId, appWidget);
                 view.switchToErrorView();
-                return  view;
+                return view;
             }
         }
     }
@@ -225,6 +236,18 @@
         info.initSpans(mContext);
     }
 
+    /**
+     * Called on an appWidget is removed for a widgetId
+     * @param appWidgetId
+     * TODO: make this override when SDK is updated
+     */
+    public void onAppWidgetRemoved(int appWidgetId) {
+        if (mAppWidgetRemovedCallback == null) {
+            return;
+        }
+        mAppWidgetRemovedCallback.accept(appWidgetId);
+    }
+
     @Override
     public void deleteAppWidgetId(int appWidgetId) {
         super.deleteAppWidgetId(appWidgetId);
diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java
index 051846c..b824301 100644
--- a/src/com/android/launcher3/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java
@@ -29,6 +29,9 @@
  */
 public class LauncherAppWidgetInfo extends ItemInfo {
 
+    public static final int OPTION_SEARCH_WIDGET = 1;
+
+
     public static final int RESTORE_COMPLETED = 0;
 
     /**
@@ -97,6 +100,11 @@
     public Intent bindOptions;
 
     /**
+     * Widget options
+     */
+    public int options;
+
+    /**
      * Nonnull for pending widgets. We use this to get the icon and title for the widget.
      */
     public PackageItemInfo pendingItemInfo;
@@ -137,6 +145,7 @@
         writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId)
                 .put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, providerName.flattenToString())
                 .put(LauncherSettings.Favorites.RESTORED, restoreStatus)
+                .put(LauncherSettings.Favorites.OPTIONS, options)
                 .put(LauncherSettings.Favorites.INTENT, bindOptions);
     }
 
@@ -164,4 +173,13 @@
     public final boolean hasRestoreFlag(int flag) {
         return (restoreStatus & flag) == flag;
     }
+
+    /**
+     * returns if widget options include an option or not
+     * @param option
+     * @return
+     */
+    public final boolean hasOptionFlag(int option) {
+        return (options & option) != 0;
+    }
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index a041489..a012412 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -18,29 +18,32 @@
 
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
 import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.AddWorkspaceItemsTask;
+import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BaseModelUpdateTask;
 import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.CacheDataUpdatedTask;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.model.LoaderTask;
@@ -50,29 +53,22 @@
 import com.android.launcher3.model.ShortcutsChangedTask;
 import com.android.launcher3.model.UserLockStateChangedTask;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
-import com.android.launcher3.util.ViewOnDrawExecutor;
-import com.android.launcher3.widget.WidgetListRowEntry;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
-import androidx.annotation.Nullable;
-
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
  * LauncherModel object held in a static. Also provide APIs for updating the database state
@@ -84,21 +80,12 @@
 
     static final String TAG = "Launcher.Model";
 
-    private final MainThreadExecutor mUiExecutor = new MainThreadExecutor();
     @Thunk final LauncherAppState mApp;
     @Thunk final Object mLock = new Object();
     @Thunk
     LoaderTask mLoaderTask;
     @Thunk boolean mIsLoaderTaskRunning;
 
-    @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
-    private static final Looper mWorkerLooper;
-    static {
-        sWorkerThread.start();
-        mWorkerLooper = sWorkerThread.getLooper();
-    }
-    @Thunk static final Handler sWorker = new Handler(mWorkerLooper);
-
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
     // our monitoring of the package manager provides all updates and we never
@@ -135,33 +122,6 @@
         }
     };
 
-    public interface Callbacks {
-        public void rebindModel();
-
-        public int getCurrentWorkspaceScreen();
-        public void clearPendingBinds();
-        public void startBinding();
-        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
-        public void bindScreens(IntArray orderedScreenIds);
-        public void finishFirstPageBind(ViewOnDrawExecutor executor);
-        public void finishBindingItems(int pageBoundFirst);
-        public void bindAllApplications(ArrayList<AppInfo> apps);
-        public void bindAppsAddedOrUpdated(ArrayList<AppInfo> apps);
-        public void preAddApps();
-        public void bindAppsAdded(IntArray newScreens,
-                ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
-        public void bindPromiseAppProgressUpdated(PromiseAppInfo app);
-        public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated);
-        public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
-        public void bindRestoreItemsChange(HashSet<ItemInfo> updates);
-        public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
-        public void bindAppInfosRemoved(ArrayList<AppInfo> appInfos);
-        public void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets);
-        public void onPageBoundSynchronously(int page);
-        public void executeOnNextDraw(ViewOnDrawExecutor executor);
-        public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
-    }
-
     LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
         mApp = app;
         mBgAllAppsList = new AllAppsList(iconCache, appFilter);
@@ -174,11 +134,11 @@
     /**
      * Updates the icons and label of all pending icons for the provided package name.
      */
-    public void updateSessionDisplayInfo(final String packageName) {
+    public void updateSessionDisplayInfo(final String packageName, final UserHandle user) {
         HashSet<String> packages = new HashSet<>();
         packages.add(packageName);
         enqueueModelUpdateTask(new CacheDataUpdatedTask(
-                CacheDataUpdatedTask.OP_SESSION_UPDATE, Process.myUserHandle(), packages));
+                CacheDataUpdatedTask.OP_SESSION_UPDATE, user, packages));
     }
 
     /**
@@ -244,6 +204,7 @@
 
     public void onPackagesRemoved(UserHandle user, String... packages) {
         int op = PackageUpdatedTask.OP_REMOVE;
+        FileLog.d(TAG, "package removed received " + String.join("," + packages));
         enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages));
     }
 
@@ -299,7 +260,6 @@
     @Override
     public void onReceive(Context context, Intent intent) {
         if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent);
-
         final String action = intent.getAction();
         if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
             // If we have changed locale we need to clear out the labels in all apps/workspace.
@@ -375,7 +335,7 @@
             if (mCallbacks != null && mCallbacks.get() != null) {
                 final Callbacks oldCallbacks = mCallbacks.get();
                 // Clear any pending bind-runnables from the synchronized load process.
-                mUiExecutor.execute(oldCallbacks::clearPendingBinds);
+                MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
 
                 // If there is already one running, tell it to stop.
                 stopLoader();
@@ -419,7 +379,7 @@
 
             // Always post the loader task, instead of running directly (even on same thread) so
             // that we exit any nested synchronized blocks
-            sWorker.post(mLoaderTask);
+            MODEL_EXECUTOR.post(mLoaderTask);
         }
     }
 
@@ -437,16 +397,7 @@
             @Override
             public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
                 apps.addPromiseApp(app.getContext(), sessionInfo);
-                if (!apps.added.isEmpty()) {
-                    final ArrayList<AppInfo> arrayList = new ArrayList<>(apps.added);
-                    apps.added.clear();
-                    scheduleCallbackTask(new CallbackTask() {
-                        @Override
-                        public void execute(Callbacks callbacks) {
-                            callbacks.bindAppsAddedOrUpdated(arrayList);
-                        }
-                    });
-                }
+                bindApplicationsIfNeeded();
             }
         });
     }
@@ -495,8 +446,8 @@
      * use partial updates similar to {@link UserManagerCompat}
      */
     public void refreshShortcutsIfRequired() {
-        sWorker.removeCallbacks(mShortcutPermissionCheckRunnable);
-        sWorker.post(mShortcutPermissionCheckRunnable);
+        MODEL_EXECUTOR.getHandler().removeCallbacks(mShortcutPermissionCheckRunnable);
+        MODEL_EXECUTOR.post(mShortcutPermissionCheckRunnable);
     }
 
     /**
@@ -523,14 +474,8 @@
     }
 
     public void enqueueModelUpdateTask(ModelUpdateTask task) {
-        task.init(mApp, this, sBgDataModel, mBgAllAppsList, mUiExecutor);
-
-        if (sWorkerThread.getThreadId() == Process.myTid()) {
-            task.run();
-        } else {
-            // If we are not on the worker thread, then post to the worker handler
-            sWorker.post(task);
-        }
+        task.init(mApp, this, sBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+        MODEL_EXECUTOR.execute(task);
     }
 
     /**
@@ -605,15 +550,4 @@
     public Callbacks getCallback() {
         return mCallbacks != null ? mCallbacks.get() : null;
     }
-
-    /**
-     * @return the looper for the worker thread which can be used to start background tasks.
-     */
-    public static Looper getWorkerLooper() {
-        return mWorkerLooper;
-    }
-
-    public static void setWorkerPriority(final int priority) {
-        Process.setThreadPriority(sWorkerThread.getThreadId(), priority);
-    }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 6ad5c36..6081300 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -33,7 +33,6 @@
 import android.content.Intent;
 import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
 import android.database.Cursor;
@@ -50,7 +49,6 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.provider.BaseColumns;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -70,6 +68,7 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.NoLocaleSQLiteHelper;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
 
@@ -77,7 +76,6 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.io.StringReader;
@@ -873,7 +871,7 @@
                         continue;
                     }
 
-                    if (!Utilities.isLauncherAppTarget(intent)) {
+                    if (!PackageManagerHelper.isLauncherAppTarget(intent)) {
                         continue;
                     }
 
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index e248ba0..c509680 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -59,6 +59,10 @@
         public static final String ITEM_TYPE = "itemType";
 
         /**
+         * The gesture is a package
+         */
+        public static final int ITEM_TYPE_NON_ACTIONABLE = -1;
+        /**
          * The gesture is an application
          */
         public static final int ITEM_TYPE_APPLICATION = 0;
@@ -122,11 +126,13 @@
          */
         public static final int CONTAINER_DESKTOP = -100;
         public static final int CONTAINER_HOTSEAT = -101;
+        public static final int CONTAINER_PREDICTION = -102;
 
         static final String containerToString(int container) {
             switch (container) {
                 case CONTAINER_DESKTOP: return "desktop";
                 case CONTAINER_HOTSEAT: return "hotseat";
+                case CONTAINER_PREDICTION: return "prediction";
                 default: return String.valueOf(container);
             }
         }
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index d66e581..f673508 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -24,7 +24,8 @@
 import android.animation.AnimatorSet;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.Log;
+
+import androidx.annotation.IntDef;
 
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -32,7 +33,6 @@
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.uioverrides.UiFactory;
 
 import java.io.PrintWriter;
@@ -40,8 +40,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 
-import androidx.annotation.IntDef;
-
 /**
  * TODO: figure out what kind of tests we can write for this
  *
@@ -136,7 +134,7 @@
     }
 
     public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "LauncherState");
+        writer.println(prefix + "LauncherState:");
         writer.println(prefix + "\tmLastStableState:" + mLastStableState);
         writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
         writer.println(prefix + "\tmState:" + mState);
@@ -408,7 +406,7 @@
         }
         mState = state;
         mState.onStateEnabled(mLauncher);
-        mLauncher.onStateSet(mState);
+        mLauncher.onStateSetStart(mState);
 
         if (state.disablePageClipping) {
             // Only disable clipping if needed, otherwise leave it as previous value.
@@ -429,8 +427,7 @@
         }
 
         state.onStateTransitionEnd(mLauncher);
-        mLauncher.getWorkspace().setClipChildren(!state.disablePageClipping);
-        mLauncher.finishAutoCancelActionMode();
+        mLauncher.onStateSetEnd(state);
 
         if (state == NORMAL) {
             setRestState(null);
diff --git a/src/com/android/launcher3/MainThreadExecutor.java b/src/com/android/launcher3/MainThreadExecutor.java
deleted file mode 100644
index 5094682..0000000
--- a/src/com/android/launcher3/MainThreadExecutor.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2014 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;
-
-import android.os.Looper;
-
-import com.android.launcher3.util.LooperExecutor;
-
-/**
- * An executor service that executes its tasks on the main thread.
- *
- * Shutting down this executor is not supported.
- */
-public class MainThreadExecutor extends LooperExecutor {
-
-    public MainThreadExecutor() {
-        super(Looper.getMainLooper());
-    }
-}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 70b55a4..ff2b400 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1088,7 +1088,9 @@
         if (mFreeScroll) {
             setCurrentPage(getNextPage());
         } else if (wasFreeScroll) {
-            snapToPage(getNextPage());
+            if (getScrollForPage(getNextPage()) != getScrollX()) {
+                snapToPage(getNextPage());
+            }
         }
     }
 
@@ -1294,6 +1296,10 @@
         if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
             switch (event.getAction()) {
                 case MotionEvent.ACTION_SCROLL: {
+                    Launcher launcher = Launcher.getLauncher(getContext());
+                    if (launcher != null) {
+                        AbstractFloatingView.closeAllOpenViews(launcher);
+                    }
                     // Handle mouse (or ext. device) by shifting the page depending on the scroll
                     final float vscroll;
                     final float hscroll;
@@ -1304,6 +1310,9 @@
                         vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
                         hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
                     }
+                    if (Math.abs(vscroll) > Math.abs(hscroll) && !isVerticalScrollable()) {
+                        return true;
+                    }
                     if (hscroll != 0 || vscroll != 0) {
                         boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0)
                                                          : (hscroll > 0 || vscroll > 0);
@@ -1320,6 +1329,10 @@
         return super.onGenericMotionEvent(event);
     }
 
+    protected boolean isVerticalScrollable() {
+        return true;
+    }
+
     private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
         if (mVelocityTracker == null) {
             mVelocityTracker = VelocityTracker.obtain();
@@ -1537,7 +1550,7 @@
             snapToPage(getNextPage() - 1);
             return true;
         }
-        return false;
+        return onOverscroll(-getMeasuredWidth());
     }
 
     public boolean scrollRight() {
@@ -1545,7 +1558,15 @@
             snapToPage(getNextPage() + 1);
             return true;
         }
-        return false;
+        return onOverscroll(getMeasuredWidth());
+    }
+
+    protected boolean onOverscroll(int amount) {
+        if (!mAllowOverScroll) return false;
+        onScrollInteractionBegin();
+        overScroll(amount);
+        onScrollInteractionEnd();
+        return true;
     }
 
     @Override
@@ -1565,8 +1586,9 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         final boolean pagesFlipped = isPageOrderFlipped();
-        info.setScrollable(getPageCount() > 1);
-        if (getCurrentPage() < getPageCount() - 1) {
+        int offset = (mAllowOverScroll ? 0 : 1);
+        info.setScrollable(getPageCount() > offset);
+        if (getCurrentPage() < getPageCount() - offset) {
             info.addAction(pagesFlipped ?
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD
                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
@@ -1574,7 +1596,7 @@
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT
                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);
         }
-        if (getCurrentPage() > 0) {
+        if (getCurrentPage() >= offset) {
             info.addAction(pagesFlipped ?
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD
                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
@@ -1582,7 +1604,6 @@
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT
                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);
         }
-
         // Accessibility-wise, PagedView doesn't support long click, so disabling it.
         // Besides disabling the accessibility long-click, this also prevents this view from getting
         // accessibility focus.
@@ -1601,7 +1622,7 @@
     @Override
     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
         super.onInitializeAccessibilityEvent(event);
-        event.setScrollable(getPageCount() > 1);
+        event.setScrollable(mAllowOverScroll || getPageCount() > 1);
     }
 
     @Override
diff --git a/src/com/android/launcher3/Partner.java b/src/com/android/launcher3/Partner.java
index 380078b..af5402a 100644
--- a/src/com/android/launcher3/Partner.java
+++ b/src/com/android/launcher3/Partner.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.util.PackageManagerHelper.findSystemApk;
+
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.util.DisplayMetrics;
@@ -59,7 +61,7 @@
      */
     public static synchronized Partner get(PackageManager pm) {
         if (!sSearched) {
-            Pair<String, Resources> apkInfo = Utilities.findSystemApk(ACTION_PARTNER_CUSTOMIZATION, pm);
+            Pair<String, Resources> apkInfo = findSystemApk(ACTION_PARTNER_CUSTOMIZATION, pm);
             if (apkInfo != null) {
                 sPartner = new Partner(apkInfo.first, apkInfo.second);
             }
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 55cb6f2..c8c590d 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -2,6 +2,7 @@
 
 import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
+
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_MASK;
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_NO;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
@@ -29,6 +30,7 @@
 import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
@@ -240,6 +242,7 @@
                     .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName()))
                     .putExtra(Intent.EXTRA_USER, info.user);
             mLauncher.startActivity(i);
+            FileLog.d(TAG, "start uninstall activity " + cn.getPackageName());
             return cn;
         } catch (URISyntaxException e) {
             Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info);
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index b4078ee..a87c446 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
@@ -39,6 +38,7 @@
 import android.util.Log;
 
 import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.compat.PackageInstallerCompat;
 
 import java.util.List;
@@ -71,8 +71,13 @@
 
         SessionInfo info = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
         UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
-        PackageInstallerCompat packageInstallerCompat = PackageInstallerCompat.getInstance(context);
+        if (!PackageInstaller.ACTION_SESSION_COMMITTED.equals(intent.getAction())
+                || info == null || user == null) {
+            // Invalid intent.
+            return;
+        }
 
+        PackageInstallerCompat packageInstallerCompat = PackageInstallerCompat.getInstance(context);
         if (TextUtils.isEmpty(info.getAppPackageName())
                 || info.getInstallReason() != PackageManager.INSTALL_REASON_USER
                 || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
@@ -133,7 +138,7 @@
             // grid.
             prefs.edit().putBoolean(ADD_ICON_PREFERENCE_KEY, true).apply();
         } else if (!prefs.contains(ADD_ICON_PREFERENCE_INITIALIZED_KEY)) {
-            new PrefInitTask(context).executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
+            new PrefInitTask(context).executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
         }
     }
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index ba122f9..5d0effa 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -24,15 +24,9 @@
 import android.app.Person;
 import android.app.WallpaperManager;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.res.Resources;
@@ -47,7 +41,6 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.DeadObjectException;
 import android.os.Handler;
 import android.os.Message;
@@ -60,7 +53,6 @@
 import android.text.style.TtsSpan;
 import android.util.DisplayMetrics;
 import android.util.Log;
-import android.util.Pair;
 import android.util.TypedValue;
 import android.view.MotionEvent;
 import android.view.View;
@@ -69,7 +61,6 @@
 
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.ShortcutConfigActivityInfo;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.TintedDrawableSpan;
@@ -81,17 +72,10 @@
 import com.android.launcher3.views.Transposable;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
-import java.io.Closeable;
-import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
-import java.util.StringTokenizer;
-import java.util.concurrent.Executor;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -124,8 +108,6 @@
     public static final boolean ATLEAST_OREO =
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
 
-    public static final int SINGLE_FRAME_MS = 16;
-
     /**
      * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
      */
@@ -148,18 +130,6 @@
     public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
     public static final String EXTRA_WALLPAPER_FLAVOR = "com.android.launcher3.WALLPAPER_FLAVOR";
 
-    // These values are same as that in {@link AsyncTask}.
-    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
-    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
-    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
-    private static final int KEEP_ALIVE = 1;
-    /**
-     * An {@link Executor} to be used with async task with no limit on the queue size.
-     */
-    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
-            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
-            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
-
     public static boolean IS_RUNNING_IN_TEST_HARNESS =
                     ActivityManager.isRunningInTestHarness();
 
@@ -247,7 +217,6 @@
         return scale;
     }
 
-
     /**
      * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, float[], boolean)}.
      */
@@ -383,53 +352,6 @@
         return min + (value * (max - min));
     }
 
-    public static boolean isSystemApp(Context context, Intent intent) {
-        PackageManager pm = context.getPackageManager();
-        ComponentName cn = intent.getComponent();
-        String packageName = null;
-        if (cn == null) {
-            ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-            if ((info != null) && (info.activityInfo != null)) {
-                packageName = info.activityInfo.packageName;
-            }
-        } else {
-            packageName = cn.getPackageName();
-        }
-        if (packageName != null) {
-            try {
-                PackageInfo info = pm.getPackageInfo(packageName, 0);
-                return (info != null) && (info.applicationInfo != null) &&
-                        ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
-            } catch (NameNotFoundException e) {
-                return false;
-            }
-        } else {
-            return false;
-        }
-    }
-
-    /*
-     * Finds a system apk which had a broadcast receiver listening to a particular action.
-     * @param action intent action used to find the apk
-     * @return a pair of apk package name and the resources.
-     */
-    static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
-        final Intent intent = new Intent(action);
-        for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
-            if (info.activityInfo != null &&
-                    (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
-                final String packageName = info.activityInfo.packageName;
-                try {
-                    final Resources res = pm.getResourcesForApplication(packageName);
-                    return Pair.create(packageName, res);
-                } catch (NameNotFoundException e) {
-                    Log.w(TAG, "Failed to find resources for " + packageName);
-                }
-            }
-        }
-        return null;
-    }
-
     /**
      * Trims the string, removing all whitespace at the beginning and end of the string.
      * Non-breaking whitespaces are also removed.
@@ -454,51 +376,10 @@
         return (int) Math.ceil(fm.bottom - fm.top);
     }
 
-    /**
-     * Convenience println with multiple args.
-     */
-    public static void println(String key, Object... args) {
-        StringBuilder b = new StringBuilder();
-        b.append(key);
-        b.append(": ");
-        boolean isFirstArgument = true;
-        for (Object arg : args) {
-            if (isFirstArgument) {
-                isFirstArgument = false;
-            } else {
-                b.append(", ");
-            }
-            b.append(arg);
-        }
-        System.out.println(b.toString());
-    }
-
     public static boolean isRtl(Resources res) {
         return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
     }
 
-    /**
-     * Returns true if the intent is a valid launch intent for a launcher activity of an app.
-     * This is used to identify shortcuts which are different from the ones exposed by the
-     * applications' manifest file.
-     *
-     * @param launchIntent The intent that will be launched when the shortcut is clicked.
-     */
-    public static boolean isLauncherAppTarget(Intent launchIntent) {
-        if (launchIntent != null
-                && Intent.ACTION_MAIN.equals(launchIntent.getAction())
-                && launchIntent.getComponent() != null
-                && launchIntent.getCategories() != null
-                && launchIntent.getCategories().size() == 1
-                && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER)
-                && TextUtils.isEmpty(launchIntent.getDataString())) {
-            // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE.
-            Bundle extras = launchIntent.getExtras();
-            return extras == null || extras.keySet().isEmpty();
-        }
-        return false;
-    }
-
     public static float dpiFromPx(int size, DisplayMetrics metrics){
         float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
         return (size / densityRatio);
@@ -601,18 +482,6 @@
         return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed();
     }
 
-    public static void closeSilently(Closeable c) {
-        if (c != null) {
-            try {
-                c.close();
-            } catch (IOException e) {
-                if (FeatureFlags.IS_DOGFOOD_BUILD) {
-                    Log.d(TAG, "Error closing", e);
-                }
-            }
-        }
-    }
-
     public static boolean isBinderSizeError(Exception e) {
         return e.getCause() instanceof TransactionTooLargeException
                 || e.getCause() instanceof DeadObjectException;
@@ -715,7 +584,7 @@
             LauncherIcons li = LauncherIcons.obtain(appState.getContext());
             Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).iconBitmap;
             li.recycle();
-            float badgeSize = launcher.getResources().getDimension(R.dimen.profile_badge_size);
+            float badgeSize = LauncherIcons.getBadgeSizeForIconSize(iconSize);
             float insetFraction = (iconSize - badgeSize) / iconSize;
             return new InsetDrawable(new FastBitmapDrawable(badge),
                     insetFraction, insetFraction, 0, 0);
@@ -727,25 +596,6 @@
         }
     }
 
-    public static int[] getIntArrayFromString(String tokenized) {
-        StringTokenizer tokenizer = new StringTokenizer(tokenized, ",");
-        int[] array = new int[tokenizer.countTokens()];
-        int count = 0;
-        while (tokenizer.hasMoreTokens()) {
-            array[count] = Integer.parseInt(tokenizer.nextToken().trim());
-            count++;
-        }
-        return array;
-    }
-
-    public static String getStringFromIntArray(int[] array) {
-        StringBuilder str = new StringBuilder();
-        for (int value : array) {
-            str.append(value).append(",");
-        }
-        return str.toString();
-    }
-
     public static float squaredHypot(float x, float y) {
         return x * x + y * y;
     }
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 6d1bc1a..c6381b0 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -1,5 +1,8 @@
 package com.android.launcher3;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
@@ -23,21 +26,23 @@
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.CancellationSignal;
-import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.LongSparseArray;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.ShortcutConfigActivityInfo;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.icons.GraphicsUtils;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.ShadowGenerator;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SQLiteCacheHelper;
@@ -50,11 +55,8 @@
 import java.util.HashSet;
 import java.util.Set;
 import java.util.WeakHashMap;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
-import androidx.annotation.Nullable;
-
 public class WidgetPreviewLoader {
 
     private static final String TAG = "WidgetPreviewLoader";
@@ -68,23 +70,18 @@
      * Note: synchronized block used for this variable is expensive and the block should always
      * be posted to a background thread.
      */
-    @Thunk final Set<Bitmap> mUnusedBitmaps =
-            Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
+    @Thunk final Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<>());
 
     private final Context mContext;
     private final IconCache mIconCache;
     private final UserManagerCompat mUserManager;
     private final CacheDb mDb;
 
-    private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
-    @Thunk final Handler mWorkerHandler;
-
     public WidgetPreviewLoader(Context context, IconCache iconCache) {
         mContext = context;
         mIconCache = iconCache;
         mUserManager = UserManagerCompat.getInstance(context);
         mDb = new CacheDb(context);
-        mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
     }
 
     /**
@@ -99,7 +96,7 @@
         WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
 
         PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
-        task.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
+        task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
 
         CancellationSignal signal = new CancellationSignal();
         signal.setOnCancelListener(task);
@@ -453,7 +450,7 @@
 
     private Bitmap generateShortcutPreview(BaseActivity launcher, ShortcutConfigActivityInfo info,
             int maxWidth, int maxHeight, Bitmap preview) {
-        int iconSize = launcher.getDeviceProfile().iconSizePx;
+        int iconSize = launcher.getDeviceProfile().allAppsIconSizePx;
         int padding = launcher.getResources()
                 .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
 
@@ -494,12 +491,7 @@
 
     private Drawable mutateOnMainThread(final Drawable drawable) {
         try {
-            return mMainThreadExecutor.submit(new Callable<Drawable>() {
-                @Override
-                public Drawable call() throws Exception {
-                    return drawable.mutate();
-                }
-            }).get();
+            return MAIN_EXECUTOR.submit(drawable::mutate).get();
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             throw new RuntimeException(e);
@@ -607,7 +599,7 @@
 
             // Write the generated preview to the DB in the worker thread
             if (mVersions != null) {
-                mWorkerHandler.post(new Runnable() {
+                MODEL_EXECUTOR.post(new Runnable() {
                     @Override
                     public void run() {
                         if (!isCancelled()) {
@@ -637,7 +629,7 @@
             // recycled set immediately. Otherwise, it will be recycled after the preview is written
             // to disk.
             if (preview != null) {
-                mWorkerHandler.post(new Runnable() {
+                MODEL_EXECUTOR.post(new Runnable() {
                     @Override
                     public void run() {
                         synchronized (mUnusedBitmaps) {
@@ -658,7 +650,7 @@
             // in the tasks's onCancelled() call, and if cancelled while the task is writing to
             // disk, it will be cancelled in the task's onPostExecute() call.
             if (mBitmapToRecycle != null) {
-                mWorkerHandler.post(new Runnable() {
+                MODEL_EXECUTOR.post(new Runnable() {
                     @Override
                     public void run() {
                         synchronized (mUnusedBitmaps) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index c5e1058..9eeb286 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -67,9 +67,9 @@
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -90,8 +90,8 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageUserKey;
@@ -133,9 +133,6 @@
 
     private static final int DEFAULT_PAGE = 0;
 
-    public static final boolean MAP_NO_RECURSE = false;
-    public static final boolean MAP_RECURSE = true;
-
     private LayoutTransition mLayoutTransition;
     @Thunk final WallpaperManager mWallpaperManager;
 
@@ -1038,6 +1035,13 @@
     }
 
     @Override
+    protected boolean onOverscroll(int amount) {
+        // Enforce overscroll on -1 direction
+        if ((amount > 0 && !mIsRtl) || (amount < 0 && mIsRtl)) return false;
+        return super.onOverscroll(amount);
+    }
+
+    @Override
     protected boolean shouldFlingForVelocity(int velocityX) {
         // When the overlay is moving, the fling or settle transition is controlled by the overlay.
         return Float.compare(Math.abs(mOverlayTranslation), 0) == 0 &&
@@ -1161,7 +1165,7 @@
     }
 
     protected void setWallpaperDimension() {
-        Utilities.THREAD_POOL_EXECUTOR.execute(new Runnable() {
+        Executors.THREAD_POOL_EXECUTOR.execute(new Runnable() {
             @Override
             public void run() {
                 final Point size = LauncherAppState.getIDP(getContext()).defaultWallpaperSize;
@@ -1271,6 +1275,10 @@
         return !mLauncher.isInState(NORMAL);
     }
 
+    private boolean workspaceInScrollableState() {
+        return mLauncher.isInState(SPRING_LOADED) || !workspaceInModalState();
+    }
+
     /** Returns whether a drag should be allowed to be started from the current workspace state. */
     public boolean workspaceIconsCanBeDragged() {
         return mLauncher.getStateManager().getState().workspaceIconsCanBeDragged;
@@ -1492,7 +1500,7 @@
         Rect dragRect = null;
         if (child instanceof BubbleTextView) {
             dragRect = new Rect();
-            ((BubbleTextView) child).getIconBounds(dragRect);
+            BubbleTextView.getIconBounds(child, dragRect, grid.iconSizePx);
             dragLayerY += dragRect.top;
             // Note: The dragRect is used to calculate drag layer offsets, but the
             // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
@@ -2535,22 +2543,22 @@
             View view;
 
             switch (info.itemType) {
-            case ITEM_TYPE_APPLICATION:
-            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                if (info.container == NO_ID && info instanceof AppInfo) {
-                    // Came from all apps -- make a copy
-                    info = ((AppInfo) info).makeWorkspaceItem();
-                    d.dragInfo = info;
-                }
-                view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
-                break;
-            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
-                        (FolderInfo) info);
-                break;
-            default:
-                throw new IllegalStateException("Unknown item type: " + info.itemType);
+                case ITEM_TYPE_APPLICATION:
+                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                    if (info instanceof AppInfo) {
+                        // Came from all apps -- make a copy
+                        info = ((AppInfo) info).makeWorkspaceItem();
+                        d.dragInfo = info;
+                    }
+                    view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
+                    break;
+                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
+                    view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
+                            (FolderInfo) info);
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown item type: " + info.itemType);
             }
 
             // First we find the cell nearest to point at which the item is
@@ -2808,10 +2816,27 @@
     }
 
     /**
+     * Removed widget from workspace by appWidgetId
+     * @param appWidgetId
+     */
+    public void removeWidget(int appWidgetId) {
+        mapOverItems((info, view) -> {
+            if (info instanceof LauncherAppWidgetInfo) {
+                LauncherAppWidgetInfo appWidgetInfo = (LauncherAppWidgetInfo) info;
+                if (appWidgetInfo.appWidgetId == appWidgetId) {
+                    mLauncher.removeItem(view, appWidgetInfo, true);
+                    return true;
+                }
+            }
+            return false;
+        });
+    }
+
+    /**
      * Removes all folder listeners
      */
     public void removeFolderListeners() {
-        mapOverItems(false, new ItemOperator() {
+        mapOverItems(new ItemOperator() {
             @Override
             public boolean evaluate(ItemInfo info, View view) {
                 if (view instanceof FolderIcon) {
@@ -2858,7 +2883,7 @@
     @Override
     public boolean scrollLeft() {
         boolean result = false;
-        if (!workspaceInModalState() && !mIsSwitchingState) {
+        if (!mIsSwitchingState && workspaceInScrollableState()) {
             result = super.scrollLeft();
         }
         Folder openFolder = Folder.getOpen(mLauncher);
@@ -2871,7 +2896,7 @@
     @Override
     public boolean scrollRight() {
         boolean result = false;
-        if (!workspaceInModalState() && !mIsSwitchingState) {
+        if (!mIsSwitchingState && workspaceInScrollableState()) {
             result = super.scrollRight();
         }
         Folder openFolder = Folder.getOpen(mLauncher);
@@ -2963,7 +2988,7 @@
 
     public View getFirstMatch(final ItemOperator operator) {
         final View[] value = new View[1];
-        mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
+        mapOverItems(new ItemOperator() {
             @Override
             public boolean evaluate(ItemInfo info, View v) {
                 if (operator.evaluate(info, v)) {
@@ -2986,7 +3011,7 @@
         final View[] matches = new View[operators.length];
         // For efficiency, the outer loop should be CellLayout.
         for (CellLayout cellLayout : cellLayouts) {
-            mapOverCellLayout(MAP_NO_RECURSE, cellLayout, (info, v) -> {
+            mapOverCellLayout(cellLayout, (info, v) -> {
                 for (int i = 0; i < operators.length; ++i) {
                     if (matches[i] == null && operators[i].evaluate(info, v)) {
                         matches[i] = v;
@@ -3011,7 +3036,7 @@
     }
 
     void clearDropTargets() {
-        mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
+        mapOverItems(new ItemOperator() {
             @Override
             public boolean evaluate(ItemInfo info, View v) {
                 if (v instanceof DropTarget) {
@@ -3056,10 +3081,12 @@
                 } else if (itemToRemove.container >= 0) {
                     // The item may belong to a folder.
                     View parent = idToViewMap.get(itemToRemove.container);
-                    if (parent != null) {
+                    if (parent instanceof FolderIcon) {
                         FolderInfo folderInfo = (FolderInfo) parent.getTag();
-                        folderInfo.prepareAutoUpdate();
                         folderInfo.remove((WorkspaceItemInfo) itemToRemove, false);
+                        if (((FolderIcon) parent).getFolder().isOpen()) {
+                            ((FolderIcon) parent).getFolder().close(false /* animate */);
+                        }
                     }
                 }
             }
@@ -3083,18 +3110,17 @@
     /**
      * Map the operator over the shortcuts and widgets, return the first-non-null value.
      *
-     * @param recurse true: iterate over folder children. false: op get the folders themselves.
      * @param op the operator to map over the shortcuts
      */
-    public void mapOverItems(boolean recurse, ItemOperator op) {
+    public void mapOverItems(ItemOperator op) {
         for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
-            if (mapOverCellLayout(recurse, layout, op)) {
+            if (mapOverCellLayout(layout, op)) {
                 return;
             }
         }
     }
 
-    private boolean mapOverCellLayout(boolean recurse, CellLayout layout, ItemOperator op) {
+    private boolean mapOverCellLayout(CellLayout layout, ItemOperator op) {
         // TODO(b/128460496) Potential race condition where layout is not yet loaded
         if (layout == null) {
             return false;
@@ -3104,103 +3130,68 @@
         final int itemCount = container.getChildCount();
         for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
             View item = container.getChildAt(itemIdx);
-            ItemInfo info = (ItemInfo) item.getTag();
-            if (recurse && info instanceof FolderInfo && item instanceof FolderIcon) {
-                FolderIcon folder = (FolderIcon) item;
-                ArrayList<View> folderChildren = folder.getFolder().getItemsInReadingOrder();
-                // map over all the children in the folder
-                final int childCount = folderChildren.size();
-                for (int childIdx = 0; childIdx < childCount; childIdx++) {
-                    View child = folderChildren.get(childIdx);
-                    info = (ItemInfo) child.getTag();
-                    if (op.evaluate(info, child)) {
-                        return true;
-                    }
-                }
-            } else {
-                if (op.evaluate(info, item)) {
-                    return true;
-                }
+            if (op.evaluate((ItemInfo) item.getTag(), item)) {
+                return true;
             }
         }
         return false;
     }
 
     void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
-        int total  = shortcuts.size();
-        final HashSet<WorkspaceItemInfo> updates = new HashSet<>(total);
-        final IntSet folderIds = new IntSet();
+        final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
+        ItemOperator op = (info, v) -> {
+            if (v instanceof BubbleTextView && updates.contains(info)) {
+                WorkspaceItemInfo si = (WorkspaceItemInfo) info;
+                BubbleTextView shortcut = (BubbleTextView) v;
+                Drawable oldIcon = shortcut.getIcon();
+                boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
+                        && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
+                shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
+            } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
+                ((FolderIcon) v).updatePreviewItems(updates::contains);
+            }
 
-        for (int i = 0; i < total; i++) {
-            WorkspaceItemInfo s = shortcuts.get(i);
-            updates.add(s);
-            folderIds.add(s.container);
+            // Iterate all items
+            return false;
+        };
+
+        mapOverItems(op);
+        Folder openFolder = Folder.getOpen(mLauncher);
+        if (openFolder != null) {
+            openFolder.iterateOverItems(op);
         }
-
-        mapOverItems(MAP_RECURSE, new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View v) {
-                if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView &&
-                        updates.contains(info)) {
-                    WorkspaceItemInfo si = (WorkspaceItemInfo) info;
-                    BubbleTextView shortcut = (BubbleTextView) v;
-                    Drawable oldIcon = shortcut.getIcon();
-                    boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
-                            && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
-                    shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
-                }
-                // process all the shortcuts
-                return false;
-            }
-        });
-
-        // Update folder icons
-        mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View v) {
-                if (info instanceof FolderInfo && folderIds.contains(info.id)) {
-                    ((FolderInfo) info).itemsChanged(false);
-                }
-                // process all the shortcuts
-                return false;
-            }
-        });
     }
 
     public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
         final PackageUserKey packageUserKey = new PackageUserKey(null, null);
-        final IntSet folderIds = new IntSet();
-        mapOverItems(MAP_RECURSE, new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View v) {
-                if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
-                    if (!packageUserKey.updateFromItemInfo(info)
-                            || updatedDots.test(packageUserKey)) {
-                        ((BubbleTextView) v).applyDotState(info, true /* animate */);
-                        folderIds.add(info.container);
-                    }
-                }
-                // process all the shortcuts
-                return false;
-            }
-        });
+        Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
+                || updatedDots.test(packageUserKey);
 
-        // Update folder icons
-        mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View v) {
-                if (info instanceof FolderInfo && folderIds.contains(info.id)
-                        && v instanceof FolderIcon) {
+        ItemOperator op = (info, v) -> {
+            if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
+                if (matcher.test(info)) {
+                    ((BubbleTextView) v).applyDotState(info, true /* animate */);
+                }
+            } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
+                FolderInfo fi = (FolderInfo) info;
+                if (fi.contents.stream().anyMatch(matcher)) {
                     FolderDotInfo folderDotInfo = new FolderDotInfo();
-                    for (WorkspaceItemInfo si : ((FolderInfo) info).contents) {
+                    for (WorkspaceItemInfo si : fi.contents) {
                         folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
                     }
                     ((FolderIcon) v).setDotInfo(folderDotInfo);
                 }
-                // process all the shortcuts
-                return false;
             }
-        });
+
+            // process all the shortcuts
+            return false;
+        };
+
+        mapOverItems(op);
+        Folder folder = Folder.getOpen(mLauncher);
+        if (folder != null) {
+            folder.iterateOverItems(op);
+        }
     }
 
     public void removeAbandonedPromise(String packageName, UserHandle user) {
@@ -3212,21 +3203,25 @@
     }
 
     public void updateRestoreItems(final HashSet<ItemInfo> updates) {
-        mapOverItems(MAP_RECURSE, new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View v) {
-                if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
-                        && updates.contains(info)) {
-                    ((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
-                } else if (v instanceof PendingAppWidgetHostView
-                        && info instanceof LauncherAppWidgetInfo
-                        && updates.contains(info)) {
-                    ((PendingAppWidgetHostView) v).applyState();
-                }
-                // process all the shortcuts
-                return false;
+        ItemOperator op = (info, v) -> {
+            if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
+                    && updates.contains(info)) {
+                ((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
+            } else if (v instanceof PendingAppWidgetHostView
+                    && info instanceof LauncherAppWidgetInfo
+                    && updates.contains(info)) {
+                ((PendingAppWidgetHostView) v).applyState();
+            } else if (v instanceof FolderIcon && info instanceof FolderInfo) {
+                ((FolderIcon) v).updatePreviewItems(updates::contains);
             }
-        });
+            // process all the shortcuts
+            return false;
+        };
+        mapOverItems(op);
+        Folder folder = Folder.getOpen(mLauncher);
+        if (folder != null) {
+            folder.iterateOverItems(op);
+        }
     }
 
     public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> changedInfo) {
@@ -3250,7 +3245,7 @@
             } else {
                 // widgetRefresh will automatically run when the packages are updated.
                 // For now just update the progress bars
-                mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
+                mapOverItems(new ItemOperator() {
                     @Override
                     public boolean evaluate(ItemInfo info, View view) {
                         if (view instanceof PendingAppWidgetHostView
@@ -3374,7 +3369,7 @@
             mRefreshPending = false;
 
             ArrayList<PendingAppWidgetHostView> views = new ArrayList<>(mInfos.size());
-            mapOverItems(MAP_NO_RECURSE, (info, view) -> {
+            mapOverItems((info, view) -> {
                 if (view instanceof PendingAppWidgetHostView && mInfos.contains(info)) {
                     views.add((PendingAppWidgetHostView) view);
                 }
diff --git a/src/com/android/launcher3/WorkspaceItemInfo.java b/src/com/android/launcher3/WorkspaceItemInfo.java
index 9451aae..23795c5 100644
--- a/src/com/android/launcher3/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/WorkspaceItemInfo.java
@@ -221,4 +221,9 @@
         }
         return cn;
     }
+
+    @Override
+    public ItemInfoWithIcon clone() {
+        return new WorkspaceItemInfo(this);
+    }
 }
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 40c6b5f..7a7e1fe 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -38,7 +38,6 @@
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
 
 /**
@@ -96,14 +95,13 @@
             propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
 
             if (!hotseat.getRotationMode().isTransposed) {
-                // Set the hotseat's pivot point to match the workspace's, so that it scales together.
-                DragLayer dragLayer = mLauncher.getDragLayer();
-                float[] workspacePivot =
-                        new float[]{ mWorkspace.getPivotX(), mWorkspace.getPivotY() };
-                dragLayer.getDescendantCoordRelativeToSelf(mWorkspace, workspacePivot);
-                dragLayer.mapCoordInSelfToDescendant(hotseat, workspacePivot);
-                hotseat.setPivotX(workspacePivot[0]);
-                hotseat.setPivotY(workspacePivot[1]);
+                // Set the hotseat's pivot point to match the workspace's, so that it scales
+                // together. Since both hotseat and workspace can move, transform the point
+                // manually instead of using dragLayer.getDescendantCoordRelativeToSelf and
+                // related methods.
+                hotseat.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop() - hotseat.getTop());
+                hotseat.setPivotX(mWorkspace.getPivotX()
+                        + mWorkspace.getLeft() - hotseat.getLeft());
             }
             float hotseatScale = hotseatScaleAndTranslation.scale;
             Interpolator hotseatScaleInterpolator = builder.getInterpolator(ANIM_HOTSEAT_SCALE,
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 293b867..37ee248 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -312,6 +312,7 @@
                 + grid.cellLayoutPaddingLeftRightPx;
 
         for (int i = 0; i < mAH.length; i++) {
+            mAH[i].adapter.setAppsPerRow(grid.inv.numAllAppsColumns);
             mAH[i].padding.bottom = insets.bottom;
             mAH[i].padding.left = mAH[i].padding.right = leftRightPadding;
             mAH[i].applyPadding();
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 3cfa0b1..bb21268 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -180,7 +180,7 @@
     private final GridLayoutManager mGridLayoutMgr;
     private final GridSpanSizer mGridSizer;
 
-    private final int mAppsPerRow;
+    private int mAppsPerRow;
 
     private BindViewCallback mBindViewCallback;
     private OnFocusChangeListener mIconFocusListener;
@@ -200,7 +200,11 @@
         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
         mLayoutInflater = LayoutInflater.from(launcher);
 
-        mAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns;
+        setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
+    }
+
+    public void setAppsPerRow(int appsPerRow) {
+        mAppsPerRow = appsPerRow;
         mGridLayoutMgr.setSpanCount(mAppsPerRow);
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java
index 69068c6..5b73940 100644
--- a/src/com/android/launcher3/allapps/AllAppsPagedView.java
+++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java
@@ -81,4 +81,9 @@
     public boolean hasOverlappingRendering() {
         return false;
     }
+
+    @Override
+    protected boolean isVerticalScrollable() {
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index ca8dbeb..c4b2f68 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -15,6 +15,9 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.AppInfo.COMPONENT_KEY_COMPARATOR;
+import static com.android.launcher3.AppInfo.EMPTY_ARRAY;
+
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -26,8 +29,7 @@
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -43,27 +45,33 @@
     public static final int DEFER_UPDATES_TEST = 1 << 1;
 
     private PackageUserKey mTempKey = new PackageUserKey(null, null);
-    private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
+    private AppInfo mTempInfo = new AppInfo();
+
+    private AppInfo[] mApps = EMPTY_ARRAY;
+
     private final List<OnUpdateListener> mUpdateListeners = new ArrayList<>();
     private final ArrayList<ViewGroup> mIconContainers = new ArrayList<>();
 
     private int mDeferUpdatesFlags = 0;
     private boolean mUpdatePending = false;
 
-    public Collection<AppInfo> getApps() {
-        return mComponentToAppMap.values();
+    public AppInfo[] getApps() {
+        return mApps;
     }
 
     /**
      * Sets the current set of apps.
      */
-    public void setApps(List<AppInfo> apps) {
-        mComponentToAppMap.clear();
-        addOrUpdateApps(apps);
+    public void setApps(AppInfo[] apps) {
+        mApps = apps;
+        notifyUpdate();
     }
 
     public AppInfo getApp(ComponentKey key) {
-        return mComponentToAppMap.get(key);
+        mTempInfo.componentName = key.componentName;
+        mTempInfo.user = key.user;
+        int index = Arrays.binarySearch(mApps, mTempInfo, COMPONENT_KEY_COMPARATOR);
+        return index < 0 ? null : mApps[index];
     }
 
     public void enableDeferUpdates(int flag) {
@@ -86,27 +94,6 @@
         return mDeferUpdatesFlags;
     }
 
-    /**
-     * Adds or updates existing apps in the list
-     */
-    public void addOrUpdateApps(List<AppInfo> apps) {
-        for (AppInfo app : apps) {
-            mComponentToAppMap.put(app.toComponentKey(), app);
-        }
-        notifyUpdate();
-    }
-
-    /**
-     * Removes some apps from the list.
-     */
-    public void removeApps(List<AppInfo> apps) {
-        for (AppInfo app : apps) {
-            mComponentToAppMap.remove(app.toComponentKey());
-        }
-        notifyUpdate();
-    }
-
-
     private void notifyUpdate() {
         if (mDeferUpdatesFlags != 0) {
             mUpdatePending = true;
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 3836c9f..08ce9c2 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -2,8 +2,6 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
@@ -134,15 +132,6 @@
         } else {
             mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0);
         }
-
-        if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-            // Translate hotseat with the shelf until reaching overview.
-            float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
-            if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
-                float hotseatShift = (progress - overviewProgress) * mShiftRange;
-                mLauncher.getHotseat().setTranslationY(hotseatShift);
-            }
-        }
     }
 
     public float getProgress() {
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 1369441..0c4be62 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -21,16 +21,13 @@
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LabelComparator;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -145,9 +142,7 @@
 
     // The of ordered component names as a result of a search query
     private ArrayList<ComponentKey> mSearchResults;
-    private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
     private AllAppsGridAdapter mAdapter;
-    private AlphabeticIndexCompat mIndexer;
     private AppInfoComparator mAppNameComparator;
     private final int mNumAppsPerRow;
     private int mNumAppRowsInAdapter;
@@ -156,7 +151,6 @@
     public AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork) {
         mAllAppsStore = appsStore;
         mLauncher = Launcher.getLauncher(context);
-        mIndexer = new AlphabeticIndexCompat(context);
         mAppNameComparator = new AppInfoComparator(context);
         mIsWork = isWork;
         mNumAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns;
@@ -263,7 +257,7 @@
             TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator());
             for (AppInfo info : mApps) {
                 // Add the section to the cache
-                String sectionName = getAndUpdateCachedSectionName(info.title);
+                String sectionName = info.sectionName;
 
                 // Add it to the mapping
                 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
@@ -279,12 +273,6 @@
             for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
                 mApps.addAll(entry.getValue());
             }
-        } else {
-            // Just compute the section headers for use below
-            for (AppInfo info : mApps) {
-                // Add the section to the cache
-                getAndUpdateCachedSectionName(info.title);
-            }
         }
 
         // Recompose the set of adapter items from the current set of apps
@@ -320,7 +308,7 @@
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
         for (AppInfo info : getFiltersAppInfos()) {
-            String sectionName = getAndUpdateCachedSectionName(info.title);
+            String sectionName = info.sectionName;
 
             // Create a new section if the section names do not match
             if (!sectionName.equals(lastSectionName)) {
@@ -428,18 +416,4 @@
         }
         return result;
     }
-
-    /**
-     * Returns the cached section name for the given title, recomputing and updating the cache if
-     * the title has no cached section name.
-     */
-    private String getAndUpdateCachedSectionName(CharSequence title) {
-        String sectionName = mCachedSectionNames.get(title);
-        if (sectionName == null) {
-            sectionName = mIndexer.computeSectionName(title);
-            mCachedSectionNames.put(title, sectionName);
-        }
-        return sectionName;
-    }
-
 }
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
index 8ac9d66..eabd283 100644
--- a/src/com/android/launcher3/anim/AlphaUpdateListener.java
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -27,7 +27,7 @@
  */
 public class AlphaUpdateListener extends AnimationSuccessListener
         implements AnimatorUpdateListener {
-    private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
+    public static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
 
     private View mView;
 
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 2c440bb..4a52795 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -26,15 +26,15 @@
 import android.animation.ValueAnimator;
 import android.util.Log;
 
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
+
 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
  * and durations.
@@ -250,6 +250,17 @@
         }
     }
 
+    /**
+     * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This
+     * is intended to be used only if you need to cancel but want to defer cleaning up yourself.
+     */
+    public void dispatchOnCancelWithoutCancelRunnable() {
+        Runnable onCancel = mOnCancelRunnable;
+        setOnCancelRunnable(null);
+        dispatchOnCancel();
+        setOnCancelRunnable(onCancel);
+    }
+
     public void dispatchOnCancel() {
         dispatchOnCancelRecursively(mAnim);
     }
@@ -283,10 +294,6 @@
         mOnCancelRunnable = runnable;
     }
 
-    public Runnable getOnCancelRunnable() {
-        return mOnCancelRunnable;
-    }
-
     public void skipToEnd() {
         mSkipToEnd = true;
         for (SpringAnimation spring : mSprings) {
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index c45cd85..fccc120 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -39,6 +39,7 @@
     public static final Interpolator LINEAR = new LinearInterpolator();
 
     public static final Interpolator ACCEL = new AccelerateInterpolator();
+    public static final Interpolator ACCEL_0_75 = new AccelerateInterpolator(0.75f);
     public static final Interpolator ACCEL_1_5 = new AccelerateInterpolator(1.5f);
     public static final Interpolator ACCEL_2 = new AccelerateInterpolator(2);
 
@@ -48,6 +49,7 @@
     public static final Interpolator DEACCEL_2 = new DecelerateInterpolator(2);
     public static final Interpolator DEACCEL_2_5 = new DecelerateInterpolator(2.5f);
     public static final Interpolator DEACCEL_3 = new DecelerateInterpolator(3f);
+    public static final Interpolator DEACCEL_5 = new DecelerateInterpolator(5f);
 
     public static final Interpolator ACCEL_DEACCEL = new AccelerateDecelerateInterpolator();
 
diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
index dfdcc70..46c9006 100644
--- a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
+++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
@@ -3,7 +3,6 @@
 import android.content.Context;
 import android.icu.text.AlphabeticIndex;
 import android.os.LocaleList;
-import android.util.Log;
 
 import com.android.launcher3.Utilities;
 
@@ -12,28 +11,32 @@
 import androidx.annotation.NonNull;
 
 public class AlphabeticIndexCompat {
-    private static final String TAG = "AlphabeticIndexCompat";
 
     private static final String MID_DOT = "\u2219";
-    private final BaseIndex mBaseIndex;
     private final String mDefaultMiscLabel;
 
+    private final AlphabeticIndex.ImmutableIndex mBaseIndex;
+
     public AlphabeticIndexCompat(Context context) {
-        BaseIndex index = null;
+        this(context.getResources().getConfiguration().getLocales());
+    }
 
-        try {
-            index = new AlphabeticIndexVN(context);
-        } catch (Exception e) {
-            Log.d(TAG, "Unable to load the system index", e);
+    public AlphabeticIndexCompat(LocaleList locales) {
+        int localeCount = locales.size();
+
+        Locale primaryLocale = localeCount == 0 ? Locale.ENGLISH : locales.get(0);
+        AlphabeticIndex indexBuilder = new AlphabeticIndex(primaryLocale);
+        for (int i = 1; i < localeCount; i++) {
+            indexBuilder.addLabels(locales.get(i));
         }
+        indexBuilder.addLabels(Locale.ENGLISH);
+        mBaseIndex = indexBuilder.buildImmutableIndex();
 
-        mBaseIndex = index == null ? new BaseIndex() : index;
-
-        if (context.getResources().getConfiguration().locale
-                .getLanguage().equals(Locale.JAPANESE.getLanguage())) {
+        if (primaryLocale.getLanguage().equals(Locale.JAPANESE.getLanguage())) {
             // Japanese character 他 ("misc")
             mDefaultMiscLabel = "\u4ed6";
-            // TODO(winsonc, omakoto): We need to handle Japanese sections better, especially the kanji
+            // TODO(winsonc, omakoto): We need to handle Japanese sections better,
+            // especially the kanji
         } else {
             // Dot
             mDefaultMiscLabel = MID_DOT;
@@ -45,7 +48,7 @@
      */
     public String computeSectionName(@NonNull CharSequence cs) {
         String s = Utilities.trim(cs);
-        String sectionName = mBaseIndex.getBucketLabel(mBaseIndex.getBucketIndex(s));
+        String sectionName = mBaseIndex.getBucket(mBaseIndex.getBucketIndex(s)).getLabel();
         if (Utilities.trim(sectionName).isEmpty() && s.length() > 0) {
             int c = s.codePointAt(0);
             boolean startsWithDigit = Character.isDigit(c);
@@ -66,71 +69,4 @@
         }
         return sectionName;
     }
-
-    /**
-     * Base class to support Alphabetic indexing if not supported by the framework.
-     * TODO(winsonc): disable for non-english locales
-     */
-    private static class BaseIndex {
-
-        private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-";
-        private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1;
-
-        /**
-         * Returns the index of the bucket in which the given string should appear.
-         */
-        protected int getBucketIndex(@NonNull String s) {
-            if (s.isEmpty()) {
-                return UNKNOWN_BUCKET_INDEX;
-            }
-            int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase());
-            if (index != -1) {
-                return index;
-            }
-            return UNKNOWN_BUCKET_INDEX;
-        }
-
-        /**
-         * Returns the label for the bucket at the given index (as returned by getBucketIndex).
-         */
-        protected String getBucketLabel(int index) {
-            return BUCKETS.substring(index, index + 1);
-        }
-    }
-
-    /**
-     * Implementation based on {@link AlphabeticIndex}.
-     */
-    private static class AlphabeticIndexVN extends BaseIndex {
-
-        private final AlphabeticIndex.ImmutableIndex mAlphabeticIndex;
-
-        public AlphabeticIndexVN(Context context) {
-            LocaleList locales = context.getResources().getConfiguration().getLocales();
-            int localeCount = locales.size();
-
-            Locale primaryLocale = localeCount == 0 ? Locale.ENGLISH : locales.get(0);
-            AlphabeticIndex indexBuilder = new AlphabeticIndex(primaryLocale);
-            for (int i = 1; i < localeCount; i++) {
-                indexBuilder.addLabels(locales.get(i));
-            }
-            indexBuilder.addLabels(Locale.ENGLISH);
-
-            mAlphabeticIndex = indexBuilder.buildImmutableIndex();
-        }
-
-        /**
-         * Returns the index of the bucket in which {@param s} should appear.
-         */
-        protected int getBucketIndex(String s) {
-            return mAlphabeticIndex.getBucketIndex(s);
-        }
-
-        /**
-         * Returns the label for the bucket at the given index
-         */
-        protected String getBucketLabel(int index) {
-            return mAlphabeticIndex.getBucket(index).getLabel();
-        }
-    }
 }
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
index 3243256..fc5d11c 100644
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
+++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java
@@ -23,19 +23,18 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.custom.CustomWidgetParser;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.HashMap;
 import java.util.List;
 
-import androidx.annotation.Nullable;
-
 public abstract class AppWidgetManagerCompat {
 
     private static final Object sInstanceLock = new Object();
@@ -63,11 +62,9 @@
     }
 
     public LauncherAppWidgetProviderInfo getLauncherAppWidgetInfo(int appWidgetId) {
-        if (FeatureFlags.ENABLE_CUSTOM_WIDGETS
-                && appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
-            return CustomWidgetParser.getWidgetProvider(mContext, appWidgetId);
+        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
+            return CustomWidgetManager.INSTANCE.get(mContext).getWidgetProvider(appWidgetId);
         }
-
         AppWidgetProviderInfo info = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
         return info == null ? null : LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
     }
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
index 1065748..c8b1f67 100644
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
+++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVL.java
@@ -24,12 +24,15 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.custom.CustomWidgetParser;
+import com.android.launcher3.widget.custom.CustomAppWidgetProviderInfo;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -37,8 +40,6 @@
 import java.util.Iterator;
 import java.util.List;
 
-import androidx.annotation.Nullable;
-
 class AppWidgetManagerCompatVL extends AppWidgetManagerCompat {
 
     private final UserManager mUserManager;
@@ -54,14 +55,11 @@
             return Collections.emptyList();
         }
         if (packageUser == null) {
-            ArrayList<AppWidgetProviderInfo> providers = new ArrayList<AppWidgetProviderInfo>();
+            ArrayList<AppWidgetProviderInfo> providers = new ArrayList<>();
             for (UserHandle user : mUserManager.getUserProfiles()) {
                 providers.addAll(mAppWidgetManager.getInstalledProvidersForProfile(user));
             }
-
-            if (FeatureFlags.ENABLE_CUSTOM_WIDGETS) {
-                providers.addAll(CustomWidgetParser.getCustomWidgets(mContext));
-            }
+            providers.addAll(getCustomWidgets());
             return providers;
         }
         // Only get providers for the given package/user.
@@ -74,9 +72,9 @@
             }
         }
 
-        if (FeatureFlags.ENABLE_CUSTOM_WIDGETS && Process.myUserHandle().equals(packageUser.mUser)
+        if (Process.myUserHandle().equals(packageUser.mUser)
                 && mContext.getPackageName().equals(packageUser.mPackageName)) {
-            providers.addAll(CustomWidgetParser.getCustomWidgets(mContext));
+            providers.addAll(getCustomWidgets());
         }
         return providers;
     }
@@ -87,9 +85,7 @@
         if (FeatureFlags.GO_DISABLE_WIDGETS) {
             return false;
         }
-
-        if (FeatureFlags.ENABLE_CUSTOM_WIDGETS
-                && appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
+        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
             return true;
         }
         return mAppWidgetManager.bindAppWidgetIdIfAllowed(
@@ -108,9 +104,8 @@
             }
         }
 
-        if (FeatureFlags.ENABLE_CUSTOM_WIDGETS && Process.myUserHandle().equals(user)) {
-            for (LauncherAppWidgetProviderInfo info :
-                    CustomWidgetParser.getCustomWidgets(mContext)) {
+        if (Process.myUserHandle().equals(user)) {
+            for (LauncherAppWidgetProviderInfo info : getCustomWidgets()) {
                 if (info.provider.equals(provider)) {
                     return info;
                 }
@@ -131,13 +126,13 @@
                 result.put(new ComponentKey(info.provider, user), info);
             }
         }
-
-        if (FeatureFlags.ENABLE_CUSTOM_WIDGETS) {
-            for (LauncherAppWidgetProviderInfo info :
-                    CustomWidgetParser.getCustomWidgets(mContext)) {
-                result.put(new ComponentKey(info.provider, info.getProfile()), info);
-            }
+        for (LauncherAppWidgetProviderInfo info : getCustomWidgets()) {
+            result.put(new ComponentKey(info.provider, info.getProfile()), info);
         }
         return result;
     }
+
+    List<CustomAppWidgetProviderInfo> getCustomWidgets() {
+        return CustomWidgetManager.INSTANCE.get(mContext).getCustomWidgets();
+    }
 }
diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java
index b7b0563..11ec333 100644
--- a/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java
+++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatVO.java
@@ -19,14 +19,14 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.Collections;
 import java.util.List;
 
-import androidx.annotation.Nullable;
-
 class AppWidgetManagerCompatVO extends AppWidgetManagerCompatVL {
 
     AppWidgetManagerCompatVO(Context context) {
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java
index 58fc73d..39f6949 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompat.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java
@@ -22,30 +22,34 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionCallback;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.List;
 
-import androidx.annotation.Nullable;
-
 public abstract class LauncherAppsCompat {
 
     public interface OnAppsChangedCallbackCompat {
-        void onPackageRemoved(String packageName, UserHandle user);
-        void onPackageAdded(String packageName, UserHandle user);
-        void onPackageChanged(String packageName, UserHandle user);
-        void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing);
-        void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing);
-        void onPackagesSuspended(String[] packageNames, UserHandle user);
-        void onPackagesUnsuspended(String[] packageNames, UserHandle user);
-        void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts,
-                UserHandle user);
+        default void onPackageRemoved(String packageName, UserHandle user) { }
+        default void onPackageAdded(String packageName, UserHandle user) { }
+        default void onPackageChanged(String packageName, UserHandle user) { }
+        default void onPackagesAvailable(String[] packageNames, UserHandle user,
+                boolean replacing) { }
+        default void onPackagesUnavailable(String[] packageNames, UserHandle user,
+                boolean replacing) { }
+        default void onPackagesSuspended(String[] packageNames, UserHandle user) { }
+        default void onPackagesUnsuspended(String[] packageNames, UserHandle user) { }
+        default void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts,
+                UserHandle user) { }
     }
 
     protected LauncherAppsCompat() {
@@ -88,4 +92,8 @@
             @Nullable PackageUserKey packageUser);
 
     public abstract List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions();
+
+    public abstract void registerSessionCallback(LooperExecutor executor,
+                                                 SessionCallback sessionCallback);
+    public abstract void unregisterSessionCallback(SessionCallback sessionCallback);
 }
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
index 1885d8f..281274c 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java
@@ -23,6 +23,7 @@
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionCallback;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
@@ -35,6 +36,7 @@
 
 import com.android.launcher3.compat.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVL;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
@@ -212,5 +214,17 @@
     public List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions() {
         return mContext.getPackageManager().getPackageInstaller().getAllSessions();
     }
+
+    @Override
+    public void registerSessionCallback(LooperExecutor executor, SessionCallback sessionCallback) {
+        mContext.getPackageManager().getPackageInstaller().registerSessionCallback(sessionCallback,
+                executor.getHandler());
+    }
+
+    @Override
+    public void unregisterSessionCallback(SessionCallback sessionCallback) {
+        mContext.getPackageManager().getPackageInstaller()
+                .unregisterSessionCallback(sessionCallback);
+    }
 }
 
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java
index 6e7a1bd..5e13d00 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.compat;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
@@ -30,19 +32,17 @@
 import android.os.Process;
 import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.compat.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVO;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.annotation.Nullable;
-
 @TargetApi(26)
 public class LauncherAppsCompatVO extends LauncherAppsCompatVL {
 
@@ -120,7 +120,7 @@
                 }
             } else {
                 // Block the worker thread until the accept() is called.
-                new LooperExecutor(LauncherModel.getWorkerLooper()).execute(new Runnable() {
+                MODEL_EXECUTOR.execute(new Runnable() {
                     @Override
                     public void run() {
                         try {
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java b/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java
index 0a1811e..48805af 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatVQ.java
@@ -18,8 +18,10 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.content.Intent;
 import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionCallback;
+
+import com.android.launcher3.util.LooperExecutor;
 
 import java.util.List;
 
@@ -33,4 +35,14 @@
     public List<PackageInstaller.SessionInfo> getAllPackageInstallerSessions() {
         return mLauncherApps.getAllPackageInstallerSessions();
     }
+
+    @Override
+    public void registerSessionCallback(LooperExecutor executor, SessionCallback sessionCallback) {
+        mLauncherApps.registerPackageInstallerSessionCallback(executor, sessionCallback);
+    }
+
+    @Override
+    public void unregisterSessionCallback(SessionCallback sessionCallback) {
+        mLauncherApps.unregisterPackageInstallerSessionCallback(sessionCallback);
+    }
 }
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
index 879d963..409b21d 100644
--- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
+++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
@@ -16,13 +16,14 @@
 
 package com.android.launcher3.compat;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionCallback;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
-import android.os.Handler;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.SparseArray;
@@ -31,7 +32,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -53,7 +53,6 @@
 
     @Thunk final PackageInstaller mInstaller;
     private final IconCache mCache;
-    private final Handler mWorker;
     private final Context mAppContext;
     private final HashMap<String,Boolean> mSessionVerifiedMap = new HashMap<>();
     private final LauncherAppsCompat mLauncherApps;
@@ -63,11 +62,10 @@
         mAppContext = context.getApplicationContext();
         mInstaller = context.getPackageManager().getPackageInstaller();
         mCache = LauncherAppState.getInstance(context).getIconCache();
-        mWorker = new Handler(LauncherModel.getWorkerLooper());
-        mInstaller.registerSessionCallback(mCallback, mWorker);
         mLauncherApps = LauncherAppsCompat.getInstance(context);
-        mPromiseIconIds = IntSet.wrap(IntArray.wrap(Utilities.getIntArrayFromString(
-                getPrefs(context).getString(PROMISE_ICON_IDS, ""))));
+        mLauncherApps.registerSessionCallback(MODEL_EXECUTOR, mCallback);
+        mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString(
+                getPrefs(context).getString(PROMISE_ICON_IDS, "")));
 
         cleanUpPromiseIconIds();
     }
@@ -127,7 +125,7 @@
 
     @Override
     public void onStop() {
-        mInstaller.unregisterSessionCallback(mCallback);
+        mLauncherApps.unregisterSessionCallback(mCallback);
     }
 
     @Thunk void sendUpdate(PackageInstallInfo info) {
@@ -223,12 +221,14 @@
         private SessionInfo pushSessionDisplayToLauncher(int sessionId) {
             SessionInfo session = verify(mInstaller.getSessionInfo(sessionId));
             if (session != null && session.getAppPackageName() != null) {
+                UserHandle user = getUserHandle(session);
                 mActiveSessions.put(session.getSessionId(),
-                        new PackageUserKey(session.getAppPackageName(), getUserHandle(session)));
-                addSessionInfoToCache(session, getUserHandle(session));
+                        new PackageUserKey(session.getAppPackageName(), user));
+                addSessionInfoToCache(session, user);
                 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
                 if (app != null) {
-                    app.getModel().updateSessionDisplayInfo(session.getAppPackageName());
+                    app.getModel().updateSessionDisplayInfo(session.getAppPackageName(),
+                            user);
                 }
                 return session;
             }
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 025087b..64d236f 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -23,11 +23,11 @@
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.Keep;
-
 import androidx.annotation.VisibleForTesting;
-import com.android.launcher3.Utilities;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.uioverrides.TogglableFlag;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.SortedMap;
@@ -74,9 +74,6 @@
     //Feature flag to enable pulling down navigation shade from workspace.
     public static final boolean PULL_DOWN_STATUS_BAR = true;
 
-    // When true, custom widgets are loaded using CustomWidgetParser.
-    public static final boolean ENABLE_CUSTOM_WIDGETS = false;
-
     // Features to control Launcher3Go behavior
     public static final boolean GO_DISABLE_WIDGETS = false;
 
@@ -111,10 +108,21 @@
             "FAKE_LANDSCAPE_UI", false,
             "Rotate launcher UI instead of using transposed layout");
 
+    public static final TogglableFlag FOLDER_NAME_SUGGEST = new TogglableFlag(
+            "FOLDER_NAME_SUGGEST", true,
+            "Suggests folder names instead of blank text.");
+
     public static final TogglableFlag APP_SEARCH_IMPROVEMENTS = new TogglableFlag(
             "APP_SEARCH_IMPROVEMENTS", false,
             "Adds localized title and keyword search and ranking");
 
+    public static final TogglableFlag ENABLE_PREDICTION_DISMISS = new TogglableFlag(
+            "ENABLE_PREDICTION_DISMISS", false, "Allow option to dimiss apps from predicted list");
+
+    public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
+            "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
+            "Allow Launcher to handle nav bar gestures while Assistant is running over it");
+
     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/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index a2dcbf8..9fb1090 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.logging.LoggerUtils.newItemTarget;
 import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.ActivityOptions;
@@ -47,7 +48,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHost;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.R;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompatVO;
@@ -55,7 +55,6 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.InstantAppResolver;
-import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -234,7 +233,7 @@
                 mWidgetCell.applyFromCellItem(item, mApp.getWidgetCache());
                 mWidgetCell.ensurePreview();
             }
-        }.executeOnExecutor(new LooperExecutor(LauncherModel.getWorkerLooper()));
+        }.executeOnExecutor(MODEL_EXECUTOR);
         // TODO: Create a worker looper executor and reuse that everywhere.
     }
 
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index b59164a..cdc7061 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -284,7 +284,8 @@
             // The child may be scaled (always about the center of the view) so to account for it,
             // we have to offset the position by the scaled size.  Once we do that, we can center
             // the drag view about the scaled child view.
-            toY += Math.round(toScale * tv.getPaddingTop());
+            // padding will remain constant (does not scale with size)
+            toY += tv.getPaddingTop();
             toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2;
             if (dragView.getDragVisualizeOffset() != null) {
                 toY -=  Math.round(toScale * dragView.getDragVisualizeOffset().y);
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 09c5e5b..f66d07e 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.dragndrop;
 
 import static com.android.launcher3.Utilities.getBadge;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -41,16 +42,19 @@
 import android.os.Looper;
 import android.view.View;
 
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
 import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.FirstFrameAnimatorHelper;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.FirstFrameAnimatorHelper;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.util.Themes;
@@ -58,10 +62,6 @@
 
 import java.util.Arrays;
 
-import androidx.dynamicanimation.animation.FloatPropertyCompat;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
 public class DragView extends View implements LauncherStateManager.StateListener {
     private static final ColorMatrix sTempMatrix1 = new ColorMatrix();
     private static final ColorMatrix sTempMatrix2 = new ColorMatrix();
@@ -210,7 +210,7 @@
             return;
         }
         // Load the adaptive icon on a background thread and add the view in ui thread.
-        new Handler(LauncherModel.getWorkerLooper()).postAtFrontOfQueue(new Runnable() {
+        MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(new Runnable() {
             @Override
             public void run() {
                 Object[] outObj = new Object[1];
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 589ad25..06b5c40 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -106,6 +106,7 @@
      * @return the vector at which the item was flung, or null if no fling was detected.
      */
     private PointF isFlingingToDelete() {
+        if (mVelocityTracker == null) return null;
         if (mDropTarget == null) {
             mDropTarget = (ButtonDropTarget) mLauncher.findViewById(R.id.delete_target_text);
         }
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
index d8a1f99..0bb3fba 100644
--- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.dragndrop;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.annotation.TargetApi;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -31,7 +33,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.folder.PreviewBackground;
@@ -85,7 +86,7 @@
         // Create the actual drawable on the UI thread to avoid race conditions with
         // FolderIcon draw pass
         try {
-            return new MainThreadExecutor().submit(() -> {
+            return MAIN_EXECUTOR.submit(() -> {
                 FolderIcon icon = launcher.findFolderIcon(folderId);
                 return icon == null ? null : createDrawableOnUiThread(icon, badge, dragViewSize);
             }).get();
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f22b533..0bd2c9a 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -26,12 +26,12 @@
 import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.text.InputType;
 import android.text.Selection;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
@@ -74,7 +74,10 @@
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.ClipPathView;
@@ -126,9 +129,6 @@
     private static final Rect sTempRect = new Rect();
     private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
 
-    private static String sDefaultFolderName;
-    private static String sHintText;
-
     private final Alarm mReorderAlarm = new Alarm();
     private final Alarm mOnExitAlarm = new Alarm();
     private final Alarm mOnScrollHintAlarm = new Alarm();
@@ -148,7 +148,7 @@
     public ExtendedEditText mFolderName;
     private PageIndicatorDots mPageIndicator;
 
-    private View mFooter;
+    protected View mFooter;
     private int mFooterHeight;
 
     // Cell ranks used for drag and drop
@@ -173,8 +173,6 @@
     private boolean mDeleteFolderOnDropCompleted = false;
     private boolean mSuppressFolderDeletion = false;
     private boolean mItemAddedBackToSelfViaIcon = false;
-    @Thunk float mFolderIconPivotX;
-    @Thunk float mFolderIconPivotY;
     private boolean mIsEditingName = false;
 
     @ViewDebug.ExportedProperty(category = "launcher")
@@ -196,8 +194,6 @@
         super(context, attrs);
         setAlwaysDrawnWithCacheEnabled(false);
 
-        setLocaleDependentFields(getResources(), false /* force */);
-
         mLauncher = Launcher.getLauncher(context);
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
@@ -315,10 +311,15 @@
         // Convert to a string here to ensure that no other state associated with the text field
         // gets saved.
         String newTitle = mFolderName.getText().toString();
-        mInfo.setTitle(newTitle);
+        mInfo.title = newTitle;
+        mFolderIcon.onTitleChanged(newTitle);
         mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
-        mFolderName.setHint(sDefaultFolderName.contentEquals(newTitle) ? sHintText : null);
+        if (TextUtils.isEmpty(mInfo.title)) {
+            mFolderName.setHint(R.string.folder_hint_text);
+        } else {
+            mFolderName.setHint(null);
+        }
 
         sendCustomAccessibilityEvent(
                 this, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
@@ -385,7 +386,7 @@
         mInfo = info;
         ArrayList<WorkspaceItemInfo> children = info.contents;
         Collections.sort(children, ITEM_POS_COMPARATOR);
-        mContent.bindItems(children);
+        updateItemLocationsInDatabaseBatch();
 
         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
         if (lp == null) {
@@ -393,30 +394,40 @@
             lp.customPosition = true;
             setLayoutParams(lp);
         }
-        centerAboutIcon();
-
         mItemsInvalidated = true;
-        updateTextViewFocus();
         mInfo.addListener(this);
 
-        if (!sDefaultFolderName.contentEquals(mInfo.title)) {
+        if (!TextUtils.isEmpty(mInfo.title)) {
             mFolderName.setText(mInfo.title);
             mFolderName.setHint(null);
         } else {
             mFolderName.setText("");
-            mFolderName.setHint(sHintText);
+            mFolderName.setHint(R.string.folder_hint_text);
         }
-
         // In case any children didn't come across during loading, clean up the folder accordingly
-        mFolderIcon.post(new Runnable() {
-            public void run() {
-                if (getItemCount() <= 1) {
-                    replaceFolderWithFinalItem();
-                }
+        mFolderIcon.post(() -> {
+            if (getItemCount() <= 1) {
+                replaceFolderWithFinalItem();
             }
         });
     }
 
+
+    /**
+     * Show suggested folder title.
+     */
+    public void showSuggestedTitle(CharSequence suggestName) {
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && mInfo.contents.size() == 2) {
+            if (!TextUtils.isEmpty(suggestName)) {
+                mFolderName.setHint(suggestName);
+                mFolderName.setText(suggestName);
+                mFolderName.showKeyboard();
+                mInfo.title = suggestName;
+            }
+            animateOpen();
+        }
+    }
+
     /**
      * Creates a new UserFolder, inflated from R.layout.user_folder.
      *
@@ -473,17 +484,49 @@
     }
 
     /**
+     * Opens the folder as part of a drag operation
+     */
+    public void beginExternalDrag() {
+        mIsExternalDrag = true;
+        mDragInProgress = true;
+
+        // Since this folder opened by another controller, it might not get onDrop or
+        // onDropComplete. Perform cleanup once drag-n-drop ends.
+        mDragController.addDragListener(this);
+
+        ArrayList<WorkspaceItemInfo> items = new ArrayList<>(mInfo.contents);
+        mEmptyCellRank = items.size();
+        items.add(null);    // Add an empty spot at the end
+
+        animateOpen(items, mEmptyCellRank / mContent.itemsPerPage());
+    }
+
+    /**
      * Opens the user folder described by the specified tag. The opening of the folder
      * is animated relative to the specified View. If the View is null, no animation
      * is played.
      */
     public void animateOpen() {
+        animateOpen(mInfo.contents, 0);
+    }
+
+    /**
+     * Opens the user folder described by the specified tag. The opening of the folder
+     * is animated relative to the specified View. If the View is null, no animation
+     * is played.
+     */
+    private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
         Folder openFolder = getOpen(mLauncher);
         if (openFolder != null && openFolder != this) {
             // Close any open folder before opening a folder.
             openFolder.close(true);
         }
 
+        mContent.bindItems(items);
+        centerAboutIcon();
+        mItemsInvalidated = true;
+        updateTextViewFocus();
+
         mIsOpen = true;
 
         DragLayer dragLayer = mLauncher.getDragLayer();
@@ -500,18 +543,13 @@
         }
 
         mContent.completePendingPageChanges();
-        if (!mDragInProgress) {
-            // Open on the first page.
-            mContent.snapToPageImmediately(0);
-        }
+        mContent.snapToPageImmediately(pageNo);
 
         // This is set to true in close(), but isn't reset to false until onDropCompleted(). This
         // leads to an inconsistent state if you drag out of the folder and drag back in without
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
-        centerAboutIcon();
-
         AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator();
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -524,7 +562,11 @@
                 mState = STATE_OPEN;
                 announceAccessibilityChanges();
 
-                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("folder opened");
+                mLauncher.getUserEventDispatcher().logActionOnItem(
+                        Touch.TAP,
+                        Direction.NONE,
+                        ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+
                 mContent.setFocusOnFirstChild();
             }
         });
@@ -570,20 +612,9 @@
         if (mDragController.isDragging()) {
             mDragController.forceTouchMove();
         }
-
         mContent.verifyVisibleHighResIcons(mContent.getNextPage());
     }
 
-    public void beginExternalDrag() {
-        mEmptyCellRank = mContent.allocateRankForNewItem();
-        mIsExternalDrag = true;
-        mDragInProgress = true;
-
-        // Since this folder opened by another controller, it might not get onDrop or
-        // onDropComplete. Perform cleanup once drag-n-drop ends.
-        mDragController.addDragListener(this);
-    }
-
     @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_FOLDER) != 0;
@@ -668,6 +699,8 @@
             } else if (mDragInProgress) {
                 mDeleteFolderOnDropCompleted = true;
             }
+        } else if (!mDragInProgress) {
+            mContent.unbindItems();
         }
         mSuppressFolderDeletion = false;
         clearDragInfo();
@@ -822,9 +855,9 @@
         }
     }
 
+    @Override
     public void onDropCompleted(final View target, final DragObject d,
             final boolean success) {
-
         if (success) {
             if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
                 replaceFolderWithFinalItem();
@@ -834,9 +867,9 @@
             WorkspaceItemInfo info = (WorkspaceItemInfo) d.dragInfo;
             View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
                     ? mCurrentDragView : mContent.createNewView(info);
-            ArrayList<View> views = getItemsInReadingOrder();
+            ArrayList<View> views = getIconsInReadingOrder();
             views.add(info.rank, icon);
-            mContent.arrangeChildren(views, views.size());
+            mContent.arrangeChildren(views);
             mItemsInvalidated = true;
 
             try (SuppressInfoChanges s = new SuppressInfoChanges()) {
@@ -863,7 +896,6 @@
         // Reordering may have occured, and we need to save the new item locations. We do this once
         // at the end to prevent unnecessary database operations.
         updateItemLocationsInDatabaseBatch();
-
         // Use the item count to check for multi-page as the folder UI may not have
         // been refreshed yet.
         if (getItemCount() <= mContent.itemsPerPage()) {
@@ -874,16 +906,21 @@
     }
 
     private void updateItemLocationsInDatabaseBatch() {
-        ArrayList<View> list = getItemsInReadingOrder();
-        ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
-        for (int i = 0; i < list.size(); i++) {
-            View v = list.get(i);
-            ItemInfo info = (ItemInfo) v.getTag();
-            info.rank = i;
-            items.add(info);
+        FolderGridOrganizer verifier = new FolderGridOrganizer(
+                mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+
+        ArrayList<ItemInfo> items = new ArrayList<>();
+        int total = mInfo.contents.size();
+        for (int i = 0; i < total; i++) {
+            WorkspaceItemInfo itemInfo = mInfo.contents.get(i);
+            if (verifier.updateRankAndPos(itemInfo, i)) {
+                items.add(itemInfo);
+            }
         }
 
-        mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
+        if (!items.isEmpty()) {
+            mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
+        }
     }
 
     public void notifyDrop() {
@@ -948,28 +985,16 @@
         setPivotX(folderPivotX);
         setPivotY(folderPivotY);
 
-        mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
-                (1.0f * folderPivotX / width));
-        mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
-                (1.0f * folderPivotY / height));
-
         lp.width = width;
         lp.height = height;
         lp.x = left;
         lp.y = top;
     }
 
-    public float getPivotXForIconAnimation() {
-        return mFolderIconPivotX;
-    }
-    public float getPivotYForIconAnimation() {
-        return mFolderIconPivotY;
-    }
-
-    private int getContentAreaHeight() {
+    protected int getContentAreaHeight() {
         DeviceProfile grid = mLauncher.getDeviceProfile();
-        int maxContentAreaHeight = grid.availableHeightPx
-                - grid.getTotalWorkspacePadding().y - mFooterHeight;
+        int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
+                - mFooterHeight;
         int height = Math.min(maxContentAreaHeight,
                 mContent.getDesiredHeight());
         return Math.max(height, MIN_CONTENT_DIMEN);
@@ -1021,22 +1046,12 @@
      * Rearranges the children based on their rank.
      */
     public void rearrangeChildren() {
-        rearrangeChildren(-1);
-    }
-
-    /**
-     * Rearranges the children based on their rank.
-     * @param itemCount if greater than the total children count, empty spaces are left at the end,
-     * otherwise it is ignored.
-     */
-    public void rearrangeChildren(int itemCount) {
-        ArrayList<View> views = getItemsInReadingOrder();
-        mContent.arrangeChildren(views, Math.max(itemCount, views.size()));
+        mContent.arrangeChildren(getIconsInReadingOrder());
         mItemsInvalidated = true;
     }
 
     public int getItemCount() {
-        return mContent.getItemCount();
+        return mInfo.contents.size();
     }
 
     @Thunk void replaceFolderWithFinalItem() {
@@ -1044,7 +1059,7 @@
         Runnable onCompleteRunnable = new Runnable() {
             @Override
             public void run() {
-                int itemCount = mInfo.contents.size();
+                int itemCount = getItemCount();
                 if (itemCount <= 1) {
                     View newIcon = null;
 
@@ -1121,9 +1136,12 @@
                     return false;
                 }
             });
+        } else {
+            setOnKeyListener(null);
         }
     }
 
+    @Override
     public void onDrop(DragObject d, DragOptions options) {
         // If the icon was dropped while the page was being scrolled, we need to compute
         // the target location again such that the icon is placed of the final page.
@@ -1171,12 +1189,6 @@
                 // before creating the view, so that WorkspaceItemInfo is updated appropriately.
                 mLauncher.getModelWriter().addOrMoveItemInDatabase(
                         si, mInfo.id, 0, si.cellX, si.cellY);
-
-                // We only need to update the locations if it doesn't get handled in
-                // #onDropCompleted.
-                if (d.dragSource != this) {
-                    updateItemLocationsInDatabaseBatch();
-                }
                 mIsExternalDrag = false;
             } else {
                 currentDragView = mCurrentDragView;
@@ -1203,7 +1215,13 @@
 
             // Temporarily suppress the listener, as we did all the work already here.
             try (SuppressInfoChanges s = new SuppressInfoChanges()) {
-                mInfo.add(si, false);
+                mInfo.add(si, mEmptyCellRank, false);
+            }
+
+            // We only need to update the locations if it doesn't get handled in
+            // #onDropCompleted.
+            if (d.dragSource != this) {
+                updateItemLocationsInDatabaseBatch();
             }
         }
 
@@ -1226,22 +1244,29 @@
     // to correspond to the animation of the icon back into the folder. This is
     public void hideItem(WorkspaceItemInfo info) {
         View v = getViewForInfo(info);
-        v.setVisibility(INVISIBLE);
+        if (v != null) {
+            v.setVisibility(INVISIBLE);
+        }
     }
     public void showItem(WorkspaceItemInfo info) {
         View v = getViewForInfo(info);
-        v.setVisibility(VISIBLE);
+        if (v != null) {
+            v.setVisibility(VISIBLE);
+        }
     }
 
     @Override
     public void onAdd(WorkspaceItemInfo item, int rank) {
-        View view = mContent.createAndAddViewForRank(item, rank);
+        FolderGridOrganizer verifier = new FolderGridOrganizer(
+                mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+        verifier.updateRankAndPos(item, rank);
         mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
                 item.cellY);
+        updateItemLocationsInDatabaseBatch();
 
-        ArrayList<View> items = new ArrayList<>(getItemsInReadingOrder());
-        items.add(rank, view);
-        mContent.arrangeChildren(items, items.size());
+        if (mContent.areViewsBound()) {
+            mContent.createAndAddViewForRank(item, rank);
+        }
         mItemsInvalidated = true;
     }
 
@@ -1264,13 +1289,7 @@
     }
 
     private View getViewForInfo(final WorkspaceItemInfo item) {
-        return mContent.iterateOverItems(new ItemOperator() {
-
-            @Override
-            public boolean evaluate(ItemInfo info, View view) {
-                return info == item;
-            }
-        });
+        return mContent.iterateOverItems((info, view) -> info == item);
     }
 
     @Override
@@ -1278,32 +1297,27 @@
         updateTextViewFocus();
     }
 
-    @Override
-    public void prepareAutoUpdate() {
-        close(false);
+    /**
+     * Utility methods to iterate over items of the view
+     */
+    public void iterateOverItems(ItemOperator op) {
+        mContent.iterateOverItems(op);
     }
 
-    public void onTitleChanged(CharSequence title) {
-    }
-
-    public ArrayList<View> getItemsInReadingOrder() {
+    /**
+     * Returns the sorted list of all the icons in the folder
+     */
+    public ArrayList<View> getIconsInReadingOrder() {
         if (mItemsInvalidated) {
             mItemsInReadingOrder.clear();
-            mContent.iterateOverItems(new ItemOperator() {
-
-                @Override
-                public boolean evaluate(ItemInfo info, View view) {
-                    mItemsInReadingOrder.add(view);
-                    return false;
-                }
-            });
+            mContent.iterateOverItems((i, v) -> !mItemsInReadingOrder.add(v));
             mItemsInvalidated = false;
         }
         return mItemsInReadingOrder;
     }
 
     public List<BubbleTextView> getItemsOnPage(int page) {
-        ArrayList<View> allItems = getItemsInReadingOrder();
+        ArrayList<View> allItems = getIconsInReadingOrder();
         int lastPage = mContent.getPageCount() - 1;
         int totalItemsInFolder = allItems.size();
         int itemsPerPage = mContent.itemsPerPage();
@@ -1482,15 +1496,6 @@
         return false;
     }
 
-    public static void setLocaleDependentFields(Resources res, boolean force) {
-        if (sDefaultFolderName == null || force) {
-            sDefaultFolderName = res.getString(R.string.folder_name);
-        }
-        if (sHintText == null || force) {
-            sHintText = res.getString(R.string.folder_hint_text);
-        }
-    }
-
     /**
      * Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
      * rounded rect.
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 962f215..1310d37 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -59,6 +59,8 @@
  */
 public class FolderAnimationManager {
 
+    private static final int FOLDER_NAME_ALPHA_DURATION = 32;
+
     private Folder mFolder;
     private FolderPagedView mContent;
     private GradientDrawable mFolderBackground;
@@ -79,7 +81,7 @@
     private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator;
 
     private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
-
+    private final FolderGridOrganizer mPreviewVerifier;
 
     public FolderAnimationManager(Folder folder, boolean isOpening) {
         mFolder = folder;
@@ -91,6 +93,7 @@
 
         mContext = folder.getContext();
         mLauncher = folder.mLauncher;
+        mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
 
         mIsOpening = isOpening;
 
@@ -113,7 +116,7 @@
     public AnimatorSet getAnimator() {
         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
-        final List<BubbleTextView> itemsInPreview = mFolderIcon.getPreviewItems();
+        final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0);
 
         // Match position of the FolderIcon
         final Rect folderIconPos = new Rect();
@@ -129,11 +132,19 @@
                 * scaleRelativeToDragLayer;
         final float finalScale = 1f;
         float scale = mIsOpening ? initialScale : finalScale;
-        mFolder.setScaleX(scale);
-        mFolder.setScaleY(scale);
         mFolder.setPivotX(0);
         mFolder.setPivotY(0);
 
+        // Scale the contents of the folder.
+        mFolder.mContent.setScaleX(scale);
+        mFolder.mContent.setScaleY(scale);
+        mFolder.mContent.setPivotX(0);
+        mFolder.mContent.setPivotY(0);
+        mFolder.mFooter.setScaleX(scale);
+        mFolder.mFooter.setScaleY(scale);
+        mFolder.mFooter.setPivotX(0);
+        mFolder.mFooter.setPivotY(0);
+
         // We want to create a small X offset for the preview items, so that they follow their
         // expected path to their final locations. ie. an icon should not move right, if it's final
         // location is to its left. This value is arbitrarily defined.
@@ -142,14 +153,13 @@
             previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
         }
 
-        final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft())
-                * initialScale);
-        final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop())
-                * initialScale);
+        final int paddingOffsetX = (int) (mContent.getPaddingLeft() * initialScale);
+        final int paddingOffsetY = (int) (mContent.getPaddingTop() * initialScale);
 
-        int initialX = folderIconPos.left + mPreviewBackground.getOffsetX() - paddingOffsetX
-                - previewItemOffsetX;
-        int initialY = folderIconPos.top + mPreviewBackground.getOffsetY() - paddingOffsetY;
+        int initialX = folderIconPos.left + mFolder.getPaddingLeft()
+                + mPreviewBackground.getOffsetX() - paddingOffsetX - previewItemOffsetX;
+        int initialY = folderIconPos.top + mFolder.getPaddingTop()
+                + mPreviewBackground.getOffsetY() - paddingOffsetY;
         final float xDistance = initialX - lp.x;
         final float yDistance = initialY - lp.y;
 
@@ -163,11 +173,10 @@
 
         // Set up the reveal animation that clips the Folder.
         int totalOffsetX = paddingOffsetX + previewItemOffsetX;
-        Rect startRect = new Rect(
-                Math.round(totalOffsetX / initialScale),
-                Math.round(paddingOffsetY / initialScale),
-                Math.round((totalOffsetX + initialSize) / initialScale),
-                Math.round((paddingOffsetY + initialSize) / initialScale));
+        Rect startRect = new Rect(totalOffsetX,
+                paddingOffsetY,
+                Math.round((totalOffsetX + initialSize)),
+                Math.round((paddingOffsetY + initialSize)));
         Rect endRect = new Rect(0, 0, lp.width, lp.height);
         float finalRadius = ResourceUtils.pxFromDp(2, mContext.getResources().getDisplayMetrics());
 
@@ -188,17 +197,46 @@
 
         play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
         play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
-        play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale));
+        play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale));
+        play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale));
         play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor));
         play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening));
         play(a, getShape().createRevealAnimator(
                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
+        // Fade in the folder name, as the text can overlap the icons when grid size is small.
+        mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
+        play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
+                mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
+                mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
+
+        // Translate the footer so that it tracks the bottom of the content.
+        float normalHeight = mFolder.getContentAreaHeight();
+        float scaledHeight = normalHeight * initialScale;
+        float diff = normalHeight - scaledHeight;
+        play(a, getAnimator(mFolder.mFooter, View.TRANSLATION_Y, -diff, 0f));
 
         // Animate the elevation midway so that the shadow is not noticeable in the background.
         int midDuration = mDuration / 2;
         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
         play(a, z, mIsOpening ? midDuration : 0, midDuration);
 
+
+        // Store clip variables
+        CellLayout cellLayout = mContent.getCurrentCellLayout();
+        boolean folderClipChildren = mFolder.getClipChildren();
+        boolean folderClipToPadding = mFolder.getClipToPadding();
+        boolean contentClipChildren = mContent.getClipChildren();
+        boolean contentClipToPadding = mContent.getClipToPadding();
+        boolean cellLayoutClipChildren = cellLayout.getClipChildren();
+        boolean cellLayoutClipPadding = cellLayout.getClipToPadding();
+
+        mFolder.setClipChildren(false);
+        mFolder.setClipToPadding(false);
+        mContent.setClipChildren(false);
+        mContent.setClipToPadding(false);
+        cellLayout.setClipChildren(false);
+        cellLayout.setClipToPadding(false);
+
         a.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
@@ -206,8 +244,20 @@
                 mFolder.setTranslationX(0.0f);
                 mFolder.setTranslationY(0.0f);
                 mFolder.setTranslationZ(0.0f);
-                mFolder.setScaleX(1f);
-                mFolder.setScaleY(1f);
+                mFolder.mContent.setScaleX(1f);
+                mFolder.mContent.setScaleY(1f);
+                mFolder.mFooter.setScaleX(1f);
+                mFolder.mFooter.setScaleY(1f);
+                mFolder.mFooter.setTranslationX(0f);
+                mFolder.mFolderName.setAlpha(1f);
+
+                mFolder.setClipChildren(folderClipChildren);
+                mFolder.setClipToPadding(folderClipToPadding);
+                mContent.setClipChildren(contentClipChildren);
+                mContent.setClipToPadding(contentClipToPadding);
+                cellLayout.setClipChildren(cellLayoutClipChildren);
+                cellLayout.setClipToPadding(cellLayoutClipPadding);
+
             }
         });
 
@@ -226,15 +276,22 @@
     }
 
     /**
+     * Returns the list of "preview items" on {@param page}.
+     */
+    private List<BubbleTextView> getPreviewIconsOnPage(int page) {
+        return mPreviewVerifier.setFolderInfo(mFolder.mInfo)
+                .previewItemsForPage(page, mFolder.getIconsInReadingOrder());
+    }
+
+    /**
      * Animate the items on the current page.
      */
     private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale,
             int previewItemOffsetX, int previewItemOffsetY) {
         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
         boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0;
-        final List<BubbleTextView> itemsInPreview = isOnFirstPage
-                ? mFolderIcon.getPreviewItems()
-                : mFolderIcon.getPreviewItemsOnPage(mFolder.mContent.getCurrentPage());
+        final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(
+                isOnFirstPage ? 0 : mFolder.mContent.getCurrentPage());
         final int numItemsInPreview = itemsInPreview.size();
         final int numItemsInFirstPagePreview = isOnFirstPage
                 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW;
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
new file mode 100644
index 0000000..9d14a5f
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -0,0 +1,196 @@
+/*
+ * 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.folder;
+
+import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+
+import android.graphics.Point;
+
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for managing item positions in a folder based on rank
+ */
+public class FolderGridOrganizer {
+
+    private final Point mPoint = new Point();
+    private final int mMaxCountX;
+    private final int mMaxCountY;
+    private final int mMaxItemsPerPage;
+
+    private int mNumItemsInFolder;
+    private int mCountX;
+    private int mCountY;
+    private boolean mDisplayingUpperLeftQuadrant = false;
+
+    /**
+     * Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work.
+     */
+    public FolderGridOrganizer(InvariantDeviceProfile profile) {
+        mMaxCountX = profile.numFolderColumns;
+        mMaxCountY = profile.numFolderRows;
+        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
+    }
+
+    /**
+     * Updates the organizer with the provided folder info
+     */
+    public FolderGridOrganizer setFolderInfo(FolderInfo info) {
+        return setContentSize(info.contents.size());
+    }
+
+    /**
+     * Updates the organizer to reflect the content size
+     */
+    public FolderGridOrganizer setContentSize(int contentSize) {
+        if (contentSize != mNumItemsInFolder) {
+            calculateGridSize(contentSize);
+
+            mDisplayingUpperLeftQuadrant = contentSize > MAX_NUM_ITEMS_IN_PREVIEW;
+            mNumItemsInFolder = contentSize;
+        }
+        return this;
+    }
+
+    public int getCountX() {
+        return mCountX;
+    }
+
+    public int getCountY() {
+        return mCountY;
+    }
+
+    public int getMaxItemsPerPage() {
+        return mMaxItemsPerPage;
+    }
+
+    /**
+     * Calculates the grid size such that {@param count} items can fit in the grid.
+     * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
+     * maintaining the restrictions of {@link #mMaxCountX} &amp; {@link #mMaxCountY}.
+     */
+    private void calculateGridSize(int count) {
+        boolean done;
+        int gridCountX = mCountX;
+        int gridCountY = mCountY;
+
+        if (count >= mMaxItemsPerPage) {
+            gridCountX = mMaxCountX;
+            gridCountY = mMaxCountY;
+            done = true;
+        } else {
+            done = false;
+        }
+
+        while (!done) {
+            int oldCountX = gridCountX;
+            int oldCountY = gridCountY;
+            if (gridCountX * gridCountY < count) {
+                // Current grid is too small, expand it
+                if ((gridCountX <= gridCountY || gridCountY == mMaxCountY)
+                        && gridCountX < mMaxCountX) {
+                    gridCountX++;
+                } else if (gridCountY < mMaxCountY) {
+                    gridCountY++;
+                }
+                if (gridCountY == 0) gridCountY++;
+            } else if ((gridCountY - 1) * gridCountX >= count && gridCountY >= gridCountX) {
+                gridCountY = Math.max(0, gridCountY - 1);
+            } else if ((gridCountX - 1) * gridCountY >= count) {
+                gridCountX = Math.max(0, gridCountX - 1);
+            }
+            done = gridCountX == oldCountX && gridCountY == oldCountY;
+        }
+
+        mCountX = gridCountX;
+        mCountY = gridCountY;
+    }
+
+    /**
+     * Updates the item's cellX, cellY and rank corresponding to the provided rank.
+     * @return true if there was any change
+     */
+    public boolean updateRankAndPos(ItemInfo item, int rank) {
+        Point pos = getPosForRank(rank);
+        if (!pos.equals(item.cellX, item.cellY) || rank != item.rank) {
+            item.rank = rank;
+            item.cellX = pos.x;
+            item.cellY = pos.y;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the position of the item in the grid
+     */
+    public Point getPosForRank(int rank) {
+        int pagePos = rank % mMaxItemsPerPage;
+        mPoint.x = pagePos % mCountX;
+        mPoint.y = pagePos / mCountX;
+        return mPoint;
+    }
+
+    /**
+     * Returns the preview items for the provided pageNo using the full list of contents
+     */
+    public <T, R extends T> ArrayList<R> previewItemsForPage(int page, List<T> contents) {
+        ArrayList<R> result = new ArrayList<>();
+        int itemsPerPage = mCountX * mCountY;
+        int start = itemsPerPage * page;
+        int end = Math.min(start + itemsPerPage, contents.size());
+
+        for (int i = start, rank = 0; i < end; i++, rank++) {
+            if (isItemInPreview(page, rank)) {
+                result.add((R) contents.get(i));
+            }
+
+            if (result.size() == MAX_NUM_ITEMS_IN_PREVIEW) {
+                break;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns whether the item with rank is in the default Folder icon preview.
+     */
+    public boolean isItemInPreview(int rank) {
+        return isItemInPreview(0, rank);
+    }
+
+    /**
+     * @param page The page the item is on.
+     * @param rank The rank of the item.
+     * @return True iff the icon is in the 2x2 upper left quadrant of the Folder.
+     */
+    public boolean isItemInPreview(int page, int rank) {
+        // First page items are laid out such that the first 4 items are always in the upper
+        // left quadrant. For all other pages, we need to check the row and col.
+        if (page > 0 || mDisplayingUpperLeftQuadrant) {
+            int col = rank % mCountX;
+            int row = rank / mCountX;
+            return col < 2 && row < 2;
+        }
+        return rank < MAX_NUM_ITEMS_IN_PREVIEW;
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 0e2d467..fd6d1e3 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -58,18 +58,21 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.BaseItemDragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.IconLabelDotView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Predicate;
 
 /**
  * An icon that can appear on in the workspace representing an {@link Folder}.
@@ -96,11 +99,11 @@
     PreviewBackground mBackground = new PreviewBackground();
     private boolean mBackgroundIsVisible = true;
 
-    FolderIconPreviewVerifier mPreviewVerifier;
+    FolderGridOrganizer mPreviewVerifier;
     ClippedFolderIconLayoutRule mPreviewLayoutRule;
     private PreviewItemManager mPreviewItemManager;
     private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
-    private List<BubbleTextView> mCurrentPreviewItems = new ArrayList<>();
+    private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>();
 
     boolean mAnimating = false;
 
@@ -175,7 +178,7 @@
         icon.setOnClickListener(ItemClickHandler.INSTANCE);
         icon.mInfo = folderInfo;
         icon.mLauncher = launcher;
-        icon.mDotRenderer = grid.mDotRenderer;
+        icon.mDotRenderer = grid.mDotRendererWorkSpace;
         icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
         Folder folder = Folder.fromXml(launcher);
         folder.setDragController(launcher.getDragController());
@@ -214,7 +217,7 @@
 
     private void setFolder(Folder folder) {
         mFolder = folder;
-        mPreviewVerifier = new FolderIconPreviewVerifier(mLauncher.getDeviceProfile().inv);
+        mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
         mPreviewVerifier.setFolderInfo(mFolder.getInfo());
         updatePreviewItems(false);
     }
@@ -232,11 +235,7 @@
     }
 
     public void addItem(WorkspaceItemInfo item) {
-        addItem(item, true);
-    }
-
-    public void addItem(WorkspaceItemInfo item, boolean animate) {
-        mInfo.add(item, animate);
+        mInfo.add(item, true);
     }
 
     public void removeItem(WorkspaceItemInfo item, boolean animate) {
@@ -261,7 +260,6 @@
     OnAlarmListener mOnOpenListener = new OnAlarmListener() {
         public void onAlarm(Alarm alarm) {
             mFolder.beginExternalDrag();
-            mFolder.animateOpen();
         }
     };
 
@@ -296,8 +294,7 @@
     }
 
     private void onDrop(final WorkspaceItemInfo item, DragView animateView, Rect finalRect,
-            float scaleRelativeToDragLayer, int index,
-            boolean itemReturnedOnFailedDrop) {
+            float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) {
         item.cellX = -1;
         item.cellY = -1;
 
@@ -328,18 +325,17 @@
             int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1);
             boolean itemAdded = false;
             if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) {
-                List<BubbleTextView> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems);
-                addItem(item, false);
+                List<WorkspaceItemInfo> oldPreviewItems = new ArrayList<>(mCurrentPreviewItems);
+                mInfo.add(item, index, false);
                 mCurrentPreviewItems.clear();
-                mCurrentPreviewItems.addAll(getPreviewItems());
+                mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
 
                 if (!oldPreviewItems.equals(mCurrentPreviewItems)) {
-                    for (int i = 0; i < mCurrentPreviewItems.size(); ++i) {
-                        if (mCurrentPreviewItems.get(i).getTag().equals(item)) {
-                            // If the item dropped is going to be in the preview, we update the
-                            // index here to reflect its position in the preview.
-                            index = i;
-                        }
+                    int newIndex = mCurrentPreviewItems.indexOf(item);
+                    if (newIndex >= 0) {
+                        // If the item dropped is going to be in the preview, we update the
+                        // index here to reflect its position in the preview.
+                        index = newIndex;
                     }
 
                     mPreviewItemManager.hidePreviewItem(index, true);
@@ -351,13 +347,13 @@
             }
 
             if (!itemAdded) {
-                addItem(item);
+                mInfo.add(item, index, true);
             }
 
             int[] center = new int[2];
             float scale = getLocalCenterForIndex(index, numItemsInPreview, center);
-            center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
-            center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
+            center[0] = Math.round(scaleRelativeToDragLayer * center[0]);
+            center[1] = Math.round(scaleRelativeToDragLayer * center[1]);
 
             to.offset(center[0] - animateView.getMeasuredWidth() / 2,
                     center[1] - animateView.getMeasuredHeight() / 2);
@@ -374,12 +370,17 @@
 
             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
             final int finalIndex = index;
-            postDelayed(new Runnable() {
-                public void run() {
-                    mPreviewItemManager.hidePreviewItem(finalIndex, false);
-                    mFolder.showItem(item);
-                    invalidate();
-                }
+
+            String[] suggestedNameOut = new String[1];
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                Executors.UI_HELPER_EXECUTOR.post(() -> mLauncher.getFolderNameProvider()
+                        .getSuggestedFolderName(getContext(), mInfo.contents, suggestedNameOut));
+            }
+            postDelayed(() -> {
+                mPreviewItemManager.hidePreviewItem(finalIndex, false);
+                mFolder.showItem(item);
+                invalidate();
+                mFolder.showSuggestedTitle(suggestedNameOut[0]);
             }, DROP_IN_ANIMATION_DURATION);
         } else {
             addItem(item);
@@ -398,7 +399,8 @@
             item = (WorkspaceItemInfo) d.dragInfo;
         }
         mFolder.notifyDrop();
-        onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(),
+        onDrop(item, d.dragView, null, 1.0f,
+                itemReturnedOnFailedDrop ? item.rank : mInfo.contents.size(),
                 itemReturnedOnFailedDrop);
     }
 
@@ -510,8 +512,7 @@
             mBackground.drawBackground(canvas);
         }
 
-        if (mFolder == null) return;
-        if (mFolder.getItemCount() == 0 && !mAnimating) return;
+        if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
 
         final int saveCount = canvas.save();
         canvas.clipPath(mBackground.getClipPath());
@@ -553,31 +554,10 @@
     }
 
     /**
-     * Returns the list of preview items displayed in the icon.
+     * Returns the list of items which should be visible in the preview
      */
-    public List<BubbleTextView> getPreviewItems() {
-        return getPreviewItemsOnPage(0);
-    }
-
-    /**
-     * Returns the list of "preview items" on {@param page}.
-     */
-    public List<BubbleTextView> getPreviewItemsOnPage(int page) {
-        mPreviewVerifier.setFolderInfo(mFolder.getInfo());
-
-        List<BubbleTextView> itemsToDisplay = new ArrayList<>();
-        List<BubbleTextView> itemsOnPage = mFolder.getItemsOnPage(page);
-        int numItems = itemsOnPage.size();
-        for (int rank = 0; rank < numItems; ++rank) {
-            if (mPreviewVerifier.isItemInPreview(page, rank)) {
-                itemsToDisplay.add(itemsOnPage.get(rank));
-            }
-
-            if (itemsToDisplay.size() == MAX_NUM_ITEMS_IN_PREVIEW) {
-                break;
-            }
-        }
-        return itemsToDisplay;
+    public List<WorkspaceItemInfo> getPreviewItemsOnPage(int page) {
+        return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.contents);
     }
 
     @Override
@@ -595,11 +575,14 @@
     private void updatePreviewItems(boolean animate) {
         mPreviewItemManager.updatePreviewItems(animate);
         mCurrentPreviewItems.clear();
-        mCurrentPreviewItems.addAll(getPreviewItems());
+        mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
     }
 
-    @Override
-    public void prepareAutoUpdate() {
+    /**
+     * Updates the preview items which match the provided condition
+     */
+    public void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
+        mPreviewItemManager.updatePreviewItems(itemCheck);
     }
 
     @Override
@@ -622,7 +605,6 @@
         requestLayout();
     }
 
-    @Override
     public void onTitleChanged(CharSequence title) {
         mFolderName.setText(title);
         setContentDescription(getContext().getString(R.string.folder_name_format, title));
diff --git a/src/com/android/launcher3/folder/FolderIconPreviewVerifier.java b/src/com/android/launcher3/folder/FolderIconPreviewVerifier.java
deleted file mode 100644
index 4c84e35..0000000
--- a/src/com/android/launcher3/folder/FolderIconPreviewVerifier.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2017 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.folder;
-
-import android.util.Log;
-
-import com.android.launcher3.FolderInfo;
-import com.android.launcher3.InvariantDeviceProfile;
-
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
-
-/**
- * Verifies whether an item in a Folder is displayed in the FolderIcon preview.
- */
-public class FolderIconPreviewVerifier {
-
-    private static final String TAG = "FolderPreviewVerifier";
-
-    private final int mMaxGridCountX;
-    private final int mMaxGridCountY;
-    private final int mMaxItemsPerPage;
-    private final int[] mGridSize = new int[] { 1, 1 };
-
-    private int mNumItemsInFolder;
-    private int mGridCountX;
-    private boolean mDisplayingUpperLeftQuadrant = false;
-
-    /**
-     * Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work.
-     */
-    public FolderIconPreviewVerifier(InvariantDeviceProfile profile) {
-        mMaxGridCountX = profile.numFolderColumns;
-        mMaxGridCountY = profile.numFolderRows;
-        mMaxItemsPerPage = mMaxGridCountX * mMaxGridCountY;
-    }
-
-    public void setFolderInfo(FolderInfo info) {
-        int numItemsInFolder = info.contents.size();
-        if (numItemsInFolder != mNumItemsInFolder) {
-            FolderPagedView.calculateGridSize(numItemsInFolder, 0, 0, mMaxGridCountX,
-                    mMaxGridCountY, mMaxItemsPerPage, mGridSize);
-            mGridCountX = mGridSize[0];
-
-            mDisplayingUpperLeftQuadrant = numItemsInFolder > MAX_NUM_ITEMS_IN_PREVIEW;
-            mNumItemsInFolder = numItemsInFolder;
-        }
-    }
-
-    /**
-     * Returns whether the item with {@param rank} is in the default Folder icon preview.
-     */
-    public boolean isItemInPreview(int rank) {
-        return isItemInPreview(0, rank);
-    }
-
-    /**
-     * @param page The page the item is on.
-     * @param rank The rank of the item.
-     * @return True iff the icon is in the 2x2 upper left quadrant of the Folder.
-     */
-    public boolean isItemInPreview(int page, int rank) {
-        if (mGridSize[0] == 1) {
-            Log.w(TAG, "setFolderInfo not called before checking if item is in preview.");
-        }
-
-        // First page items are laid out such that the first 4 items are always in the upper
-        // left quadrant. For all other pages, we need to check the row and col.
-        if (page > 0 || mDisplayingUpperLeftQuadrant) {
-            int col = rank % mGridCountX;
-            int row = rank / mGridCountX;
-            return col < 2 && row < 2;
-        }
-        return rank < MAX_NUM_ITEMS_IN_PREVIEW;
-    }
-}
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
new file mode 100644
index 0000000..0a1221e
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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.folder;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.WorkspaceItemInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Locates provider for the folder name.
+ */
+public class FolderNameProvider {
+
+    /**
+     * Returns suggested folder name.
+     */
+    public CharSequence getSuggestedFolderName(Context context,
+            ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] suggestName) {
+        // Currently only run the algorithm on initial folder creation.
+        // For more than 2 items in the folder, the ranking algorithm for finding
+        // candidate folder name should be rewritten.
+        if (workspaceItemInfos.size() == 2) {
+            ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent();
+            ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent();
+
+            String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName();
+            String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName();
+            // If the two icons are from the same package,
+            // then assign the main icon's name
+            if (pkgName0.equals(pkgName1)) {
+                WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0);
+                WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1);
+                if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                    suggestName[0] = wInfo0.title;
+                } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                    suggestName[0] = wInfo1.title;
+                }
+                return suggestName[0];
+                // two icons are all shortcuts. Don't assign title
+            }
+        }
+        return suggestName[0];
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 57105e7..3b5fd59 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -24,10 +24,10 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewDebug;
 
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
@@ -38,18 +38,22 @@
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.ViewCache;
 
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.function.ToIntFunction;
+import java.util.stream.Collectors;
 
 public class FolderPagedView extends PagedView<PageIndicatorDots> {
 
@@ -68,17 +72,12 @@
 
     public final boolean mIsRtl;
 
-    private final LayoutInflater mInflater;
     private final ViewGroupFocusHelper mFocusIndicatorHelper;
 
     @Thunk final ArrayMap<View, Runnable> mPendingAnimations = new ArrayMap<>();
 
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private final int mMaxCountX;
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private final int mMaxCountY;
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private final int mMaxItemsPerPage;
+    private final FolderGridOrganizer mOrganizer;
+    private final ViewCache mViewCache;
 
     private int mAllocatedContentSize;
     @ViewDebug.ExportedProperty(category = "launcher")
@@ -88,20 +87,20 @@
 
     private Folder mFolder;
 
+    // If the views are attached to the folder or not. A folder should be bound when its
+    // animating or is open.
+    private boolean mViewsBound = false;
+
     public FolderPagedView(Context context, AttributeSet attrs) {
         super(context, attrs);
         InvariantDeviceProfile profile = LauncherAppState.getIDP(context);
-        mMaxCountX = profile.numFolderColumns;
-        mMaxCountY = profile.numFolderRows;
-
-        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
-
-        mInflater = LayoutInflater.from(context);
+        mOrganizer = new FolderGridOrganizer(profile);
 
         mIsRtl = Utilities.isRtl(getResources());
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
 
         mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
+        mViewCache = BaseActivity.fromContext(context).getViewCache();
     }
 
     public void setFolder(Folder folder) {
@@ -111,57 +110,13 @@
     }
 
     /**
-     * Calculates the grid size such that {@param count} items can fit in the grid.
-     * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
-     * maintaining the restrictions of {@link #mMaxCountX} &amp; {@link #mMaxCountY}.
-     */
-    public static void calculateGridSize(int count, int countX, int countY, int maxCountX,
-            int maxCountY, int maxItemsPerPage, int[] out) {
-        boolean done;
-        int gridCountX = countX;
-        int gridCountY = countY;
-
-        if (count >= maxItemsPerPage) {
-            gridCountX = maxCountX;
-            gridCountY = maxCountY;
-            done = true;
-        } else {
-            done = false;
-        }
-
-        while (!done) {
-            int oldCountX = gridCountX;
-            int oldCountY = gridCountY;
-            if (gridCountX * gridCountY < count) {
-                // Current grid is too small, expand it
-                if ((gridCountX <= gridCountY || gridCountY == maxCountY)
-                        && gridCountX < maxCountX) {
-                    gridCountX++;
-                } else if (gridCountY < maxCountY) {
-                    gridCountY++;
-                }
-                if (gridCountY == 0) gridCountY++;
-            } else if ((gridCountY - 1) * gridCountX >= count && gridCountY >= gridCountX) {
-                gridCountY = Math.max(0, gridCountY - 1);
-            } else if ((gridCountX - 1) * gridCountY >= count) {
-                gridCountX = Math.max(0, gridCountX - 1);
-            }
-            done = gridCountX == oldCountX && gridCountY == oldCountY;
-        }
-
-        out[0] = gridCountX;
-        out[1] = gridCountY;
-    }
-
-    /**
      * Sets up the grid size such that {@param count} items can fit in the grid.
      */
-    public void setupContentDimensions(int count) {
+    private void setupContentDimensions(int count) {
         mAllocatedContentSize = count;
-        calculateGridSize(count, mGridCountX, mGridCountY, mMaxCountX, mMaxCountY, mMaxItemsPerPage,
-                sTmpArray);
-        mGridCountX = sTmpArray[0];
-        mGridCountY = sTmpArray[1];
+        mOrganizer.setContentSize(count);
+        mGridCountX = mOrganizer.getCountX();
+        mGridCountY = mOrganizer.getCountY();
 
         // Update grid size
         for (int i = getPageCount() - 1; i >= 0; i--) {
@@ -178,35 +133,50 @@
     /**
      * Binds items to the layout.
      */
-    public void bindItems(ArrayList<WorkspaceItemInfo> items) {
-        ArrayList<View> icons = new ArrayList<>();
-        for (WorkspaceItemInfo item : items) {
-            icons.add(createNewView(item));
+    public void bindItems(List<WorkspaceItemInfo> items) {
+        if (mViewsBound) {
+            unbindItems();
         }
-        arrangeChildren(icons, icons.size(), false);
-    }
-
-    public void allocateSpaceForRank(int rank) {
-        ArrayList<View> views = new ArrayList<>(mFolder.getItemsInReadingOrder());
-        views.add(rank, null);
-        arrangeChildren(views, views.size(), false);
+        arrangeChildren(items.stream().map(this::createNewView).collect(Collectors.toList()));
+        mViewsBound = true;
     }
 
     /**
-     * Create space for a new item at the end, and returns the rank for that item.
-     * Also sets the current page to the last page.
+     * Removes all the icons from the folder
      */
-    public int allocateRankForNewItem() {
-        int rank = getItemCount();
-        allocateSpaceForRank(rank);
-        setCurrentPage(rank / mMaxItemsPerPage);
-        return rank;
+    public void unbindItems() {
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            CellLayout page = (CellLayout) getChildAt(i);
+            ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets();
+            for (int j = container.getChildCount() - 1; j >= 0; j--) {
+                mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j));
+            }
+            page.removeAllViews();
+            mViewCache.recycleView(R.layout.folder_page, page);
+        }
+        removeAllViews();
+        mViewsBound = false;
     }
 
+    /**
+     * Returns true if the icons are bound to the folder
+     */
+    public boolean areViewsBound() {
+        return mViewsBound;
+    }
+
+    /**
+     * Creates and adds an icon corresponding to the provided rank
+     * @return the created icon
+     */
     public View createAndAddViewForRank(WorkspaceItemInfo item, int rank) {
         View icon = createNewView(item);
-        allocateSpaceForRank(rank);
-        addViewForRank(icon, item, rank);
+        if (!mViewsBound) {
+            return icon;
+        }
+        ArrayList<View> views = new ArrayList<>(mFolder.getIconsInReadingOrder());
+        views.add(rank, icon);
+        arrangeChildren(views);
         return icon;
     }
 
@@ -215,31 +185,33 @@
      * related attributes. It assumes that {@param item} is already attached to the view.
      */
     public void addViewForRank(View view, WorkspaceItemInfo item, int rank) {
-        int pagePos = rank % mMaxItemsPerPage;
-        int pageNo = rank / mMaxItemsPerPage;
-
-        item.rank = rank;
-        item.cellX = pagePos % mGridCountX;
-        item.cellY = pagePos / mGridCountX;
+        int pageNo = rank / mOrganizer.getMaxItemsPerPage();
 
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
-        lp.cellX = item.cellX;
-        lp.cellY = item.cellY;
+        lp.setXY(mOrganizer.getPosForRank(rank));
         getPageAt(pageNo).addViewToCellLayout(view, -1, item.getViewId(), lp, true);
     }
 
     @SuppressLint("InflateParams")
     public View createNewView(WorkspaceItemInfo item) {
-        final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
-                R.layout.folder_application, null, false);
+        if (item == null) {
+            return null;
+        }
+        final BubbleTextView textView = mViewCache.getView(
+                R.layout.folder_application, getContext(), null);
         textView.applyFromWorkspaceItem(item);
-        textView.setHapticFeedbackEnabled(false);
         textView.setOnClickListener(ItemClickHandler.INSTANCE);
         textView.setOnLongClickListener(mFolder);
         textView.setOnFocusChangeListener(mFocusIndicatorHelper);
-
-        textView.setLayoutParams(new CellLayout.LayoutParams(
-                item.cellX, item.cellY, item.spanX, item.spanY));
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) textView.getLayoutParams();
+        if (lp == null) {
+            textView.setLayoutParams(new CellLayout.LayoutParams(
+                    item.cellX, item.cellY, item.spanX, item.spanY));
+        } else {
+            lp.cellX = item.cellX;
+            lp.cellY = item.cellY;
+            lp.cellHSpan = lp.cellVSpan = 1;
+        }
         return textView;
     }
 
@@ -254,7 +226,7 @@
 
     private CellLayout createAndAddNewPage() {
         DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
-        CellLayout page = (CellLayout) mInflater.inflate(R.layout.folder_page, this, false);
+        CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this);
         page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
         page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
         page.setInvertIfRtl(true);
@@ -295,37 +267,28 @@
      * page.
      *
      * @param list the ordered list of children.
-     * @param itemCount if greater than the total children count, empty spaces are left
-     * at the end, otherwise it is ignored.
-     *
      */
-    public void arrangeChildren(ArrayList<View> list, int itemCount) {
-        arrangeChildren(list, itemCount, true);
-    }
-
     @SuppressLint("RtlHardcoded")
-    private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
+    public void arrangeChildren(List<View> list) {
+        int itemCount = list.size();
         ArrayList<CellLayout> pages = new ArrayList<>();
         for (int i = 0; i < getChildCount(); i++) {
             CellLayout page = (CellLayout) getChildAt(i);
             page.removeAllViews();
             pages.add(page);
         }
+        mOrganizer.setFolderInfo(mFolder.getInfo());
         setupContentDimensions(itemCount);
 
         Iterator<CellLayout> pageItr = pages.iterator();
         CellLayout currentPage = null;
 
         int position = 0;
-        int newX, newY, rank;
+        int rank = 0;
 
-        FolderIconPreviewVerifier verifier = new FolderIconPreviewVerifier(
-                Launcher.getLauncher(getContext()).getDeviceProfile().inv);
-        verifier.setFolderInfo(mFolder.getInfo());
-        rank = 0;
         for (int i = 0; i < itemCount; i++) {
             View v = list.size() > i ? list.get(i) : null;
-            if (currentPage == null || position >= mMaxItemsPerPage) {
+            if (currentPage == null || position >= mOrganizer.getMaxItemsPerPage()) {
                 // Next page
                 if (pageItr.hasNext()) {
                     currentPage = pageItr.next();
@@ -337,28 +300,16 @@
 
             if (v != null) {
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
-                newX = position % mGridCountX;
-                newY = position / mGridCountX;
                 ItemInfo info = (ItemInfo) v.getTag();
-                if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
-                    info.cellX = newX;
-                    info.cellY = newY;
-                    info.rank = rank;
-                    if (saveChanges) {
-                        mFolder.mLauncher.getModelWriter().addOrMoveItemInDatabase(info,
-                                mFolder.mInfo.id, 0, info.cellX, info.cellY);
-                    }
-                }
-                lp.cellX = info.cellX;
-                lp.cellY = info.cellY;
+                lp.setXY(mOrganizer.getPosForRank(rank));
                 currentPage.addViewToCellLayout(v, -1, info.getViewId(), lp, true);
 
-                if (verifier.isItemInPreview(rank) && v instanceof BubbleTextView) {
+                if (mOrganizer.isItemInPreview(rank) && v instanceof BubbleTextView) {
                     ((BubbleTextView) v).verifyHighRes();
                 }
             }
 
-            rank ++;
+            rank++;
             position++;
         }
 
@@ -391,16 +342,6 @@
                 (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0;
     }
 
-    public int getItemCount() {
-        int lastPageIndex = getChildCount() - 1;
-        if (lastPageIndex < 0) {
-            // If there are no pages, nothing has yet been added to the folder.
-            return 0;
-        }
-        return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount()
-                + lastPageIndex * mMaxItemsPerPage;
-    }
-
     /**
      * @return the rank of the cell nearest to the provided pixel position.
      */
@@ -412,31 +353,28 @@
             sTmpArray[0] = page.getCountX() - sTmpArray[0] - 1;
         }
         return Math.min(mAllocatedContentSize - 1,
-                pageIndex * mMaxItemsPerPage + sTmpArray[1] * mGridCountX + sTmpArray[0]);
+                pageIndex * mOrganizer.getMaxItemsPerPage()
+                        + sTmpArray[1] * mGridCountX + sTmpArray[0]);
     }
 
     public View getFirstItem() {
-        if (getChildCount() < 1) {
-            return null;
-        }
-        ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets();
-        if (mGridCountX > 0) {
-            return currContainer.getChildAt(0, 0);
-        } else {
-            return currContainer.getChildAt(0);
-        }
+        return getViewInCurrentPage(c -> 0);
     }
 
     public View getLastItem() {
+        return getViewInCurrentPage(c -> c.getChildCount() - 1);
+    }
+
+    private View getViewInCurrentPage(ToIntFunction<ShortcutAndWidgetContainer> rankProvider) {
         if (getChildCount() < 1) {
             return null;
         }
-        ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets();
-        int lastRank = currContainer.getChildCount() - 1;
+        ShortcutAndWidgetContainer container = getCurrentCellLayout().getShortcutsAndWidgets();
+        int rank = rankProvider.applyAsInt(container);
         if (mGridCountX > 0) {
-            return currContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX);
+            return container.getChildAt(rank % mGridCountX, rank / mGridCountX);
         } else {
-            return currContainer.getChildAt(lastRank);
+            return container.getChildAt(rank);
         }
     }
 
@@ -517,7 +455,7 @@
     }
 
     public boolean rankOnCurrentPage(int rank) {
-        int p = rank / mMaxItemsPerPage;
+        int p = rank / mOrganizer.getMaxItemsPerPage();
         return p == getNextPage();
     }
 
@@ -563,15 +501,16 @@
 
         // Animation only happens on the current page.
         int pageToAnimate = getNextPage();
+        int maxItemsPerPage = mOrganizer.getMaxItemsPerPage();
 
-        int pageT = target / mMaxItemsPerPage;
-        int pagePosT = target % mMaxItemsPerPage;
+        int pageT = target / maxItemsPerPage;
+        int pagePosT = target % maxItemsPerPage;
 
         if (pageT != pageToAnimate) {
             Log.e(TAG, "Cannot animate when the target cell is invisible");
         }
-        int pagePosE = empty % mMaxItemsPerPage;
-        int pageE = empty / mMaxItemsPerPage;
+        int pagePosE = empty % maxItemsPerPage;
+        int pageE = empty / maxItemsPerPage;
 
         int startPos, endPos;
         int moveStart, moveEnd;
@@ -588,7 +527,7 @@
             if (pageE < pageToAnimate) {
                 moveStart = empty;
                 // Instantly move the first item in the current page.
-                moveEnd = pageToAnimate * mMaxItemsPerPage;
+                moveEnd = pageToAnimate * maxItemsPerPage;
                 // Animate the 2nd item in the current page, as the first item was already moved to
                 // the last page.
                 startPos = 0;
@@ -606,10 +545,10 @@
                 // Move the items immediately.
                 moveStart = empty;
                 // Instantly move the last item in the current page.
-                moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1;
+                moveEnd = (pageToAnimate + 1) * maxItemsPerPage - 1;
 
                 // Animations start with the second last item in the page
-                startPos = mMaxItemsPerPage - 1;
+                startPos = maxItemsPerPage - 1;
             } else {
                 moveStart = moveEnd = -1;
                 startPos = pagePosE;
@@ -621,8 +560,8 @@
         // Instant moving views.
         while (moveStart != moveEnd) {
             int rankToMove = moveStart + direction;
-            int p = rankToMove / mMaxItemsPerPage;
-            int pagePos = rankToMove % mMaxItemsPerPage;
+            int p = rankToMove / maxItemsPerPage;
+            int pagePos = rankToMove % maxItemsPerPage;
             int x = pagePos % mGridCountX;
             int y = pagePos / mGridCountX;
 
@@ -667,9 +606,6 @@
         for (int i = startPos; i != endPos; i += direction) {
             int nextPos = i + direction;
             View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
-            if (v != null) {
-                ((ItemInfo) v.getTag()).rank -= direction;
-            }
             if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
                     REORDER_ANIMATION_DURATION, delay, true, true)) {
                 delay += delayAmount;
@@ -679,6 +615,6 @@
     }
 
     public int itemsPerPage() {
-        return mMaxItemsPerPage;
+        return mOrganizer.getMaxItemsPerPage();
     }
 }
diff --git a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
index c818462..caf6e55 100644
--- a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
+++ b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
@@ -17,6 +17,8 @@
 
 import android.graphics.drawable.Drawable;
 
+import com.android.launcher3.WorkspaceItemInfo;
+
 /**
  * Manages the parameters used to draw a Folder preview item.
  */
@@ -25,9 +27,10 @@
     float transY;
     float scale;
     float overlayAlpha;
-    FolderPreviewItemAnim anim;
+    public FolderPreviewItemAnim anim;
     public boolean hidden;
-    Drawable drawable;
+    public Drawable drawable;
+    public WorkspaceItemInfo item;
 
     PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
         this.transX = transX;
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 49763ba..2d817e6 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -23,28 +23,51 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.util.FloatProperty;
 import android.view.View;
 import android.widget.TextView;
 
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.WorkspaceItemInfo;
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.graphics.DrawableFactory;
+import com.android.launcher3.graphics.PreloadIconDrawable;
 
 import java.util.ArrayList;
 import java.util.List;
-
-import androidx.annotation.NonNull;
+import java.util.function.Predicate;
 
 /**
  * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}.
  */
 public class PreviewItemManager {
 
-    private FolderIcon mIcon;
+    private static final FloatProperty<PreviewItemManager> CURRENT_PAGE_ITEMS_TRANS_X =
+            new FloatProperty<PreviewItemManager>("currentPageItemsTransX") {
+                @Override
+                public void setValue(PreviewItemManager manager, float v) {
+                    manager.mCurrentPageItemsTransX = v;
+                    manager.onParamsChanged();
+                }
+
+                @Override
+                public Float get(PreviewItemManager manager) {
+                    return manager.mCurrentPageItemsTransX;
+                }
+            };
+
+    private final Context mContext;
+    private final FolderIcon mIcon;
+    private final DrawableFactory mDrawableFactory;
+    private final int mIconSize;
 
     // These variables are all associated with the drawing of the preview; they are stored
     // as member variables for shared usage and to avoid computation on each frame
@@ -69,7 +92,10 @@
     private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200;
 
     public PreviewItemManager(FolderIcon icon) {
+        mContext = icon.getContext();
         mIcon = icon;
+        mDrawableFactory = DrawableFactory.INSTANCE.get(mContext);
+        mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
     }
 
     /**
@@ -200,7 +226,7 @@
     }
 
     void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
-        List<BubbleTextView> items = mIcon.getPreviewItemsOnPage(page);
+        List<WorkspaceItemInfo> items = mIcon.getPreviewItemsOnPage(page);
         int prevNumItems = params.size();
 
         // We adjust the size of the list to match the number of items in the preview.
@@ -214,13 +240,7 @@
         int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
         for (int i = 0; i < params.size(); i++) {
             PreviewItemDrawingParams p = params.get(i);
-            p.drawable = items.get(i).getCompoundDrawables()[1];
-
-            if (p.drawable != null && !mIcon.mFolder.isOpen()) {
-                // Set the callback to FolderIcon as it is responsible to drawing the icon. The
-                // callback will be released when the folder is opened.
-                p.drawable.setCallback(mIcon);
-            }
+            setDrawable(p, items.get(i));
 
             if (!animate) {
                 computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
@@ -253,14 +273,8 @@
             buildParamsForPage(currentPage, mCurrentPageParams, false);
             onParamsChanged();
 
-            ValueAnimator slideAnimator = ValueAnimator.ofFloat(0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
-            slideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-                @Override
-                public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                    mCurrentPageItemsTransX = (float) valueAnimator.getAnimatedValue();
-                    onParamsChanged();
-                }
-            });
+            ValueAnimator slideAnimator = ObjectAnimator
+                    .ofFloat(this, CURRENT_PAGE_ITEMS_TRANS_X, 0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
             slideAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -277,6 +291,25 @@
         buildParamsForPage(0, mFirstPageParams, animate);
     }
 
+    void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
+        boolean modified = false;
+        for (PreviewItemDrawingParams param : mFirstPageParams) {
+            if (itemCheck.test(param.item)) {
+                setDrawable(param, param.item);
+                modified = true;
+            }
+        }
+        for (PreviewItemDrawingParams param : mCurrentPageParams) {
+            if (itemCheck.test(param.item)) {
+                setDrawable(param, param.item);
+                modified = true;
+            }
+        }
+        if (modified) {
+            mIcon.invalidate();
+        }
+    }
+
     boolean verifyDrawable(@NonNull Drawable who) {
         for (int i = 0; i < mFirstPageParams.size(); i++) {
             if (mFirstPageParams.get(i).drawable == who) {
@@ -296,46 +329,46 @@
      *  - Moving into a new position
      *  - Moving out of the preview
      *
-     * @param oldParams The list of items in the old preview.
-     * @param newParams The list of items in the new preview.
+     * @param oldItems The list of items in the old preview.
+     * @param newItems The list of items in the new preview.
      * @param dropped The item that was dropped onto the FolderIcon.
      */
-    public void onDrop(List<BubbleTextView> oldParams, List<BubbleTextView> newParams,
+    public void onDrop(List<WorkspaceItemInfo> oldItems, List<WorkspaceItemInfo> newItems,
             WorkspaceItemInfo dropped) {
-        int numItems = newParams.size();
+        int numItems = newItems.size();
         final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams;
         buildParamsForPage(0, params, false);
 
         // New preview items for items that are moving in (except for the dropped item).
-        List<BubbleTextView> moveIn = new ArrayList<>();
-        for (BubbleTextView btv : newParams) {
-            if (!oldParams.contains(btv) && !btv.getTag().equals(dropped)) {
-                moveIn.add(btv);
+        List<WorkspaceItemInfo> moveIn = new ArrayList<>();
+        for (WorkspaceItemInfo newItem : newItems) {
+            if (!oldItems.contains(newItem) && !newItem.equals(dropped)) {
+                moveIn.add(newItem);
             }
         }
         for (int i = 0; i < moveIn.size(); ++i) {
-            int prevIndex = newParams.indexOf(moveIn.get(i));
+            int prevIndex = newItems.indexOf(moveIn.get(i));
             PreviewItemDrawingParams p = params.get(prevIndex);
             computePreviewItemDrawingParams(prevIndex, numItems, p);
-            updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newParams.indexOf(moveIn.get(i)),
+            updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newItems.indexOf(moveIn.get(i)),
                     numItems);
         }
 
         // Items that are moving into new positions within the preview.
-        for (int newIndex = 0; newIndex < newParams.size(); ++newIndex) {
-            int oldIndex = oldParams.indexOf(newParams.get(newIndex));
+        for (int newIndex = 0; newIndex < newItems.size(); ++newIndex) {
+            int oldIndex = oldItems.indexOf(newItems.get(newIndex));
             if (oldIndex >= 0 && newIndex != oldIndex) {
                 PreviewItemDrawingParams p = params.get(newIndex);
-                updateTransitionParam(p, newParams.get(newIndex), oldIndex, newIndex, numItems);
+                updateTransitionParam(p, newItems.get(newIndex), oldIndex, newIndex, numItems);
             }
         }
 
         // Old preview items that need to be moved out.
-        List<BubbleTextView> moveOut = new ArrayList<>(oldParams);
-        moveOut.removeAll(newParams);
+        List<WorkspaceItemInfo> moveOut = new ArrayList<>(oldItems);
+        moveOut.removeAll(newItems);
         for (int i = 0; i < moveOut.size(); ++i) {
-            BubbleTextView item = moveOut.get(i);
-            int oldIndex = oldParams.indexOf(item);
+            WorkspaceItemInfo item = moveOut.get(i);
+            int oldIndex = oldItems.indexOf(item);
             PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null);
             updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems);
             params.add(0, p); // We want these items first so that they are on drawn last.
@@ -348,14 +381,9 @@
         }
     }
 
-    private void updateTransitionParam(final PreviewItemDrawingParams p, BubbleTextView btv,
+    private void updateTransitionParam(final PreviewItemDrawingParams p, WorkspaceItemInfo item,
             int prevIndex, int newIndex, int numItems) {
-        p.drawable = btv.getCompoundDrawables()[1];
-        if (!mIcon.mFolder.isOpen()) {
-            // Set the callback to FolderIcon as it is responsible to drawing the icon. The
-            // callback will be released when the folder is opened.
-            p.drawable.setCallback(mIcon);
-        }
+        setDrawable(p, item);
 
         FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, numItems,
                 newIndex, numItems, DROP_IN_ANIMATION_DURATION, null);
@@ -364,4 +392,20 @@
         }
         p.anim = anim;
     }
+
+    private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
+        if (item.hasPromiseIconUi()) {
+            PreloadIconDrawable drawable = mDrawableFactory.newPendingIcon(mContext, item);
+            drawable.setLevel(item.getInstallProgress());
+            p.drawable = drawable;
+        } else {
+            p.drawable = mDrawableFactory.newIcon(mContext, item);
+        }
+        p.drawable.setBounds(0, 0, mIconSize, mIconSize);
+        p.item = item;
+
+        // Set the callback to FolderIcon as it is responsible to drawing the icon. The
+        // callback will be released when the folder is opened.
+        p.drawable.setCallback(mIcon);
+    }
 }
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index 9263a2a..f579451 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.graphics;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BlurMaskFilter;
@@ -25,7 +27,6 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.os.Handler;
 import android.view.View;
 
 import com.android.launcher3.BubbleTextView;
@@ -35,7 +36,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BitmapRenderer;
-import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 
@@ -157,7 +157,7 @@
         }
 
         mOutlineGeneratorCallback = new OutlineGeneratorCallback(preview);
-        new Handler(UiThreadHelper.getBackgroundLooper()).post(mOutlineGeneratorCallback);
+        UI_HELPER_EXECUTOR.post(mOutlineGeneratorCallback);
     }
 
     protected static Rect getDrawableBounds(Drawable d) {
@@ -195,15 +195,22 @@
 
         private final Bitmap mPreviewSnapshot;
         private final Context mContext;
+        private final boolean mIsIcon;
 
         OutlineGeneratorCallback(Bitmap preview) {
             mPreviewSnapshot = preview;
             mContext = mView.getContext();
+            mIsIcon = mView instanceof BubbleTextView;
         }
 
         @Override
         public void run() {
             Bitmap preview = convertPreviewToAlphaBitmap(mPreviewSnapshot);
+            if (mIsIcon) {
+                int size = Launcher.getLauncher(mContext).getDeviceProfile().iconSizePx;
+                preview = Bitmap.createScaledBitmap(preview, size, size, false);
+            }
+            //else case covers AppWidgetHost (doesn't drag/drop across different device profiles)
 
             // We start by removing most of the alpha channel so as to ignore shadows, and
             // other types of partial transparency when defining the shape of the object
diff --git a/src/com/android/launcher3/graphics/DrawableFactory.java b/src/com/android/launcher3/graphics/DrawableFactory.java
index 288749f..837301f 100644
--- a/src/com/android/launcher3/graphics/DrawableFactory.java
+++ b/src/com/android/launcher3/graphics/DrawableFactory.java
@@ -80,26 +80,25 @@
      * Returns a drawable that can be used as a badge for the user or null.
      */
     @UiThread
-    public Drawable getBadgeForUser(UserHandle user, Context context) {
+    public Drawable getBadgeForUser(UserHandle user, Context context, int badgeSize) {
         if (mMyUser.equals(user)) {
             return null;
         }
 
-        Bitmap badgeBitmap = getUserBadge(user, context);
+        Bitmap badgeBitmap = getUserBadge(user, context, badgeSize);
         FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
         d.setFilterBitmap(true);
         d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
         return d;
     }
 
-    protected synchronized Bitmap getUserBadge(UserHandle user, Context context) {
+    protected synchronized Bitmap getUserBadge(UserHandle user, Context context, int badgeSize) {
         Bitmap badgeBitmap = mUserBadges.get(user);
         if (badgeBitmap != null) {
             return badgeBitmap;
         }
 
         final Resources res = context.getApplicationContext().getResources();
-        int badgeSize = res.getDimensionPixelSize(R.dimen.profile_badge_size);
         badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
 
         Drawable drawable = context.getPackageManager().getUserBadgedDrawableForDensity(
diff --git a/src/com/android/launcher3/graphics/GridOptionsProvider.java b/src/com/android/launcher3/graphics/GridOptionsProvider.java
index efd39ee..71b4366 100644
--- a/src/com/android/launcher3/graphics/GridOptionsProvider.java
+++ b/src/com/android/launcher3/graphics/GridOptionsProvider.java
@@ -1,5 +1,7 @@
 package com.android.launcher3.graphics;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.res.XmlResourceParser;
@@ -17,8 +19,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
 import com.android.launcher3.R;
-import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.UiThreadHelper;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -180,10 +180,10 @@
             throw new FileNotFoundException(e.getMessage());
         }
 
-        LooperExecutor executor = new LooperExecutor(UiThreadHelper.getBackgroundLooper());
         try {
             return openPipeHelper(uri, MIME_TYPE_PNG, null,
-                    executor.submit(new LauncherPreviewRenderer(getContext(), idp)), BITMAP_WRITER);
+                    UI_HELPER_EXECUTOR.submit(new LauncherPreviewRenderer(getContext(), idp)),
+                    BITMAP_WRITER);
         } catch (Exception e) {
             throw new FileNotFoundException(e.getMessage());
         }
diff --git a/src/com/android/launcher3/icons/ComponentWithLabel.java b/src/com/android/launcher3/icons/ComponentWithLabel.java
index 46b5002..832956d 100644
--- a/src/com/android/launcher3/icons/ComponentWithLabel.java
+++ b/src/com/android/launcher3/icons/ComponentWithLabel.java
@@ -34,9 +34,11 @@
     class ComponentCachingLogic implements CachingLogic<ComponentWithLabel> {
 
         private final PackageManager mPackageManager;
+        private final boolean mAddToMemCache;
 
-        public ComponentCachingLogic(Context context) {
+        public ComponentCachingLogic(Context context, boolean addToMemCache) {
             mPackageManager = context.getPackageManager();
+            mAddToMemCache = addToMemCache;
         }
 
         @Override
@@ -60,5 +62,10 @@
             // Do not load icon.
             target.icon = BitmapInfo.LOW_RES_ICON;
         }
+
+        @Override
+        public boolean addToMemCache() {
+            return mAddToMemCache;
+        }
     }
 }
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index abff237..9c46260 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.icons;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -36,8 +39,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherFiles;
-import com.android.launcher3.LauncherModel;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.compat.LauncherAppsCompat;
@@ -60,8 +61,6 @@
 
     private static final String TAG = "Launcher.IconCache";
 
-    private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
-
     private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic;
     private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic;
 
@@ -73,9 +72,9 @@
     private int mPendingIconRequestCount = 0;
 
     public IconCache(Context context, InvariantDeviceProfile inv) {
-        super(context, LauncherFiles.APP_ICONS_DB, LauncherModel.getWorkerLooper(),
+        super(context, LauncherFiles.APP_ICONS_DB, MODEL_EXECUTOR.getLooper(),
                 inv.fillResIconDpi, inv.iconBitmapSize, true /* inMemoryCache */);
-        mComponentWithLabelCachingLogic = new ComponentCachingLogic(context);
+        mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
         mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context);
         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
         mUserManager = UserManagerCompat.getInstance(mContext);
@@ -124,7 +123,7 @@
             final ItemInfoWithIcon info) {
         Preconditions.assertUIThread();
         if (mPendingIconRequestCount <= 0) {
-            LauncherModel.setWorkerPriority(Process.THREAD_PRIORITY_FOREGROUND);
+            MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
         }
         mPendingIconRequestCount ++;
 
@@ -136,7 +135,7 @@
                 } else if (info instanceof PackageItemInfo) {
                     getTitleAndIconForApp((PackageItemInfo) info, false);
                 }
-                mMainThreadExecutor.execute(() -> {
+                MAIN_EXECUTOR.execute(() -> {
                     caller.reapplyItemInfo(info);
                     onEnd();
                 });
@@ -149,7 +148,7 @@
     private void onIconRequestEnd() {
         mPendingIconRequestCount --;
         if (mPendingIconRequestCount <= 0) {
-            LauncherModel.setWorkerPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
         }
     }
 
@@ -195,7 +194,7 @@
     public synchronized String getTitleNoCache(ComponentWithLabel info) {
         CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info,
                 mComponentWithLabelCachingLogic, false /* usePackageIcon */,
-                true /* useLowResIcon */, false /* addToMemCache */);
+                true /* useLowResIcon */);
         return Utilities.trim(entry.title);
     }
 
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 7632408..adc92c4 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -24,6 +24,8 @@
 import android.graphics.drawable.Drawable;
 import android.os.Process;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -37,8 +39,6 @@
 
 import java.util.function.Supplier;
 
-import androidx.annotation.Nullable;
-
 /**
  * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
  * that are threadsafe.
diff --git a/src/com/android/launcher3/logging/EventLogArray.java b/src/com/android/launcher3/logging/EventLogArray.java
index f20f365..3ecfb23 100644
--- a/src/com/android/launcher3/logging/EventLogArray.java
+++ b/src/com/android/launcher3/logging/EventLogArray.java
@@ -16,11 +16,13 @@
 package com.android.launcher3.logging;
 
 
+import android.util.Log;
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.Locale;
+import java.util.Random;
 
 /**
  * A utility class to record and log events. Events are stored in a fixed size array and old logs
@@ -37,6 +39,7 @@
     private final String name;
     private final EventEntry[] logs;
     private int nextIndex;
+    private int mLogId;
 
     public EventLogArray(String name, int size) {
         this.name = name;
@@ -52,10 +55,6 @@
         addLog(TYPE_INTEGER, event, extras);
     }
 
-    public void addLog(String event, float extras) {
-        addLog(TYPE_FLOAT, event, extras);
-    }
-
     public void addLog(String event, boolean extras) {
         addLog(extras ? TYPE_BOOL_TRUE : TYPE_BOOL_FALSE, event, 0);
     }
@@ -65,7 +64,7 @@
         int last = (nextIndex + logs.length - 1) % logs.length;
         int secondLast = (nextIndex + logs.length - 2) % logs.length;
         if (isEntrySame(logs[last], type, event) && isEntrySame(logs[secondLast], type, event)) {
-            logs[last].update(type, event, extras);
+            logs[last].update(type, event, extras, mLogId);
             logs[secondLast].duplicateCount++;
             return;
         }
@@ -73,7 +72,7 @@
         if (logs[nextIndex] == null) {
             logs[nextIndex] = new EventEntry();
         }
-        logs[nextIndex].update(type, event, extras);
+        logs[nextIndex].update(type, event, extras, mLogId);
         nextIndex = (nextIndex + 1) % logs.length;
     }
 
@@ -113,10 +112,18 @@
             if (log.duplicateCount > 0) {
                 msg.append(" & ").append(log.duplicateCount).append(" similar events");
             }
+            msg.append(" traceId: ").append(log.traceId);
             writer.println(msg);
         }
     }
 
+    /** Returns a 3 digit random number between 100-999 */
+    public int generateAndSetLogId() {
+        Random r = new Random();
+        mLogId = r.nextInt(900) + 100;
+        return mLogId;
+    }
+
     private boolean isEntrySame(EventEntry entry, int type, String event) {
         return entry != null && entry.type == type && entry.event.equals(event);
     }
@@ -129,11 +136,13 @@
         private float extras;
         private long time;
         private int duplicateCount;
+        private int traceId;
 
-        public void update(int type, String event, float extras) {
+        public void update(int type, String event, float extras, int traceId) {
             this.type = type;
             this.event = event;
             this.extras = extras;
+            this.traceId = traceId;
             time = System.currentTimeMillis();
             duplicateCount = 0;
         }
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index f7f8ef1..923a89b 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -1,13 +1,14 @@
 package com.android.launcher3.logging;
 
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
 import android.util.Log;
 import android.util.Pair;
 
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.IOUtils;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -29,8 +30,7 @@
  */
 public final class FileLog {
 
-    protected static final boolean ENABLED =
-            FeatureFlags.IS_DOGFOOD_BUILD || Utilities.IS_DEBUG_DEVICE;
+    protected static final boolean ENABLED = true;
     private static final String FILE_NAME_PREFIX = "log-";
     private static final DateFormat DATE_FORMAT =
             DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
@@ -91,9 +91,8 @@
     private static Handler getHandler() {
         synchronized (DATE_FORMAT) {
             if (sHandler == null) {
-                HandlerThread thread = new HandlerThread("file-logger");
-                thread.start();
-                sHandler = new Handler(thread.getLooper(), new LogWriterCallback());
+                sHandler = new Handler(createAndStartNewLooper("file-logger"),
+                        new LogWriterCallback());
             }
         }
         return sHandler;
@@ -131,7 +130,7 @@
         private PrintWriter mCurrentWriter = null;
 
         private void closeWriter() {
-            Utilities.closeSilently(mCurrentWriter);
+            IOUtils.closeSilently(mCurrentWriter);
             mCurrentWriter = null;
         }
 
@@ -219,7 +218,7 @@
             } catch (Exception e) {
                 // ignore
             } finally {
-                Utilities.closeSilently(in);
+                IOUtils.closeSilently(in);
             }
         }
     }
diff --git a/src/com/android/launcher3/logging/LoggerUtils.java b/src/com/android/launcher3/logging/LoggerUtils.java
index 9b75b43..598792a 100644
--- a/src/com/android/launcher3/logging/LoggerUtils.java
+++ b/src/com/android/launcher3/logging/LoggerUtils.java
@@ -44,6 +44,7 @@
 public class LoggerUtils {
     private static final ArrayMap<Class, SparseArray<String>> sNameCache = new ArrayMap<>();
     private static final String UNKNOWN = "UNKNOWN";
+    private static final int DEFAULT_PREDICTED_RANK = -100;
 
     public static String getFieldName(int value, Class c) {
         SparseArray<String> cache;
@@ -90,7 +91,7 @@
     }
 
     public static String getTargetStr(Target t) {
-        if (t == null){
+        if (t == null) {
             return "";
         }
         String str = "";
@@ -137,17 +138,16 @@
         if (t.intentHash != 0) {
             typeStr += ", intentHash=" + t.intentHash;
         }
-        if ((t.packageNameHash != 0 || t.componentHash != 0 || t.intentHash != 0) &&
-                t.itemType != ItemType.TASK) {
+        if (t.itemType == ItemType.FOLDER_ICON) {
+            typeStr += ", grid(" + t.gridX + "," + t.gridY + ")";
+        } else if ((t.packageNameHash != 0 || t.componentHash != 0 || t.intentHash != 0)
+                && t.itemType != ItemType.TASK) {
             typeStr += ", predictiveRank=" + t.predictedRank + ", grid(" + t.gridX + "," + t.gridY
-                    + "), span(" + t.spanX + "," + t.spanY
-                    + "), pageIdx=" + t.pageIndex;
-
+                    + "), span(" + t.spanX + "," + t.spanY + "), pageIdx=" + t.pageIndex;
         }
         if (t.searchQueryLength != 0) {
             typeStr += ", searchQueryLength=" + t.searchQueryLength;
         }
-
         if (t.itemType == ItemType.TASK) {
             typeStr += ", pageIdx=" + t.pageIndex;
         }
@@ -168,17 +168,17 @@
 
     public static Target newItemTarget(ItemInfo info, InstantAppResolver instantAppResolver) {
         Target t = newTarget(Target.Type.ITEM);
-
         switch (info.itemType) {
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                 t.itemType = (instantAppResolver != null && info instanceof AppInfo
-                        && instantAppResolver.isInstantApp(((AppInfo) info)) )
+                        && instantAppResolver.isInstantApp(((AppInfo) info)))
                         ? ItemType.WEB_APP
                         : ItemType.APP_ICON;
-                t.predictedRank = -100; // Never assigned
+                t.predictedRank = DEFAULT_PREDICTED_RANK;
                 break;
             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                 t.itemType = ItemType.SHORTCUT;
+                t.predictedRank = DEFAULT_PREDICTED_RANK;
                 break;
             case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                 t.itemType = ItemType.FOLDER_ICON;
@@ -188,6 +188,7 @@
                 break;
             case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                 t.itemType = ItemType.DEEPSHORTCUT;
+                t.predictedRank = DEFAULT_PREDICTED_RANK;
                 break;
         }
         return t;
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index c72b07a..99906fe 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -26,6 +26,8 @@
 import static com.android.launcher3.logging.LoggerUtils.newTarget;
 import static com.android.launcher3.logging.LoggerUtils.newTouchAction;
 
+import static java.util.Optional.ofNullable;
+
 import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Context;
@@ -35,6 +37,7 @@
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DropTarget;
@@ -58,7 +61,7 @@
 /**
  * Manages the creation of {@link LauncherEvent}.
  * To debug this class, execute following command before side loading a new apk.
- *
+ * <p>
  * $ adb shell setprop log.tag.UserEvent VERBOSE
  */
 public class UserEventDispatcher implements ResourceBasedOverride {
@@ -94,19 +97,26 @@
 
     /**
      * Fills in the container data on the given event if the given view is not null.
+     *
      * @return whether container data was added.
      */
-    public static boolean fillInLogContainerData(LauncherLogProto.LauncherEvent event, @Nullable View v) {
+    public boolean fillInLogContainerData(LauncherLogProto.LauncherEvent event, @Nullable View v) {
         // Fill in grid(x,y), pageIndex of the child and container type of the parent
         LogContainerProvider provider = StatsLogUtils.getLaunchProviderRecursive(v);
         if (v == null || !(v.getTag() instanceof ItemInfo) || provider == null) {
             return false;
         }
-        ItemInfo itemInfo = (ItemInfo) v.getTag();
-        provider.fillInLogContainerData(v, itemInfo, event.srcTarget[0], event.srcTarget[1]);
+        final ItemInfo itemInfo = (ItemInfo) v.getTag();
+        final Target target = event.srcTarget[0];
+        final Target targetParent = event.srcTarget[1];
+        onFillInLogContainerData(itemInfo, target, targetParent);
+        provider.fillInLogContainerData(v, itemInfo, target, targetParent);
         return true;
     }
 
+    protected void onFillInLogContainerData(
+            @NonNull ItemInfo itemInfo, @NonNull Target target, @NonNull Target targetParent) { }
+
     private boolean mSessionStarted;
     private long mElapsedContainerMillis;
     private long mElapsedSessionMillis;
@@ -139,7 +149,11 @@
         mAppOrTaskLaunch = true;
     }
 
-    public void logActionTip(int actionType, int viewType) { }
+    /**
+     * Dummy method.
+     */
+    public void logActionTip(int actionType, int viewType) {
+    }
 
     @Deprecated
     public void logTaskLaunchOrDismiss(int action, int direction, int taskIndex,
@@ -184,15 +198,15 @@
 
     public void logActionCommand(int command, int srcContainerType, int dstContainerType) {
         logActionCommand(command, newContainerTarget(srcContainerType),
-                dstContainerType >=0 ? newContainerTarget(dstContainerType) : null);
+                dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null);
     }
 
     public void logActionCommand(int command, int srcContainerType, int dstContainerType,
-                                 int pageIndex) {
+            int pageIndex) {
         Target srcTarget = newContainerTarget(srcContainerType);
         srcTarget.pageIndex = pageIndex;
         logActionCommand(command, srcTarget,
-                dstContainerType >=0 ? newContainerTarget(dstContainerType) : null);
+                dstContainerType >= 0 ? newContainerTarget(dstContainerType) : null);
     }
 
     public void logActionCommand(int command, Target srcTarget, Target dstTarget) {
@@ -241,7 +255,7 @@
     }
 
     public void logActionOnControl(int action, int controlType, int parentContainer,
-                                   int grandParentContainer){
+            int grandParentContainer) {
         LauncherEvent event = newLauncherEvent(newTouchAction(action),
                 newControlTarget(controlType),
                 newContainerTarget(parentContainer),
@@ -250,11 +264,11 @@
     }
 
     public void logActionOnControl(int action, int controlType, @Nullable View controlInContainer,
-                                   int parentContainerType) {
+            int parentContainerType) {
         final LauncherEvent event = (controlInContainer == null && parentContainerType < 0)
                 ? newLauncherEvent(newTouchAction(action), newTarget(Target.Type.CONTROL))
                 : newLauncherEvent(newTouchAction(action), newTarget(Target.Type.CONTROL),
-                        newTarget(Target.Type.CONTAINER));
+                newTarget(Target.Type.CONTAINER));
         event.srcTarget[0].controlType = controlType;
         if (controlInContainer != null) {
             fillInLogContainerData(event, controlInContainer);
@@ -301,9 +315,9 @@
      * (1) WORKSPACE: if the launcher is the foreground activity
      * (2) APP: if another app was the foreground activity
      */
-    public void logStateChangeAction(int action, int dir, int downX, int downY, int srcChildTargetType,
-                                     int srcParentContainerType, int dstContainerType,
-                                     int pageIndex) {
+    public void logStateChangeAction(int action, int dir, int downX, int downY,
+            int srcChildTargetType, int srcParentContainerType, int dstContainerType,
+            int pageIndex) {
         LauncherEvent event;
         if (srcChildTargetType == LauncherLogProto.ItemType.TASK) {
             event = newLauncherEvent(newTouchAction(action),
@@ -326,9 +340,25 @@
     }
 
     public void logActionOnItem(int action, int dir, int itemType) {
+        logActionOnItem(action, dir, itemType, null, null);
+    }
+
+    /**
+     * Creates new {@link LauncherEvent} of ITEM target type with input arguments and dispatches it.
+     *
+     * @param touchAction ENUM value of {@link LauncherLogProto.Action.Touch} Action
+     * @param dir         ENUM value of {@link LauncherLogProto.Action.Direction} Action
+     * @param itemType    ENUM value of {@link LauncherLogProto.ItemType}
+     * @param gridX       Nullable X coordinate of item's position on the workspace grid
+     * @param gridY       Nullable Y coordinate of item's position on the workspace grid
+     */
+    public void logActionOnItem(int touchAction, int dir, int itemType,
+            @Nullable Integer gridX, @Nullable Integer gridY) {
         Target itemTarget = newTarget(Target.Type.ITEM);
         itemTarget.itemType = itemType;
-        LauncherEvent event = newLauncherEvent(newTouchAction(action), itemTarget);
+        ofNullable(gridX).ifPresent(value -> itemTarget.gridX = value);
+        ofNullable(gridY).ifPresent(value -> itemTarget.gridY = value);
+        LauncherEvent event = newLauncherEvent(newTouchAction(touchAction), itemTarget);
         event.action.dir = dir;
         dispatchUserEvent(event, null);
     }
@@ -351,7 +381,7 @@
         LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.DRAGDROP),
                 newItemTarget(dragObj.originalDragInfo, mInstantAppResolver),
                 newTarget(Target.Type.CONTAINER));
-        event.destTarget = new Target[] {
+        event.destTarget = new Target[]{
                 newItemTarget(dragObj.originalDragInfo, mInstantAppResolver),
                 newDropTarget(dropTargetAsView)
         };
@@ -373,14 +403,10 @@
         int actionTouch = isButton ? Action.Touch.TAP : Action.Touch.SWIPE;
         Action action = newCommandAction(actionTouch);
         action.command = Action.Command.BACK;
-        action.dir = isButton
-                ? Action.Direction.NONE
-                : gestureSwipeLeft
-                        ? Action.Direction.LEFT
-                        : Action.Direction.RIGHT;
-        Target target = newControlTarget(isButton
-                ? LauncherLogProto.ControlType.BACK_BUTTON
-                : LauncherLogProto.ControlType.BACK_GESTURE);
+        action.dir = isButton ? Action.Direction.NONE :
+                gestureSwipeLeft ? Action.Direction.LEFT : Action.Direction.RIGHT;
+        Target target = newControlTarget(isButton ? LauncherLogProto.ControlType.BACK_BUTTON :
+                LauncherLogProto.ControlType.BACK_GESTURE);
         target.spanX = downX;
         target.spanY = downY;
         target.cardinality = completed ? 1 : 0;
@@ -391,6 +417,7 @@
 
     /**
      * Currently logs following containers: workspace, allapps, widget tray.
+     *
      * @param reason
      */
     public final void resetElapsedContainerMillis(String reason) {
@@ -427,10 +454,16 @@
         mAppOrTaskLaunch = false;
         ev.elapsedContainerMillis = SystemClock.uptimeMillis() - mElapsedContainerMillis;
         ev.elapsedSessionMillis = SystemClock.uptimeMillis() - mElapsedSessionMillis;
-
         if (!IS_VERBOSE) {
             return;
         }
+        Log.d(TAG, generateLog(ev));
+    }
+
+    /**
+     * Returns a human-readable log for given user event.
+     */
+    public static String generateLog(LauncherEvent ev) {
         String log = "\n-----------------------------------------------------"
                 + "\naction:" + LoggerUtils.getActionStr(ev.action);
         if (ev.srcTarget != null && ev.srcTarget.length > 0) {
@@ -445,8 +478,7 @@
                 ev.elapsedSessionMillis,
                 ev.actionDurationMillis);
         log += "\n\n";
-        Log.d(TAG, log);
-        return;
+        return log;
     }
 
     private static String getTargetsStr(Target[] targets) {
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 7d4f2f7..b8c583c 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -22,7 +22,6 @@
 import android.util.LongSparseArray;
 import android.util.Pair;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FolderInfo;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -30,14 +29,14 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -77,6 +76,11 @@
                     if (shortcutExists(dataModel, item.getIntent(), item.user)) {
                         continue;
                     }
+
+                    // b/139663018 Short-circuit this logic if the icon is a system app
+                    if (PackageManagerHelper.isSystemApp(app.getContext(), item.getIntent())) {
+                        continue;
+                    }
                 }
 
                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
@@ -118,25 +122,39 @@
                     }
                     SessionInfo sessionInfo = packageInstaller.getActiveSessionInfo(item.user,
                             packageName);
+                    List<LauncherActivityInfo> activities = launcherApps
+                            .getActivityList(packageName, item.user);
+                    boolean hasActivity = activities != null && !activities.isEmpty();
+
                     if (sessionInfo == null) {
-                        List<LauncherActivityInfo> activities = launcherApps
-                                .getActivityList(packageName, item.user);
-                        if (activities != null && !activities.isEmpty()) {
-                            // App was installed while launcher was in the background.
-                            itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user)
-                                    .makeWorkspaceItem();
-                            WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
-                            wii.title = "";
-                            wii.applyFrom(app.getIconCache().getDefaultIcon(item.user));
-                            app.getIconCache().getTitleAndIcon(wii,
-                                    ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
-                        } else {
+                        if (!hasActivity) {
                             // Session was cancelled, do not add.
                             continue;
                         }
                     } else {
                         workspaceInfo.setInstallProgress((int) sessionInfo.getProgress());
                     }
+
+                    if (hasActivity) {
+                        // App was installed while launcher was in the background,
+                        // or app was already installed for another user.
+                        itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user)
+                                .makeWorkspaceItem();
+
+                        if (shortcutExists(dataModel, itemInfo.getIntent(), itemInfo.user)) {
+                            // We need this additional check here since we treat all auto added
+                            // workspace items as promise icons. At this point we now have the
+                            // correct intent to compare against existing workspace icons.
+                            // Icon already exists on the workspace and should not be auto-added.
+                            continue;
+                        }
+
+                        WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
+                        wii.title = "";
+                        wii.applyFrom(app.getIconCache().getDefaultIcon(item.user));
+                        app.getIconCache().getTitleAndIcon(wii,
+                                ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
+                    }
                 }
 
                 // Add the shortcut to the db
@@ -200,7 +218,7 @@
             intentWithoutPkg = intent.toUri(0);
         }
 
-        boolean isLauncherAppTarget = Utilities.isLauncherAppTarget(intent);
+        boolean isLauncherAppTarget = PackageManagerHelper.isLauncherAppTarget(intent);
         synchronized (dataModel) {
             for (ItemInfo item : dataModel.itemsIdMap) {
                 if (item instanceof WorkspaceItemInfo) {
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
similarity index 69%
rename from src/com/android/launcher3/AllAppsList.java
rename to src/com/android/launcher3/model/AllAppsList.java
index 8b49c06..3873a17 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -14,25 +14,37 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.model;
+
+import static com.android.launcher3.AppInfo.COMPONENT_KEY_COMPARATOR;
+import static com.android.launcher3.AppInfo.EMPTY_ARRAY;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.os.LocaleList;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.launcher3.AppFilter;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.PromiseAppInfo;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.SafeCloseable;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.function.Consumer;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -42,29 +54,40 @@
  * Stores the list of all applications for the all apps view.
  */
 public class AllAppsList {
+
     private static final String TAG = "AllAppsList";
+    private static final Consumer<AppInfo> NO_OP_CONSUMER = a -> { };
+
 
     public static final int DEFAULT_APPLICATIONS_NUMBER = 42;
 
     /** The list off all apps. */
     public final ArrayList<AppInfo> data = new ArrayList<>(DEFAULT_APPLICATIONS_NUMBER);
-    /** The list of apps that have been added since the last notify() call. */
-    public ArrayList<AppInfo> added = new ArrayList<>(DEFAULT_APPLICATIONS_NUMBER);
-    /** The list of apps that have been removed since the last notify() call. */
-    public ArrayList<AppInfo> removed = new ArrayList<>();
-    /** The list of apps that have been modified since the last notify() call. */
-    public ArrayList<AppInfo> modified = new ArrayList<>();
 
     private IconCache mIconCache;
-
     private AppFilter mAppFilter;
 
+    private boolean mDataChanged = false;
+    private Consumer<AppInfo> mRemoveListener = NO_OP_CONSUMER;
+
+    private AlphabeticIndexCompat mIndex;
+
     /**
      * Boring constructor.
      */
     public AllAppsList(IconCache iconCache, AppFilter appFilter) {
         mIconCache = iconCache;
         mAppFilter = appFilter;
+        mIndex = new AlphabeticIndexCompat(LocaleList.getDefault());
+    }
+
+    /**
+     * Returns true if there have been any changes since last call.
+     */
+    public boolean getAndResetChangeFlag() {
+        boolean result = mDataChanged;
+        mDataChanged = false;
+        return result;
     }
 
     /**
@@ -81,9 +104,10 @@
             return;
         }
         mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);
+        info.sectionName = mIndex.computeSectionName(info.title);
 
         data.add(info);
-        added.add(info);
+        mDataChanged = true;
     }
 
     public void addPromiseApp(Context context,
@@ -94,31 +118,46 @@
         if (applicationInfo == null) {
             PromiseAppInfo info = new PromiseAppInfo(installInfo);
             mIconCache.getTitleAndIcon(info, info.usingLowResIcon());
+            info.sectionName = mIndex.computeSectionName(info.title);
+
             data.add(info);
-            added.add(info);
+            mDataChanged = true;
         }
     }
 
-    public void removePromiseApp(AppInfo appInfo) {
-        // the <em>removed</em> list is handled by the caller
-        // so not adding it here
-        data.remove(appInfo);
+    public PromiseAppInfo updatePromiseInstallInfo(PackageInstallInfo installInfo) {
+        UserHandle user = Process.myUserHandle();
+        for (int i=0; i < data.size(); i++) {
+            final AppInfo appInfo = data.get(i);
+            final ComponentName tgtComp = appInfo.getTargetComponent();
+            if (tgtComp != null && tgtComp.getPackageName().equals(installInfo.packageName)
+                    && appInfo.user.equals(user)
+                    && appInfo instanceof PromiseAppInfo) {
+                final PromiseAppInfo promiseAppInfo = (PromiseAppInfo) appInfo;
+                if (installInfo.state == PackageInstallerCompat.STATUS_INSTALLING) {
+                    promiseAppInfo.level = installInfo.progress;
+                    return promiseAppInfo;
+                } else if (installInfo.state == PackageInstallerCompat.STATUS_FAILED) {
+                    removeApp(i);
+                }
+            }
+        }
+        return null;
+    }
+
+    private void removeApp(int index) {
+        AppInfo removed = data.remove(index);
+        if (removed != null) {
+            mDataChanged = true;
+            mRemoveListener.accept(removed);
+        }
     }
 
     public void clear() {
         data.clear();
-        // TODO: do we clear these too?
-        added.clear();
-        removed.clear();
-        modified.clear();
-    }
-
-    public int size() {
-        return data.size();
-    }
-
-    public AppInfo get(int index) {
-        return data.get(index);
+        mDataChanged = false;
+        // Reset the index as locales might have changed
+        mIndex = new AlphabeticIndexCompat(LocaleList.getDefault());
     }
 
     /**
@@ -142,8 +181,7 @@
         for (int i = data.size() - 1; i >= 0; i--) {
             AppInfo info = data.get(i);
             if (info.user.equals(user) && packageName.equals(info.componentName.getPackageName())) {
-                removed.add(info);
-                data.remove(i);
+                removeApp(i);
             }
         }
     }
@@ -157,17 +195,17 @@
             AppInfo info = data.get(i);
             if (matcher.matches(info, info.componentName)) {
                 info.runtimeStatusFlags = op.apply(info.runtimeStatusFlags);
-                modified.add(info);
+                mDataChanged = true;
             }
         }
     }
 
-    public void updateIconsAndLabels(HashSet<String> packages, UserHandle user,
-            ArrayList<AppInfo> outUpdates) {
+    public void updateIconsAndLabels(HashSet<String> packages, UserHandle user) {
         for (AppInfo info : data) {
             if (info.user.equals(user) && packages.contains(info.componentName.getPackageName())) {
                 mIconCache.updateTitleAndIcon(info);
-                outUpdates.add(info);
+                info.sectionName = mIndex.computeSectionName(info.title);
+                mDataChanged = true;
             }
         }
     }
@@ -188,8 +226,7 @@
                         && packageName.equals(applicationInfo.componentName.getPackageName())) {
                     if (!findActivity(matches, applicationInfo.componentName)) {
                         Log.w(TAG, "Changing shortcut target due to app component name change.");
-                        removed.add(applicationInfo);
-                        data.remove(i);
+                        removeApp(i);
                     }
                 }
             }
@@ -202,7 +239,9 @@
                     add(new AppInfo(context, info, user), info);
                 } else {
                     mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */);
-                    modified.add(applicationInfo);
+                    applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title);
+
+                    mDataChanged = true;
                 }
             }
         } else {
@@ -211,15 +250,13 @@
                 final AppInfo applicationInfo = data.get(i);
                 if (user.equals(applicationInfo.user)
                         && packageName.equals(applicationInfo.componentName.getPackageName())) {
-                    removed.add(applicationInfo);
                     mIconCache.remove(applicationInfo.componentName, user);
-                    data.remove(i);
+                    removeApp(i);
                 }
             }
         }
     }
 
-
     /**
      * Returns whether <em>apps</em> contains <em>component</em>.
      */
@@ -247,4 +284,16 @@
         }
         return null;
     }
+
+    public AppInfo[] copyData() {
+        AppInfo[] result = data.toArray(EMPTY_ARRAY);
+        Arrays.sort(result, COMPONENT_KEY_COMPARATOR);
+        return result;
+    }
+
+    public SafeCloseable trackRemoves(Consumer<AppInfo> removeListener) {
+        mRemoveListener = removeListener;
+
+        return () -> mRemoveListener = NO_OP_CONSUMER;
+    }
 }
diff --git a/src/com/android/launcher3/model/AppLaunchTracker.java b/src/com/android/launcher3/model/AppLaunchTracker.java
index 29a46cf..13ab033 100644
--- a/src/com/android/launcher3/model/AppLaunchTracker.java
+++ b/src/com/android/launcher3/model/AppLaunchTracker.java
@@ -51,5 +51,8 @@
     public void onStartApp(ComponentName componentName, UserHandle user,
             @Nullable String container) { }
 
+    public void onDismissApp(ComponentName componentName, UserHandle user,
+             @Nullable String container){}
+
     public void onReturnedToHome() { }
 }
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 97cf267..0a4f005 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -16,21 +16,21 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.os.Looper;
 import android.util.Log;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherModel.Callbacks;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.LooperIdleLock;
@@ -65,7 +65,7 @@
 
     public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel,
             AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        mUiExecutor = new MainThreadExecutor();
+        mUiExecutor = MAIN_EXECUTOR;
         mApp = app;
         mBgDataModel = dataModel;
         mBgAllAppsList = allAppsList;
@@ -279,9 +279,8 @@
 
     public void bindAllApps() {
         // shallow copy
-        @SuppressWarnings("unchecked")
-        ArrayList<AppInfo> list = (ArrayList<AppInfo>) mBgAllAppsList.data.clone();
-        executeCallbacksTask(c -> c.bindAllApplications(list), mUiExecutor);
+        AppInfo[] apps = mBgAllAppsList.copyData();
+        executeCallbacksTask(c -> c.bindAllApplications(apps), mUiExecutor);
     }
 
     public abstract void bindWidgets();
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index eea3d8c..e12633b 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -17,12 +17,12 @@
 
 import android.util.Log;
 
-import com.android.launcher3.AllAppsList;
+import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -30,6 +30,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -95,12 +96,7 @@
 
     public void bindUpdatedWorkspaceItems(final ArrayList<WorkspaceItemInfo> updatedShortcuts) {
         if (!updatedShortcuts.isEmpty()) {
-            scheduleCallbackTask(new CallbackTask() {
-                @Override
-                public void execute(Callbacks callbacks) {
-                    callbacks.bindWorkspaceItemsChanged(updatedShortcuts);
-                }
-            });
+            scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(updatedShortcuts));
         }
     }
 
@@ -113,23 +109,20 @@
     public void bindUpdatedWidgets(BgDataModel dataModel) {
         final ArrayList<WidgetListRowEntry> widgets =
                 dataModel.widgetsModel.getWidgetsList(mApp.getContext());
-        scheduleCallbackTask(new CallbackTask() {
-            @Override
-            public void execute(Callbacks callbacks) {
-                callbacks.bindAllWidgets(widgets);
-            }
-        });
+        scheduleCallbackTask(c -> c.bindAllWidgets(widgets));
     }
 
     public void deleteAndBindComponentsRemoved(final ItemInfoMatcher matcher) {
         getModelWriter().deleteItemsFromDatabase(matcher);
 
         // Call the components-removed callback
-        scheduleCallbackTask(new CallbackTask() {
-            @Override
-            public void execute(Callbacks callbacks) {
-                callbacks.bindWorkspaceComponentsRemoved(matcher);
-            }
-        });
+        scheduleCallbackTask(c -> c.bindWorkspaceComponentsRemoved(matcher));
+    }
+
+    public void bindApplicationsIfNeeded() {
+        if (mAllAppsList.getAndResetChangeFlag()) {
+            AppInfo[] apps = mAllAppsList.copyData();
+            scheduleCallbackTask(c -> c.bindAllApplications(apps));
+        }
     }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 8f0cd08..0e20270 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -22,11 +22,13 @@
 import android.util.Log;
 import android.util.MutableInt;
 
+import com.android.launcher3.AppInfo;
 import com.android.launcher3.FolderInfo;
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
@@ -40,6 +42,10 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.launcher3.widget.WidgetListRowEntry;
+
 import com.google.protobuf.nano.MessageNano;
 
 import java.io.FileDescriptor;
@@ -49,6 +55,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -391,4 +398,30 @@
             }
         }
     }
+
+    public interface Callbacks {
+        void rebindModel();
+
+        int getCurrentWorkspaceScreen();
+        void clearPendingBinds();
+        void startBinding();
+        void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
+        void bindScreens(IntArray orderedScreenIds);
+        void finishFirstPageBind(ViewOnDrawExecutor executor);
+        void finishBindingItems(int pageBoundFirst);
+        void preAddApps();
+        void bindAppsAdded(IntArray newScreens,
+                ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
+        void bindPromiseAppProgressUpdated(PromiseAppInfo app);
+        void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated);
+        void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
+        void bindRestoreItemsChange(HashSet<ItemInfo> updates);
+        void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
+        void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets);
+        void onPageBoundSynchronously(int page);
+        void executeOnNextDraw(ViewOnDrawExecutor executor);
+        void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
+
+        void bindAllApplications(AppInfo[] apps);
+    }
 }
diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
index 7852444..c1c8be3 100644
--- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java
+++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
@@ -18,14 +18,10 @@
 import android.content.ComponentName;
 import android.os.UserHandle;
 
-import com.android.launcher3.AllAppsList;
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherModel.Callbacks;
 import com.android.launcher3.LauncherSettings;
 
 import java.util.ArrayList;
@@ -53,9 +49,9 @@
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
         IconCache iconCache = app.getIconCache();
 
-        final ArrayList<AppInfo> updatedApps = new ArrayList<>();
 
         ArrayList<WorkspaceItemInfo> updatedShortcuts = new ArrayList<>();
+
         synchronized (dataModel) {
             for (ItemInfo info : dataModel.itemsIdMap) {
                 if (info instanceof WorkspaceItemInfo && mUser.equals(info.user)) {
@@ -69,18 +65,10 @@
                     }
                 }
             }
-            apps.updateIconsAndLabels(mPackages, mUser, updatedApps);
+            apps.updateIconsAndLabels(mPackages, mUser);
         }
         bindUpdatedWorkspaceItems(updatedShortcuts);
-
-        if (!updatedApps.isEmpty()) {
-            scheduleCallbackTask(new CallbackTask() {
-                @Override
-                public void execute(Callbacks callbacks) {
-                    callbacks.bindAppsAddedOrUpdated(updatedApps);
-                }
-            });
-        }
+        bindApplicationsIfNeeded();
     }
 
     public boolean isValidShortcut(WorkspaceItemInfo si) {
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 7593a33..50e1d56 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -19,8 +19,9 @@
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
-import static com.android.launcher3.compat.PackageInstallerCompat.getUserHandle;
 import static com.android.launcher3.model.LoaderResults.filterCurrentWorkspaceItems;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
 
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
@@ -32,7 +33,6 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.ShortcutInfo;
-import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -40,7 +40,6 @@
 import android.util.LongSparseArray;
 import android.util.MutableInt;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FolderInfo;
 import com.android.launcher3.InstallShortcutReceiver;
@@ -58,7 +57,7 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderIconPreviewVerifier;
+import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
 import com.android.launcher3.icons.IconCache;
@@ -67,9 +66,11 @@
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.provider.ImportDataTask;
+import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -228,9 +229,10 @@
             mResults.bindWidgets();
 
             verifyNotStopped();
-            TraceHelper.partitionSection(TAG, "step 4.3: Update icon cache");
-            updateHandler.updateIcons(allWidgetsList, new ComponentCachingLogic(mApp.getContext()),
-                    mApp.getModel()::onWidgetLabelsUpdated);
+
+            TraceHelper.partitionSection(TAG, "step 4.3: save widgets in icon cache");
+            updateHandler.updateIcons(allWidgetsList, new ComponentCachingLogic(
+                    mApp.getContext(), true), mApp.getModel()::onWidgetLabelsUpdated);
 
             verifyNotStopped();
             TraceHelper.partitionSection(TAG, "step 5: Finish icon cache update");
@@ -320,9 +322,9 @@
 
                     // We can only query for shortcuts when the user is unlocked.
                     if (userUnlocked) {
-                        List<ShortcutInfo> pinnedShortcuts =
+                        DeepShortcutManager.QueryResult pinnedShortcuts =
                                 mShortcutManager.queryForPinnedShortcuts(null, user);
-                        if (mShortcutManager.wasLastCallSuccess()) {
+                        if (pinnedShortcuts.wasSuccess()) {
                             for (ShortcutInfo shortcut : pinnedShortcuts) {
                                 shortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
                                         shortcut);
@@ -387,7 +389,9 @@
                             boolean validTarget = TextUtils.isEmpty(targetPkg) ||
                                     mLauncherApps.isPackageEnabledForProfile(targetPkg, c.user);
 
-                            if (cn != null && validTarget) {
+                            // If it's a deep shortcut, we'll use pinned shortcuts to restore it
+                            if (cn != null && validTarget && c.itemType
+                                    != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                                 // If the apk is present and the shortcut points to a specific
                                 // component.
 
@@ -535,7 +539,7 @@
                                 info.spanX = 1;
                                 info.spanY = 1;
                                 info.runtimeStatusFlags |= disabledState;
-                                if (isSafeMode && !Utilities.isSystemApp(context, intent)) {
+                                if (isSafeMode && !isSystemApp(context, intent)) {
                                     info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
                                 }
 
@@ -584,10 +588,19 @@
 
                             int appWidgetId = c.getInt(appWidgetIdIndex);
                             String savedProvider = c.getString(appWidgetProviderIndex);
+                            final ComponentName component;
 
-                            final ComponentName component =
-                                    ComponentName.unflattenFromString(savedProvider);
-
+                            boolean isSearchWidget = (c.getInt(optionsIndex)
+                                    & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0;
+                            if (isSearchWidget) {
+                                component  = QsbContainerView.getSearchComponentName(context);
+                                if (component == null) {
+                                    c.markDeleted("Discarding SearchWidget without packagename ");
+                                    continue;
+                                }
+                            } else {
+                                component = ComponentName.unflattenFromString(savedProvider);
+                            }
                             final boolean isIdValid = !c.hasRestoreFlag(
                                     LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
                             final boolean wasProviderReady = !c.hasRestoreFlag(
@@ -597,9 +610,7 @@
                                 widgetProvidersMap = mAppWidgetManager.getAllProvidersMap();
                             }
                             final AppWidgetProviderInfo provider = widgetProvidersMap.get(
-                                    new ComponentKey(
-                                            ComponentName.unflattenFromString(savedProvider),
-                                            c.user));
+                                    new ComponentKey(component, c.user));
 
                             final boolean isProviderReady = isValidProvider(provider);
                             if (!isSafeMode && !customWidget &&
@@ -665,6 +676,7 @@
                                 c.applyCommonProperties(appWidgetInfo);
                                 appWidgetInfo.spanX = c.getInt(spanXIndex);
                                 appWidgetInfo.spanY = c.getInt(spanYIndex);
+                                appWidgetInfo.options = c.getInt(optionsIndex);
                                 appWidgetInfo.user = c.user;
 
                                 if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
@@ -710,7 +722,7 @@
                     }
                 }
             } finally {
-                Utilities.closeSilently(c);
+                IOUtils.closeSilently(c);
             }
 
             // Break early if we've stopped loading
@@ -750,8 +762,8 @@
             }
 
             // Sort the folder items, update ranks, and make sure all preview items are high res.
-            FolderIconPreviewVerifier verifier =
-                    new FolderIconPreviewVerifier(mApp.getInvariantDeviceProfile());
+            FolderGridOrganizer verifier =
+                    new FolderGridOrganizer(mApp.getInvariantDeviceProfile());
             for (FolderInfo folder : mBgDataModel.folders) {
                 Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
                 verifier.setFolderInfo(folder);
@@ -778,7 +790,7 @@
                         new SdCardAvailableReceiver(mApp, pendingPackages),
                         new IntentFilter(Intent.ACTION_BOOT_COMPLETED),
                         null,
-                        new Handler(LauncherModel.getWorkerLooper()));
+                        MODEL_EXECUTOR.getHandler());
             }
         }
     }
@@ -836,7 +848,7 @@
             }
         }
 
-        mBgAllAppsList.added = new ArrayList<>();
+        mBgAllAppsList.getAndResetChangeFlag();
         return allActivityList;
     }
 
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
index b353810..2bd6cd4 100644
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ b/src/com/android/launcher3/model/ModelPreload.java
@@ -18,7 +18,6 @@
 import android.content.Context;
 import android.util.Log;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index 4ce2f4b..c69ace9 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -31,23 +33,25 @@
 import com.android.launcher3.LauncherAppWidgetHost;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.Callbacks;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.LooperExecutor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 /**
  * Class for handling model updates.
@@ -61,7 +65,6 @@
     private final BgDataModel mBgDataModel;
     private final Handler mUiHandler;
 
-    private final Executor mWorkerExecutor;
     private final boolean mHasVerticalHotseat;
     private final boolean mVerifyChanges;
 
@@ -74,7 +77,6 @@
         mContext = context;
         mModel = model;
         mBgDataModel = dataModel;
-        mWorkerExecutor = new LooperExecutor(LauncherModel.getWorkerLooper());
         mHasVerticalHotseat = hasVerticalHotseat;
         mVerifyChanges = verifyChanges;
         mUiHandler = new Handler(Looper.getMainLooper());
@@ -194,7 +196,7 @@
         item.spanX = spanX;
         item.spanY = spanY;
 
-        mWorkerExecutor.execute(new UpdateItemRunnable(item, () ->
+        ((Executor) MODEL_EXECUTOR).execute(new UpdateItemRunnable(item, () ->
                 new ContentWriter(mContext)
                         .put(Favorites.CONTAINER, item.container)
                         .put(Favorites.CELLX, item.cellX)
@@ -209,7 +211,7 @@
      * Update an item to the database in a specified container.
      */
     public void updateItemInDatabase(ItemInfo item) {
-        mWorkerExecutor.execute(new UpdateItemRunnable(item, () -> {
+        ((Executor) MODEL_EXECUTOR).execute(new UpdateItemRunnable(item, () -> {
             ContentWriter writer = new ContentWriter(mContext);
             item.onAddToDatabase(writer);
             return writer;
@@ -229,7 +231,7 @@
 
         ModelVerifier verifier = new ModelVerifier();
         final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
-        mWorkerExecutor.execute(() -> {
+        ((Executor) MODEL_EXECUTOR).execute(() -> {
             // Write the item on background thread, as some properties might have been updated in
             // the background.
             final ContentWriter writer = new ContentWriter(mContext);
@@ -263,9 +265,12 @@
     /**
      * Removes the specified items from the database
      */
-    public void deleteItemsFromDatabase(final Iterable<? extends ItemInfo> items) {
+    public void deleteItemsFromDatabase(final Collection<? extends ItemInfo> items) {
         ModelVerifier verifier = new ModelVerifier();
-
+        FileLog.d(TAG, "removing items from db " + items.stream().map(
+                (item) -> item.getTargetComponent() == null ? ""
+                        : item.getTargetComponent().getPackageName()).collect(
+                Collectors.joining(",")), new Exception());
         enqueueDeleteRunnable(() -> {
             for (ItemInfo item : items) {
                 final Uri uri = Favorites.getContentUri(item.id);
@@ -333,14 +338,14 @@
         if (mPreparingToUndo) {
             mDeleteRunnables.add(r);
         } else {
-            mWorkerExecutor.execute(r);
+            ((Executor) MODEL_EXECUTOR).execute(r);
         }
     }
 
     public void commitDelete() {
         mPreparingToUndo = false;
         for (Runnable runnable : mDeleteRunnables) {
-            mWorkerExecutor.execute(runnable);
+            ((Executor) MODEL_EXECUTOR).execute(runnable);
         }
         mDeleteRunnables.clear();
     }
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 9fcab38..802cbc7 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -19,20 +19,18 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.util.InstantAppResolver;
 
-import java.util.ArrayList;
 import java.util.HashSet;
 
 /**
@@ -65,41 +63,11 @@
         }
 
         synchronized (apps) {
-            PromiseAppInfo updated = null;
-            final ArrayList<AppInfo> removed = new ArrayList<>();
-            for (int i=0; i < apps.size(); i++) {
-                final AppInfo appInfo = apps.get(i);
-                final ComponentName tgtComp = appInfo.getTargetComponent();
-                if (tgtComp != null && tgtComp.getPackageName().equals(mInstallInfo.packageName)) {
-                    if (appInfo instanceof PromiseAppInfo) {
-                        final PromiseAppInfo promiseAppInfo = (PromiseAppInfo) appInfo;
-                        if (mInstallInfo.state == PackageInstallerCompat.STATUS_INSTALLING) {
-                            promiseAppInfo.level = mInstallInfo.progress;
-                            updated = promiseAppInfo;
-                        } else if (mInstallInfo.state == PackageInstallerCompat.STATUS_FAILED) {
-                            apps.removePromiseApp(appInfo);
-                            removed.add(appInfo);
-                        }
-                    }
-                }
-            }
+            PromiseAppInfo updated = apps.updatePromiseInstallInfo(mInstallInfo);
             if (updated != null) {
-                final PromiseAppInfo updatedPromiseApp = updated;
-                scheduleCallbackTask(new CallbackTask() {
-                    @Override
-                    public void execute(Callbacks callbacks) {
-                        callbacks.bindPromiseAppProgressUpdated(updatedPromiseApp);
-                    }
-                });
+                scheduleCallbackTask(c -> c.bindPromiseAppProgressUpdated(updated));
             }
-            if (!removed.isEmpty()) {
-                scheduleCallbackTask(new CallbackTask() {
-                    @Override
-                    public void execute(Callbacks callbacks) {
-                        callbacks.bindAppInfosRemoved(removed);
-                    }
-                });
-            }
+            bindApplicationsIfNeeded();
         }
 
         synchronized (dataModel) {
diff --git a/src/com/android/launcher3/model/PackageItemInfo.java b/src/com/android/launcher3/model/PackageItemInfo.java
index baeaa94..3ef48cd 100644
--- a/src/com/android/launcher3/model/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/PackageItemInfo.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.model;
 
 import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.LauncherSettings;
 
 /**
  * Represents a {@link Package} in the widget tray section.
@@ -30,10 +31,21 @@
 
     public PackageItemInfo(String packageName) {
         this.packageName = packageName;
+        this.itemType = LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE;
+    }
+
+    public PackageItemInfo(PackageItemInfo copy) {
+        this.packageName = copy.packageName;
+        this.itemType = LauncherSettings.Favorites.ITEM_TYPE_NON_ACTIONABLE;
     }
 
     @Override
     protected String dumpProperties() {
         return super.dumpProperties() + " packageName=" + packageName;
     }
+
+    @Override
+    public PackageItemInfo clone() {
+        return new PackageItemInfo(this);
+    }
 }
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 2d1e94f..d6ebaaf 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -23,23 +25,19 @@
 import android.os.UserHandle;
 import android.util.Log;
 
-import com.android.launcher3.AllAppsList;
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.LauncherModel.Callbacks;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.SessionCommitReceiver;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -49,6 +47,7 @@
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SafeCloseable;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -101,6 +100,8 @@
         FlagOp flagOp = FlagOp.NO_OP;
         final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
         ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packageSet, mUser);
+        final HashSet<ComponentName> removedComponents = new HashSet<>();
+
         switch (mOp) {
             case OP_ADD: {
                 for (int i = 0; i < N; i++) {
@@ -120,17 +121,21 @@
                 break;
             }
             case OP_UPDATE:
-                for (int i = 0; i < N; i++) {
-                    if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
-                    iconCache.updateIconsForPkg(packages[i], mUser);
-                    appsList.updatePackage(context, packages[i], mUser);
-                    app.getWidgetCache().removePackage(packages[i], mUser);
+                try (SafeCloseable t =
+                             appsList.trackRemoves(a -> removedComponents.add(a.componentName))) {
+                    for (int i = 0; i < N; i++) {
+                        if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
+                        iconCache.updateIconsForPkg(packages[i], mUser);
+                        appsList.updatePackage(context, packages[i], mUser);
+                        app.getWidgetCache().removePackage(packages[i], mUser);
+                    }
                 }
                 // Since package was just updated, the target must be available now.
                 flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
             case OP_REMOVE: {
                 for (int i = 0; i < N; i++) {
+                    FileLog.d(TAG, "Removing app icon" + packages[i]);
                     iconCache.removeIconsForPkg(packages[i], mUser);
                 }
                 // Fall through
@@ -161,23 +166,7 @@
                 break;
         }
 
-        final ArrayList<AppInfo> addedOrModified = new ArrayList<>();
-        addedOrModified.addAll(appsList.added);
-        appsList.added.clear();
-        addedOrModified.addAll(appsList.modified);
-        appsList.modified.clear();
-        if (!addedOrModified.isEmpty()) {
-            scheduleCallbackTask((callbacks) -> callbacks.bindAppsAddedOrUpdated(addedOrModified));
-        }
-
-        final ArrayList<AppInfo> removedApps = new ArrayList<>(appsList.removed);
-        appsList.removed.clear();
-        final HashSet<ComponentName> removedComponents = new HashSet<>();
-        if (mOp == OP_UPDATE) {
-            for (AppInfo ai : removedApps) {
-                removedComponents.add(ai.componentName);
-            }
-        }
+        bindApplicationsIfNeeded();
 
         final IntSparseArrayMap<Boolean> removedShortcuts = new IntSparseArrayMap<>();
 
@@ -303,12 +292,7 @@
             }
 
             if (!widgets.isEmpty()) {
-                scheduleCallbackTask(new CallbackTask() {
-                    @Override
-                    public void execute(Callbacks callbacks) {
-                        callbacks.bindWidgetsRestored(widgets);
-                    }
-                });
+                scheduleCallbackTask(c -> c.bindWidgetsRestored(widgets));
             }
         }
 
@@ -339,16 +323,6 @@
             InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
         }
 
-        if (!removedApps.isEmpty()) {
-            // Remove corresponding apps from All-Apps
-            scheduleCallbackTask(new CallbackTask() {
-                @Override
-                public void execute(Callbacks callbacks) {
-                    callbacks.bindAppInfosRemoved(removedApps);
-                }
-            });
-        }
-
         if (Utilities.ATLEAST_OREO && mOp == OP_ADD) {
             // Load widgets for the new package. Changes due to app updates are handled through
             // AppWidgetHost events, this is just to initialize the long-press options.
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 8528228..6c358b1 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -19,7 +19,6 @@
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
@@ -65,7 +64,7 @@
         for (ItemInfo itemInfo : dataModel.itemsIdMap) {
             if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                 WorkspaceItemInfo si = (WorkspaceItemInfo) itemInfo;
-                if (si.getIntent().getPackage().equals(mPackageName) && si.user.equals(mUser)) {
+                if (mPackageName.equals(si.getIntent().getPackage()) && si.user.equals(mUser)) {
                     keyToShortcutInfo.addToList(ShortcutKey.fromItemInfo(si), si);
                     allIds.add(si.getDeepShortcutId());
                 }
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 2cb256e..4b773d7 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -21,7 +21,6 @@
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
@@ -37,7 +36,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
 
 /**
  * Task to handle changing of lock state of the user
@@ -58,9 +56,9 @@
 
         HashMap<ShortcutKey, ShortcutInfo> pinnedShortcuts = new HashMap<>();
         if (isUserUnlocked) {
-            List<ShortcutInfo> shortcuts =
+            DeepShortcutManager.QueryResult shortcuts =
                     deepShortcutManager.queryForPinnedShortcuts(null, mUser);
-            if (deepShortcutManager.wasLastCallSuccess()) {
+            if (shortcuts.wasSuccess()) {
                 for (ShortcutInfo shortcut : shortcuts) {
                     pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
                 }
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 32410a6..021fb30 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+
 import android.app.Notification;
 import android.content.Context;
 import android.graphics.Color;
@@ -28,13 +30,11 @@
 import com.android.launcher3.R;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.Themes;
 
 import java.util.List;
 
-import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;
-
 /**
  * Utility class to manage notification UI
  */
@@ -49,7 +49,7 @@
     private final TextView mHeaderCount;
     private final NotificationMainView mMainView;
     private final NotificationFooterLayout mFooter;
-    private final SwipeDetector mSwipeDetector;
+    private final SingleAxisSwipeDetector mSwipeDetector;
     private final View mIconView;
 
     private final View mHeader;
@@ -74,8 +74,8 @@
         mHeader = container.findViewById(R.id.header);
         mDivider = container.findViewById(R.id.divider);
 
-        mSwipeDetector = new SwipeDetector(mContext, mMainView, HORIZONTAL);
-        mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
+        mSwipeDetector = new SingleAxisSwipeDetector(mContext, mMainView, HORIZONTAL);
+        mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
         mMainView.setSwipeDetector(mSwipeDetector);
         mFooter.setContainer(this);
     }
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index e57a051..10378ee 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.annotation.TargetApi;
@@ -31,7 +32,8 @@
 import android.util.Log;
 import android.util.Pair;
 
-import com.android.launcher3.LauncherModel;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.SecureSettingsObserver;
@@ -43,8 +45,6 @@
 import java.util.List;
 import java.util.Map;
 
-import androidx.annotation.Nullable;
-
 /**
  * A {@link NotificationListenerService} that sends updates to its
  * {@link NotificationsChangedListener} when notifications are posted or canceled,
@@ -141,7 +141,7 @@
 
     public NotificationListener() {
         super();
-        mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
+        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), mWorkerCallback);
         mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
         sNotificationListenerInstance = this;
     }
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index 78627ec..b67adbb 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -38,8 +38,9 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.OverScroll;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.Themes;
 
@@ -48,7 +49,7 @@
  * e.g. icon + title + text.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class NotificationMainView extends FrameLayout implements SwipeDetector.Listener {
+public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener {
 
     private static FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
             new FloatProperty<NotificationMainView>("contentTranslation") {
@@ -75,7 +76,7 @@
     private TextView mTextView;
     private View mIconView;
 
-    private SwipeDetector mSwipeDetector;
+    private SingleAxisSwipeDetector mSwipeDetector;
 
     public NotificationMainView(Context context) {
         this(context, null, 0);
@@ -107,7 +108,7 @@
         mIconView = findViewById(R.id.popup_item_icon);
     }
 
-    public void setSwipeDetector(SwipeDetector swipeDetector) {
+    public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
         mSwipeDetector = swipeDetector;
     }
 
@@ -173,7 +174,7 @@
                 LauncherLogProto.ItemType.NOTIFICATION);
     }
 
-    // SwipeDetector.Listener's
+    // SingleAxisSwipeDetector.Listener's
     @Override
     public void onDragStart(boolean start) { }
 
@@ -187,7 +188,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
         final boolean willExit;
         final float endTranslation;
         final float startTranslation = mTextAndBackground.getTranslationX();
@@ -195,7 +196,7 @@
         if (!canChildBeDismissed()) {
             willExit = false;
             endTranslation = 0;
-        } else if (fling) {
+        } else if (mSwipeDetector.isFling(velocity)) {
             willExit = true;
             endTranslation = velocity < 0 ? - getWidth() : getWidth();
         } else if (Math.abs(startTranslation) > getWidth() / 2) {
@@ -206,7 +207,7 @@
             endTranslation = 0;
         }
 
-        long duration = SwipeDetector.calculateDuration(velocity,
+        long duration = BaseSwipeDetector.calculateDuration(velocity,
                 (endTranslation - startTranslation) / getWidth());
 
         mContentTranslateAnimator.removeAllListeners();
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 28000b9..98f7fd8 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -360,10 +360,14 @@
         final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
 
         // Rectangular reveal.
+        mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
         final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
                 .createRevealAnimator(this, false);
         revealAnim.setDuration(revealDuration);
         revealAnim.setInterpolator(revealInterpolator);
+        // Clip the popup to the initial outline while the notification dot and arrow animate.
+        revealAnim.start();
+        revealAnim.pause();
 
         ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1);
         fadeIn.setDuration(revealDuration + arrowDuration);
@@ -399,7 +403,6 @@
         if (!mIsOpen) {
             return;
         }
-        mEndRect.setEmpty();
         if (getOutlineProvider() instanceof RevealOutlineAnimation) {
             ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
         }
@@ -471,9 +474,6 @@
 
         mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
                 arrowCenterY);
-        if (mEndRect.isEmpty()) {
-            mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
-        }
 
         return new RoundedRectRevealOutlineProvider
                 (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 9857efe..e8ac1d4 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.animation.AnimatorSet;
 import android.animation.LayoutTransition;
@@ -51,7 +52,6 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
@@ -168,7 +168,10 @@
     }
 
     public OnClickListener getItemClickListener() {
-        return ItemClickHandler.INSTANCE;
+        return (view) -> {
+            ItemClickHandler.INSTANCE.onClick(view);
+            close(true);
+        };
     }
 
     @Override
@@ -311,8 +314,7 @@
         setLayoutTransition(new LayoutTransition());
 
         // Load the shortcuts on a background thread and update the container as it animates.
-        final Looper workerLooper = LauncherModel.getWorkerLooper();
-        new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
+        MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
                 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
                 this, mShortcuts, notificationKeys));
     }
@@ -573,9 +575,12 @@
 
     @Override
     protected void closeComplete() {
+        PopupContainerWithArrow openPopup = getOpen(mLauncher);
+        if (openPopup == null || openPopup.mOriginalIcon != mOriginalIcon) {
+            mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
+            mOriginalIcon.setForceHideDot(false);
+        }
         super.closeComplete();
-        mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
-        mOriginalIcon.setForceHideDot(false);
     }
 
     @Override
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 78bd81b..a87b7b8 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,14 +1,11 @@
 package com.android.launcher3.popup;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
 import android.graphics.drawable.Icon;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.view.View;
@@ -20,23 +17,30 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.WidgetsBottomSheet;
 
 import java.util.List;
-
 /**
  * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an
  * onClickListener that depends on the item that the shortcut services.
  *
  * Example system shortcuts, defined as inner classes, include Widgets and AppInfo.
+ * @param <T>
  */
-public abstract class SystemShortcut<T extends BaseDraggingActivity> extends ItemInfo {
+public abstract class SystemShortcut<T extends BaseDraggingActivity>
+        extends ItemInfo {
     private final int mIconResId;
     private final int mLabelResId;
     private final Icon mIcon;
@@ -202,6 +206,27 @@
         }
     }
 
+    public static class DismissPrediction extends SystemShortcut<Launcher> {
+        public DismissPrediction() {
+            super(R.drawable.ic_remove_no_shadow, R.string.dismiss_prediction_label);
+        }
+
+        @Override
+        public View.OnClickListener getOnClickListener(Launcher activity, ItemInfo itemInfo) {
+            if (!FeatureFlags.ENABLE_PREDICTION_DISMISS.get()) return null;
+            if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) return null;
+            return (view) -> {
+                PopupContainerWithArrow.closeAllOpenViews(activity);
+                activity.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
+                        ControlType.DISMISS_PREDICTION, ContainerType.DEEPSHORTCUTS);
+                AppLaunchTracker.INSTANCE.get(view.getContext())
+                        .onDismissApp(itemInfo.getTargetComponent(),
+                                itemInfo.user,
+                                AppLaunchTracker.CONTAINER_PREDICTIONS);
+            };
+        }
+    }
+
     protected static void dismissTaskMenuView(BaseDraggingActivity activity) {
         AbstractFloatingView.closeOpenViews(activity, true,
             AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
diff --git a/src/com/android/launcher3/popup/SystemShortcutFactory.java b/src/com/android/launcher3/popup/SystemShortcutFactory.java
index 37a2092..dfcc2f8 100644
--- a/src/com/android/launcher3/popup/SystemShortcutFactory.java
+++ b/src/com/android/launcher3/popup/SystemShortcutFactory.java
@@ -39,7 +39,9 @@
     @SuppressWarnings("unused")
     public SystemShortcutFactory() {
         this(new SystemShortcut.AppInfo(),
-                new SystemShortcut.Widgets(), new SystemShortcut.Install());
+                new SystemShortcut.Widgets(),
+                new SystemShortcut.Install(),
+                new SystemShortcut.DismissPrediction());
     }
 
     protected SystemShortcutFactory(SystemShortcut... shortcuts) {
@@ -53,6 +55,7 @@
                 systemShortcuts.add(systemShortcut);
             }
         }
+
         return systemShortcuts;
     }
 }
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java
index 7b62f53..970a03e 100644
--- a/src/com/android/launcher3/provider/ImportDataTask.java
+++ b/src/com/android/launcher3/provider/ImportDataTask.java
@@ -42,7 +42,6 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
@@ -50,6 +49,7 @@
 import com.android.launcher3.model.GridSizeMigrationTask;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import java.net.URISyntaxException;
 import java.util.ArrayList;
@@ -223,7 +223,7 @@
                     case Favorites.ITEM_TYPE_SHORTCUT:
                     case Favorites.ITEM_TYPE_APPLICATION: {
                         intent = Intent.parseUri(c.getString(intentIndex), 0);
-                        if (Utilities.isLauncherAppTarget(intent)) {
+                        if (PackageManagerHelper.isLauncherAppTarget(intent)) {
                             type = Favorites.ITEM_TYPE_APPLICATION;
                         } else {
                             values.put(Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index d643a0b..fb33551 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.provider;
 
-import static com.android.launcher3.Utilities.getIntArrayFromString;
-import static com.android.launcher3.Utilities.getStringFromIntArray;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
 import android.app.backup.BackupManager;
@@ -40,6 +38,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 
 import java.io.InvalidObjectException;
@@ -240,8 +239,8 @@
         SharedPreferences prefs = Utilities.getPrefs(context);
         if (prefs.contains(APPWIDGET_OLD_IDS) && prefs.contains(APPWIDGET_IDS)) {
             AppWidgetsRestoredReceiver.restoreAppWidgetIds(context,
-                    getIntArrayFromString(prefs.getString(APPWIDGET_OLD_IDS, "")),
-                    getIntArrayFromString(prefs.getString(APPWIDGET_IDS, "")));
+                    IntArray.fromConcatString(prefs.getString(APPWIDGET_OLD_IDS, "")).toArray(),
+                    IntArray.fromConcatString(prefs.getString(APPWIDGET_IDS, "")).toArray());
         } else {
             FileLog.d(TAG, "No app widget ids to restore.");
         }
@@ -253,8 +252,8 @@
     public static void setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds,
             @NonNull int[] newIds) {
         Utilities.getPrefs(context).edit()
-                .putString(APPWIDGET_OLD_IDS, getStringFromIntArray(oldIds))
-                .putString(APPWIDGET_IDS, getStringFromIntArray(newIds))
+                .putString(APPWIDGET_OLD_IDS, IntArray.wrap(oldIds).toConcatString())
+                .putString(APPWIDGET_IDS, IntArray.wrap(newIds).toConcatString())
                 .commit();
     }
 
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 857ea05..0eb4285 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -32,12 +32,16 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.provider.Settings;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
@@ -55,6 +59,74 @@
  */
 public class QsbContainerView extends FrameLayout {
 
+    public static final String SEARCH_PROVIDER_SETTINGS_KEY = "SEARCH_PROVIDER_PACKAGE_NAME";
+
+    /**
+     * Returns the package name for user configured search provider or from searchManager
+     * @param context
+     * @return String
+     */
+    @Nullable
+    public static String getSearchWidgetPackageName(@NonNull Context context) {
+        String providerPkg = Settings.Global.getString(context.getContentResolver(),
+                SEARCH_PROVIDER_SETTINGS_KEY);
+        if (providerPkg == null) {
+            SearchManager searchManager = context.getSystemService(SearchManager.class);
+            ComponentName componentName = searchManager.getGlobalSearchActivity();
+            if (componentName != null) {
+                providerPkg = searchManager.getGlobalSearchActivity().getPackageName();
+            }
+        }
+        return providerPkg;
+    }
+
+    /**
+     * returns it's AppWidgetProviderInfo using package name from getSearchWidgetPackageName
+     * @param context
+     * @return AppWidgetProviderInfo
+     */
+    @Nullable
+    public static AppWidgetProviderInfo getSearchWidgetProviderInfo(@NonNull Context context) {
+        String providerPkg = getSearchWidgetPackageName(context);
+        if (providerPkg == null) {
+            return null;
+        }
+
+        AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
+        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+        for (AppWidgetProviderInfo info :
+                appWidgetManager.getInstalledProvidersForPackage(providerPkg, null)) {
+            if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
+                if ((info.widgetCategory
+                        & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
+                    return info;
+                } else if (defaultWidgetForSearchPackage == null) {
+                    defaultWidgetForSearchPackage = info;
+                }
+            }
+        }
+        return defaultWidgetForSearchPackage;
+    }
+
+    /**
+     * returns componentName for searchWidget if package name is known.
+     */
+    @Nullable
+    public static ComponentName getSearchComponentName(@NonNull  Context context) {
+        AppWidgetProviderInfo providerInfo =
+                QsbContainerView.getSearchWidgetProviderInfo(context);
+        if (providerInfo != null) {
+            return providerInfo.provider;
+        } else {
+            String pkgName = QsbContainerView.getSearchWidgetPackageName(context);
+            if (pkgName != null) {
+                //we don't know the class name yet. we'll put the package name as placeholder
+                return new ComponentName(pkgName, pkgName);
+            }
+            return null;
+        }
+    }
+
     public QsbContainerView(Context context) {
         super(context);
     }
@@ -101,7 +173,7 @@
 
         protected QsbWidgetHost createHost() {
             return new QsbWidgetHost(getContext(), QSB_WIDGET_HOST_ID,
-                    (c) -> new QsbWidgetHostView(c));
+                    (c) -> new QsbWidgetHostView(c), this::rebindFragment);
         }
 
         private FrameLayout mWrapper;
@@ -111,9 +183,9 @@
                 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 
             mWrapper = new FrameLayout(getContext());
-
             // Only add the view when enabled
             if (isQsbEnabled()) {
+                mQsbWidgetHost.startListening();
                 mWrapper.addView(createQsb(mWrapper));
             }
             return mWrapper;
@@ -155,7 +227,8 @@
             }
 
             if (isWidgetBound) {
-                mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId, mWidgetInfo);
+                mQsb = (QsbWidgetHostView) mQsbWidgetHost.createView(context, widgetId,
+                        mWidgetInfo);
                 mQsb.setId(R.id.qsb_widget);
 
                 if (!isInPreviewMode()) {
@@ -163,7 +236,6 @@
                             .getAppWidgetOptions(widgetId), opts)) {
                         mQsb.updateAppWidgetOptions(opts);
                     }
-                    mQsbWidgetHost.startListening();
                 }
                 return mQsb;
             }
@@ -246,43 +318,32 @@
             return v;
         }
 
+
         /**
          * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
-         * provided by the same package which is set to be global search activity.
+         * provided by the package from getSearchProviderPackageName
          * If widgetCategory is not supported, or no such widget is found, returns the first widget
          * provided by the package.
          */
         protected AppWidgetProviderInfo getSearchWidgetProvider() {
-            SearchManager searchManager =
-                    (SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE);
-            ComponentName searchComponent = searchManager.getGlobalSearchActivity();
-            if (searchComponent == null) return null;
-            String providerPkg = searchComponent.getPackageName();
-
-            AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
-
-            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
-            for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
-                if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
-                    if ((info.widgetCategory
-                            & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
-                        return info;
-                    } else if (defaultWidgetForSearchPackage == null) {
-                        defaultWidgetForSearchPackage = info;
-                    }
-                }
-            }
-            return defaultWidgetForSearchPackage;
+            return getSearchWidgetProviderInfo(getContext());
         }
     }
 
     public static class QsbWidgetHost extends AppWidgetHost {
 
         private final WidgetViewFactory mViewFactory;
+        private final WidgetProvidersUpdateCallback mWidgetsUpdateCallback;
 
-        public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory) {
+        public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory,
+                WidgetProvidersUpdateCallback widgetProvidersUpdateCallback) {
             super(context, hostId);
             mViewFactory = viewFactory;
+            mWidgetsUpdateCallback = widgetProvidersUpdateCallback;
+        }
+
+        public QsbWidgetHost(Context context, int hostId, WidgetViewFactory viewFactory) {
+            this(context, hostId, viewFactory, null);
         }
 
         @Override
@@ -290,6 +351,14 @@
                 Context context, int appWidgetId, AppWidgetProviderInfo appWidget) {
             return mViewFactory.newView(context);
         }
+
+        @Override
+        protected void onProvidersChanged() {
+            super.onProvidersChanged();
+            if (mWidgetsUpdateCallback != null) {
+                mWidgetsUpdateCallback.onProvidersUpdated();
+            }
+        }
     }
 
     public interface WidgetViewFactory {
@@ -298,6 +367,17 @@
     }
 
     /**
+     * Callback interface for packages list update.
+     */
+    @FunctionalInterface
+    public interface WidgetProvidersUpdateCallback {
+        /**
+         * Gets called when widget providers list changes
+         */
+        void onProvidersUpdated();
+    }
+
+    /**
      * Returns true if {@param original} contains all entries defined in {@param updates} and
      * have the same value.
      * The comparison uses {@link Object#equals(Object)} to compare the values.
@@ -316,4 +396,5 @@
         }
         return true;
     }
+
 }
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
index c6370c5..a23cd6d 100644
--- a/src/com/android/launcher3/states/InternalStateHandler.java
+++ b/src/com/android/launcher3/states/InternalStateHandler.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.states;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.Intent;
 import android.os.Binder;
 import android.os.Bundle;
@@ -22,8 +24,7 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.Callbacks;
-import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 
 import java.lang.ref.WeakReference;
 
@@ -94,16 +95,12 @@
     private static class Scheduler implements Runnable {
 
         private WeakReference<InternalStateHandler> mPendingHandler = new WeakReference<>(null);
-        private MainThreadExecutor mMainThreadExecutor;
 
         public void schedule(InternalStateHandler handler) {
             synchronized (this) {
                 mPendingHandler = new WeakReference<>(handler);
-                if (mMainThreadExecutor == null) {
-                    mMainThreadExecutor = new MainThreadExecutor();
-                }
             }
-            mMainThreadExecutor.execute(this);
+            MAIN_EXECUTOR.execute(this);
         }
 
         @Override
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 9965572..c6de9ca 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -17,6 +17,8 @@
 
 import static android.graphics.Bitmap.Config.ARGB_8888;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
@@ -29,7 +31,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -96,20 +97,20 @@
                 break;
 
             case TestProtocol.REQUEST_FREEZE_APP_LIST:
-                new MainThreadExecutor().execute(() ->
+                MAIN_EXECUTOR.execute(() ->
                         mLauncher.getAppsView().getAppsStore().enableDeferUpdates(
                                 AllAppsStore.DEFER_UPDATES_TEST));
                 break;
 
             case TestProtocol.REQUEST_UNFREEZE_APP_LIST:
-                new MainThreadExecutor().execute(() ->
+                MAIN_EXECUTOR.execute(() ->
                         mLauncher.getAppsView().getAppsStore().disableDeferUpdates(
                                 AllAppsStore.DEFER_UPDATES_TEST));
                 break;
 
             case TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS: {
                 try {
-                    final int deferUpdatesFlags = new MainThreadExecutor().submit(() ->
+                    final int deferUpdatesFlags = MAIN_EXECUTOR.submit(() ->
                             mLauncher.getAppsView().getAppsStore().getDeferUpdatesFlags()).get();
                     response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
                             deferUpdatesFlags);
@@ -150,6 +151,12 @@
                 mLeaks.add(bitmap);
                 break;
             }
+
+            case TestProtocol.REQUEST_ICON_HEIGHT: {
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        mDeviceProfile.allAppsCellHeightPx);
+                break;
+            }
         }
         return response;
     }
@@ -158,4 +165,5 @@
         final LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
         return model.getCallback() == null || model.isModelLoaded();
     }
-}
\ No newline at end of file
+}
+
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 62bb564..07ddbdc 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -66,6 +66,8 @@
             "all-apps-to-overview-swipe-height";
     public static final String REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT =
             "home-to-all-apps-swipe-height";
+    public static final String REQUEST_ICON_HEIGHT =
+            "icon-height";
     public static final String REQUEST_HOTSEAT_TOP = "hotseat-top";
     public static final String REQUEST_IS_LAUNCHER_INITIALIZED = "is-launcher-initialized";
     public static final String REQUEST_FREEZE_APP_LIST = "freeze-app-list";
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index c5ba5ba..f40f976 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -53,7 +53,7 @@
  * TouchController for handling state changes
  */
 public abstract class AbstractStateChangeTouchController
-        implements TouchController, SwipeDetector.Listener {
+        implements TouchController, SingleAxisSwipeDetector.Listener {
 
     // Progress after which the transition is assumed to be a success in case user does not fling
     public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
@@ -65,8 +65,8 @@
     protected final long ATOMIC_DURATION = getAtomicDuration();
 
     protected final Launcher mLauncher;
-    protected final SwipeDetector mDetector;
-    protected final SwipeDetector.Direction mSwipeDirection;
+    protected final SingleAxisSwipeDetector mDetector;
+    protected final SingleAxisSwipeDetector.Direction mSwipeDirection;
 
     private boolean mNoIntercept;
     private boolean mIsLogContainerSet;
@@ -101,9 +101,9 @@
 
     private float mAtomicComponentsStartProgress;
 
-    public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
+    public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
         mLauncher = l;
-        mDetector = new SwipeDetector(l, this, dir);
+        mDetector = new SingleAxisSwipeDetector(l, this, dir);
         mSwipeDirection = dir;
     }
 
@@ -127,7 +127,7 @@
             boolean ignoreSlopWhenSettling = false;
 
             if (mCurrentAnimation != null) {
-                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH;
                 ignoreSlopWhenSettling = true;
             } else {
                 directionsToDetectScroll = getSwipeDirection();
@@ -152,10 +152,10 @@
         LauncherState fromState = mLauncher.getStateManager().getState();
         int swipeDirection = 0;
         if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) {
-            swipeDirection |= SwipeDetector.DIRECTION_POSITIVE;
+            swipeDirection |= SingleAxisSwipeDetector.DIRECTION_POSITIVE;
         }
         if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
-            swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE;
+            swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
         }
         return swipeDirection;
     }
@@ -369,7 +369,8 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
+        boolean fling = mDetector.isFling(velocity);
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
 
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -406,16 +407,13 @@
             } else {
                 startProgress = Utilities.boundToRange(progress
                         + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
-                duration = SwipeDetector.calculateDuration(velocity,
+                duration = BaseSwipeDetector.calculateDuration(velocity,
                         endProgress - Math.max(progress, 0)) * durationMultiplier;
             }
         } else {
             // Let the state manager know that the animation didn't go to the target state,
             // but don't cancel ourselves (we already clean up when the animation completes).
-            Runnable onCancel = mCurrentAnimation.getOnCancelRunnable();
-            mCurrentAnimation.setOnCancelRunnable(null);
-            mCurrentAnimation.dispatchOnCancel();
-            mCurrentAnimation.setOnCancelRunnable(onCancel);
+            mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable();
 
             endProgress = 0;
             if (progress <= 0) {
@@ -424,7 +422,7 @@
             } else {
                 startProgress = Utilities.boundToRange(progress
                         + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
-                duration = SwipeDetector.calculateDuration(velocity,
+                duration = BaseSwipeDetector.calculateDuration(velocity,
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
         }
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
new file mode 100644
index 0000000..12ca5ee
--- /dev/null
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2017 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.touch;
+
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Scroll/drag/swipe gesture detector.
+ *
+ * Definition of swipe is different from android system in that this detector handles
+ * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
+ * swipe action happens.
+ *
+ * @see SingleAxisSwipeDetector
+ * @see BothAxesSwipeDetector
+ */
+public abstract class BaseSwipeDetector {
+
+    private static final boolean DBG = false;
+    private static final String TAG = "BaseSwipeDetector";
+    private static final float ANIMATION_DURATION = 1200;
+    /** The minimum release velocity in pixels per millisecond that triggers fling.*/
+    private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
+    private static final PointF sTempPoint = new PointF();
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    protected final boolean mIsRtl;
+    protected final float mTouchSlop;
+    protected final float mMaxVelocity;
+
+    private int mActivePointerId = INVALID_POINTER_ID;
+    private VelocityTracker mVelocityTracker;
+    private PointF mLastDisplacement = new PointF();
+    private PointF mDisplacement = new PointF();
+    protected PointF mSubtractDisplacement = new PointF();
+    private ScrollState mState = ScrollState.IDLE;
+
+    protected boolean mIgnoreSlopWhenSettling;
+
+    private enum ScrollState {
+        IDLE,
+        DRAGGING,      // onDragStart, onDrag
+        SETTLING       // onDragEnd
+    }
+
+    protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) {
+        mTouchSlop = config.getScaledTouchSlop();
+        mMaxVelocity = config.getScaledMaximumFlingVelocity();
+        mIsRtl = isRtl;
+    }
+
+    public static long calculateDuration(float velocity, float progressNeeded) {
+        // TODO: make these values constants after tuning.
+        float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
+        float travelDistance = Math.max(0.2f, progressNeeded);
+        long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
+        if (DBG) {
+            Log.d(TAG, String.format(
+                    "calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
+        }
+        return duration;
+    }
+
+    public int getDownX() {
+        return (int) mDownPos.x;
+    }
+
+    public int getDownY() {
+        return (int) mDownPos.y;
+    }
+    /**
+     * There's no touch and there's no animation.
+     */
+    public boolean isIdleState() {
+        return mState == ScrollState.IDLE;
+    }
+
+    public boolean isSettlingState() {
+        return mState == ScrollState.SETTLING;
+    }
+
+    public boolean isDraggingState() {
+        return mState == ScrollState.DRAGGING;
+    }
+
+    public boolean isDraggingOrSettling() {
+        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
+    }
+
+    public void finishedScrolling() {
+        setState(ScrollState.IDLE);
+    }
+
+    public boolean isFling(float velocity) {
+        return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS;
+    }
+
+    public boolean onTouchEvent(MotionEvent ev) {
+        int actionMasked = ev.getActionMasked();
+        if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
+            mVelocityTracker.clear();
+        }
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        switch (actionMasked) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = ev.getPointerId(0);
+                mDownPos.set(ev.getX(), ev.getY());
+                mLastPos.set(mDownPos);
+                mLastDisplacement.set(0, 0);
+                mDisplacement.set(0, 0);
+
+                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
+                    setState(ScrollState.DRAGGING);
+                }
+                break;
+            //case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_POINTER_UP:
+                int ptrIdx = ev.getActionIndex();
+                int ptrId = ev.getPointerId(ptrIdx);
+                if (ptrId == mActivePointerId) {
+                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                    mDownPos.set(
+                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                    mActivePointerId = ev.getPointerId(newPointerIdx);
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == INVALID_POINTER_ID) {
+                    break;
+                }
+                mDisplacement.set(ev.getX(pointerIndex) - mDownPos.x,
+                        ev.getY(pointerIndex) - mDownPos.y);
+                if (mIsRtl) {
+                    mDisplacement.x = -mDisplacement.x;
+                }
+
+                // handle state and listener calls.
+                if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
+                    setState(ScrollState.DRAGGING);
+                }
+                if (mState == ScrollState.DRAGGING) {
+                    reportDragging(ev);
+                }
+                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                // These are synthetic events and there is no need to update internal values.
+                if (mState == ScrollState.DRAGGING) {
+                    setState(ScrollState.SETTLING);
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+            default:
+                break;
+        }
+        return true;
+    }
+
+    //------------------- ScrollState transition diagram -----------------------------------
+    //
+    // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
+    // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
+    // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
+    // SETTLING -> (View settled) -> IDLE
+
+    private void setState(ScrollState newState) {
+        if (DBG) {
+            Log.d(TAG, "setState:" + mState + "->" + newState);
+        }
+        // onDragStart and onDragEnd is reported ONLY on state transition
+        if (newState == ScrollState.DRAGGING) {
+            initializeDragging();
+            if (mState == ScrollState.IDLE) {
+                reportDragStart(false /* recatch */);
+            } else if (mState == ScrollState.SETTLING) {
+                reportDragStart(true /* recatch */);
+            }
+        }
+        if (newState == ScrollState.SETTLING) {
+            reportDragEnd();
+        }
+
+        mState = newState;
+    }
+
+    private void initializeDragging() {
+        if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
+            mSubtractDisplacement.set(0, 0);
+        } else {
+            mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
+            mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
+        }
+    }
+
+    protected abstract boolean shouldScrollStart(PointF displacement);
+
+    private void reportDragStart(boolean recatch) {
+        reportDragStartInternal(recatch);
+        if (DBG) {
+            Log.d(TAG, "onDragStart recatch:" + recatch);
+        }
+    }
+
+    protected abstract void reportDragStartInternal(boolean recatch);
+
+    private void reportDragging(MotionEvent event) {
+        if (mDisplacement != mLastDisplacement) {
+            if (DBG) {
+                Log.d(TAG, String.format("onDrag disp=%s", mDisplacement));
+            }
+
+            mLastDisplacement.set(mDisplacement);
+            sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x,
+                    mDisplacement.y - mSubtractDisplacement.y);
+            reportDraggingInternal(sTempPoint, event);
+        }
+    }
+
+    protected abstract void reportDraggingInternal(PointF displacement, MotionEvent event);
+
+    private void reportDragEnd() {
+        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+        PointF velocity = new PointF(mVelocityTracker.getXVelocity() / 1000,
+                mVelocityTracker.getYVelocity() / 1000);
+        if (mIsRtl) {
+            velocity.x = -velocity.x;
+        }
+        if (DBG) {
+            Log.d(TAG, String.format("onScrollEnd disp=%.1s, velocity=%.1s",
+                    mDisplacement, velocity));
+        }
+
+        reportDragEndInternal(velocity);
+    }
+
+    protected abstract void reportDragEndInternal(PointF velocity);
+}
diff --git a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
new file mode 100644
index 0000000..944391e
--- /dev/null
+++ b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
@@ -0,0 +1,99 @@
+/*
+ * 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.touch;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Two dimensional scroll/drag/swipe gesture detector that reports x and y displacement/velocity.
+ */
+public class BothAxesSwipeDetector extends BaseSwipeDetector {
+
+    public static final int DIRECTION_UP = 1 << 0;
+    // Note that this will track left instead of right in RTL.
+    public static final int DIRECTION_RIGHT = 1 << 1;
+    public static final int DIRECTION_DOWN = 1 << 2;
+    // Note that this will track right instead of left in RTL.
+    public static final int DIRECTION_LEFT = 1 << 3;
+
+    /* Client of this gesture detector can register a callback. */
+    private final Listener mListener;
+
+    private int mScrollDirections;
+
+    public BothAxesSwipeDetector(@NonNull Context context, @NonNull Listener l) {
+        this(ViewConfiguration.get(context), l, Utilities.isRtl(context.getResources()));
+    }
+
+    @VisibleForTesting
+    protected BothAxesSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
+            boolean isRtl) {
+        super(config, isRtl);
+        mListener = l;
+    }
+
+    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
+        mScrollDirections = scrollDirectionFlags;
+        mIgnoreSlopWhenSettling = ignoreSlop;
+    }
+
+    @Override
+    protected boolean shouldScrollStart(PointF displacement) {
+        // Check if the client is interested in scroll in current direction.
+        boolean canScrollUp = (mScrollDirections & DIRECTION_UP) > 0
+                && displacement.y <= -mTouchSlop;
+        boolean canScrollRight = (mScrollDirections & DIRECTION_RIGHT) > 0
+                && displacement.x >= mTouchSlop;
+        boolean canScrollDown = (mScrollDirections & DIRECTION_DOWN) > 0
+                && displacement.y >= mTouchSlop;
+        boolean canScrollLeft = (mScrollDirections & DIRECTION_LEFT) > 0
+                && displacement.x <= -mTouchSlop;
+        return canScrollUp || canScrollRight || canScrollDown || canScrollLeft;
+    }
+
+    @Override
+    protected void reportDragStartInternal(boolean recatch) {
+        mListener.onDragStart(!recatch);
+    }
+
+    @Override
+    protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
+        mListener.onDrag(displacement, event);
+    }
+
+    @Override
+    protected void reportDragEndInternal(PointF velocity) {
+        mListener.onDragEnd(velocity);
+    }
+
+    /** Listener to receive updates on the swipe. */
+    public interface Listener {
+        /** @param start whether this was the original drag start, as opposed to a recatch. */
+        void onDragStart(boolean start);
+
+        boolean onDrag(PointF displacement, MotionEvent motionEvent);
+
+        void onDragEnd(PointF velocity);
+    }
+}
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 0f808c1..86d2b39 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -17,6 +17,7 @@
 
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
+
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -64,7 +65,7 @@
         if (info.container >= 0) {
             Folder folder = Folder.getOpen(launcher);
             if (folder != null) {
-                if (!folder.getItemsInReadingOrder().contains(v)) {
+                if (!folder.getIconsInReadingOrder().contains(v)) {
                     folder.close(true);
                 } else {
                     folder.startDrag(v, dragOptions);
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
new file mode 100644
index 0000000..f2ebc45
--- /dev/null
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -0,0 +1,190 @@
+/*
+ * 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.touch;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL).
+ */
+public class SingleAxisSwipeDetector extends BaseSwipeDetector {
+
+    public static final int DIRECTION_POSITIVE = 1 << 0;
+    public static final int DIRECTION_NEGATIVE = 1 << 1;
+    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
+
+    public static final Direction VERTICAL = new Direction() {
+
+        @Override
+        boolean isPositive(float displacement) {
+            // Up
+            return displacement < 0;
+        }
+
+        @Override
+        boolean isNegative(float displacement) {
+            // Down
+            return displacement > 0;
+        }
+
+        @Override
+        float extractDirection(PointF direction) {
+            return direction.y;
+        }
+
+        @Override
+        boolean canScrollStart(PointF displacement, float touchSlop) {
+            return Math.abs(displacement.y) >= Math.max(Math.abs(displacement.x), touchSlop);
+        }
+
+    };
+
+    public static final Direction HORIZONTAL = new Direction() {
+
+        @Override
+        boolean isPositive(float displacement) {
+            // Right
+            return displacement > 0;
+        }
+
+        @Override
+        boolean isNegative(float displacement) {
+            // Left
+            return displacement < 0;
+        }
+
+        @Override
+        float extractDirection(PointF direction) {
+            return direction.x;
+        }
+
+        @Override
+        boolean canScrollStart(PointF displacement, float touchSlop) {
+            return Math.abs(displacement.x) >= Math.max(Math.abs(displacement.y), touchSlop);
+        }
+    };
+
+    private final Direction mDir;
+    /* Client of this gesture detector can register a callback. */
+    private final Listener mListener;
+
+    private int mScrollDirections;
+
+    public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l,
+            @NonNull Direction dir) {
+        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
+    }
+
+    @VisibleForTesting
+    protected SingleAxisSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
+            @NonNull Direction dir, boolean isRtl) {
+        super(config, isRtl);
+        mListener = l;
+        mDir = dir;
+    }
+
+    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
+        mScrollDirections = scrollDirectionFlags;
+        mIgnoreSlopWhenSettling = ignoreSlop;
+    }
+
+    public int getScrollDirections() {
+        return mScrollDirections;
+    }
+
+    /**
+     * Returns if the start drag was towards the positive direction or negative.
+     *
+     * @see #setDetectableScrollConditions(int, boolean)
+     * @see #DIRECTION_BOTH
+     */
+    public boolean wasInitialTouchPositive() {
+        return mDir.isPositive(mDir.extractDirection(mSubtractDisplacement));
+    }
+
+    @Override
+    protected boolean shouldScrollStart(PointF displacement) {
+        // Reject cases where the angle or slop condition is not met.
+        if (!mDir.canScrollStart(displacement, mTouchSlop)) {
+            return false;
+        }
+
+        // Check if the client is interested in scroll in current direction.
+        float displacementComponent = mDir.extractDirection(displacement);
+        return canScrollNegative(displacementComponent) || canScrollPositive(displacementComponent);
+    }
+
+    private boolean canScrollNegative(float displacement) {
+        return (mScrollDirections & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(displacement);
+    }
+
+    private boolean canScrollPositive(float displacement) {
+        return (mScrollDirections & DIRECTION_POSITIVE) > 0 && mDir.isPositive(displacement);
+    }
+
+    @Override
+    protected void reportDragStartInternal(boolean recatch) {
+        mListener.onDragStart(!recatch);
+    }
+
+    @Override
+    protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
+        mListener.onDrag(mDir.extractDirection(displacement), event);
+    }
+
+    @Override
+    protected void reportDragEndInternal(PointF velocity) {
+        float velocityComponent = mDir.extractDirection(velocity);
+        mListener.onDragEnd(velocityComponent);
+    }
+
+    /** Listener to receive updates on the swipe. */
+    public interface Listener {
+        /** @param start whether this was the original drag start, as opposed to a recatch. */
+        void onDragStart(boolean start);
+
+        // TODO remove
+        boolean onDrag(float displacement);
+
+        default boolean onDrag(float displacement, MotionEvent event) {
+            return onDrag(displacement);
+        }
+
+        void onDragEnd(float velocity);
+    }
+
+    public abstract static class Direction {
+
+        abstract boolean isPositive(float displacement);
+
+        abstract boolean isNegative(float displacement);
+
+        /** Returns the part of the given {@link PointF} that is relevant to this direction. */
+        abstract float extractDirection(PointF point);
+
+        /** Reject cases where the angle or slop condition is not met. */
+        abstract boolean canScrollStart(PointF displacement, float touchSlop);
+
+    }
+}
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
deleted file mode 100644
index 3777a41..0000000
--- a/src/com/android/launcher3/touch/SwipeDetector.java
+++ /dev/null
@@ -1,407 +0,0 @@
-/*
- * Copyright (C) 2017 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.touch;
-
-import static android.view.MotionEvent.INVALID_POINTER_ID;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-/**
- * One dimensional scroll/drag/swipe gesture detector.
- *
- * Definition of swipe is different from android system in that this detector handles
- * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
- * swipe action happens
- */
-public class SwipeDetector {
-
-    private static final boolean DBG = false;
-    private static final String TAG = "SwipeDetector";
-
-    private int mScrollConditions;
-    public static final int DIRECTION_POSITIVE = 1 << 0;
-    public static final int DIRECTION_NEGATIVE = 1 << 1;
-    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
-
-    private static final float ANIMATION_DURATION = 1200;
-
-    protected int mActivePointerId = INVALID_POINTER_ID;
-
-    /**
-     * The minimum release velocity in pixels per millisecond that triggers fling..
-     */
-    public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
-
-    /* Scroll state, this is set to true during dragging and animation. */
-    private ScrollState mState = ScrollState.IDLE;
-
-    enum ScrollState {
-        IDLE,
-        DRAGGING,      // onDragStart, onDrag
-        SETTLING       // onDragEnd
-    }
-
-    public static abstract class Direction {
-
-        abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint,
-                boolean isRtl);
-
-        /**
-         * Distance in pixels a touch can wander before we think the user is scrolling.
-         */
-        abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
-
-        abstract float getVelocity(VelocityTracker tracker, boolean isRtl);
-
-        abstract boolean isPositive(float displacement);
-
-        abstract boolean isNegative(float displacement);
-    }
-
-    public static final Direction VERTICAL = new Direction() {
-
-        @Override
-        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
-            return ev.getY(pointerIndex) - refPoint.y;
-        }
-
-        @Override
-        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
-            return Math.abs(ev.getX(pointerIndex) - downPos.x);
-        }
-
-        @Override
-        float getVelocity(VelocityTracker tracker, boolean isRtl) {
-            return tracker.getYVelocity();
-        }
-
-        @Override
-        boolean isPositive(float displacement) {
-            // Up
-            return displacement < 0;
-        }
-
-        @Override
-        boolean isNegative(float displacement) {
-            // Down
-            return displacement > 0;
-        }
-    };
-
-    public static final Direction HORIZONTAL = new Direction() {
-
-        @Override
-        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
-            float displacement = ev.getX(pointerIndex) - refPoint.x;
-            if (isRtl) {
-                displacement = -displacement;
-            }
-            return displacement;
-        }
-
-        @Override
-        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
-            return Math.abs(ev.getY(pointerIndex) - downPos.y);
-        }
-
-        @Override
-        float getVelocity(VelocityTracker tracker, boolean isRtl) {
-            float velocity = tracker.getXVelocity();
-            if (isRtl) {
-                velocity = -velocity;
-            }
-            return velocity;
-        }
-
-        @Override
-        boolean isPositive(float displacement) {
-            // Right
-            return displacement > 0;
-        }
-
-        @Override
-        boolean isNegative(float displacement) {
-            // Left
-            return displacement < 0;
-        }
-    };
-
-    //------------------- ScrollState transition diagram -----------------------------------
-    //
-    // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
-    // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
-    // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
-    // SETTLING -> (View settled) -> IDLE
-
-    private void setState(ScrollState newState) {
-        if (DBG) {
-            Log.d(TAG, "setState:" + mState + "->" + newState);
-        }
-        // onDragStart and onDragEnd is reported ONLY on state transition
-        if (newState == ScrollState.DRAGGING) {
-            initializeDragging();
-            if (mState == ScrollState.IDLE) {
-                reportDragStart(false /* recatch */);
-            } else if (mState == ScrollState.SETTLING) {
-                reportDragStart(true /* recatch */);
-            }
-        }
-        if (newState == ScrollState.SETTLING) {
-            reportDragEnd();
-        }
-
-        mState = newState;
-    }
-
-    public boolean isDraggingOrSettling() {
-        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
-    }
-
-    public int getDownX() {
-        return (int) mDownPos.x;
-    }
-
-    public int getDownY() {
-        return (int) mDownPos.y;
-    }
-    /**
-     * There's no touch and there's no animation.
-     */
-    public boolean isIdleState() {
-        return mState == ScrollState.IDLE;
-    }
-
-    public boolean isSettlingState() {
-        return mState == ScrollState.SETTLING;
-    }
-
-    public boolean isDraggingState() {
-        return mState == ScrollState.DRAGGING;
-    }
-
-    private final PointF mDownPos = new PointF();
-    private final PointF mLastPos = new PointF();
-    private final Direction mDir;
-    private final boolean mIsRtl;
-
-    private final float mTouchSlop;
-    private final float mMaxVelocity;
-
-    /* Client of this gesture detector can register a callback. */
-    private final Listener mListener;
-
-    private VelocityTracker mVelocityTracker;
-
-    private float mLastDisplacement;
-    private float mDisplacement;
-
-    private float mSubtractDisplacement;
-    private boolean mIgnoreSlopWhenSettling;
-
-    public interface Listener {
-        void onDragStart(boolean start);
-
-        boolean onDrag(float displacement);
-
-        default boolean onDrag(float displacement, MotionEvent event) {
-            return onDrag(displacement);
-        }
-
-        void onDragEnd(float velocity, boolean fling);
-    }
-
-    public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
-        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
-    }
-
-    @VisibleForTesting
-    protected SwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            @NonNull Direction dir, boolean isRtl) {
-        mListener = l;
-        mDir = dir;
-        mIsRtl = isRtl;
-        mTouchSlop = config.getScaledTouchSlop();
-        mMaxVelocity = config.getScaledMaximumFlingVelocity();
-    }
-
-    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
-        mScrollConditions = scrollDirectionFlags;
-        mIgnoreSlopWhenSettling = ignoreSlop;
-    }
-
-    public int getScrollDirections() {
-        return mScrollConditions;
-    }
-
-    private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
-        // reject cases where the angle or slop condition is not met.
-        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
-                > Math.abs(mDisplacement)) {
-            return false;
-        }
-
-        // Check if the client is interested in scroll in current direction.
-        if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(mDisplacement)) ||
-                ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDir.isPositive(mDisplacement))) {
-            return true;
-        }
-        return false;
-    }
-
-    public boolean onTouchEvent(MotionEvent ev) {
-        int actionMasked = ev.getActionMasked();
-        if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
-            mVelocityTracker.clear();
-        }
-        if (mVelocityTracker == null) {
-            mVelocityTracker = VelocityTracker.obtain();
-        }
-        mVelocityTracker.addMovement(ev);
-
-        switch (actionMasked) {
-            case MotionEvent.ACTION_DOWN:
-                mActivePointerId = ev.getPointerId(0);
-                mDownPos.set(ev.getX(), ev.getY());
-                mLastPos.set(mDownPos);
-                mLastDisplacement = 0;
-                mDisplacement = 0;
-
-                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
-                    setState(ScrollState.DRAGGING);
-                }
-                break;
-            //case MotionEvent.ACTION_POINTER_DOWN:
-            case MotionEvent.ACTION_POINTER_UP:
-                int ptrIdx = ev.getActionIndex();
-                int ptrId = ev.getPointerId(ptrIdx);
-                if (ptrId == mActivePointerId) {
-                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
-                    mDownPos.set(
-                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
-                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
-                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
-                    mActivePointerId = ev.getPointerId(newPointerIdx);
-                }
-                break;
-            case MotionEvent.ACTION_MOVE:
-                int pointerIndex = ev.findPointerIndex(mActivePointerId);
-                if (pointerIndex == INVALID_POINTER_ID) {
-                    break;
-                }
-                mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos, mIsRtl);
-
-                // handle state and listener calls.
-                if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
-                    setState(ScrollState.DRAGGING);
-                }
-                if (mState == ScrollState.DRAGGING) {
-                    reportDragging(ev);
-                }
-                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
-                break;
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                // These are synthetic events and there is no need to update internal values.
-                if (mState == ScrollState.DRAGGING) {
-                    setState(ScrollState.SETTLING);
-                }
-                mVelocityTracker.recycle();
-                mVelocityTracker = null;
-                break;
-            default:
-                break;
-        }
-        return true;
-    }
-
-    public void finishedScrolling() {
-        setState(ScrollState.IDLE);
-    }
-
-    private boolean reportDragStart(boolean recatch) {
-        mListener.onDragStart(!recatch);
-        if (DBG) {
-            Log.d(TAG, "onDragStart recatch:" + recatch);
-        }
-        return true;
-    }
-
-    private void initializeDragging() {
-        if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
-            mSubtractDisplacement = 0;
-        }
-        if (mDisplacement > 0) {
-            mSubtractDisplacement = mTouchSlop;
-        } else {
-            mSubtractDisplacement = -mTouchSlop;
-        }
-    }
-
-    /**
-     * Returns if the start drag was towards the positive direction or negative.
-     *
-     * @see #setDetectableScrollConditions(int, boolean)
-     * @see #DIRECTION_BOTH
-     */
-    public boolean wasInitialTouchPositive() {
-        return mDir.isPositive(mSubtractDisplacement);
-    }
-
-    private boolean reportDragging(MotionEvent event) {
-        if (mDisplacement != mLastDisplacement) {
-            if (DBG) {
-                Log.d(TAG, String.format("onDrag disp=%.1f", mDisplacement));
-            }
-
-            mLastDisplacement = mDisplacement;
-            return mListener.onDrag(mDisplacement - mSubtractDisplacement, event);
-        }
-        return true;
-    }
-
-    private void reportDragEnd() {
-        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
-        float velocity = mDir.getVelocity(mVelocityTracker, mIsRtl) / 1000;
-        if (DBG) {
-            Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
-                    mDisplacement, velocity));
-        }
-
-        mListener.onDragEnd(velocity, Math.abs(velocity) > RELEASE_VELOCITY_PX_MS);
-    }
-
-    public static long calculateDuration(float velocity, float progressNeeded) {
-        // TODO: make these values constants after tuning.
-        float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
-        float travelDistance = Math.max(0.2f, progressNeeded);
-        long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
-        if (DBG) {
-            Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
-        }
-        return duration;
-    }
-}
diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java
index 12d35e9..0f81520 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -16,20 +16,15 @@
  * limitations under the License.
  */
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.graphics.Point;
-import android.hardware.display.DisplayManager;
-import android.hardware.display.DisplayManager.DisplayListener;
-import android.os.Handler;
 import android.util.Log;
-import android.view.Display;
-import android.view.WindowManager;
-
-import com.android.launcher3.MainThreadExecutor;
 
 import java.util.function.Consumer;
 
@@ -37,7 +32,8 @@
  * {@link BroadcastReceiver} which watches configuration changes and
  * notifies the callback in case changes which affect the device profile occur.
  */
-public class ConfigMonitor extends BroadcastReceiver implements DisplayListener {
+public class ConfigMonitor extends BroadcastReceiver implements
+        DefaultDisplay.DisplayInfoChangeListener {
 
     private static final String TAG = "ConfigMonitor";
 
@@ -61,24 +57,19 @@
         mFontScale = config.fontScale;
         mDensity = config.densityDpi;
 
-        Display display = getDefaultDisplay(context);
-        mDisplayId = display.getDisplayId();
+        DefaultDisplay display = DefaultDisplay.INSTANCE.get(context);
+        display.addChangeListener(this);
+        DefaultDisplay.Info displayInfo = display.getInfo();
+        mDisplayId = displayInfo.id;
 
-        mRealSize = new Point();
-        display.getRealSize(mRealSize);
-
-        mSmallestSize = new Point();
-        mLargestSize = new Point();
-        display.getCurrentSizeRange(mSmallestSize, mLargestSize);
+        mRealSize = new Point(displayInfo.realSize);
+        mSmallestSize = new Point(displayInfo.smallestSize);
+        mLargestSize = new Point(displayInfo.largestSize);
 
         mCallback = callback;
 
         // Listen for configuration change
         mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
-
-        // Listen for display manager change
-        mContext.getSystemService(DisplayManager.class)
-                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
     }
 
     @Override
@@ -91,26 +82,19 @@
     }
 
     @Override
-    public void onDisplayAdded(int displayId) { }
-
-    @Override
-    public void onDisplayRemoved(int displayId) { }
-
-    @Override
-    public void onDisplayChanged(int displayId) {
-        if (displayId != mDisplayId) {
+    public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+        if (info.id != mDisplayId) {
             return;
         }
-        Display display = getDefaultDisplay(mContext);
-        display.getRealSize(mTmpPoint1);
-
+        mTmpPoint1.set(info.realSize.x, info.realSize.y);
         if (!mRealSize.equals(mTmpPoint1) && !mRealSize.equals(mTmpPoint1.y, mTmpPoint1.x)) {
             Log.d(TAG, String.format("Display size changed from %s to %s", mRealSize, mTmpPoint1));
             notifyChange();
             return;
         }
 
-        display.getCurrentSizeRange(mTmpPoint1, mTmpPoint2);
+        mTmpPoint1.set(info.smallestSize.x, info.smallestSize.y);
+        mTmpPoint2.set(info.largestSize.x, info.largestSize.y);
         if (!mSmallestSize.equals(mTmpPoint1) || !mLargestSize.equals(mTmpPoint2)) {
             Log.d(TAG, String.format("Available size changed from [%s, %s] to [%s, %s]",
                     mSmallestSize, mLargestSize, mTmpPoint1, mTmpPoint2));
@@ -122,18 +106,15 @@
         if (mCallback != null) {
             Consumer<Context> callback = mCallback;
             mCallback = null;
-            new MainThreadExecutor().execute(() -> callback.accept(mContext));
+            MAIN_EXECUTOR.execute(() -> callback.accept(mContext));
         }
     }
 
-    private Display getDefaultDisplay(Context context) {
-        return context.getSystemService(WindowManager.class).getDefaultDisplay();
-    }
-
     public void unregister() {
         try {
             mContext.unregisterReceiver(this);
-            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+            DefaultDisplay display = DefaultDisplay.INSTANCE.get(mContext);
+            display.removeChangeListener(this);
         } catch (Exception e) {
             Log.e(TAG, "Failed to unregister config monitor", e);
         }
diff --git a/src/com/android/launcher3/util/DefaultDisplay.java b/src/com/android/launcher3/util/DefaultDisplay.java
index 7719f08..8529d50 100644
--- a/src/com/android/launcher3/util/DefaultDisplay.java
+++ b/src/com/android/launcher3/util/DefaultDisplay.java
@@ -15,12 +15,15 @@
  */
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.Context;
 import android.graphics.Point;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.os.Handler;
 import android.os.Message;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
 import android.view.WindowManager;
@@ -54,7 +57,7 @@
         mChangeHandler = new Handler(this::onChange);
 
         context.getSystemService(DisplayManager.class)
-                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
+                .registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
     }
 
     @Override
@@ -122,6 +125,8 @@
         public final Point smallestSize;
         public final Point largestSize;
 
+        public final DisplayMetrics metrics;
+
         private Info(Context context) {
             Display display = context.getSystemService(WindowManager.class).getDefaultDisplay();
 
@@ -136,6 +141,9 @@
             largestSize = new Point();
             display.getRealSize(realSize);
             display.getCurrentSizeRange(smallestSize, largestSize);
+
+            metrics = new DisplayMetrics();
+            display.getMetrics(metrics);
         }
 
         private boolean hasDifferentSize(Info info) {
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
new file mode 100644
index 0000000..4d5ee49
--- /dev/null
+++ b/src/com/android/launcher3/util/Executors.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 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.util;
+
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Various different executors used in Launcher
+ */
+public class Executors {
+
+    // These values are same as that in {@link AsyncTask}.
+    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
+    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+    private static final int KEEP_ALIVE = 1;
+
+    /**
+     * An {@link Executor} to be used with async task with no limit on the queue size.
+     */
+    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
+            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
+            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+
+    /**
+     * Returns the executor for running tasks on the main thread.
+     */
+    public static final LooperExecutor MAIN_EXECUTOR =
+            new LooperExecutor(Looper.getMainLooper());
+
+    /**
+     * A background executor for using time sensitive actions where user is waiting for response.
+     */
+    public static final LooperExecutor UI_HELPER_EXECUTOR =
+            new LooperExecutor(createAndStartNewForegroundLooper("UiThreadHelper"));
+
+    /**
+     * Utility method to get a started handler thread statically
+     */
+    public static Looper createAndStartNewLooper(String name) {
+        return createAndStartNewLooper(name, Process.THREAD_PRIORITY_DEFAULT);
+    }
+
+    /**
+     * Utility method to get a started handler thread statically with the provided priority
+     */
+    public static Looper createAndStartNewLooper(String name, int priority) {
+        HandlerThread thread = new HandlerThread(name, priority);
+        thread.start();
+        return thread.getLooper();
+    }
+
+    /**
+     * Similar to {@link #createAndStartNewLooper(String)}, but starts the thread with
+     * foreground priority.
+     * Think before using
+     */
+    public static Looper createAndStartNewForegroundLooper(String name) {
+        return createAndStartNewLooper(name, Process.THREAD_PRIORITY_FOREGROUND);
+    }
+
+    /**
+     * Executor used for running Launcher model related tasks (eg loading icons or updated db)
+     */
+    public static final LooperExecutor MODEL_EXECUTOR =
+            new LooperExecutor(createAndStartNewLooper("launcher-loader"));
+}
diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java
index f95f74d..4a4a5ca 100644
--- a/src/com/android/launcher3/util/IOUtils.java
+++ b/src/com/android/launcher3/util/IOUtils.java
@@ -17,10 +17,13 @@
 package com.android.launcher3.util;
 
 import android.content.Context;
+import android.util.Log;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 
 import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -35,6 +38,7 @@
 public class IOUtils {
 
     private static final int BUF_SIZE = 0x1000; // 4K
+    private static final String TAG = "IOUtils";
 
     public static byte[] toByteArray(File file) throws IOException {
         try (InputStream in = new FileInputStream(file)) {
@@ -77,4 +81,16 @@
         }
         return file.getAbsolutePath();
     }
+
+    public static void closeSilently(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException e) {
+                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                    Log.d(TAG, "Error closing", e);
+                }
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/util/IntArray.java b/src/com/android/launcher3/util/IntArray.java
index d2a551f..7252f7a 100644
--- a/src/com/android/launcher3/util/IntArray.java
+++ b/src/com/android/launcher3/util/IntArray.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.util;
 
 import java.util.Arrays;
+import java.util.StringTokenizer;
 
 /**
  * Copy of the platform hidden implementation of android.util.IntArray.
@@ -248,6 +249,17 @@
         return b.toString();
     }
 
+    public static IntArray fromConcatString(String concatString) {
+        StringTokenizer tokenizer = new StringTokenizer(concatString, ",");
+        int[] array = new int[tokenizer.countTokens()];
+        int count = 0;
+        while (tokenizer.hasMoreTokens()) {
+            array[count] = Integer.parseInt(tokenizer.nextToken().trim());
+            count++;
+        }
+        return new IntArray(array, array.length);
+    }
+
     /**
      * Throws {@link ArrayIndexOutOfBoundsException} if the index is out of bounds.
      *
diff --git a/src/com/android/launcher3/util/LooperExecutor.java b/src/com/android/launcher3/util/LooperExecutor.java
index cc07469..8ac600f 100644
--- a/src/com/android/launcher3/util/LooperExecutor.java
+++ b/src/com/android/launcher3/util/LooperExecutor.java
@@ -16,7 +16,9 @@
 package com.android.launcher3.util;
 
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.Looper;
+import android.os.Process;
 
 import java.util.List;
 import java.util.concurrent.AbstractExecutorService;
@@ -47,6 +49,13 @@
     }
 
     /**
+     * Same as execute, but never runs the action inline.
+     */
+    public void post(Runnable runnable) {
+        mHandler.post(runnable);
+    }
+
+    /**
      * Not supported and throws an exception when used.
      */
     @Override
@@ -79,7 +88,31 @@
      */
     @Override
     @Deprecated
-    public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
+    public boolean awaitTermination(long l, TimeUnit timeUnit) {
         throw new UnsupportedOperationException();
     }
+
+    /**
+     * Returns the thread for this executor
+     */
+    public Thread getThread() {
+        return mHandler.getLooper().getThread();
+    }
+
+    /**
+     * Returns the looper for this executor
+     */
+    public Looper getLooper() {
+        return mHandler.getLooper();
+    }
+
+    /**
+     * Set the priority of a thread, based on Linux priorities.
+     * @param priority Linux priority level, from -20 for highest scheduling priority
+     *                to 19 for lowest scheduling priority.
+     * @see Process#setThreadPriority(int, int)
+     */
+    public void setThreadPriority(int priority) {
+        Process.setThreadPriority(((HandlerThread) getThread()).getThreadId(), priority);
+    }
 }
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index e185a31..fe9c2c4 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -15,12 +15,13 @@
  */
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import android.content.Context;
 import android.os.Looper;
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
 
 import java.util.concurrent.ExecutionException;
@@ -43,7 +44,7 @@
                 mValue = mProvider.get(context.getApplicationContext());
             } else {
                 try {
-                    return new MainThreadExecutor().submit(() -> get(context)).get();
+                    return MAIN_EXECUTOR.submit(() -> get(context)).get();
                 } catch (InterruptedException|ExecutionException e) {
                     throw new RuntimeException(e);
                 }
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 7d3a941..e97adb5 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -24,9 +24,11 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Build;
@@ -35,6 +37,7 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.widget.Toast;
 
 import com.android.launcher3.AppInfo;
@@ -220,4 +223,76 @@
         packageFilter.addDataSchemeSpecificPart(pkg, PatternMatcher.PATTERN_LITERAL);
         return packageFilter;
     }
+
+    public static boolean isSystemApp(Context context, Intent intent) {
+        PackageManager pm = context.getPackageManager();
+        ComponentName cn = intent.getComponent();
+        String packageName = null;
+        if (cn == null) {
+            ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+            if ((info != null) && (info.activityInfo != null)) {
+                packageName = info.activityInfo.packageName;
+            }
+        } else {
+            packageName = cn.getPackageName();
+        }
+        if (packageName == null) {
+            packageName = intent.getPackage();
+        }
+        if (packageName != null) {
+            try {
+                PackageInfo info = pm.getPackageInfo(packageName, 0);
+                return (info != null) && (info.applicationInfo != null) &&
+                        ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
+            } catch (NameNotFoundException e) {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Finds a system apk which had a broadcast receiver listening to a particular action.
+     * @param action intent action used to find the apk
+     * @return a pair of apk package name and the resources.
+     */
+    public static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
+        final Intent intent = new Intent(action);
+        for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
+            if (info.activityInfo != null &&
+                    (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+                final String packageName = info.activityInfo.packageName;
+                try {
+                    final Resources res = pm.getResourcesForApplication(packageName);
+                    return Pair.create(packageName, res);
+                } catch (NameNotFoundException e) {
+                    Log.w(TAG, "Failed to find resources for " + packageName);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the intent is a valid launch intent for a launcher activity of an app.
+     * This is used to identify shortcuts which are different from the ones exposed by the
+     * applications' manifest file.
+     *
+     * @param launchIntent The intent that will be launched when the shortcut is clicked.
+     */
+    public static boolean isLauncherAppTarget(Intent launchIntent) {
+        if (launchIntent != null
+                && Intent.ACTION_MAIN.equals(launchIntent.getAction())
+                && launchIntent.getComponent() != null
+                && launchIntent.getCategories() != null
+                && launchIntent.getCategories().size() == 1
+                && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER)
+                && TextUtils.isEmpty(launchIntent.getDataString())) {
+            // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE.
+            Bundle extras = launchIntent.getExtras();
+            return extras == null || extras.keySet().isEmpty();
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/util/PackageUserKey.java b/src/com/android/launcher3/util/PackageUserKey.java
index aa11968..f243ca6 100644
--- a/src/com/android/launcher3/util/PackageUserKey.java
+++ b/src/com/android/launcher3/util/PackageUserKey.java
@@ -6,7 +6,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.ItemInfo;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 
 import java.util.Arrays;
 
diff --git a/src/com/android/launcher3/util/Preconditions.java b/src/com/android/launcher3/util/Preconditions.java
index 7ab0d31..ed66422 100644
--- a/src/com/android/launcher3/util/Preconditions.java
+++ b/src/com/android/launcher3/util/Preconditions.java
@@ -16,9 +16,10 @@
 
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.os.Looper;
 
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.config.FeatureFlags;
 
 /**
@@ -33,7 +34,7 @@
     }
 
     public static void assertWorkerThread() {
-        if (FeatureFlags.IS_DOGFOOD_BUILD && !isSameLooper(LauncherModel.getWorkerLooper())) {
+        if (FeatureFlags.IS_DOGFOOD_BUILD && !isSameLooper(MODEL_EXECUTOR.getLooper())) {
             throw new IllegalStateException();
         }
     }
diff --git a/src/com/android/launcher3/util/SafeCloseable.java b/src/com/android/launcher3/util/SafeCloseable.java
new file mode 100644
index 0000000..ba8ee04
--- /dev/null
+++ b/src/com/android/launcher3/util/SafeCloseable.java
@@ -0,0 +1,26 @@
+/*
+ * 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.util;
+
+/**
+ * Extension of closeable which does not throw an exception
+ */
+public interface SafeCloseable extends AutoCloseable {
+
+    @Override
+    void close();
+}
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index cc442f9..f8d1632 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -15,14 +15,13 @@
  */
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.app.Activity;
 import android.content.Context;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.Message;
-import android.os.Process;
 import android.view.inputmethod.InputMethodManager;
 
 /**
@@ -30,25 +29,15 @@
  */
 public class UiThreadHelper {
 
-    private static HandlerThread sHandlerThread;
     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) {
-            sHandlerThread =
-                    new HandlerThread("UiThreadHelper", Process.THREAD_PRIORITY_FOREGROUND);
-            sHandlerThread.start();
-        }
-        return sHandlerThread.getLooper();
-    }
-
     private static Handler getHandler(Context context) {
         if (sHandler == null) {
-            sHandler = new Handler(getBackgroundLooper(),
+            sHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(),
                     new UiCallbacks(context.getApplicationContext()));
         }
         return sHandler;
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
new file mode 100644
index 0000000..04741a1
--- /dev/null
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.util;
+
+import static android.os.VibrationEffect.createPredefined;
+import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Build;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+
+/**
+ * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class VibratorWrapper {
+
+    public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
+            new MainThreadInitializedObject<>(VibratorWrapper::new);
+
+    private static final VibrationEffect EFFECT_CLICK =
+            createPredefined(VibrationEffect.EFFECT_CLICK);
+
+    /**
+     * Haptic when entering overview.
+     */
+    public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
+
+    private final Vibrator mVibrator;
+    private final boolean mHasVibrator;
+
+    private boolean mIsHapticFeedbackEnabled;
+
+    public VibratorWrapper(Context context) {
+        mVibrator = context.getSystemService(Vibrator.class);
+        mHasVibrator = mVibrator.hasVibrator();
+        if (mHasVibrator) {
+            final ContentResolver resolver = context.getContentResolver();
+            mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
+            final ContentObserver observer = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
+                }
+            };
+            resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
+                    false /* notifyForDescendents */, observer);
+        } else {
+            mIsHapticFeedbackEnabled = false;
+        }
+    }
+
+    private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
+        return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
+    }
+
+    /** Vibrates with the given effect if haptic feedback is available and enabled. */
+    public void vibrate(VibrationEffect vibrationEffect) {
+        if (mHasVibrator && mIsHapticFeedbackEnabled) {
+            UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect));
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index acce308..5a131c8 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -16,13 +16,14 @@
 
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
 import android.os.Process;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherModel;
 
 import java.util.ArrayList;
 import java.util.concurrent.Executor;
@@ -54,7 +55,9 @@
             mLoadAnimationCompleted = true;
         }
 
-        attachObserver();
+        if (mAttachedView.isAttachedToWindow()) {
+            attachObserver();
+        }
     }
 
     private void attachObserver() {
@@ -66,7 +69,7 @@
     @Override
     public void execute(Runnable command) {
         mTasks.add(command);
-        LauncherModel.setWorkerPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
     }
 
     @Override
@@ -108,7 +111,7 @@
         if (mLauncher != null) {
             mLauncher.clearPendingExecutor(this);
         }
-        LauncherModel.setWorkerPriority(Process.THREAD_PRIORITY_DEFAULT);
+        MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
     }
 
     protected boolean isCompleted() {
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index 8af048d..5b33f18 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -21,12 +21,12 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.launcher3.util.ViewPool.Reusable;
-
 import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
+import com.android.launcher3.util.ViewPool.Reusable;
+
 /**
  * Utility class to maintain a pool of reusable views.
  * During initialization, views are inflated on the background thread.
@@ -58,14 +58,18 @@
         Preconditions.assertUIThread();
         Handler handler = new Handler();
 
+        // LayoutInflater is not thread save as it maintains a global variable 'mConstructorArgs'.
+        // Create a different copy to use on the background thread.
+        LayoutInflater inflater = mInflater.cloneInContext(mInflater.getContext());
+
         // Inflate views on a non looper thread. This allows us to catch errors like calling
         // "new Handler()" in constructor easily.
         new Thread(() -> {
             for (int i = 0; i < initialSize; i++) {
-                T view = inflateNewView();
+                T view = inflateNewView(inflater);
                 handler.post(() -> addToPool(view));
             }
-        }).start();
+        }, "ViewPool-init").start();
     }
 
     @UiThread
@@ -94,12 +98,12 @@
             mCurrentSize--;
             return (T) mPool[mCurrentSize];
         }
-        return inflateNewView();
+        return inflateNewView(mInflater);
     }
 
     @AnyThread
-    private T inflateNewView() {
-        return (T) mInflater.inflate(mLayoutId, mParent, false);
+    private T inflateNewView(LayoutInflater inflater) {
+        return (T) inflater.inflate(mLayoutId, mParent, false);
     }
 
     /**
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index 5c24687..2ad80cf 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -1,5 +1,7 @@
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.app.WallpaperManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -198,7 +200,7 @@
         private float mOffsetX;
 
         public OffsetHandler(Context context) {
-            super(UiThreadHelper.getBackgroundLooper());
+            super(UI_HELPER_EXECUTOR.getLooper());
             mInterpolator = Interpolators.DEACCEL_1_5;
             mWM = WallpaperManager.getInstance(context);
         }
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index a4518ba..195a77a 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -32,13 +32,14 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 
 /**
  * Extension of AbstractFloatingView with common methods for sliding in from bottom
  */
 public abstract class AbstractSlideInView extends AbstractFloatingView
-        implements SwipeDetector.Listener {
+        implements SingleAxisSwipeDetector.Listener {
 
     protected static Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
             new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
@@ -57,7 +58,7 @@
     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
 
     protected final Launcher mLauncher;
-    protected final SwipeDetector mSwipeDetector;
+    protected final SingleAxisSwipeDetector mSwipeDetector;
     protected final ObjectAnimator mOpenCloseAnimator;
 
     protected View mContent;
@@ -73,7 +74,8 @@
         mLauncher = Launcher.getLauncher(context);
 
         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
-        mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
+        mSwipeDetector = new SingleAxisSwipeDetector(context, this,
+                SingleAxisSwipeDetector.VERTICAL);
 
         mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@@ -97,7 +99,7 @@
         }
 
         int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
-                SwipeDetector.DIRECTION_NEGATIVE : 0;
+                SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
         mSwipeDetector.setDetectableScrollConditions(
                 directionsToDetectScroll, false);
         mSwipeDetector.onTouchEvent(ev);
@@ -122,7 +124,7 @@
         return mIsOpen && mOpenCloseAnimator.isRunning();
     }
 
-    /* SwipeDetector.Listener */
+    /* SingleAxisSwipeDetector.Listener */
 
     @Override
     public void onDragStart(boolean start) { }
@@ -136,17 +138,17 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        if ((fling && velocity > 0) || mTranslationShift > 0.5f) {
+    public void onDragEnd(float velocity) {
+        if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) {
             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
-            mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(
+            mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
                     velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
             close(true);
         } else {
             mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
                     TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
             mOpenCloseAnimator.setDuration(
-                    SwipeDetector.calculateDuration(velocity, mTranslationShift))
+                    BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
                     .setInterpolator(Interpolators.DEACCEL);
             mOpenCloseAnimator.start();
         }
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index c08b659..e43fc8a 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -170,10 +170,8 @@
             // Only look for controllers if we are not dispatching from gesture area and proxy is
             // not active
             mActiveController = findControllerToHandleTouch(ev);
-
-            if (mActiveController != null) return true;
         }
-        return false;
+        return mActiveController != null;
     }
 
     @Override
@@ -465,7 +463,7 @@
     }
 
     public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "DragLayer");
+        writer.println(prefix + "DragLayer:");
         if (mActiveController != null) {
             writer.println(prefix + "\tactiveController: " + mActiveController);
             mActiveController.dump(prefix + "\t", writer);
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index f728a67..49d94f0 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -41,7 +42,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.CancellationSignal;
-import android.os.Handler;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -50,11 +50,17 @@
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.widget.ImageView;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.InsettableFrameLayout.LayoutParams;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherModel;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -66,13 +72,6 @@
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-import androidx.dynamicanimation.animation.FloatPropertyCompat;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
 /**
  * A view that is created to look like another view with the purpose of creating fluid animations.
  */
@@ -561,7 +560,7 @@
      * Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a
      * callback to set the icon once the icon result is loaded.
      */
-    private void checkIconResult(View originalView, boolean isOpening) {
+    private void checkIconResult(View originalView) {
         CancellationSignal cancellationSignal = new CancellationSignal();
 
         if (mIconLoadResult == null) {
@@ -573,9 +572,7 @@
             if (mIconLoadResult.isIconLoaded) {
                 setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
                         mIconLoadResult.iconOffset);
-                if (isOpening) {
-                    hideOriginalView(originalView);
-                }
+                hideOriginalView(originalView);
             } else {
                 mIconLoadResult.onIconLoaded = () -> {
                     if (cancellationSignal.isCanceled()) {
@@ -584,12 +581,8 @@
 
                     setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
                             mIconLoadResult.iconOffset);
-
                     setVisibility(VISIBLE);
-                    if (isOpening) {
-                        // Delay swapping views until the icon is loaded to prevent a flash.
-                        hideOriginalView(originalView);
-                    }
+                    hideOriginalView(originalView);
                 };
                 mLoadIconSignal = cancellationSignal;
             }
@@ -597,9 +590,9 @@
     }
 
     private void hideOriginalView(View originalView) {
-        if (originalView instanceof BubbleTextView) {
-            ((BubbleTextView) originalView).setIconVisible(false);
-            ((BubbleTextView) originalView).setForceHideDot(true);
+        if (originalView instanceof IconLabelDotView) {
+            ((IconLabelDotView) originalView).setIconVisible(false);
+            ((IconLabelDotView) originalView).setForceHideDot(true);
         } else {
             originalView.setVisibility(INVISIBLE);
         }
@@ -675,6 +668,9 @@
     }
 
     public void fastFinish() {
+        if (mLoadIconSignal != null) {
+            mLoadIconSignal.cancel();
+        }
         if (mEndRunnable != null) {
             mEndRunnable.run();
             mEndRunnable = null;
@@ -690,6 +686,10 @@
         if (mIconLoadResult != null && mIconLoadResult.isIconLoaded) {
             setVisibility(View.VISIBLE);
         }
+        if (!mIsOpening) {
+            // When closing an app, we want the item on the workspace to be invisible immediately
+            hideOriginalView(mOriginalIcon);
+        }
     }
 
     @Override
@@ -722,7 +722,7 @@
     @UiThread
     public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) {
         IconLoadResult result = new IconLoadResult(info);
-        new Handler(LauncherModel.getWorkerLooper()).postAtFrontOfQueue(() -> {
+        MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
             RectF position = new RectF();
             getLocationBoundsForView(l, v, isOpening, position);
             getIconResult(l, v, info, position, result);
@@ -768,11 +768,6 @@
         // Match the position of the original view.
         view.matchPositionOf(launcher, originalView, isOpening, positionOut);
 
-        // Must be called after matchPositionOf so that we know what size to load.
-        if (shouldLoadIcon) {
-            view.checkIconResult(originalView, isOpening);
-        }
-
         // We need to add it to the overlay, but keep it invisible until animation starts..
         view.setVisibility(INVISIBLE);
         parent.addView(view);
@@ -799,6 +794,14 @@
                 view.finish(dragLayer);
             }
         };
+
+        // Must be called after matchPositionOf so that we know what size to load.
+        // Must be called after the fastFinish listener and end runnable is created so that
+        // the icon is not left in a hidden state.
+        if (shouldLoadIcon) {
+            view.checkIconResult(originalView);
+        }
+
         return view;
     }
 
@@ -840,6 +843,7 @@
                 @Override
                 public void onAnimationStart(Animator animation) {
                     btv.setIconVisible(true);
+                    btv.setForceHideDot(true);
                 }
             });
             fade.play(ObjectAnimator.ofInt(btv.getIcon(), DRAWABLE_ALPHA, 0, 255));
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index da1df3f..9f59d78 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -288,6 +288,7 @@
             anim.addUpdateListener((v) -> invalidate(invalidateRegion));
             getOverlay().add(drawable);
             anim.start();
+            return true;
         }
         return value;
     }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index dce839f..f3fd7ca 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
 
@@ -96,7 +97,7 @@
         setBackgroundResource(R.drawable.widget_internal_focus_bg);
 
         if (Utilities.ATLEAST_OREO) {
-            setExecutor(Utilities.THREAD_POOL_EXECUTOR);
+            setExecutor(Executors.THREAD_POOL_EXECUTOR);
         }
         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
@@ -332,12 +333,7 @@
         if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
             mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
             if (mAutoAdvanceRunnable == null) {
-                mAutoAdvanceRunnable = new Runnable() {
-                    @Override
-                    public void run() {
-                        runAutoAdvance();
-                    }
-                };
+                mAutoAdvanceRunnable = this::runAutoAdvance;
             }
 
             handler.removeCallbacks(mAutoAdvanceRunnable);
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index dc4af8c..6944879 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.graphics.DrawableFactory;
+import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.model.WidgetItem;
 
 /**
@@ -80,6 +81,7 @@
     private Bitmap mDeferredBitmap;
 
     protected final BaseActivity mActivity;
+    protected DeviceProfile mDeviceProfile;
 
     public WidgetCell(Context context) {
         this(context, null);
@@ -93,6 +95,7 @@
         super(context, attrs, defStyle);
 
         mActivity = BaseActivity.fromContext(context);
+        mDeviceProfile = mActivity.getDeviceProfile();
         mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
 
         setContainerWidth();
@@ -102,8 +105,7 @@
     }
 
     private void setContainerWidth() {
-        DeviceProfile profile = mActivity.getDeviceProfile();
-        mCellSize = (int) (profile.cellWidthPx * WIDTH_SCALE);
+        mCellSize = (int) (mDeviceProfile.allAppsCellWidthPx * WIDTH_SCALE);
         mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
     }
 
@@ -180,8 +182,10 @@
             return;
         }
         if (bitmap != null) {
-            mWidgetImage.setBitmap(bitmap, DrawableFactory.INSTANCE.get(getContext())
-                    .getBadgeForUser(mItem.user, getContext()));
+            mWidgetImage.setBitmap(bitmap,
+                    DrawableFactory.INSTANCE.get(getContext()).getBadgeForUser(mItem.user,
+                            getContext(), BaseIconFactory.getBadgeSizeForIconSize(
+                                    mDeviceProfile.allAppsIconSizePx)));
             if (mAnimatePreview) {
                 mWidgetImage.setAlpha(0f);
                 ViewPropertyAnimator anim = mWidgetImage.animate();
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
new file mode 100644
index 0000000..f20c83d
--- /dev/null
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -0,0 +1,178 @@
+/*
+ * 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.widget.custom;
+
+import static com.android.launcher3.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Process;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.systemui.plugins.CustomWidgetPlugin;
+import com.android.systemui.plugins.PluginListener;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * CustomWidgetManager handles custom widgets implemented as a plugin.
+ */
+public class CustomWidgetManager implements PluginListener<CustomWidgetPlugin> {
+
+    public static final MainThreadInitializedObject<CustomWidgetManager> INSTANCE =
+            new MainThreadInitializedObject<>(CustomWidgetManager::new);
+
+    /**
+     * auto provider Id is an ever-increasing number that serves as the providerId whenever a new
+     * custom widget has been connected.
+     */
+    private int mAutoProviderId = 0;
+    private final SparseArray<CustomWidgetPlugin> mPlugins;
+    private final SparseArray<WeakReference<Context>> mContexts;
+    private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
+    private final SparseArray<ComponentName> mWidgetsIdMap;
+    private Consumer<PackageUserKey> mWidgetRefreshCallback;
+
+    private CustomWidgetManager(Context context) {
+        mPlugins = new SparseArray<>();
+        mContexts = new SparseArray<>();
+        mCustomWidgets = new ArrayList<>();
+        mWidgetsIdMap = new SparseArray<>();
+        PluginManagerWrapper.INSTANCE.get(context)
+                .addPluginListener(this, CustomWidgetPlugin.class, true);
+    }
+
+    @Override
+    public void onPluginConnected(CustomWidgetPlugin plugin, Context context) {
+        mPlugins.put(mAutoProviderId, plugin);
+        mContexts.put(mAutoProviderId, new WeakReference<>(context));
+        List<AppWidgetProviderInfo> providers = AppWidgetManager.getInstance(context)
+                .getInstalledProvidersForProfile(Process.myUserHandle());
+        if (providers.isEmpty()) return;
+        Parcel parcel = Parcel.obtain();
+        providers.get(0).writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        CustomAppWidgetProviderInfo info = newInfo(mAutoProviderId, plugin, parcel, context);
+        parcel.recycle();
+        mCustomWidgets.add(info);
+        mWidgetsIdMap.put(mAutoProviderId, info.provider);
+        mWidgetRefreshCallback.accept(null);
+        mAutoProviderId++;
+    }
+
+    @Override
+    public void onPluginDisconnected(CustomWidgetPlugin plugin) {
+        int providerId = findProviderId(plugin);
+        if (providerId == -1) return;
+        mPlugins.remove(providerId);
+        mContexts.remove(providerId);
+        mCustomWidgets.remove(getWidgetProvider(providerId));
+        mWidgetsIdMap.remove(providerId);
+    }
+
+    /**
+     * Inject a callback function to refresh the widgets.
+     */
+    public void setWidgetRefreshCallback(Consumer<PackageUserKey> cb) {
+        mWidgetRefreshCallback = cb;
+    }
+
+    /**
+     * Callback method to inform a plugin it's corresponding widget has been created.
+     */
+    public void onViewCreated(LauncherAppWidgetHostView view) {
+        CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo();
+        CustomWidgetPlugin plugin = mPlugins.get(info.providerId);
+        WeakReference<Context> context = mContexts.get(info.providerId);
+        if (plugin == null) return;
+        plugin.onViewCreated(context == null ? null : context.get(), view);
+    }
+
+    /**
+     * Returns the list of custom widgets.
+     */
+    @NonNull
+    public List<CustomAppWidgetProviderInfo> getCustomWidgets() {
+        return mCustomWidgets;
+    }
+
+    /**
+     * Returns the widget id for a specific provider.
+     */
+    public int getWidgetIdForCustomProvider(@NonNull ComponentName provider) {
+        int index = mWidgetsIdMap.indexOfValue(provider);
+        if (index >= 0) {
+            return LauncherAppWidgetInfo.CUSTOM_WIDGET_ID - mWidgetsIdMap.keyAt(index);
+        } else {
+            return AppWidgetManager.INVALID_APPWIDGET_ID;
+        }
+    }
+
+    /**
+     * Returns the widget provider in respect to given widget id.
+     */
+    @Nullable
+    public LauncherAppWidgetProviderInfo getWidgetProvider(int widgetId) {
+        ComponentName cn = mWidgetsIdMap.get(LauncherAppWidgetInfo.CUSTOM_WIDGET_ID - widgetId);
+        for (LauncherAppWidgetProviderInfo info : mCustomWidgets) {
+            if (info.provider.equals(cn)) return info;
+        }
+        return null;
+    }
+
+    private static CustomAppWidgetProviderInfo newInfo(int providerId, CustomWidgetPlugin plugin,
+            Parcel parcel, Context context) {
+        CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(
+                parcel, false, providerId);
+        info.provider = new ComponentName(
+                context.getPackageName(), CLS_CUSTOM_WIDGET_PREFIX + providerId);
+
+        info.label = plugin.getLabel(context);
+        info.resizeMode = plugin.getResizeMode(context);
+
+        info.spanX = plugin.getSpanX(context);
+        info.spanY = plugin.getSpanY(context);
+        info.minSpanX = plugin.getMinSpanX(context);
+        info.minSpanY = plugin.getMinSpanY(context);
+        return info;
+    }
+
+    private int findProviderId(CustomWidgetPlugin plugin) {
+        for (int i = 0; i < mPlugins.size(); i++) {
+            int providerId = mPlugins.keyAt(i);
+            if (mPlugins.get(providerId) == plugin) {
+                return providerId;
+            }
+        }
+        return -1;
+    }
+}
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetParser.java b/src/com/android/launcher3/widget/custom/CustomWidgetParser.java
deleted file mode 100644
index 00720c4..0000000
--- a/src/com/android/launcher3/widget/custom/CustomWidgetParser.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2017 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.widget.custom;
-
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.os.Parcel;
-import android.os.Process;
-import android.util.SparseArray;
-import android.util.Xml;
-
-import com.android.launcher3.LauncherAppWidgetInfo;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.R;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.android.launcher3.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
-
-/**
- * Utility class to parse {@ink CustomAppWidgetProviderInfo} definitions from xml
- */
-public class CustomWidgetParser {
-
-    private static List<LauncherAppWidgetProviderInfo> sCustomWidgets;
-    private static SparseArray<ComponentName> sWidgetsIdMap;
-
-    public static List<LauncherAppWidgetProviderInfo> getCustomWidgets(Context context) {
-        if (sCustomWidgets == null) {
-            // Synchronization not needed as it it safe to load multiple times
-            parseCustomWidgets(context);
-        }
-
-        return sCustomWidgets;
-    }
-
-    public static int getWidgetIdForCustomProvider(Context context, ComponentName provider) {
-        if (sWidgetsIdMap == null) {
-            parseCustomWidgets(context);
-        }
-        int index = sWidgetsIdMap.indexOfValue(provider);
-        if (index >= 0) {
-            return LauncherAppWidgetInfo.CUSTOM_WIDGET_ID - sWidgetsIdMap.keyAt(index);
-        } else {
-            return AppWidgetManager.INVALID_APPWIDGET_ID;
-        }
-    }
-
-    public static LauncherAppWidgetProviderInfo getWidgetProvider(Context context, int widgetId) {
-        if (sWidgetsIdMap == null || sCustomWidgets == null) {
-            parseCustomWidgets(context);
-        }
-        ComponentName cn = sWidgetsIdMap.get(LauncherAppWidgetInfo.CUSTOM_WIDGET_ID - widgetId);
-        for (LauncherAppWidgetProviderInfo info : sCustomWidgets) {
-            if (info.provider.equals(cn)) {
-                return info;
-            }
-        }
-        return null;
-    }
-
-    private static void parseCustomWidgets(Context context) {
-        ArrayList<LauncherAppWidgetProviderInfo> widgets = new ArrayList<>();
-        SparseArray<ComponentName> idMap = new SparseArray<>();
-
-        List<AppWidgetProviderInfo> providers = AppWidgetManager.getInstance(context)
-                .getInstalledProvidersForProfile(Process.myUserHandle());
-        if (providers.isEmpty()) {
-            sCustomWidgets = widgets;
-            sWidgetsIdMap = idMap;
-            return;
-        }
-
-        Parcel parcel = Parcel.obtain();
-        providers.get(0).writeToParcel(parcel, 0);
-
-        try (XmlResourceParser parser = context.getResources().getXml(R.xml.custom_widgets)) {
-            final int depth = parser.getDepth();
-            int type;
-
-            while (((type = parser.next()) != XmlPullParser.END_TAG ||
-                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
-                if ((type == XmlPullParser.START_TAG) && "widget".equals(parser.getName())) {
-                    TypedArray a = context.obtainStyledAttributes(
-                            Xml.asAttributeSet(parser), R.styleable.CustomAppWidgetProviderInfo);
-
-                    parcel.setDataPosition(0);
-                    CustomAppWidgetProviderInfo info = newInfo(a, parcel, context);
-                    widgets.add(info);
-                    a.recycle();
-
-                    idMap.put(info.providerId, info.provider);
-                }
-            }
-        } catch (IOException | XmlPullParserException e) {
-            throw new RuntimeException(e);
-        }
-        parcel.recycle();
-        sCustomWidgets = widgets;
-        sWidgetsIdMap = idMap;
-    }
-
-    private static CustomAppWidgetProviderInfo newInfo(TypedArray a, Parcel parcel, Context context) {
-        int providerId = a.getInt(R.styleable.CustomAppWidgetProviderInfo_providerId, 0);
-        CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false, providerId);
-        info.provider = new ComponentName(context.getPackageName(), CLS_CUSTOM_WIDGET_PREFIX + providerId);
-
-        info.label = a.getString(R.styleable.CustomAppWidgetProviderInfo_android_label);
-        info.initialLayout = a.getResourceId(R.styleable.CustomAppWidgetProviderInfo_android_initialLayout, 0);
-        info.icon = a.getResourceId(R.styleable.CustomAppWidgetProviderInfo_android_icon, 0);
-        info.previewImage = a.getResourceId(R.styleable.CustomAppWidgetProviderInfo_android_previewImage, 0);
-        info.resizeMode = a.getInt(R.styleable.CustomAppWidgetProviderInfo_android_resizeMode, 0);
-
-        info.spanX = a.getInt(R.styleable.CustomAppWidgetProviderInfo_numColumns, 1);
-        info.spanY = a.getInt(R.styleable.CustomAppWidgetProviderInfo_numRows, 1);
-        info.minSpanX = a.getInt(R.styleable.CustomAppWidgetProviderInfo_numMinColumns, 1);
-        info.minSpanY = a.getInt(R.styleable.CustomAppWidgetProviderInfo_numMinRows, 1);
-        return info;
-    }
-}
diff --git a/src_plugins/com/android/systemui/plugins/AppLaunchEventsPlugin.java b/src_plugins/com/android/systemui/plugins/AppLaunchEventsPlugin.java
new file mode 100644
index 0000000..15a0ffa
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/AppLaunchEventsPlugin.java
@@ -0,0 +1,54 @@
+/*
+ * 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.systemui.plugins;
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Plugin interface which sends app launch events.
+ */
+@ProvidesInterface(action = AppLaunchEventsPlugin.ACTION, version = AppLaunchEventsPlugin.VERSION)
+public interface AppLaunchEventsPlugin extends Plugin {
+    String ACTION = "com.android.systemui.action.PLUGIN_APP_EVENTS";
+    int VERSION = 1;
+
+    /**
+     * Receives onStartShortcut event from
+     * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
+     */
+    void onStartShortcut(String packageName, String shortcutId, UserHandle user, String container);
+
+    /**
+     * Receives onStartApp event from
+     * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
+     */
+    void onStartApp(ComponentName componentName, UserHandle user, String container);
+
+    /**
+     * Receives onDismissApp event from
+     * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
+     */
+    void onDismissApp(ComponentName componentName, UserHandle user, String container);
+
+    /**
+     * Receives onReturnedToHome event from
+     * {@link com.android.launcher3.appprediction.PredictionAppTracker}.
+     */
+    void onReturnedToHome();
+}
diff --git a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
new file mode 100644
index 0000000..47aa94b
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
@@ -0,0 +1,72 @@
+/*
+ * 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.systemui.plugins;
+
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this plugin interface to add a custom widget.
+ */
+@ProvidesInterface(action = CustomWidgetPlugin.ACTION, version = CustomWidgetPlugin.VERSION)
+public interface CustomWidgetPlugin extends Plugin {
+
+    String ACTION = "com.android.systemui.action.PLUGIN_CUSTOM_WIDGET";
+    int VERSION = 1;
+
+    /**
+     * The label to display to the user in the AppWidget picker.
+     */
+    String getLabel(Context context);
+
+    /**
+     * The default width of the widget when added to a host, in dp. The widget will get
+     * at least this width, and will often be given more, depending on the host.
+     */
+    int getSpanX(Context context);
+
+    /**
+     * The default height of the widget when added to a host, in dp. The widget will get
+     * at least this height, and will often be given more, depending on the host.
+     */
+    int getSpanY(Context context);
+
+    /**
+     * Minimum width (in dp) which the widget can be resized to. This field has no effect if it
+     * is greater than minWidth or if horizontal resizing isn't enabled.
+     */
+    int getMinSpanX(Context context);
+
+    /**
+     * Minimum height (in dp) which the widget can be resized to. This field has no effect if it
+     * is greater than minHeight or if vertical resizing isn't enabled.
+     */
+    int getMinSpanY(Context context);
+
+    /**
+     * The rules by which a widget can be resized.
+     */
+    int getResizeMode(Context context);
+
+    /**
+     * Notify the plugin that container of the widget has been rendered, where the custom widget
+     * can be attached to.
+     */
+    void onViewCreated(Context context, AppWidgetHostView parent);
+}
diff --git a/src_plugins/com/android/systemui/plugins/HotseatPlugin.java b/src_plugins/com/android/systemui/plugins/HotseatPlugin.java
new file mode 100644
index 0000000..1264e0d
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/HotseatPlugin.java
@@ -0,0 +1,20 @@
+package com.android.systemui.plugins;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this plugin interface to add a sub-view in the Hotseat.
+ */
+@ProvidesInterface(action = HotseatPlugin.ACTION, version = HotseatPlugin.VERSION)
+public interface HotseatPlugin extends Plugin {
+    String ACTION = "com.android.systemui.action.PLUGIN_HOTSEAT";
+    int VERSION = 1;
+
+    /**
+     * Creates a plugin view which will be added to the Hotseat.
+     */
+    View createView(ViewGroup parent);
+}
diff --git a/src_plugins/com/android/systemui/plugins/OverviewScreenshotActions.java b/src_plugins/com/android/systemui/plugins/OverviewScreenshotActions.java
new file mode 100644
index 0000000..8d9c0f4
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/OverviewScreenshotActions.java
@@ -0,0 +1,41 @@
+/*
+ * 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.systemui.plugins;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.view.ViewGroup;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this interface to add action buttons for overview screenshots, e.g. share, edit etc.
+ */
+@ProvidesInterface(
+        action = OverviewScreenshotActions.ACTION, version = OverviewScreenshotActions.VERSION)
+public interface OverviewScreenshotActions extends Plugin {
+    String ACTION = "com.android.systemui.action.PLUGIN_OVERVIEW_SCREENSHOT_ACTIONS";
+    int VERSION = 1;
+
+    /**
+     * Setup the actions for the screenshot, including edit, save, etc.
+     * @param parent The parent view to add buttons on.
+     * @param screenshot The screenshot we will do actions on.
+     * @param activity THe host activity.
+     */
+    void setupActions(ViewGroup parent, Bitmap screenshot, Activity activity);
+}
diff --git a/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java b/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
new file mode 100644
index 0000000..0ebea3d
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
@@ -0,0 +1,42 @@
+/*
+ * 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.systemui.plugins;
+
+import android.app.Activity;
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this interface to allow extra card on recents overview.
+ */
+@ProvidesInterface(action = RecentsExtraCard.ACTION, version = RecentsExtraCard.VERSION)
+public interface RecentsExtraCard extends Plugin {
+
+    String ACTION = "com.android.systemui.action.PLUGIN_RECENTS_EXTRA_CARD";
+    int VERSION = 1;
+
+    /**
+     * Sets up the recents overview extra card and fills in data.
+     *
+     * @param context Plugin context
+     * @param frameLayout PlaceholderView
+     * @param activity Recents activity to hold extra view
+     */
+    void setupView(Context context, FrameLayout frameLayout, Activity activity);
+}
diff --git a/src_plugins/com/android/systemui/plugins/UserEventPlugin.java b/src_plugins/com/android/systemui/plugins/UserEventPlugin.java
new file mode 100644
index 0000000..0e3664a
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/UserEventPlugin.java
@@ -0,0 +1,34 @@
+/*
+ * 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.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this plugin interface to access user event log on the device for prototype purpose.
+ * NOTE: plugin is for internal prototype only and is not visible in production environment.
+ */
+@ProvidesInterface(action = UserEventPlugin.ACTION, version = UserEventPlugin.VERSION)
+public interface UserEventPlugin extends Plugin {
+    String ACTION = "com.android.launcher3.action.PLUGIN_USER_EVENT_LOG";
+    int VERSION = 1;
+
+    /**
+     * Callback to be triggered whenever an user event occurs.
+     */
+    void onUserEvent(Object event);
+}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 1710aef..789bfd8 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -16,9 +16,8 @@
 
 package com.android.launcher3.model;
 
-import com.android.launcher3.AllAppsList;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
diff --git a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
index 09b1890..57f4164 100644
--- a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ b/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -43,35 +43,27 @@
             | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
 
     private static DeepShortcutManager sInstance;
-    private static final Object sInstanceLock = new Object();
 
     public static DeepShortcutManager getInstance(Context context) {
-        synchronized (sInstanceLock) {
-            if (sInstance == null) {
-                sInstance = new DeepShortcutManager(context.getApplicationContext());
-            }
-            return sInstance;
+        if (sInstance == null) {
+            sInstance = new DeepShortcutManager(context.getApplicationContext());
         }
+        return sInstance;
     }
 
     private final LauncherApps mLauncherApps;
-    private boolean mWasLastCallSuccess;
 
     private DeepShortcutManager(Context context) {
         mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
     }
 
-    public boolean wasLastCallSuccess() {
-        return mWasLastCallSuccess;
-    }
-
     /**
      * Queries for the shortcuts with the package name and provided ids.
      *
      * This method is intended to get the full details for shortcuts when they are added or updated,
      * because we only get "key" fields in onShortcutsChanged().
      */
-    public List<ShortcutInfo> queryForFullDetails(String packageName,
+    public QueryResult queryForFullDetails(String packageName,
             List<String> shortcutIds, UserHandle user) {
         return query(FLAG_GET_ALL, packageName, null, shortcutIds, user);
     }
@@ -80,9 +72,9 @@
      * Gets all the manifest and dynamic shortcuts associated with the given package and user,
      * to be displayed in the shortcuts container on long press.
      */
-    public List<ShortcutInfo> queryForShortcutsContainer(@Nullable ComponentName activity,
+    public QueryResult queryForShortcutsContainer(@Nullable ComponentName activity,
             UserHandle user) {
-        if (activity == null) return Collections.EMPTY_LIST;
+        if (activity == null) return QueryResult.FAILURE;
         return query(ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_DYNAMIC,
                 activity.getPackageName(), activity, null, user);
     }
@@ -99,10 +91,8 @@
         pinnedIds.remove(id);
         try {
             mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
-            mWasLastCallSuccess = true;
         } catch (SecurityException|IllegalStateException e) {
             Log.w(TAG, "Failed to unpin shortcut", e);
-            mWasLastCallSuccess = false;
         }
     }
 
@@ -118,10 +108,8 @@
         pinnedIds.add(id);
         try {
             mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
-            mWasLastCallSuccess = true;
         } catch (SecurityException|IllegalStateException e) {
             Log.w(TAG, "Failed to pin shortcut", e);
-            mWasLastCallSuccess = false;
         }
     }
 
@@ -130,23 +118,18 @@
         try {
             mLauncherApps.startShortcut(packageName, id, sourceBounds,
                     startActivityOptions, user);
-            mWasLastCallSuccess = true;
         } catch (SecurityException|IllegalStateException e) {
             Log.e(TAG, "Failed to start shortcut", e);
-            mWasLastCallSuccess = false;
         }
     }
 
     public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
         try {
-            Drawable icon = mLauncherApps.getShortcutIconDrawable(shortcutInfo, density);
-            mWasLastCallSuccess = true;
-            return icon;
+            return mLauncherApps.getShortcutIconDrawable(shortcutInfo, density);
         } catch (SecurityException|IllegalStateException e) {
             Log.e(TAG, "Failed to get shortcut icon", e);
-            mWasLastCallSuccess = false;
+            return null;
         }
-        return null;
     }
 
     /**
@@ -154,20 +137,20 @@
      *
      * If packageName is null, returns all pinned shortcuts regardless of package.
      */
-    public List<ShortcutInfo> queryForPinnedShortcuts(String packageName, UserHandle user) {
+    public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
         return queryForPinnedShortcuts(packageName, null, user);
     }
 
-    public List<ShortcutInfo> queryForPinnedShortcuts(String packageName,
-            List<String> shortcutIds, UserHandle user) {
+    public QueryResult queryForPinnedShortcuts(String packageName, List<String> shortcutIds,
+            UserHandle user) {
         return query(ShortcutQuery.FLAG_MATCH_PINNED, packageName, null, shortcutIds, user);
     }
 
-    public List<ShortcutInfo> queryForAllShortcuts(UserHandle user) {
+    public QueryResult queryForAllShortcuts(UserHandle user) {
         return query(FLAG_GET_ALL, null, null, null, user);
     }
 
-    private List<String> extractIds(List<ShortcutInfo> shortcuts) {
+    private static List<String> extractIds(List<ShortcutInfo> shortcuts) {
         List<String> shortcutIds = new ArrayList<>(shortcuts.size());
         for (ShortcutInfo shortcut : shortcuts) {
             shortcutIds.add(shortcut.getId());
@@ -181,8 +164,8 @@
      *
      * TODO: Use the cache to optimize this so we don't make an RPC every time.
      */
-    private List<ShortcutInfo> query(int flags, String packageName,
-            ComponentName activity, List<String> shortcutIds, UserHandle user) {
+    private QueryResult query(int flags, String packageName, ComponentName activity,
+            List<String> shortcutIds, UserHandle user) {
         ShortcutQuery q = new ShortcutQuery();
         q.setQueryFlags(flags);
         if (packageName != null) {
@@ -190,18 +173,12 @@
             q.setActivity(activity);
             q.setShortcutIds(shortcutIds);
         }
-        List<ShortcutInfo> shortcutInfos = null;
         try {
-            shortcutInfos = mLauncherApps.getShortcuts(q, user);
-            mWasLastCallSuccess = true;
+            return new QueryResult(mLauncherApps.getShortcuts(q, user));
         } catch (SecurityException|IllegalStateException e) {
             Log.e(TAG, "Failed to query for shortcuts", e);
-            mWasLastCallSuccess = false;
+            return QueryResult.FAILURE;
         }
-        if (shortcutInfos == null) {
-            return Collections.EMPTY_LIST;
-        }
-        return shortcutInfos;
     }
 
     public boolean hasHostPermission() {
@@ -212,4 +189,25 @@
         }
         return false;
     }
+
+    public static class QueryResult extends ArrayList<ShortcutInfo> {
+
+        static QueryResult FAILURE = new QueryResult();
+
+        private final boolean mWasSuccess;
+
+        QueryResult(List<ShortcutInfo> result) {
+            super(result == null ? Collections.emptyList() : result);
+            mWasSuccess = true;
+        }
+
+        QueryResult() {
+            mWasSuccess = false;
+        }
+
+
+        public boolean wasSuccess() {
+            return mWasSuccess;
+        }
+    }
 }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
index bd6ea50..23f21a1 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
@@ -10,7 +10,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 /**
@@ -21,7 +21,7 @@
     private MotionEvent mTouchDownEvent;
 
     public AllAppsSwipeController(Launcher l) {
-        super(l, SwipeDetector.VERTICAL);
+        super(l, SingleAxisSwipeDetector.VERTICAL);
     }
 
     @Override
diff --git a/tests/Android.mk b/tests/Android.mk
index 0c41241..02ead4e 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -27,7 +27,7 @@
 
 ifneq (,$(wildcard frameworks/base))
 else
-    LOCAL_STATIC_JAVA_LIBRARIES += libSharedSystemUI
+    LOCAL_STATIC_JAVA_LIBRARIES += SystemUISharedLib
 
     LOCAL_SRC_FILES := $(call all-java-files-under, tapl) \
         ../src/com/android/launcher3/ResourceUtils.java \
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index c6f55a7..ffa90b9 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -62,6 +62,12 @@
                 <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
             </intent-filter>
         </activity>
+        <activity android:name="com.android.launcher3.testcomponent.CustomShortcutConfigActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
         <activity
             android:name="com.android.launcher3.testcomponent.RequestPinItemActivity"
             android:icon="@drawable/test_drawable_pin_item"
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 7d60ad6..64df8e0 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -15,9 +15,9 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -115,7 +115,7 @@
         WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
                 new Intent().setComponent(cn), false /* allowMissingTarget */, true);
         assertNotNull(info);
-        assertTrue(Utilities.isLauncherAppTarget(info.intent));
+        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
     }
 
     @Test
@@ -127,7 +127,7 @@
         WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
                 new Intent().setComponent(cn), true  /* allowMissingTarget */, true);
         assertNotNull(info);
-        assertTrue(Utilities.isLauncherAppTarget(info.intent));
+        assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/testcomponent/CustomShortcutConfigActivity.java b/tests/src/com/android/launcher3/testcomponent/CustomShortcutConfigActivity.java
new file mode 100644
index 0000000..b673faa
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/CustomShortcutConfigActivity.java
@@ -0,0 +1,66 @@
+/*
+ * 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.testcomponent;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+import com.android.launcher3.R;
+
+import java.util.UUID;
+
+/**
+ * A custom shortcut is a 1x1 widget that launches a specific intent when user tap on it.
+ * Custom shortcuts are replaced by deep shortcuts after api 25.
+ */
+public class CustomShortcutConfigActivity extends BaseTestingActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent launchIntent = new Intent(this, BaseTestingActivity.class)
+                .setAction("com.android.launcher3.intent.action.test_shortcut");
+        Intent shortcutIntent = createShortcutResultIntent(
+                this, UUID.randomUUID().toString(), "Shortcut",
+                R.drawable.ic_widget, launchIntent);
+        setResult(RESULT_OK, shortcutIntent);
+        finish();
+    }
+
+    private static Intent createShortcutResultIntent(
+            Context context, String uniqueId, String name, int iconId, Intent launchIntent) {
+        ShortcutInfo shortcutInfo =
+                createShortcutInfo(context, uniqueId, name, iconId, launchIntent);
+        ShortcutManager sm = context.getSystemService(ShortcutManager.class);
+        return sm.createShortcutResultIntent(shortcutInfo);
+    }
+
+    private static ShortcutInfo createShortcutInfo(
+            Context context, String uniqueId, String name, int iconId, Intent launchIntent) {
+        return new ShortcutInfo.Builder(context, uniqueId)
+                .setShortLabel(name)
+                .setLongLabel(name)
+                .setIcon(Icon.createWithResource(context, iconId))
+                .setIntent(launchIntent)
+                .build();
+    }
+}
diff --git a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
similarity index 72%
rename from tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
rename to tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index e042357..5174e4d 100644
--- a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -15,6 +15,12 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyFloat;
 import static org.mockito.Matchers.anyObject;
@@ -25,6 +31,10 @@
 import android.util.Log;
 import android.view.ViewConfiguration;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
 import com.android.launcher3.testcomponent.TouchEventGenerator;
 
 import org.junit.Before;
@@ -33,25 +43,21 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SwipeDetectorTest {
+public class SingleAxisSwipeDetectorTest {
 
-    private static final String TAG = SwipeDetectorTest.class.getSimpleName();
+    private static final String TAG = SingleAxisSwipeDetectorTest.class.getSimpleName();
     public static void L(String s, Object... parts) {
         Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts));
     }
 
     private TouchEventGenerator mGenerator;
-    private SwipeDetector mDetector;
+    private SingleAxisSwipeDetector mDetector;
     private int mTouchSlop;
 
     @Mock
-    private SwipeDetector.Listener mMockListener;
+    private SingleAxisSwipeDetector.Listener mMockListener;
 
     @Mock
     private ViewConfiguration mMockConfig;
@@ -65,8 +71,8 @@
         doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig)
                 .getScaledMaximumFlingVelocity();
 
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_BOTH, false);
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
 
@@ -75,8 +81,8 @@
 
     @Test
     public void testDragStart_verticalPositive() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 - mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
@@ -85,8 +91,8 @@
 
     @Test
     public void testDragStart_verticalNegative() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
@@ -103,8 +109,8 @@
 
     @Test
     public void testDragStart_horizontalPositive() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
@@ -114,8 +120,8 @@
 
     @Test
     public void testDragStart_horizontalNegative() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
@@ -125,8 +131,8 @@
 
     @Test
     public void testDragStart_horizontalRtlPositive() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
@@ -136,8 +142,8 @@
 
     @Test
     public void testDragStart_horizontalRtlNegative() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
@@ -160,6 +166,6 @@
         mGenerator.move(0, 100, 100 + mTouchSlop * 2);
         mGenerator.lift(0);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragEnd(anyFloat(), anyBoolean());
+        verify(mMockListener).onDragEnd(anyFloat());
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 843fd33..9a894b1 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -15,15 +15,16 @@
  */
 package com.android.launcher3.ui;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
 import static com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import static org.junit.Assert.assertTrue;
 
 import static java.lang.System.exit;
 
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -47,7 +48,6 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.model.AppLaunchTracker;
@@ -55,6 +55,7 @@
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
@@ -90,7 +91,7 @@
     public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866
     private static final String TAG = "AbstractLauncherUiTest";
 
-    protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
+    protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
     protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation());
     protected final LauncherInstrumentation mLauncher = new LauncherInstrumentation();
     protected Context mTargetContext;
@@ -347,7 +348,8 @@
         startIntent(
                 getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
                         packageName),
-                By.pkg(packageName).depth(0));
+                By.pkg(packageName).depth(0),
+                true /* newTask */);
     }
 
     protected void startTestActivity(int activityNumber) {
@@ -356,12 +358,17 @@
                 getLaunchIntentForPackage(packageName);
         intent.setComponent(new ComponentName(packageName,
                 "com.android.launcher3.tests.Activity" + activityNumber));
-        startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber));
+        startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber),
+                false /* newTask */);
     }
 
-    private void startIntent(Intent intent, BySelector selector) {
+    private void startIntent(Intent intent, BySelector selector, boolean newTask) {
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        if (newTask) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        } else {
+            intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        }
         getInstrumentation().getTargetContext().startActivity(intent);
         assertTrue("App didn't start: " + selector,
                 mDevice.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index b8ca5de..f9d1d93 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -69,4 +69,22 @@
         assertNotNull("Widget not found on the workspace", widget);
         widget.launch(getAppPackageName());
     }
+
+    /**
+     * Test dragging a custom shortcut to the workspace and launch it.
+     *
+     * A custom shortcut is a 1x1 widget that launches a specific intent when user tap on it.
+     * Custom shortcuts are replaced by deep shortcuts after api 25.
+     */
+    @Test
+    @PortraitLandscape
+    public void testDragCustomShortcut() throws Throwable {
+        clearHomescreen();
+        mDevice.pressHome();
+        mLauncher.getWorkspace().openAllWidgets()
+                .getWidget("com.android.launcher3.testcomponent.CustomShortcutConfigActivity")
+                .dragToWorkspace();
+        mLauncher.getWorkspace().getWorkspaceAppIcon("Shortcut")
+                .launch(getAppPackageName());
+    }
 }
diff --git a/tests/src/com/android/launcher3/util/Condition.java b/tests/src/com/android/launcher3/util/Condition.java
index b564a1a..d85dd3a 100644
--- a/tests/src/com/android/launcher3/util/Condition.java
+++ b/tests/src/com/android/launcher3/util/Condition.java
@@ -1,8 +1,8 @@
 package com.android.launcher3.util;
 
-import androidx.test.uiautomator.UiObject2;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import com.android.launcher3.MainThreadExecutor;
+import androidx.test.uiautomator.UiObject2;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -16,7 +16,7 @@
      * Converts the condition to be run on UI thread.
      */
     static Condition runOnUiThread(final Condition condition) {
-        final MainThreadExecutor executor = new MainThreadExecutor();
+        final LooperExecutor executor = MAIN_EXECUTOR;
         return () -> {
             final AtomicBoolean value = new AtomicBoolean(false);
             final Throwable[] exceptions = new Throwable[1];
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
index 0235f95..8f89173 100644
--- a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
 import static com.android.launcher3.util.RaceConditionTracker.ENTER_POSTFIX;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT_POSTFIX;
 
@@ -23,7 +24,6 @@
 import static org.junit.Assert.fail;
 
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.util.Log;
 
 import java.util.ArrayList;
@@ -72,9 +72,7 @@
     private static final Handler POSTPONED_EVENT_RESUME_HANDLER = createEventResumeHandler();
 
     private static Handler createEventResumeHandler() {
-        final HandlerThread thread = new HandlerThread("RaceConditionEventResumer");
-        thread.start();
-        return new Handler(thread.getLooper());
+        return new Handler(createAndStartNewLooper("RaceConditionEventResumer"));
     }
 
     /**
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 6dced8c..96e4b8c 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -36,6 +36,7 @@
     private static final int MAX_SCROLL_ATTEMPTS = 40;
 
     private final int mHeight;
+    private final int mIconHeight;
 
     AllApps(LauncherInstrumentation launcher) {
         super(launcher);
@@ -46,6 +47,10 @@
         // Wait for the recycler to populate.
         mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class));
         verifyNotFrozen("All apps freeze flags upon opening all apps");
+        mIconHeight = mLauncher.getTestInfo(
+                TestProtocol.REQUEST_ICON_HEIGHT)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+
     }
 
     @Override
@@ -62,6 +67,10 @@
         }
         final Rect iconBounds = icon.getVisibleBounds();
         LauncherInstrumentation.log("hasClickableIcon: icon bounds: " + iconBounds);
+        if (iconBounds.height() < mIconHeight / 2) {
+            LauncherInstrumentation.log("hasClickableIcon: icon has insufficient height");
+            return false;
+        }
         if (iconCenterInSearchBox(allAppsContainer, icon)) {
             LauncherInstrumentation.log("hasClickableIcon: icon center is under search box");
             return false;
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index bcce8ef..db3d846 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -23,6 +23,7 @@
 import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
 
 import com.android.launcher3.testing.TestProtocol;
 
@@ -114,6 +115,60 @@
         }
     }
 
+    /**
+     * Swipes right or double presses the square button to switch to the previous app.
+     */
+    public Background quickSwitchToPreviousApp() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to quick switch to the previous app")) {
+            verifyActiveContainer();
+            quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
+            return new Background(mLauncher);
+        }
+    }
+
+    protected int getExpectedStateForQuickSwitch() {
+        return BACKGROUND_APP_STATE_ORDINAL;
+    }
+
+    protected void quickSwitchToPreviousApp(int expectedState) {
+        boolean transposeInLandscape = false;
+        switch (mLauncher.getNavigationModel()) {
+            case TWO_BUTTON:
+                transposeInLandscape = true;
+                // Fall through, zero button and two button modes behave the same.
+            case ZERO_BUTTON: {
+                final int startX;
+                final int startY;
+                final int endX;
+                final int endY;
+                if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+                    // Swipe from the bottom left to the bottom right of the screen.
+                    startX = 0;
+                    startY = getSwipeStartY();
+                    endX = mLauncher.getDevice().getDisplayWidth();
+                    endY = startY;
+                } else {
+                    // Swipe from the bottom right to the top right of the screen.
+                    startX = getSwipeStartX();
+                    startY = mLauncher.getRealDisplaySize().y - 1;
+                    endX = startX;
+                    endY = 0;
+                }
+                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState);
+                break;
+            }
+
+            case THREE_BUTTON:
+                // Double press the recents button.
+                UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
+                recentsButton.click();
+                mLauncher.getOverview();
+                recentsButton.click();
+                break;
+        }
+    }
+
     protected String getSwipeHeightRequestName() {
         return TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index cfc4374..e0fe933 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
 
 import androidx.annotation.NonNull;
 
@@ -58,4 +59,9 @@
             }
         }
     }
+
+    @Override
+    protected int getExpectedStateForQuickSwitch() {
+        return QUICK_SWITCH_STATE_ORDINAL;
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 01edcc2..0aa36dd 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -140,7 +140,7 @@
     }
 
     private boolean isWorkspaceScrollable(UiObject2 workspace) {
-        return workspace.isScrollable();
+        return workspace.getChildCount() > 1;
     }
 
     @NonNull