Merge "Close system dialogs when opening all apps from system action" into ub-launcher3-master
diff --git a/Android.mk b/Android.mk
index 7805b32..91ecccb 100644
--- a/Android.mk
+++ b/Android.mk
@@ -48,7 +48,7 @@
 LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java
 
 LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
+LOCAL_MIN_SDK_VERSION := 26
 LOCAL_MODULE := Launcher3CommonDepsLib
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_MANIFEST_FILE := AndroidManifest-common.xml
@@ -77,7 +77,7 @@
 LOCAL_PROGUARD_ENABLED := disabled
 
 LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
+LOCAL_MIN_SDK_VERSION := 26
 LOCAL_PACKAGE_NAME := Launcher3
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_SYSTEM_EXT_MODULE := true
@@ -108,7 +108,7 @@
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
 LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 21
+LOCAL_MIN_SDK_VERSION := 26
 LOCAL_PACKAGE_NAME := Launcher3Go
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_SYSTEM_EXT_MODULE := true
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 19a16e3..97e3786 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -29,12 +29,6 @@
     at compile time. Note that the components defined in AndroidManifest.xml are also required,
     with some minor changed based on the derivative app.
     -->
-    <permission
-        android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
-        android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
-        android:protectionLevel="dangerous"
-        android:label="@string/permlab_install_shortcut"
-        android:description="@string/permdesc_install_shortcut" />
 
     <uses-permission android:name="android.permission.CALL_PHONE" />
     <uses-permission android:name="android.permission.SET_WALLPAPER" />
@@ -79,17 +73,6 @@
         android:restoreAnyVersion="true"
         android:supportsRtl="true" >
 
-        <!-- Intent received used to install shortcuts from other applications -->
-        <receiver
-            android:name="com.android.launcher3.InstallShortcutReceiver"
-            android:permission="com.android.launcher.permission.INSTALL_SHORTCUT"
-            android:exported="true"
-            android:enabled="@bool/enable_install_shortcut_api" >
-            <intent-filter>
-                <action android:name="com.android.launcher.action.INSTALL_SHORTCUT" />
-            </intent-filter>
-        </receiver>
-
         <!-- Intent received when a session is committed -->
         <receiver
             android:name="com.android.launcher3.SessionCommitReceiver"
@@ -116,7 +99,6 @@
         <service
             android:name="com.android.launcher3.notification.NotificationListener"
             android:label="@string/notification_dots_service_title"
-            android:enabled="@bool/notification_dots_enabled"
             android:exported="true"
             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
             <intent-filter>
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8e01f82..97bce9c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -20,7 +20,7 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3">
-    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="26"/>
     <!--
     Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
     Refer comments around specific entries on how to extend individual components.
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/OWNERS b/OWNERS
index 3069afa..1d6ad8c 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,6 +28,7 @@
 peanutbutter@google.com
 xuqiu@google.com
 sreyasr@google.com
+thiruram@google.com
 
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, zakcohen@google.com, mrcasey@google.com, adamcohen@google.com, hyunyoungs@google.com
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 53910e3..19f85e4 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 4e7c3fa..5be32a8 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -59,7 +59,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="unspecified"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:taskAffinity=""/>
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index 6aa9619..1937164 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -25,8 +25,11 @@
 
   <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>
+  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
 
   <string name="prediction_model_class" translatable="false">com.android.launcher3.hybridhotseat.HotseatPredictionModel</string>
+
+  <string name="model_delegate_class" translatable="false">com.android.launcher3.model.QuickstepModelDelegate</string>
+
 </resources>
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
index 914d9e9..7dc7bfc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -42,6 +42,7 @@
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.Themes;
 
@@ -90,7 +91,8 @@
         mLauncher = Launcher.getLauncher(context);
 
         boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
-        mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
+        mPaint.setStrokeWidth(
+                getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height));
 
         mStrokeColor = ContextCompat.getColor(context, isMainColorDark
                 ? R.color.all_apps_prediction_row_separator_dark
@@ -134,7 +136,7 @@
                 if (row == this) {
                     break;
                 } else if (row.shouldDraw()) {
-                    sectionCount ++;
+                    sectionCount++;
                 }
             }
 
@@ -181,6 +183,11 @@
     }
 
     private void updateViewVisibility() {
+        // hide divider since we have item decoration for prediction row
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            setVisibility(GONE);
+            return;
+        }
         setVisibility(mDividerType == DividerType.NONE
                 ? GONE
                 : (mIsScrolledOut ? INVISIBLE : VISIBLE));
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 e11c701..8a810e3 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
@@ -30,7 +30,6 @@
 import android.os.Build;
 import android.util.AttributeSet;
 import android.util.IntProperty;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -44,10 +43,9 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.allapps.AllAppsSectionDecorator;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.AlphaUpdateListener;
@@ -56,7 +54,6 @@
 import com.android.launcher3.keyboard.FocusIndicatorHelper;
 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -67,15 +64,12 @@
 import com.android.quickstep.AnimatedFloat;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 @TargetApi(Build.VERSION_CODES.P)
 public class PredictionRowView extends LinearLayout implements
         LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
 
-    private static final String TAG = "PredictionRowView";
-
     private static final IntProperty<PredictionRowView> TEXT_ALPHA =
             new IntProperty<PredictionRowView>("textAlpha") {
                 @Override
@@ -93,16 +87,14 @@
             (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f;
 
     private final Launcher mLauncher;
-    private final PredictionUiStateManager mPredictionUiStateManager;
     private int mNumPredictedAppsPerRow;
 
-    // The set of predicted app component names
-    private final List<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
-    // The set of predicted apps resolved from the component names and the current set of apps
-    private final ArrayList<ItemInfoWithIcon> mPredictedApps = new ArrayList<>();
     // Helper to drawing the focus indicator.
     private final FocusIndicatorHelper mFocusHelper;
 
+    // The set of predicted apps resolved from the component names and the current set of apps
+    private final List<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
+
     private final int mIconTextColor;
     private final int mIconFullTextAlpha;
     private int mIconLastSetTextAlpha;
@@ -120,6 +112,8 @@
 
     private boolean mPredictionsEnabled = false;
 
+    AllAppsSectionDecorator.SectionDecorationHandler mDecorationHandler;
+
     public PredictionRowView(@NonNull Context context) {
         this(context, null);
     }
@@ -134,36 +128,24 @@
         mLauncher = Launcher.getLauncher(context);
         mLauncher.addOnDeviceProfileChangeListener(this);
 
-        mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(context);
-
         mIconTextColor = Themes.getAttrColor(context, android.R.attr.textColorSecondary);
         mIconFullTextAlpha = Color.alpha(mIconTextColor);
         mIconCurrentTextAlpha = mIconFullTextAlpha;
 
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            mDecorationHandler = new AllAppsSectionDecorator.SectionDecorationHandler(getContext(),
+                    false);
+        }
+
         updateVisibility();
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-
-        mPredictionUiStateManager.setTargetAppsView(mLauncher.getAppsView());
-        getAppsStore().registerIconContainer(this);
         AllAppsTipView.scheduleShowIfNeeded(mLauncher);
     }
 
-    private AllAppsStore getAppsStore() {
-        return mLauncher.getAppsView().getAppsStore();
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-
-        mPredictionUiStateManager.setTargetAppsView(null);
-        getAppsStore().unregisterIconContainer(this);
-    }
-
     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
         mParent = parent;
     }
@@ -180,6 +162,14 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
+        if (mDecorationHandler != null) {
+            mDecorationHandler.reset();
+            int childrenCount = getChildCount();
+            for (int i = 0; i < childrenCount; i++) {
+                mDecorationHandler.extendBounds(getChildAt(i));
+            }
+            mDecorationHandler.onDraw(canvas);
+        }
         mFocusHelper.draw(canvas);
         super.dispatchDraw(canvas);
     }
@@ -205,7 +195,7 @@
      * Returns the predicted apps.
      */
     public List<ItemInfoWithIcon> getPredictedApps() {
-        return mPredictedApps;
+        return new ArrayList<>(mPredictedApps);
     }
 
     /**
@@ -217,12 +207,12 @@
      * If the number of predicted apps is the same as the previous list of predicted apps,
      * we can optimize by swapping them in place.
      */
-    public void setPredictedApps(List<ComponentKeyMapper> apps) {
-        mPredictedAppComponents.clear();
-        mPredictedAppComponents.addAll(apps);
-
+    public void setPredictedApps(List<ItemInfo> items) {
         mPredictedApps.clear();
-        mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+        items.stream()
+                .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo)
+                .map(itemInfo -> (WorkspaceItemInfo) itemInfo)
+                .forEach(mPredictedApps::add);
         applyPredictionApps();
     }
 
@@ -264,11 +254,7 @@
             icon.reset();
             if (predictionCount > i) {
                 icon.setVisibility(View.VISIBLE);
-                if (mPredictedApps.get(i) instanceof AppInfo) {
-                    icon.applyFromApplicationInfo((AppInfo) mPredictedApps.get(i));
-                } else if (mPredictedApps.get(i) instanceof WorkspaceItemInfo) {
-                    icon.applyFromWorkspaceItem((WorkspaceItemInfo) mPredictedApps.get(i));
-                }
+                icon.applyFromWorkspaceItem(mPredictedApps.get(i));
                 icon.setTextColor(iconColor);
             } else {
                 icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
@@ -284,33 +270,6 @@
         mParent.onHeightUpdated();
     }
 
-    private List<ItemInfoWithIcon> processPredictedAppComponents(
-            List<ComponentKeyMapper> components) {
-        if (getAppsStore().getApps().length == 0) {
-            // Apps have not been bound yet.
-            return Collections.emptyList();
-        }
-
-        List<ItemInfoWithIcon> predictedApps = new ArrayList<>();
-        for (ComponentKeyMapper mapper : components) {
-            ItemInfoWithIcon info = mapper.getApp(getAppsStore());
-            if (info != null) {
-                ItemInfoWithIcon predictedApp = info.clone();
-                predictedApp.container = LauncherSettings.Favorites.CONTAINER_PREDICTION;
-                predictedApps.add(predictedApp);
-            } else {
-                if (FeatureFlags.IS_STUDIO_BUILD) {
-                    Log.e(TAG, "Predicted app not found: " + mapper);
-                }
-            }
-            // Stop at the number of predicted apps
-            if (predictedApps.size() == mNumPredictedAppsPerRow) {
-                break;
-            }
-        }
-        return predictedApps;
-    }
-
     @Override
     public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
             ArrayList<LauncherLogProto.Target> parents) {
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
deleted file mode 100644
index 830c103..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * 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.appprediction;
-
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.content.ComponentName;
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
-import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-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.OptionalInt;
-import java.util.stream.IntStream;
-
-/**
- * Handler responsible to updating the UI due to predicted apps changes. Operations:
- * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes
- * invisible again before applying the changes. This ensures that the UI does not change abruptly
- * in front of the user, even if an app launched and user pressed back button to return to the
- * all-apps UI again.
- * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory
- * even if all-apps is not opened as they are shown in search UI as well
- * 3) Load instant app if it is not already in memory. As predictions are persisted on disk,
- * instant app will not be in memory when launcher starts.
- * 4) Maintains the current active client id (for the predictions) and all updates are performed on
- * that client id.
- */
-public class PredictionUiStateManager implements StateListener<LauncherState>,
-        ItemInfoUpdateReceiver, OnIDPChangeListener, OnUpdateListener {
-
-    public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
-
-    // TODO (b/129421797): Update the client constants
-    public enum Client {
-        HOME("home");
-
-        public final String id;
-
-        Client(String id) {
-            this.id = id;
-        }
-    }
-
-    public static final MainThreadInitializedObject<PredictionUiStateManager> INSTANCE =
-            new MainThreadInitializedObject<>(PredictionUiStateManager::new);
-
-    private final Context mContext;
-
-    private final DynamicItemCache mDynamicItemCache;
-    private List mPredictionServicePredictions = Collections.emptyList();
-
-    private int mMaxIconsPerRow;
-
-    private AllAppsContainerView mAppsView;
-
-    private PredictionState mPendingState;
-    private PredictionState mCurrentState;
-
-    private boolean mGettingValidPredictionResults;
-
-    private PredictionUiStateManager(Context context) {
-        mContext = context;
-
-        mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated);
-
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-        mMaxIconsPerRow = idp.numColumns;
-
-        idp.addOnChangeListener(this);
-        mGettingValidPredictionResults = Utilities.getDevicePrefs(context)
-                .getBoolean(LAST_PREDICTION_ENABLED_STATE, true);
-
-        // Call this last
-        mCurrentState = parseLastState();
-    }
-
-    @Override
-    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
-        mMaxIconsPerRow = profile.numColumns;
-    }
-
-    public void setTargetAppsView(AllAppsContainerView appsView) {
-        if (mAppsView != null) {
-            mAppsView.getAppsStore().removeUpdateListener(this);
-        }
-        mAppsView = appsView;
-        if (mAppsView != null) {
-            mAppsView.getAppsStore().addUpdateListener(this);
-        }
-        if (mPendingState != null) {
-            applyState(mPendingState);
-            mPendingState = null;
-        } else {
-            applyState(mCurrentState);
-        }
-        updateDependencies(mCurrentState);
-    }
-
-    @Override
-    public void reapplyItemInfo(ItemInfoWithIcon info) { }
-
-    @Override
-    public void onStateTransitionComplete(LauncherState state) {
-        if (mAppsView == null) {
-            return;
-        }
-        if (mPendingState != null && canApplyPredictions(mPendingState)) {
-            applyState(mPendingState);
-            mPendingState = null;
-        }
-        if (mPendingState == null) {
-            Launcher.getLauncher(mAppsView.getContext()).getStateManager()
-                    .removeStateListener(this);
-        }
-    }
-
-    private void scheduleApplyPredictedApps(PredictionState state) {
-        boolean registerListener = mPendingState == null;
-        mPendingState = state;
-        if (registerListener) {
-            // Add a listener and wait until appsView is invisible again.
-            Launcher.getLauncher(mAppsView.getContext()).getStateManager().addStateListener(this);
-        }
-    }
-
-    private void applyState(PredictionState state) {
-        mCurrentState = state;
-        if (mAppsView != null) {
-            mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
-                    .setPredictedApps(mCurrentState.apps);
-        }
-    }
-
-    private void updatePredictionStateAfterCallback() {
-        boolean validResults = mPredictionServicePredictions != null
-                && !mPredictionServicePredictions.isEmpty();
-        if (validResults != mGettingValidPredictionResults) {
-            mGettingValidPredictionResults = validResults;
-            Utilities.getDevicePrefs(mContext).edit()
-                    .putBoolean(LAST_PREDICTION_ENABLED_STATE, true)
-                    .apply();
-        }
-        dispatchOnChange(true);
-    }
-
-    public AppPredictor.Callback appPredictorCallback(Client client) {
-        return targets -> {
-            mPredictionServicePredictions = targets;
-            updatePredictionStateAfterCallback();
-        };
-    }
-
-    private void dispatchOnChange(boolean changed) {
-        PredictionState newState = changed
-                ? parseLastState()
-                : mPendingState != null && canApplyPredictions(mPendingState)
-                        ? mPendingState
-                        : mCurrentState;
-        if (changed && mAppsView != null && !canApplyPredictions(newState)) {
-            scheduleApplyPredictedApps(newState);
-        } else {
-            applyState(newState);
-        }
-    }
-
-    private PredictionState parseLastState() {
-        PredictionState state = new PredictionState();
-        state.isEnabled = mGettingValidPredictionResults;
-        if (!state.isEnabled) {
-            state.apps = Collections.EMPTY_LIST;
-            return state;
-        }
-
-        state.apps = new ArrayList<>();
-
-        List<AppTarget> appTargets = mPredictionServicePredictions;
-        if (!appTargets.isEmpty()) {
-            for (AppTarget appTarget : appTargets) {
-                ComponentKey key;
-                if (appTarget.getShortcutInfo() != null) {
-                    key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
-                } else {
-                    key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
-                            appTarget.getClassName()), appTarget.getUser());
-                }
-                state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache));
-            }
-        }
-        updateDependencies(state);
-        return state;
-    }
-
-    private void updateDependencies(PredictionState state) {
-        if (!state.isEnabled || mAppsView == null) {
-            return;
-        }
-        mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
-                mMaxIconsPerRow);
-    }
-
-    @Override
-    public void onAppsUpdated() {
-        dispatchOnChange(false);
-    }
-
-    private boolean canApplyPredictions(PredictionState newState) {
-        if (mAppsView == null) {
-            // If there is no apps view, no need to schedule.
-            return true;
-        }
-        Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
-        PredictionRowView predictionRow = mAppsView.getFloatingHeaderView().
-                findFixedRowByType(PredictionRowView.class);
-        if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 ||
-                launcher.isForceInvisible()) {
-            return true;
-        }
-
-        if (mCurrentState.isEnabled != newState.isEnabled
-                || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) {
-            // If the visibility of the prediction row is changing, apply immediately.
-            return true;
-        }
-
-        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way
-            return false;
-        }
-        if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) {
-            // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI
-            return false;
-        }
-
-        // Instead of checking against 1, we should check against (1 + delta), where delta accounts
-        // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking
-        // against 1, keeps the logic simple :)
-        return launcher.getAllAppsController().getProgress() > 1;
-    }
-
-    public PredictionState getCurrentState() {
-        return mCurrentState;
-    }
-
-    /**
-     * Returns ranking info for the app within all apps 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 OptionalInt getAllAppsRank(@Nullable ItemInfo itemInfo) {
-        if (itemInfo == null || itemInfo.getTargetComponent() == null || itemInfo.user == null) {
-            return OptionalInt.empty();
-        }
-
-        if (itemInfo.itemType == ITEM_TYPE_APPLICATION
-                || itemInfo.itemType == ITEM_TYPE_SHORTCUT
-                || itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
-            ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(),
-                    itemInfo.user);
-            final List<ComponentKeyMapper> apps = getCurrentState().apps;
-            return IntStream.range(0, apps.size())
-                    .filter(index -> key.equals(apps.get(index).getComponentKey()))
-                    .findFirst();
-        }
-
-        return OptionalInt.empty();
-    }
-
-    /**
-     * 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;
-        }
-        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) {
-            HotseatPredictionController.encodeHotseatLayoutIntoPredictionRank(itemInfo, target);
-            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 = 0 - rank);
-    }
-
-    public static class PredictionState {
-
-        public boolean isEnabled;
-        public List<ComponentKeyMapper> apps;
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
index c15a596..20e1edc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
@@ -71,10 +71,9 @@
     }
 
     private PrintWriter getWriter() {
-        String fName = FILE_NAME_PREFIX + (LOG_DAYS % 10);
-        if (fName.equals(mFileName)) return mCurrentWriter;
-
         Calendar cal = Calendar.getInstance();
+        String fName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % 10);
+        if (fName.equals(mFileName)) return mCurrentWriter;
 
         boolean append = false;
         File logFile = new File(mLogsDir, fName);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/AppEventProducer.java
similarity index 64%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/model/AppEventProducer.java
index 8cabe3d..8e4c43f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/AppEventProducer.java
@@ -13,175 +13,76 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.appprediction;
+package com.android.launcher3.model;
 
-import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.annotation.TargetApi;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
-import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.app.prediction.AppTargetId;
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.text.TextUtils;
-import android.util.Log;
 
 import androidx.annotation.AnyThread;
 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.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import com.android.launcher3.logger.LauncherAtom.FolderContainer;
 import com.android.launcher3.logger.LauncherAtom.HotseatContainer;
 import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer;
 import com.android.launcher3.logging.StatsLogManager.EventEnum;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.pm.UserCache;
-import com.android.quickstep.logging.StatsLogCompatManager;
 import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer;
 
 import java.util.Locale;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /**
- * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
+ * Utility class to track stats log and emit corresponding app events
  */
-@TargetApi(Build.VERSION_CODES.Q)
-public class PredictionAppTracker extends AppLaunchTracker implements StatsLogConsumer {
+@TargetApi(Build.VERSION_CODES.R)
+public class AppEventProducer implements StatsLogConsumer {
 
-    private static final String TAG = "PredictionAppTracker";
-    private static final boolean DBG = false;
+    private static final int MSG_LAUNCH = 0;
 
-    private static final int MSG_INIT = 0;
-    private static final int MSG_DESTROY = 1;
-    private static final int MSG_LAUNCH = 2;
-    private static final int MSG_PREDICT = 3;
-
-    protected final Context mContext;
+    private final Context mContext;
     private final Handler mMessageHandler;
+    private final Consumer<AppTargetEvent> mCallback;
 
-    // Accessed only on worker thread
-    private AppPredictor mHomeAppPredictor;
-
-    public PredictionAppTracker(Context context) {
+    public AppEventProducer(Context context, Consumer<AppTargetEvent> callback) {
         mContext = context;
-        mMessageHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessage);
-        InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
-
-        mMessageHandler.sendEmptyMessage(MSG_INIT);
-    }
-
-    @UiThread
-    private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
-        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
-            // Reinitialize everything
-            mMessageHandler.sendEmptyMessage(MSG_INIT);
-        }
-    }
-
-    @WorkerThread
-    private void destroy() {
-        if (mHomeAppPredictor != null) {
-            mHomeAppPredictor.destroy();
-            mHomeAppPredictor = null;
-        }
-        StatsLogCompatManager.LOGS_CONSUMER.remove(this);
-    }
-
-    @WorkerThread
-    private AppPredictor createPredictor(Client client, int count) {
-        AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
-
-        if (apm == null) {
-            return null;
-        }
-
-        AppPredictor predictor = apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(mContext)
-                        .setUiSurface(client.id)
-                        .setPredictedTargetCount(count)
-                        .setExtras(getAppPredictionContextExtras(client))
-                        .build());
-        predictor.registerPredictionUpdates(mContext.getMainExecutor(),
-                PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
-        predictor.requestPredictionUpdate();
-        return predictor;
-    }
-
-    /**
-     * Override to add custom extras.
-     */
-    @WorkerThread
-    @Nullable
-    public Bundle getAppPredictionContextExtras(Client client) {
-        return null;
+        mMessageHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleMessage);
+        mCallback = callback;
     }
 
     @WorkerThread
     private boolean handleMessage(Message msg) {
         switch (msg.what) {
-            case MSG_INIT: {
-                // Destroy any existing clients
-                destroy();
-
-                // Initialize the clients
-                int count = InvariantDeviceProfile.INSTANCE.get(mContext).numAllAppsColumns;
-                mHomeAppPredictor = createPredictor(Client.HOME, count);
-                StatsLogCompatManager.LOGS_CONSUMER.add(this);
-                return true;
-            }
-            case MSG_DESTROY: {
-                destroy();
-                return true;
-            }
             case MSG_LAUNCH: {
-                if (mHomeAppPredictor != null) {
-                    mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
-                }
-                return true;
-            }
-            case MSG_PREDICT: {
-                if (mHomeAppPredictor != null) {
-                    mHomeAppPredictor.requestPredictionUpdate();
-                }
+                mCallback.accept((AppTargetEvent) msg.obj);
                 return true;
             }
         }
         return false;
     }
 
-    @Override
-    @UiThread
-    public void onReturnedToHome() {
-        String client = Client.HOME.id;
-        mMessageHandler.removeMessages(MSG_PREDICT, client);
-        Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget();
-        if (DBG) {
-            Log.d(TAG, String.format("Sent immediate message to update %s", client));
-        }
-    }
-
     @AnyThread
     private void sendEvent(LauncherAtom.ItemInfo atomInfo, int eventId) {
         AppTarget target = toAppTarget(atomInfo);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
new file mode 100644
index 0000000..721e2be
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Task to update model as a result of predicted apps update
+ */
+public class PredictionUpdateTask extends BaseModelUpdateTask {
+
+    private final List<AppTarget> mTargets;
+    private final int mContainerId;
+
+    PredictionUpdateTask(int containerId, List<AppTarget> targets) {
+        mContainerId = containerId;
+        mTargets = targets;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        // TODO: persist the whole list
+        Utilities.getDevicePrefs(app.getContext()).edit()
+                .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply();
+
+        FixedContainerItems fci;
+        synchronized (dataModel) {
+            fci = dataModel.extraItems.get(mContainerId);
+            if (fci == null) {
+                return;
+            }
+        }
+
+        Set<UserHandle> usersForChangedShortcuts = new HashSet<>(fci.items.stream()
+                .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT)
+                .map(info -> info.user)
+                .collect(Collectors.toSet()));
+        fci.items.clear();
+
+        for (AppTarget target : mTargets) {
+            WorkspaceItemInfo itemInfo;
+            ShortcutInfo si = target.getShortcutInfo();
+            if (si != null) {
+                usersForChangedShortcuts.add(si.getUserHandle());
+                itemInfo = new WorkspaceItemInfo(si, app.getContext());
+                app.getIconCache().getShortcutIcon(itemInfo, si);
+            } else {
+                String className = target.getClassName();
+                if (COMPONENT_CLASS_MARKER.equals(className)) {
+                    // TODO: Implement this
+                    continue;
+                }
+                ComponentName cn = new ComponentName(target.getPackageName(), className);
+                UserHandle user = target.getUser();
+                itemInfo = apps.data.stream()
+                        .filter(info -> user.equals(info.user) && cn.equals(info.componentName))
+                        .map(AppInfo::makeWorkspaceItem)
+                        .findAny()
+                        .orElseGet(() -> {
+                            LauncherActivityInfo lai = app.getContext()
+                                    .getSystemService(LauncherApps.class)
+                                    .resolveActivity(AppInfo.makeLaunchIntent(cn), user);
+                            if (lai == null) {
+                                return null;
+                            }
+                            AppInfo ai = new AppInfo(app.getContext(), lai, user);
+                            app.getIconCache().getTitleAndIcon(ai, lai, false);
+                            return ai.makeWorkspaceItem();
+                        });
+
+                if (itemInfo == null) {
+                    continue;
+                }
+            }
+
+            itemInfo.container = mContainerId;
+            fci.items.add(itemInfo);
+        }
+
+        bindExtraContainerItems(fci);
+        usersForChangedShortcuts.forEach(
+                u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
new file mode 100644
index 0000000..b516469
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.content.Context;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.util.Executors;
+import com.android.quickstep.logging.StatsLogCompatManager;
+
+import java.util.List;
+
+/**
+ * Model delegate which loads prediction items
+ */
+public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChangeListener {
+
+    public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
+
+    private final InvariantDeviceProfile mIDP;
+    private final AppEventProducer mAppEventProducer;
+
+    private AppPredictor mAllAppsPredictor;
+    private boolean mActive = false;
+
+    public QuickstepModelDelegate(Context context) {
+        mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
+
+        mIDP = InvariantDeviceProfile.INSTANCE.get(context);
+        mIDP.addOnChangeListener(this);
+        StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
+    }
+
+    @Override
+    public void loadItems() {
+        // TODO: Implement caching and preloading
+        super.loadItems();
+        mDataModel.extraItems.put(
+                CONTAINER_PREDICTION, new FixedContainerItems(CONTAINER_PREDICTION));
+
+        mActive = true;
+        recreatePredictors();
+    }
+
+    @Override
+    public void validateData() {
+        super.validateData();
+        if (mAllAppsPredictor != null) {
+            mAllAppsPredictor.requestPredictionUpdate();
+        }
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        mActive = false;
+        StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
+
+        destroyPredictors();
+        mIDP.removeOnChangeListener(this);
+    }
+
+    private void destroyPredictors() {
+        if (mAllAppsPredictor != null) {
+            mAllAppsPredictor.destroy();
+            mAllAppsPredictor = null;
+        }
+    }
+
+    @WorkerThread
+    private void recreatePredictors() {
+        destroyPredictors();
+        if (!mActive) {
+            return;
+        }
+
+        Context context = mApp.getContext();
+        AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+        if (apm == null) {
+            return;
+        }
+
+        int count = mIDP.numAllAppsColumns;
+
+        mAllAppsPredictor = apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(context)
+                        .setUiSurface("home")
+                        .setPredictedTargetCount(count)
+                        .build());
+        mAllAppsPredictor.registerPredictionUpdates(
+                Executors.MODEL_EXECUTOR, this::onAllAppsPredictionChanged);
+        mAllAppsPredictor.requestPredictionUpdate();
+    }
+
+    private void onAllAppsPredictionChanged(List<AppTarget> targets) {
+        mApp.getModel().enqueueModelUpdateTask(
+                new PredictionUpdateTask(CONTAINER_PREDICTION, targets));
+    }
+
+    @Override
+    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+            // Reinitialize everything
+            Executors.MODEL_EXECUTOR.execute(this::recreatePredictors);
+        }
+    }
+
+    private void onAppTargetEvent(AppTargetEvent event) {
+        if (mAllAppsPredictor != null) {
+            mAllAppsPredictor.notifyAppTargetEvent(event);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 8f4aea7..4107698 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -17,6 +17,9 @@
 
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
@@ -38,16 +41,18 @@
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -81,7 +86,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.OptionalInt;
+import java.util.Objects;
 import java.util.stream.Stream;
 
 public class QuickstepLauncher extends BaseQuickstepLauncher {
@@ -93,6 +98,8 @@
     public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
             SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
 
+    private FixedContainerItems mAllAppsPredictions;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -113,8 +120,23 @@
     protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
         StatsLogger logger = getStatsLogManager()
                 .logger().withItemInfo(info).withInstanceId(instanceId);
-        OptionalInt allAppsRank = PredictionUiStateManager.INSTANCE.get(this).getAllAppsRank(info);
-        allAppsRank.ifPresent(logger::withRank);
+
+        if (mAllAppsPredictions != null
+                && (info.itemType == ITEM_TYPE_APPLICATION
+                || info.itemType == ITEM_TYPE_SHORTCUT
+                || info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) {
+            int count = mAllAppsPredictions.items.size();
+            for (int i = 0; i < count; i++) {
+                ItemInfo targetInfo = mAllAppsPredictions.items.get(i);
+                if (targetInfo.itemType == info.itemType
+                        && targetInfo.user.equals(info.user)
+                        && Objects.equals(targetInfo.getIntent(), info.getIntent())) {
+                    logger.withRank(i);
+                    break;
+                }
+
+            }
+        }
         logger.log(LAUNCHER_APP_LAUNCH_TAP);
 
         if (mHotseatPredictionController != null) {
@@ -209,6 +231,15 @@
     }
 
     @Override
+    public void bindExtraContainerItems(FixedContainerItems item) {
+        if (item.containerId == Favorites.CONTAINER_PREDICTION) {
+            mAllAppsPredictions = item;
+            getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
+                    .setPredictedApps(item.items);
+        }
+    }
+
+    @Override
     public void onDestroy() {
         super.onDestroy();
         if (mHotseatPredictionController != null) {
@@ -224,10 +255,8 @@
         switch (state.ordinal) {
             case HINT_STATE_ORDINAL: {
                 Workspace workspace = getWorkspace();
-                boolean willMoveScreens = workspace.getNextPage() != Workspace.DEFAULT_PAGE;
-                getStateManager().goToState(NORMAL, true,
-                        willMoveScreens ? null : getScrimView()::startDragHandleEducationAnim);
-                if (willMoveScreens) {
+                getStateManager().goToState(NORMAL);
+                if (workspace.getNextPage() != Workspace.DEFAULT_PAGE) {
                     workspace.post(workspace::moveToDefaultScreen);
                 }
                 break;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index a0af797..131fcbf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -17,7 +17,6 @@
 
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
@@ -52,6 +51,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -212,7 +212,7 @@
                 // Scale up the recents, if it is not coming from the side
                 RecentsView overview = mActivity.getOverviewPanel();
                 if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
-                    SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
+                    RECENTS_SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
                 }
             }
             config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
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 c1a585e..e45fa9d 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
@@ -159,8 +159,7 @@
             builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
                     -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-                builder.addOnFrameCallback(
-                        () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
+                builder.addOnFrameCallback(recentsView::redrawLiveTile);
             }
         } else if (mStartState == ALL_APPS) {
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
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
index 1b439d1..821ada4 100644
--- 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
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -47,6 +46,7 @@
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
@@ -244,7 +244,7 @@
         final LauncherState toState = OVERVIEW;
 
         // Set RecentView's initial properties.
-        SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+        RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
         ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
@@ -266,7 +266,8 @@
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
         PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
-        yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+        yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
+                SCALE_DOWN_INTERPOLATOR);
         yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
         mYOverviewAnim = yAnim.createPlaybackController();
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 0ee5d04..fce019b 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
@@ -16,7 +16,6 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 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;
@@ -262,11 +261,6 @@
         mCurrentAnimation.setPlayFraction(Utilities.boundToRange(
                 totalDisplacement * mProgressMultiplier, 0, 1));
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
-                mRecentsView.redrawLiveTile(true);
-            }
-        }
         return true;
     }
 
@@ -297,13 +291,6 @@
         }
 
         mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
-                if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
-                    mRecentsView.redrawLiveTile(true);
-                }
-            });
-        }
         mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
                 velocity, mEndDisplacement, animationDuration);
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
similarity index 80%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
index 3fea146..b6d44eb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -15,13 +15,14 @@
  */
 package com.android.quickstep;
 
+import static android.widget.Toast.LENGTH_SHORT;
+
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
@@ -29,7 +30,9 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
 import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
@@ -44,20 +47,23 @@
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.Animator;
-import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.os.Build;
 import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.WindowInsets;
 import android.view.animation.Interpolator;
+import android.widget.Toast;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -67,23 +73,30 @@
 import com.android.launcher3.R;
 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.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestProtocol;
 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.util.TraceHelper;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface.AnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -95,17 +108,34 @@
 import com.android.systemui.shared.system.TaskInfoCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
- * TODO: Merge this with BaseSwipeUpHandler
  */
-@TargetApi(Build.VERSION_CODES.O)
-public abstract class BaseSwipeUpHandlerV2<T extends StatefulActivity<?>, Q extends RecentsView>
-        extends BaseSwipeUpHandler<T, Q> implements OnApplyWindowInsetsListener {
-    private static final String TAG = BaseSwipeUpHandlerV2.class.getSimpleName();
+@TargetApi(Build.VERSION_CODES.R)
+public abstract class AbsSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
+        extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
+        RecentsAnimationCallbacks.RecentsAnimationListener {
+    private static final String TAG = "AbsSwipeUpHandler";
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
+    protected final BaseActivityInterface<?, T> mActivityInterface;
+    protected final InputConsumerProxy mInputConsumerProxy;
+    protected final ActivityInitListener mActivityInitListener;
+    // Callbacks to be made once the recents animation starts
+    private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
+    protected RecentsAnimationController mRecentsAnimationController;
+    protected RecentsAnimationTargets mRecentsAnimationTargets;
+    protected T mActivity;
+    protected Q mRecentsView;
+    protected Runnable mGestureEndCallback;
+    protected MultiStateCallback mStateCallback;
+    protected boolean mCanceled;
+    private boolean mRecentsViewScrollLinked = false;
+
     private static int getFlagForIndex(int index, String name) {
         if (DEBUG_STATES) {
             STATE_NAMES[index] = name;
@@ -182,8 +212,7 @@
     private ThumbnailData mTaskSnapshot;
 
     // Used to control launcher components throughout the swipe gesture.
-    private AnimatorPlaybackController mLauncherTransitionController;
-    private boolean mHasLauncherTransitionControllerStarted;
+    private AnimatorControllerWithResistance mLauncherTransitionController;
 
     private AnimationFactory mAnimationFactory = (t) -> { };
 
@@ -201,11 +230,15 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
-    public BaseSwipeUpHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
+    public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
-        super(context, deviceState, gestureState, inputConsumer);
+        super(context, deviceState, gestureState, new TransformParams());
+        mActivityInterface = gestureState.getActivityInterface();
+        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
+        mInputConsumerProxy =
+                new InputConsumerProxy(inputConsumer, this::createNewInputProxyHandler);
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
@@ -274,9 +307,17 @@
         }
     }
 
-    @Override
     protected boolean onActivityInit(Boolean alreadyOnHome) {
-        super.onActivityInit(alreadyOnHome);
+        T createdActivity = mActivityInterface.getCreatedActivity();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
+        }
+        if (createdActivity != null) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
+            }
+            initTransitionEndpoints(createdActivity.getDeviceProfile());
+        }
         final T activity = mActivityInterface.getCreatedActivity();
         if (mActivity == activity) {
             return true;
@@ -315,7 +356,9 @@
         return true;
     }
 
-    @Override
+    /**
+     * Return true if the window should be translated horizontally if the recents view scrolls
+     */
     protected boolean moveWindowWithRecentsScroll() {
         return mGestureState.getEndTarget() != HOME;
     }
@@ -328,7 +371,7 @@
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
-        mTaskViewSimulator.setRecentsConfiguration(mActivity.getResources().getConfiguration());
+        mTaskViewSimulator.setRecentsRotation(mActivity.getDisplay().getRotation());
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
@@ -394,7 +437,7 @@
         mGestureState.runOnceAtState(STATE_END_TARGET_SET,
                 () -> mDeviceState.getRotationTouchHelper().
                         onEndTargetCalculated(mGestureState.getEndTarget(),
-                        mActivityInterface));
+                                mActivityInterface));
 
         notifyGestureStartedAsync();
     }
@@ -444,7 +487,9 @@
                 .getHighResLoadingState().setVisible(true);
     }
 
-    @Override
+    /**
+     * Called when motion pause is detected
+     */
     public void onMotionPauseChanged(boolean isPaused) {
         setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
     }
@@ -482,7 +527,6 @@
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
     }
 
-    @Override
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
         setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
     }
@@ -517,11 +561,11 @@
 
     /**
      * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
-     * (it has its own animation) or if we're already animating the current controller.
+     * (it has its own animation).
      * @return Whether we can create the launcher controller or update its progress.
      */
     private boolean canCreateNewOrUpdateExistingLauncherTransitionController() {
-        return mGestureState.getEndTarget() != HOME && !mHasLauncherTransitionControllerStarted;
+        return mGestureState.getEndTarget() != HOME;
     }
 
     @Override
@@ -531,18 +575,20 @@
         return result;
     }
 
-    private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
+    private void onAnimatorPlaybackControllerCreated(AnimatorControllerWithResistance anim) {
         mLauncherTransitionController = anim;
-        mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mLauncherTransitionController.dispatchOnStart();
+        mLauncherTransitionController.getNormalController().dispatchOnStart();
         updateLauncherTransitionProgress();
     }
 
-    @Override
     public Intent getLaunchIntent() {
         return mGestureState.getOverviewIntent();
     }
 
+    /**
+     * Called when the value of {@link #mCurrentShift} changes
+     */
+    @UiThread
     @Override
     public void updateFinalShift() {
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
@@ -571,10 +617,7 @@
                 || !canCreateNewOrUpdateExistingLauncherTransitionController()) {
             return;
         }
-        // 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);
+        mLauncherTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
     }
 
     /**
@@ -603,7 +646,41 @@
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
         ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
-        super.onRecentsAnimationStart(controller, targets);
+        mRecentsAnimationController = controller;
+        mRecentsAnimationTargets = targets;
+        mTransformParams.setTargetSet(mRecentsAnimationTargets);
+        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
+                mGestureState.getRunningTaskId());
+
+        if (runningTaskTarget != null) {
+            mTaskViewSimulator.setPreview(runningTaskTarget);
+        }
+
+        // Only initialize the device profile, if it has not been initialized before, as in some
+        // configurations targets.homeContentInsets may not be correct.
+        if (mActivity == null) {
+            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
+            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
+                Rect overviewStackBounds = mActivityInterface
+                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
+                dp = dp.getMultiWindowProfile(mContext,
+                        new WindowBounds(overviewStackBounds, targets.homeContentInsets));
+            } else {
+                // If we are not in multi-window mode, home insets should be same as system insets.
+                dp = dp.copy(mContext);
+            }
+            dp.updateInsets(targets.homeContentInsets);
+            dp.updateIsSeascape(mContext);
+            initTransitionEndpoints(dp);
+        }
+
+        // Notify when the animation starts
+        if (!mRecentsAnimationStartCallbacks.isEmpty()) {
+            for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
+                action.run();
+            }
+            mRecentsAnimationStartCallbacks.clear();
+        }
 
         // Only add the callback to enable the input consumer after we actually have the controller
         mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
@@ -620,10 +697,14 @@
         mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
 
         // Defer clearing the controller and the targets until after we've updated the state
-        super.onRecentsAnimationCanceled(thumbnailData);
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
+        if (mRecentsView != null) {
+            mRecentsView.setRecentsAnimationTargets(null, null);
+        }
     }
 
-    @Override
+    @UiThread
     public void onGestureStarted(boolean isLikelyToStartNewTask) {
         notifyGestureStartedAsync();
         setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
@@ -647,7 +728,7 @@
     /**
      * Called as a result on ACTION_CANCEL to return the UI to the start state.
      */
-    @Override
+    @UiThread
     public void onGestureCancelled() {
         updateDisplacement(0);
         mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -660,7 +741,7 @@
      * @param velocity The x and y components of the velocity when the gesture ends.
      * @param downPos The x and y value of where the gesture started.
      */
-    @Override
+    @UiThread
     public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
@@ -678,7 +759,10 @@
         handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
     }
 
-    @Override
+    /**
+     * Called to create a input proxy for the running task
+     */
+    @UiThread
     protected InputConsumer createNewInputProxyHandler() {
         endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
         endLauncherTransitionController();
@@ -719,7 +803,7 @@
         ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + mGestureState.getEndTarget());
     }
 
-    @Override
+    /** @return Whether this was the task we were waiting to appear, and thus handled it. */
     protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return false;
@@ -840,11 +924,9 @@
             }
         }
 
-        if (endTarget.isLauncher && mRecentsAnimationController != null) {
-            mRecentsAnimationController.enableInputProxy(mInputConsumer,
-                    this::createNewInputProxyHandler);
+        if (endTarget.isLauncher) {
+            mInputConsumerProxy.enable();
         }
-
         if (endTarget == HOME) {
             setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
             duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
@@ -1023,31 +1105,6 @@
             windowAnim.start();
             mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
         }
-        // Always play the entire launcher animation when going home, since it is separate from
-        // the animation that has been controlled thus far.
-        if (mGestureState.getEndTarget() == HOME) {
-            start = 0;
-        }
-
-        // We want to use the same interpolator as the window, but need to adjust it to
-        // interpolate over the remaining progress (end - start).
-        TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
-                interpolator, start, end);
-        if (mLauncherTransitionController == null) {
-            return;
-        }
-        if (start == end || duration <= 0) {
-            mLauncherTransitionController.dispatchSetInterpolator(t -> end);
-        } else {
-            mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
-        }
-        mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
-
-        if (UNSTABLE_SPRINGS.get()) {
-            mLauncherTransitionController.dispatchOnStart();
-        }
-        mLauncherTransitionController.getAnimationPlayer().start();
-        mHasLauncherTransitionControllerStarted = true;
     }
 
     private void computeRecentsScrollIfInvisible() {
@@ -1083,7 +1140,6 @@
         anim.addAnimatorListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
                 if (mActivity != null) {
                     removeLiveTileOverlay();
                 }
@@ -1099,10 +1155,12 @@
                 mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
             }
         });
+        if (mRecentsAnimationTargets != null) {
+            mRecentsAnimationTargets.addReleaseCheck(anim);
+        }
         return anim;
     }
 
-    @Override
     public void onConsumerAboutToBeSwitched() {
         if (mActivity != null) {
             // In the off chance that the gesture ends before Launcher is started, we should clear
@@ -1131,15 +1189,6 @@
 
     @UiThread
     private void startNewTask() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mRecentsAnimationController.finish(true /* toRecents */, this::startNewTaskInternal);
-        } else {
-            startNewTaskInternal();
-        }
-    }
-
-    @UiThread
-    private void startNewTaskInternal() {
         TaskView taskToLaunch = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
         startNewTask(success -> {
             if (!success) {
@@ -1153,9 +1202,19 @@
         });
     }
 
-    @Override
+    /**
+     * Called when we successfully startNewTask() on the task that was previously running. Normally
+     * we call resumeLastTask() when returning to the previously running task, but this handles a
+     * specific edge case: if we switch from A to B, and back to A before B appears, we need to
+     * start A again to ensure it stays on top.
+     */
+    @androidx.annotation.CallSuper
     protected void onRestartPreviouslyAppearedTask() {
-        super.onRestartPreviouslyAppearedTask();
+        // Finish the controller here, since we won't get onTaskAppeared() for a task that already
+        // appeared.
+        if (mRecentsAnimationController != null) {
+            mRecentsAnimationController.finish(false, null);
+        }
         reset();
     }
 
@@ -1170,13 +1229,10 @@
     private void cancelCurrentAnimation() {
         mCanceled = true;
         mCurrentShift.cancelAnimation();
-        if (mLauncherTransitionController != null && mLauncherTransitionController
-                .getAnimationPlayer().isStarted()) {
-            mLauncherTransitionController.getAnimationPlayer().cancel();
-        }
     }
 
     private void invalidateHandler() {
+        mInputConsumerProxy.destroy();
         endRunningWindowAnim(false /* cancel */);
 
         if (mGestureEndCallback != null) {
@@ -1198,7 +1254,10 @@
     private void endLauncherTransitionController() {
         setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
         if (mLauncherTransitionController != null) {
-            mLauncherTransitionController.getAnimationPlayer().end();
+            // End the animation, but stay at the same visual progress.
+            mLauncherTransitionController.getNormalController().dispatchSetInterpolator(
+                    t -> Utilities.boundToRange(mCurrentShift.value, 0, 1));
+            mLauncherTransitionController.getNormalController().getAnimationPlayer().end();
             mLauncherTransitionController = null;
         }
     }
@@ -1259,7 +1318,7 @@
                     // new thumbnail
                     finishTransitionPosted = ViewUtils.postDraw(taskView,
                             () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
-                                    this::isCanceled);
+                            this::isCanceled);
                 }
             }
             if (!finishTransitionPosted) {
@@ -1328,4 +1387,171 @@
         return app.isNotInRecents
                 || app.activityType == ACTIVITY_TYPE_HOME;
     }
+
+    /**
+     * To be called at the end of constructor of subclasses. This calls various methods which can
+     * depend on proper class initialization.
+     */
+    protected void initAfterSubclassConstructor() {
+        initTransitionEndpoints(
+                mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
+    }
+
+    protected void performHapticFeedback() {
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+    }
+
+    public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
+        return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
+    }
+
+    public void setGestureEndCallback(Runnable gestureEndCallback) {
+        mGestureEndCallback = gestureEndCallback;
+    }
+
+    protected void linkRecentsViewScroll() {
+        SurfaceTransactionApplier.create(mRecentsView, applier -> {
+            mTransformParams.setSyncTransactionApplier(applier);
+            runOnRecentsAnimationStart(() ->
+                    mRecentsAnimationTargets.addReleaseCheck(applier));
+        });
+
+        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+            if (moveWindowWithRecentsScroll()) {
+                updateFinalShift();
+            }
+        });
+        runOnRecentsAnimationStart(() ->
+                mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
+                        mRecentsAnimationTargets));
+        mRecentsViewScrollLinked = true;
+    }
+
+    protected void startNewTask(Consumer<Boolean> resultCallback) {
+        // 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.getNextPageTaskView().launchTask(false /* animate */,
+                    true /* freezeTaskList */);
+        } else {
+            if (!mCanceled) {
+                TaskView nextTask = mRecentsView.getNextPageTaskView();
+                if (nextTask != null) {
+                    int taskId = nextTask.getTask().key.id;
+                    mGestureState.updateLastStartedTaskId(taskId);
+                    boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
+                            .contains(taskId);
+                    nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
+                            success -> {
+                                resultCallback.accept(success);
+                                if (success) {
+                                    if (hasTaskPreviouslyAppeared) {
+                                        onRestartPreviouslyAppearedTask();
+                                    }
+                                } else {
+                                    mActivityInterface.onLaunchTaskFailed();
+                                    nextTask.notifyTaskLaunchFailed(TAG);
+                                    mRecentsAnimationController.finish(true /* toRecents */, null);
+                                }
+                            }, MAIN_EXECUTOR.getHandler());
+                } else {
+                    mActivityInterface.onLaunchTaskFailed();
+                    Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
+                    mRecentsAnimationController.finish(true /* toRecents */, null);
+                }
+            }
+            mCanceled = false;
+        }
+    }
+
+    /**
+     * Runs the given {@param action} if the recents animation has already started, or queues it to
+     * be run when it is next started.
+     */
+    protected void runOnRecentsAnimationStart(Runnable action) {
+        if (mRecentsAnimationTargets == null) {
+            mRecentsAnimationStartCallbacks.add(action);
+        } else {
+            action.run();
+        }
+    }
+
+    /**
+     * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
+     * @return whether the recents animation has started and there are valid app targets.
+     */
+    protected boolean hasTargets() {
+        return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
+    }
+
+    @Override
+    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
+        if (mRecentsView != null) {
+            mRecentsView.setRecentsAnimationTargets(null, null);
+        }
+    }
+
+    @Override
+    public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+        if (mRecentsAnimationController != null) {
+            if (handleTaskAppeared(appearedTaskTarget)) {
+                mRecentsAnimationController.finish(false /* toRecents */,
+                        null /* onFinishComplete */);
+                mActivityInterface.onLaunchTaskSuccess();
+                ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+            }
+        }
+    }
+
+    /**
+     * @return The index of the TaskView in RecentsView whose taskId matches the task that will
+     * resume if we finish the controller.
+     */
+    protected int getLastAppearedTaskIndex() {
+        return mGestureState.getLastAppearedTaskId() != -1
+                ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
+                : mRecentsView.getRunningTaskIndex();
+    }
+
+    /**
+     * @return Whether we are continuing a gesture that already landed on a new task,
+     * but before that task appeared.
+     */
+    protected boolean hasStartedNewTask() {
+        return mGestureState.getLastStartedTaskId() != -1;
+    }
+
+    /**
+     * Registers a callback to run when the activity is ready.
+     * @param intent The intent that will be used to start the activity if it doesn't exist already.
+     */
+    public void initWhenReady(Intent intent) {
+        // Preload the plan
+        RecentsModel.INSTANCE.get(mContext).getTasks(null);
+
+        mActivityInitListener.register(intent);
+    }
+
+    /**
+     * Applies the transform on the recents animation
+     */
+    protected void applyWindowTransform() {
+        if (mWindowTransitionController != null) {
+            mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
+        }
+        if (mRecentsAnimationTargets != null) {
+            if (mRecentsViewScrollLinked) {
+                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+            }
+            mTaskViewSimulator.apply(mTransformParams);
+        }
+    }
+
+    public interface Factory {
+
+        AbsSwipeUpHandler<StatefulActivity<?>, RecentsView> newHandler(
+                GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index 9310685..55f5424 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -79,8 +79,8 @@
         BaseActivityInterface.AnimationFactory factory = mActivityInterface.prepareRecentsUI(
                 mDeviceState,
                 wasVisible, (controller) -> {
-                    controller.dispatchOnStart();
-                    controller.getAnimationPlayer().end();
+                    controller.getNormalController().dispatchOnStart();
+                    controller.getNormalController().getAnimationPlayer().end();
                 });
         factory.createActivityInterface(RECENTS_LAUNCH_DURATION);
         factory.setRecentsAttachedToAppWindow(true, false);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
deleted file mode 100644
index b49299d..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ /dev/null
@@ -1,393 +0,0 @@
-/*
- * 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 android.widget.Toast.LENGTH_SHORT;
-
-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 android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Build;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.widget.Toast;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.VibratorWrapper;
-import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.quickstep.util.TransformParams;
-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.InputConsumerController;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-/**
- * Base class for swipe up handler with some utility methods
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
-        extends SwipeUpAnimationLogic implements RecentsAnimationListener {
-
-    private static final String TAG = "BaseSwipeUpHandler";
-
-    protected final BaseActivityInterface<?, T> mActivityInterface;
-    protected final InputConsumerController mInputConsumer;
-
-    protected final ActivityInitListener mActivityInitListener;
-
-    protected RecentsAnimationController mRecentsAnimationController;
-    protected RecentsAnimationTargets mRecentsAnimationTargets;
-
-    // Callbacks to be made once the recents animation starts
-    private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
-
-    protected T mActivity;
-    protected Q mRecentsView;
-
-    protected Runnable mGestureEndCallback;
-
-    protected MultiStateCallback mStateCallback;
-
-    protected boolean mCanceled;
-
-    private boolean mRecentsViewScrollLinked = false;
-
-    protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer) {
-        super(context, deviceState, gestureState, new TransformParams());
-        mActivityInterface = gestureState.getActivityInterface();
-        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
-        mInputConsumer = inputConsumer;
-    }
-
-    /**
-     * To be called at the end of constructor of subclasses. This calls various methods which can
-     * depend on proper class initialization.
-     */
-    protected void initAfterSubclassConstructor() {
-        initTransitionEndpoints(
-                mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
-    }
-
-    protected void performHapticFeedback() {
-        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
-    }
-
-    public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
-        return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
-    }
-
-    public void setGestureEndCallback(Runnable gestureEndCallback) {
-        mGestureEndCallback = gestureEndCallback;
-    }
-
-    public abstract Intent getLaunchIntent();
-
-    protected void linkRecentsViewScroll() {
-        SurfaceTransactionApplier.create(mRecentsView, applier -> {
-            mTransformParams.setSyncTransactionApplier(applier);
-            runOnRecentsAnimationStart(() ->
-                    mRecentsAnimationTargets.addReleaseCheck(applier));
-        });
-
-        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
-            if (moveWindowWithRecentsScroll()) {
-                updateFinalShift();
-            }
-        });
-        runOnRecentsAnimationStart(() ->
-                mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
-                        mRecentsAnimationTargets));
-        mRecentsViewScrollLinked = true;
-    }
-
-    protected void startNewTask(Consumer<Boolean> resultCallback) {
-        // 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.getNextPageTaskView().launchTask(false /* animate */,
-                    true /* freezeTaskList */);
-        } else {
-            if (!mCanceled) {
-                TaskView nextTask = mRecentsView.getNextPageTaskView();
-                if (nextTask != null) {
-                    int taskId = nextTask.getTask().key.id;
-                    mGestureState.updateLastStartedTaskId(taskId);
-                    boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
-                            .contains(taskId);
-                    nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
-                            success -> {
-                                resultCallback.accept(success);
-                                if (success) {
-                                    if (hasTaskPreviouslyAppeared) {
-                                        onRestartPreviouslyAppearedTask();
-                                    }
-                                } else {
-                                    mActivityInterface.onLaunchTaskFailed();
-                                    nextTask.notifyTaskLaunchFailed(TAG);
-                                    mRecentsAnimationController.finish(true /* toRecents */, null);
-                                }
-                            }, MAIN_EXECUTOR.getHandler());
-                } else {
-                    mActivityInterface.onLaunchTaskFailed();
-                    Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
-                    mRecentsAnimationController.finish(true /* toRecents */, null);
-                }
-            }
-            mCanceled = false;
-        }
-    }
-
-    /**
-     * Called when we successfully startNewTask() on the task that was previously running. Normally
-     * we call resumeLastTask() when returning to the previously running task, but this handles a
-     * specific edge case: if we switch from A to B, and back to A before B appears, we need to
-     * start A again to ensure it stays on top.
-     */
-    @CallSuper
-    protected void onRestartPreviouslyAppearedTask() {
-        // Finish the controller here, since we won't get onTaskAppeared() for a task that already
-        // appeared.
-        if (mRecentsAnimationController != null) {
-            mRecentsAnimationController.finish(false, null);
-        }
-    }
-
-    /**
-     * Runs the given {@param action} if the recents animation has already started, or queues it to
-     * be run when it is next started.
-     */
-    protected void runOnRecentsAnimationStart(Runnable action) {
-        if (mRecentsAnimationTargets == null) {
-            mRecentsAnimationStartCallbacks.add(action);
-        } else {
-            action.run();
-        }
-    }
-
-    /**
-     * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
-     * @return whether the recents animation has started and there are valid app targets.
-     */
-    protected boolean hasTargets() {
-        return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
-    }
-
-    @Override
-    public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
-            RecentsAnimationTargets targets) {
-        mRecentsAnimationController = recentsAnimationController;
-        mRecentsAnimationTargets = targets;
-        mTransformParams.setTargetSet(mRecentsAnimationTargets);
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
-                mGestureState.getRunningTaskId());
-
-        if (runningTaskTarget != null) {
-            mTaskViewSimulator.setPreview(runningTaskTarget);
-        }
-
-        // Only initialize the device profile, if it has not been initialized before, as in some
-        // configurations targets.homeContentInsets may not be correct.
-        if (mActivity == null) {
-            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
-            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
-                Rect overviewStackBounds = mActivityInterface
-                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
-                dp = dp.getMultiWindowProfile(mContext,
-                        new WindowBounds(overviewStackBounds, targets.homeContentInsets));
-            } else {
-                // If we are not in multi-window mode, home insets should be same as system insets.
-                dp = dp.copy(mContext);
-            }
-            dp.updateInsets(targets.homeContentInsets);
-            dp.updateIsSeascape(mContext);
-            initTransitionEndpoints(dp);
-        }
-
-        // Notify when the animation starts
-        if (!mRecentsAnimationStartCallbacks.isEmpty()) {
-            for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
-                action.run();
-            }
-            mRecentsAnimationStartCallbacks.clear();
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
-        if (mRecentsView != null) {
-            mRecentsView.setRecentsAnimationTargets(null, null);
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-        mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
-        if (mRecentsView != null) {
-            mRecentsView.setRecentsAnimationTargets(null, null);
-        }
-    }
-
-    @Override
-    public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
-        if (mRecentsAnimationController != null) {
-            if (handleTaskAppeared(appearedTaskTarget)) {
-                mRecentsAnimationController.finish(false /* toRecents */,
-                        null /* onFinishComplete */);
-                mActivityInterface.onLaunchTaskSuccess();
-                ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
-            }
-        }
-    }
-
-    /** @return Whether this was the task we were waiting to appear, and thus handled it. */
-    protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget);
-
-    /**
-     * @return The index of the TaskView in RecentsView whose taskId matches the task that will
-     * resume if we finish the controller.
-     */
-    protected int getLastAppearedTaskIndex() {
-        return mGestureState.getLastAppearedTaskId() != -1
-                ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
-                : mRecentsView.getRunningTaskIndex();
-    }
-
-    /**
-     * @return Whether we are continuing a gesture that already landed on a new task,
-     * but before that task appeared.
-     */
-    protected boolean hasStartedNewTask() {
-        return mGestureState.getLastStartedTaskId() != -1;
-    }
-
-    /**
-     * Return true if the window should be translated horizontally if the recents view scrolls
-     */
-    protected abstract boolean moveWindowWithRecentsScroll();
-
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
-        T createdActivity = mActivityInterface.getCreatedActivity();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
-        }
-        if (createdActivity != null) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
-            }
-            initTransitionEndpoints(createdActivity.getDeviceProfile());
-        }
-        return true;
-    }
-
-    /**
-     * Called to create a input proxy for the running task
-     */
-    @UiThread
-    protected abstract InputConsumer createNewInputProxyHandler();
-
-    /**
-     * Called when the value of {@link #mCurrentShift} changes
-     */
-    @UiThread
-    public abstract void updateFinalShift();
-
-    /**
-     * Called when motion pause is detected
-     */
-    public abstract void onMotionPauseChanged(boolean isPaused);
-
-    @UiThread
-    public void onGestureStarted(boolean isLikelyToStartNewTask) { }
-
-    @UiThread
-    public abstract void onGestureCancelled();
-
-    @UiThread
-    public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
-
-    public abstract void onConsumerAboutToBeSwitched();
-
-    public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
-
-    /**
-     * Registers a callback to run when the activity is ready.
-     * @param intent The intent that will be used to start the activity if it doesn't exist already.
-     */
-    public void initWhenReady(Intent intent) {
-        // Preload the plan
-        RecentsModel.INSTANCE.get(mContext).getTasks(null);
-
-        mActivityInitListener.register(intent);
-    }
-
-    /**
-     * Applies the transform on the recents animation
-     */
-    protected void applyWindowTransform() {
-        if (mWindowTransitionController != null) {
-            float progress = mCurrentShift.value / mDragLengthFactor;
-            mWindowTransitionController.setPlayFraction(progress);
-        }
-        if (mRecentsAnimationTargets != null) {
-            if (mRecentsViewScrollLinked) {
-                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
-            }
-            mTaskViewSimulator.apply(mTransformParams);
-        }
-    }
-
-    @Override
-    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
-            HomeAnimationFactory homeAnimationFactory) {
-        RectFSpringAnim anim =
-                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
-        if (mRecentsAnimationTargets != null) {
-            mRecentsAnimationTargets.addReleaseCheck(anim);
-        }
-        return anim;
-    }
-
-    public interface Factory {
-
-        BaseSwipeUpHandler newHandler(
-                GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index d1da0c1..3898f0b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -27,11 +27,11 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -84,7 +84,7 @@
     /** 6 */
     @Override
     public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
-            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+            boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
         factory.initUI();
         return factory;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index 1909f47..ffb05df 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -15,14 +15,35 @@
  */
 package com.android.quickstep;
 
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
+import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelUuid;
+import android.os.UserHandle;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 
 import androidx.annotation.NonNull;
 
@@ -32,18 +53,32 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
 
+import java.lang.ref.WeakReference;
+import java.util.UUID;
+import java.util.function.Consumer;
+
 /**
  * Handles the navigation gestures when a 3rd party launcher is the default home activity.
  */
+@TargetApi(Build.VERSION_CODES.R)
 public class FallbackSwipeHandler extends
-        BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
+        AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
+
+    /**
+     * Message used for receiving gesture nav contract information. We use a static messenger to
+     * avoid leaking too make binders in case the receiving launcher does not handle the contract
+     * properly.
+     */
+    private static StaticMessageReceiver sMessageReceiver = null;
 
     private FallbackHomeAnimationFactory mActiveAnimationFactory;
     private final boolean mRunningOverHome;
@@ -89,7 +124,9 @@
     protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
         mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
         ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
-        mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
+        Intent intent = new Intent(mGestureState.getHomeIntent());
+        mActiveAnimationFactory.addGestureContract(intent);
+        mContext.startActivity(intent, options.toBundle());
         return mActiveAnimationFactory;
     }
 
@@ -130,15 +167,19 @@
     }
 
     private class FallbackHomeAnimationFactory extends HomeAnimationFactory {
-
+        private final Rect mTempRect = new Rect();
         private final TransformParams mHomeAlphaParams = new TransformParams();
         private final AnimatedFloat mHomeAlpha;
 
         private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
-
         private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
 
+        private final RectF mTargetRect = new RectF();
+        private SurfaceControl mSurfaceControl;
+
         private final long mDuration;
+
+        private RectFSpringAnim mSpringAnim;
         FallbackHomeAnimationFactory(long duration) {
             mDuration = duration;
 
@@ -161,6 +202,15 @@
                     this::updateRecentsActivityTransformDuringHomeAnim);
         }
 
+        @NonNull
+        @Override
+        public RectF getWindowTargetRect() {
+            if (mTargetRect.isEmpty()) {
+                mTargetRect.set(super.getWindowTargetRect());
+            }
+            return mTargetRect;
+        }
+
         private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
                 RemoteAnimationTargetCompat app, TransformParams params) {
             builder.withAlpha(mRecentsAlpha.value);
@@ -217,5 +267,87 @@
                         .start();
             }
         }
+
+        @Override
+        public void setAnimation(RectFSpringAnim anim) {
+            mSpringAnim = anim;
+        }
+
+        private void onMessageReceived(Message msg) {
+            try {
+                Bundle data = msg.getData();
+                RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+                if (!position.isEmpty()) {
+                    mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+                    mTargetRect.set(position);
+                    if (mSpringAnim != null) {
+                        mSpringAnim.onTargetPositionChanged();
+                    }
+                }
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+
+        @Override
+        public void update(RectF currentRect, float progress, float radius) {
+            if (mSurfaceControl != null) {
+                currentRect.roundOut(mTempRect);
+                Transaction t = new Transaction();
+                t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                t.apply();
+            }
+        }
+
+        private void addGestureContract(Intent intent) {
+            if (mRunningOverHome || mGestureState.getRunningTask() == null) {
+                return;
+            }
+
+            TaskKey key = new TaskKey(mGestureState.getRunningTask());
+            if (key.getComponent() != null) {
+                if (sMessageReceiver == null) {
+                    sMessageReceiver = new StaticMessageReceiver();
+                }
+
+                Bundle gestureNavContract = new Bundle();
+                gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+                gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+                gestureNavContract.putParcelable(EXTRA_REMOTE_CALLBACK,
+                        sMessageReceiver.newCallback(this::onMessageReceived));
+                intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+            }
+        }
+    }
+
+    private static class StaticMessageReceiver implements Handler.Callback {
+
+        private final Messenger mMessenger =
+                new Messenger(new Handler(Looper.getMainLooper(), this));
+
+        private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+        private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+        public Message newCallback(Consumer<Message> callback) {
+            mCurrentUID = new ParcelUuid(UUID.randomUUID());
+            mCurrentCallback = new WeakReference<>(callback);
+
+            Message msg = Message.obtain();
+            msg.replyTo = mMessenger;
+            msg.obj = mCurrentUID;
+            return msg;
+        }
+
+        @Override
+        public boolean handleMessage(@NonNull Message message) {
+            if (mCurrentUID.equals(message.obj)) {
+                Consumer<Message> consumer = mCurrentCallback.get();
+                if (consumer != null) {
+                    consumer.accept(message);
+                    return true;
+                }
+            }
+            return false;
+        }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index b020355..4e38f49 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -39,9 +39,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
 import com.android.launcher3.statemanager.StateManager;
@@ -50,6 +48,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
 import com.android.quickstep.views.RecentsView;
@@ -119,7 +118,7 @@
 
     @Override
     public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
-            boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
+            boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
         notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
             @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 052d0a6..4411455 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -39,7 +39,7 @@
  * Temporary class to allow easier refactoring
  */
 public class LauncherSwipeHandlerV2 extends
-        BaseSwipeUpHandlerV2<BaseQuickstepLauncher, RecentsView> {
+        AbsSwipeUpHandler<BaseQuickstepLauncher, RecentsView> {
 
     public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
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 6f4d34c..5026f36 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -58,6 +58,12 @@
                         FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
                 return response;
             }
+
+            case TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        FeatureFlags.ENABLE_OVERVIEW_SHARE.get());
+                return response;
+            }
         }
 
         return super.call(method);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
index 07faab7..137f809 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -16,7 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -24,7 +24,6 @@
 import android.graphics.Matrix.ScaleToFit;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
@@ -35,6 +34,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
@@ -45,7 +45,6 @@
 public abstract class SwipeUpAnimationLogic {
 
     protected static final Rect TEMP_RECT = new Rect();
-    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
 
     protected DeviceProfile mDp;
 
@@ -66,12 +65,8 @@
     protected int mTransitionDragLength;
     // How much further we can drag past recents, as a factor of mTransitionDragLength.
     protected float mDragLengthFactor = 1;
-    // Start resisting when swiping past this factor of mTransitionDragLength.
-    private float mDragLengthFactorStartPullback = 1f;
-    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private float mDragLengthFactorMaxPullback = 1f;
 
-    protected AnimatorPlaybackController mWindowTransitionController;
+    protected AnimatorControllerWithResistance mWindowTransitionController;
 
     public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, TransformParams transformParams) {
@@ -97,19 +92,16 @@
         if (mDeviceState.isFullyGesturalNavMode()) {
             // We can drag all the way to the top of the screen.
             mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
-
-            float startScale = mTaskViewSimulator.getFullScreenScale();
-            // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
-            mDragLengthFactorStartPullback = (0.75f - startScale) / (1 - startScale);
-            mDragLengthFactorMaxPullback = (0.5f - startScale) / (1 - startScale);
         } else {
-            mDragLengthFactor = 1;
-            mDragLengthFactorStartPullback = mDragLengthFactorMaxPullback = 1;
+            mDragLengthFactor = 1 + AnimatorControllerWithResistance.TWO_BUTTON_EXTRA_DRAG_FACTOR;
         }
 
         PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
-        mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
-        mWindowTransitionController = pa.createPlaybackController();
+        mTaskViewSimulator.addAppToOverviewAnim(pa, LINEAR);
+        AnimatorPlaybackController normalController = pa.createPlaybackController();
+        mWindowTransitionController = AnimatorControllerWithResistance.createForRecents(
+                normalController, mContext, mTaskViewSimulator.getOrientationState(),
+                mDp, mTaskViewSimulator.recentsViewScale, AnimatedFloat.VALUE);
     }
 
     @UiThread
@@ -122,13 +114,6 @@
         } else {
             float translation = Math.max(displacement, 0);
             shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-            if (shift > mDragLengthFactorStartPullback) {
-                float pullbackProgress = Utilities.getProgress(shift,
-                        mDragLengthFactorStartPullback, mDragLengthFactor);
-                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
-                shift = mDragLengthFactorStartPullback + pullbackProgress
-                        * (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
-            }
         }
 
         mCurrentShift.updateValue(shift);
@@ -183,7 +168,7 @@
             HomeAnimationFactory homeAnimationFactory) {
         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
 
-        mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
+        mCurrentShift.updateValue(startProgress);
         mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
         RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
 
@@ -287,7 +272,6 @@
 
         @Override
         public void onAnimationStart(Animator animation) {
-            super.onAnimationStart(animation);
             mHomeAnim.dispatchOnStart();
         }
 
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 8837c0e..1012ab2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -58,6 +58,7 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -74,6 +75,7 @@
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
+import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
 import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
 import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
@@ -267,9 +269,9 @@
         return sIsInitialized;
     }
 
-    private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
+    private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
             this::createLauncherSwipeHandler;
-    private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
+    private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
             this::createFallbackSwipeHandler;
 
     private ActivityManagerWrapper mAM;
@@ -297,6 +299,7 @@
         mAM = ActivityManagerWrapper.getInstance();
         mDeviceState = new RecentsAnimationDeviceState(this);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+        mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         ProtoTracer.INSTANCE.get(this).add(this);
@@ -338,6 +341,13 @@
         resetHomeBounceSeenOnQuickstepEnabledFirstTime();
     }
 
+    /**
+     * Called when the one handed mode overlay package changes, to recreate touch region.
+     */
+    private void onOneHandedModeOverlayChanged(int newGesturalHeight) {
+        initInputMonitor();
+    }
+
     @UiThread
     public void onUserUnlocked() {
         mTaskAnimationManager = new TaskAnimationManager();
@@ -500,6 +510,11 @@
                             mGestureState,
                             InputConsumer.NO_OP, mInputMonitorCompat,
                             mOverviewComponentObserver.assistantGestureIsConstrained());
+                } else if (mDeviceState.canTriggerOneHandedAction(event)
+                    && !mDeviceState.isOneHandedModeActive()) {
+                    // Consume gesture event for triggering one handed feature.
+                    mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
+                        InputConsumer.NO_OP, mInputMonitorCompat);
                 } else {
                     mUncheckedConsumer = InputConsumer.NO_OP;
                 }
@@ -627,6 +642,11 @@
                 base = new ScreenPinnedInputConsumer(this, newGestureState);
             }
 
+            if (mDeviceState.canTriggerOneHandedAction(event)) {
+                base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+                        mInputMonitorCompat);
+            }
+
             if (mDeviceState.isAccessibilityMenuAvailable()) {
                 base = new AccessibilityInputConsumer(this, mDeviceState, base,
                         mInputMonitorCompat);
@@ -635,6 +655,11 @@
             if (mDeviceState.isScreenPinningActive()) {
                 base = mResetGestureInputConsumer;
             }
+
+            if (mDeviceState.canTriggerOneHandedAction(event)) {
+                base = new OneHandedModeInputConsumer(this, mDeviceState, base,
+                        mInputMonitorCompat);
+            }
         }
         return base;
     }
@@ -691,7 +716,7 @@
     private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
             MotionEvent event) {
 
-        final BaseSwipeUpHandler.Factory factory;
+        final AbsSwipeUpHandler.Factory factory;
         if (!mOverviewComponentObserver.isHomeAndOverviewSame()) {
             factory = mFallbackSwipeHandlerFactory;
         } else {
@@ -796,6 +821,13 @@
         }
         if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(),
                 activity.getResources().getConfiguration().diff(newConfig))) {
+            // Since navBar gestural height are different between portrait and landscape,
+            // can handle orientation changes and refresh navigation gestural region through
+            // onOneHandedModeChanged()
+            int newGesturalHeight = ResourceUtils.getNavbarSize(
+                    ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
+                    getApplicationContext().getResources());
+            mDeviceState.onOneHandedModeChanged(newGesturalHeight);
             return;
         }
 
@@ -857,13 +889,13 @@
         }
     }
 
-    private BaseSwipeUpHandler createLauncherSwipeHandler(
+    private AbsSwipeUpHandler createLauncherSwipeHandler(
             GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
         return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackSwipeHandler(
+    private AbsSwipeUpHandler createFallbackSwipeHandler(
             GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
         return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 3f1e7ba..163c232 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep.fallback;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -25,6 +24,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
 
 import com.android.launcher3.anim.PendingAnimation;
@@ -82,7 +82,7 @@
                 MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
 
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
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 a946304..f5f5259 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
@@ -101,20 +101,20 @@
     }
 
     @Override
-    protected boolean shouldAddDummyTaskView(RunningTaskInfo runningTaskInfo) {
+    protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
         if (mHomeTaskInfo != null && runningTaskInfo != null &&
                 mHomeTaskInfo.taskId == runningTaskInfo.taskId
                 && getTaskViewCount() == 0) {
-            // Do not add a dummy task if we are running over home with empty recents, so that we
-            // show the empty recents message instead of showing a dummy task and later removing it.
+            // Do not add a stub task if we are running over home with empty recents, so that we
+            // show the empty recents message instead of showing a stub task and later removing it.
             return false;
         }
-        return super.shouldAddDummyTaskView(runningTaskInfo);
+        return super.shouldAddStubTaskView(runningTaskInfo);
     }
 
     @Override
     protected void applyLoadPlan(ArrayList<Task> tasks) {
-        // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
+        // When quick-switching on 3p-launcher, we add a "stub" 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 if we are switching on any other app.
         if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
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 a676390..7032fae 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
@@ -21,7 +21,7 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.AbsSwipeUpHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
@@ -60,7 +60,7 @@
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
 
 /**
- * A dummy input consumer used when the device is still locked, e.g. from secure camera.
+ * A placeholder input consumer used when the device is still locked, e.g. from secure camera.
  */
 public class DeviceLockedInputConsumer implements InputConsumer,
         RecentsAnimationCallbacks.RecentsAnimationListener, BuilderProxy {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
new file mode 100644
index 0000000..22bd334
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -0,0 +1,160 @@
+/*
+ * 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.inputconsumers;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Touch consumer for handling gesture event to launch one handed
+ * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
+ */
+public class OneHandedModeInputConsumer extends DelegateInputConsumer {
+
+    private static final String TAG = "OneHandedModeInputConsumer";
+    private static final int ANGLE_MAX = 150;
+    private static final int ANGLE_MIN = 30;
+
+    private final Context mContext;
+    private final RecentsAnimationDeviceState mDeviceState;
+
+    private final float mDragDistThreshold;
+    private final float mSquaredSlop;
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+
+    private boolean mPassedSlop;
+
+    public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor) {
+        super(delegate, inputMonitor);
+        mContext = context;
+        mDeviceState = deviceState;
+        mDragDistThreshold = context.getResources().getDimensionPixelSize(
+                R.dimen.gestures_onehanded_drag_threshold);
+        mSquaredSlop = Utilities.squaredTouchSlop(context);
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_ONE_HANDED | mDelegate.getType();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        switch (ev.getActionMasked()) {
+            case ACTION_DOWN: {
+                mDownPos.set(ev.getX(), ev.getY());
+                mLastPos.set(mDownPos);
+                break;
+            }
+            case ACTION_MOVE: {
+                if (mState == STATE_DELEGATE_ACTIVE) {
+                    break;
+                }
+                if (!mDelegate.allowInterceptByParent()) {
+                    mState = STATE_DELEGATE_ACTIVE;
+                    break;
+                }
+
+                mLastPos.set(ev.getX(), ev.getY());
+                if (!mPassedSlop) {
+                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
+                            > mSquaredSlop) {
+                        if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle(
+                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))
+                                || (mDeviceState.isOneHandedModeActive() && isValidExitAngle(
+                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) {
+                            mPassedSlop = true;
+                            setActive(ev);
+                        } else {
+                            mState = STATE_DELEGATE_ACTIVE;
+                        }
+                    }
+                } else {
+                    float distance = (float) Math.hypot(mLastPos.x - mDownPos.x,
+                            mLastPos.y - mDownPos.y);
+                    if (distance > mDragDistThreshold && mPassedSlop) {
+                        onStopGestureDetected();
+                    }
+                }
+                break;
+            }
+            case ACTION_UP: {
+                if (mLastPos.y >= mDownPos.y && mPassedSlop) {
+                    onStartGestureDetected();
+                }
+
+                mPassedSlop = false;
+                mState = STATE_INACTIVE;
+                break;
+            }
+            case ACTION_CANCEL:
+                mPassedSlop = false;
+                mState = STATE_INACTIVE;
+                break;
+        }
+
+        if (mState != STATE_ACTIVE) {
+            mDelegate.onMotionEvent(ev);
+        }
+    }
+
+    private void onStartGestureDetected() {
+        if (mDeviceState.isOneHandedModeEnabled()) {
+            if (!mDeviceState.isOneHandedModeActive()) {
+                SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode();
+            }
+        } else if (mDeviceState.isSwipeToNotificationEnabled()) {
+            SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel();
+        }
+    }
+
+    private void onStopGestureDetected() {
+        if (!mDeviceState.isOneHandedModeEnabled() || !mDeviceState.isOneHandedModeActive()) {
+            return;
+        }
+
+        SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode();
+    }
+
+    private boolean isValidStartAngle(float deltaX, float deltaY) {
+        final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+        return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
+    }
+
+    private boolean isValidExitAngle(float deltaX, float deltaY) {
+        final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+        return angle > ANGLE_MIN && angle < ANGLE_MAX;
+    }
+}
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 6259f1f..f02e5e6 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
@@ -53,8 +53,8 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.BaseSwipeUpHandler;
-import com.android.quickstep.BaseSwipeUpHandler.Factory;
+import com.android.quickstep.AbsSwipeUpHandler;
+import com.android.quickstep.AbsSwipeUpHandler.Factory;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationCallbacks;
@@ -93,7 +93,7 @@
     private final InputMonitorCompat mInputMonitorCompat;
     private final BaseActivityInterface mActivityInterface;
 
-    private final BaseSwipeUpHandler.Factory mHandlerFactory;
+    private final AbsSwipeUpHandler.Factory mHandlerFactory;
 
     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
     private final MotionPauseDetector mMotionPauseDetector;
@@ -101,7 +101,7 @@
 
     private VelocityTracker mVelocityTracker;
 
-    private BaseSwipeUpHandler mInteractionHandler;
+    private AbsSwipeUpHandler mInteractionHandler;
 
     private final boolean mIsDeferredDownTarget;
     private final PointF mDownPos = new PointF();
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
deleted file mode 100644
index e000803..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.appprediction.PredictionUiStateManager;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-import java.util.ArrayList;
-
-/**
- * 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 ArrayList<LauncherLogProto.Target> targets) {
-        PredictionUiStateManager.fillInPredictedRank(itemInfo, target);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/InputConsumerProxy.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/InputConsumerProxy.java
new file mode 100644
index 0000000..3e87f48
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/InputConsumerProxy.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import android.util.Log;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import java.util.function.Supplier;
+
+/**
+ * Utility class which manages proxying input events from {@link InputConsumerController}
+ * to an {@link InputConsumer}
+ */
+public class InputConsumerProxy {
+
+    private static final String TAG = "InputConsumerProxy";
+
+    private final InputConsumerController mInputConsumerController;
+    private final Supplier<InputConsumer> mConsumerSupplier;
+
+    // The consumer is created lazily on demand.
+    private InputConsumer mInputConsumer;
+
+    private boolean mDestroyed = false;
+    private boolean mTouchInProgress = false;
+    private boolean mDestroyPending = false;
+
+    public InputConsumerProxy(InputConsumerController inputConsumerController,
+            Supplier<InputConsumer> consumerSupplier) {
+        mInputConsumerController = inputConsumerController;
+        mConsumerSupplier = consumerSupplier;
+    }
+
+    public void enable() {
+        if (mDestroyed) {
+            return;
+        }
+        mInputConsumerController.setInputListener(this::onInputConsumerEvent);
+    }
+
+    private boolean onInputConsumerEvent(InputEvent ev) {
+        if (ev instanceof MotionEvent) {
+            onInputConsumerMotionEvent((MotionEvent) ev);
+        } else if (ev instanceof KeyEvent) {
+            if (mInputConsumer == null) {
+                mInputConsumer = mConsumerSupplier.get();
+            }
+            mInputConsumer.onKeyEvent((KeyEvent) ev);
+            return true;
+        }
+        return false;
+    }
+
+    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) {
+                mInputConsumer = mConsumerSupplier.get();
+            }
+        } else if (action == ACTION_CANCEL || action == ACTION_UP) {
+            // Finish any pending actions
+            mTouchInProgress = false;
+            if (mDestroyPending) {
+                destroy();
+            }
+        }
+        if (mInputConsumer != null) {
+            mInputConsumer.onMotionEvent(ev);
+        }
+
+        return true;
+    }
+
+    public void destroy() {
+        if (mTouchInProgress) {
+            mDestroyPending = true;
+            return;
+        }
+        mDestroyPending = false;
+        mDestroyed = true;
+        mInputConsumerController.setInputListener(null);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
index 64ae1e0..94e496d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -32,7 +32,6 @@
 import android.util.IntProperty;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -73,6 +72,7 @@
     private final BaseActivityInterface mSizeStrategy;
 
     private final Rect mTaskRect = new Rect();
+    private float mOffsetY;
     private final PointF mPivot = new PointF();
     private DeviceProfile mDp;
 
@@ -93,7 +93,6 @@
     public final AnimatedFloat recentsViewScale = new AnimatedFloat();
     public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
     private final ScrollState mScrollState = new ScrollState();
-    private final int mPageSpacing;
 
     // Cached calculations
     private boolean mLayoutValid = false;
@@ -107,7 +106,6 @@
         mOrientationState.setGestureActive(true);
 
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
-        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
     }
 
     /**
@@ -130,8 +128,8 @@
     /**
      * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration)
      */
-    public void setRecentsConfiguration(Configuration configuration) {
-        mOrientationState.setActivityConfiguration(configuration);
+    public void setRecentsRotation(int recentsRotation) {
+        mOrientationState.setRecentsRotation(recentsRotation);
         mLayoutValid = false;
     }
 
@@ -178,8 +176,12 @@
         }
     }
 
+    public void setOffsetY(float offsetY) {
+        mOffsetY = offsetY;
+    }
+
     /**
-     * Adds animation for all the components corresponding to transition from an app to overview
+     * Adds animation for all the components corresponding to transition from an app to overview.
      */
     public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
         pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
@@ -187,6 +189,14 @@
     }
 
     /**
+     * Adds animation for all the components corresponding to transition from overview to the app.
+     */
+    public void addOverviewToAppAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+        pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 0, 1, interpolator);
+        pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, 1, getFullScreenScale(), interpolator);
+    }
+
+    /**
      * Returns the current clipped/visible window bounds in the window coordinate space
      */
     public RectF getCurrentCropRect() {
@@ -262,7 +272,8 @@
             int start = mOrientationState.getOrientationHandler()
                     .getPrimaryValue(mTaskRect.left, mTaskRect.top);
             mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
-            mScrollState.updateInterpolation(start, mPageSpacing);
+            mScrollState.pageParentScale = recentsViewScale.value;
+            mScrollState.updateInterpolation(start);
             mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
         }
 
@@ -281,7 +292,7 @@
         mMatrix.postScale(scale, scale);
 
         // Apply TaskView matrix: translate, scale, scroll
-        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top + mOffsetY);
         mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
         mOrientationState.getOrientationHandler().set(
                 mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
@@ -307,7 +318,7 @@
                 .withCornerRadius(getCurrentCornerRadius());
 
         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) {
-            builder.withRelativeLayerTo(params.getRecentsSurface(), Integer.MAX_VALUE);
+            builder.withRelativeLayerTo(params.getRecentsSurface(), Integer.MIN_VALUE);
         }
     }
 
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 1034234..f31bc19 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
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep.views;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -39,14 +38,11 @@
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.RecentsExtraCard;
 
@@ -57,8 +53,6 @@
 public class LauncherRecentsView extends RecentsView<BaseQuickstepLauncher>
         implements StateListener<LauncherState> {
 
-    private final TransformParams mTransformParams = new TransformParams();
-
     private RecentsExtraCard mRecentsExtraCardPlugin;
     private RecentsExtraViewContainer mRecentsExtraViewContainer;
     private PluginListener<RecentsExtraCard> mRecentsExtraCardPluginListener =
@@ -108,17 +102,6 @@
         }
     }
 
-    @Override
-    public void setTranslationY(float translationY) {
-        super.setTranslationY(translationY);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            LauncherState state = mActivity.getStateManager().getState();
-            if (state == OVERVIEW || state == ALL_APPS) {
-                redrawLiveTile(false);
-            }
-        }
-    }
-
     /**
      * Animates adjacent tasks and translate hotseat off screen as well.
      */
@@ -141,29 +124,10 @@
         }
         anim.play(ObjectAnimator.ofFloat(
                 mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen));
-
-        ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt(
-                mActivity.getScrimView(), ScrimView.DRAG_HANDLE_ALPHA, 0);
-        dragHandleAnim.setInterpolator(Interpolators.ACCEL_2);
-        anim.play(dragHandleAnim);
-
         return anim;
     }
 
     @Override
-    protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (tv.isRunningTask()) {
-                mTransformParams.setProgress(1 - progress)
-                        .setSyncTransactionApplier(mSyncTransactionApplier);
-                // TODO: Revisit live tiles
-            } else {
-                redrawLiveTile(true);
-            }
-        }
-    }
-
-    @Override
     protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             mActivity.getStateManager().goToState(NORMAL, false /* animate */);
@@ -175,46 +139,6 @@
     }
 
     @Override
-    public void scrollTo(int x, int y) {
-        super.scrollTo(x, y);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
-            redrawLiveTile(true);
-        }
-    }
-
-    @Override
-    public TransformParams getLiveTileParams(
-            boolean mightNeedToRefill) {
-        if (!mEnableDrawingLiveTile || mRecentsAnimationController == null
-                || mRecentsAnimationTargets == null) {
-            return null;
-        }
-        TaskView taskView = getRunningTaskView();
-        if (taskView != null) {
-            taskView.getThumbnail().getGlobalVisibleRect(mTempRect);
-            int offsetX = (int) (mTaskWidth * taskView.getScaleX() * getScaleX()
-                    - mTempRect.width());
-            int offsetY = (int) (mTaskHeight * taskView.getScaleY() * getScaleY()
-                    - mTempRect.height());
-            if (((mCurrentPage != 0) || mightNeedToRefill) && offsetX > 0) {
-                if (mTempRect.left - offsetX < 0) {
-                    mTempRect.left -= offsetX;
-                } else {
-                    mTempRect.right += offsetX;
-                }
-            }
-            if (mightNeedToRefill && offsetY > 0) {
-                mTempRect.top -= offsetY;
-            }
-            mTransformParams.setProgress(1f)
-                    .setTargetAlpha(taskView.getAlpha())
-                    .setSyncTransactionApplier(mSyncTransactionApplier)
-                    .setTargetSet(mRecentsAnimationTargets);
-        }
-        return mTransformParams;
-    }
-
-    @Override
     public void reset() {
         super.reset();
 
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 1ad02a9..9393e08 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
@@ -22,7 +22,6 @@
 
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
@@ -62,6 +61,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -78,6 +78,7 @@
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
@@ -129,6 +130,7 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
@@ -209,15 +211,37 @@
                 }
             };
 
+    /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
+    public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
+            new FloatProperty<RecentsView>("recentsScale") {
+                @Override
+                public void setValue(RecentsView view, float scale) {
+                    view.setScaleX(scale);
+                    view.setScaleY(scale);
+                    view.mLastComputedTaskPushOutDistance = null;
+                    view.updatePageOffsets();
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.getScaleX();
+                }
+            };
+
     protected RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
-    protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected SurfaceTransactionApplier mSyncTransactionApplier;
     protected int mTaskWidth;
     protected int mTaskHeight;
+    protected final TransformParams mLiveTileParams = new TransformParams();
+    protected final TaskViewSimulator mLiveTileTaskViewSimulator;
+    protected final Rect mLastComputedTaskSize = new Rect();
+    // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
+    protected Float mLastComputedTaskPushOutDistance = null;
     protected boolean mEnableDrawingLiveTile = false;
     protected final Rect mTempRect = new Rect();
+    protected final RectF mTempRectF = new RectF();
     private final PointF mTempPointF = new PointF();
 
     private static final int DISMISS_TASK_DURATION = 300;
@@ -393,7 +417,8 @@
         mActivity = BaseActivity.fromContext(context);
         mOrientationState = new RecentsOrientedState(
                 context, mSizeStrategy, this::animateRecentsRotationInPlace);
-        mOrientationState.setActivityConfiguration(context.getResources().getConfiguration());
+        final int rotation = mActivity.getDisplay().getRotation();
+        mOrientationState.setRecentsRotation(rotation);
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
@@ -429,6 +454,12 @@
 
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
+
+        mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy());
+        mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+        mLiveTileTaskViewSimulator.setLayoutRotation(getPagedViewOrientedState().getTouchRotation(),
+                getPagedViewOrientedState().getDisplayRotation());
+        mLiveTileTaskViewSimulator.setRecentsRotation(rotation);
     }
 
     public OverScroller getScroller() {
@@ -513,6 +544,7 @@
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
         mSyncTransactionApplier = new SurfaceTransactionApplier(this);
+        mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
         mIdp.addOnChangeListener(this);
         mIPinnedStackAnimationListener.setActivity(mActivity);
@@ -530,6 +562,7 @@
         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
         mSyncTransactionApplier = null;
+        mLiveTileParams.setSyncTransactionApplier(null);
         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
         mIdp.removeOnChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
@@ -648,6 +681,16 @@
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
+
+        TaskView taskView = getCurrentPageTaskView();
+        if (taskView != null) {
+            TouchDelegate mChildTouchDelegate = taskView.getIconTouchDelegate(ev);
+            if (mChildTouchDelegate != null && mChildTouchDelegate.onTouchEvent(ev)) {
+                // Keep consuming events to pass to delegate
+                return true;
+            }
+        }
+
         final int x = (int) ev.getX();
         final int y = (int) ev.getY();
         switch (ev.getAction()) {
@@ -806,6 +849,14 @@
                 taskView.setModalness(mTaskModalness);
             }
         }
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
+            // to reset the params after it settles in Overview from swipe up so that we don't
+            // render with obsolete param values.
+            mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
+            mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+            mLiveTileTaskViewSimulator.setOffsetY(0);
+        }
         if (mRunningTaskTileHidden) {
             setRunningTaskHidden(mRunningTaskTileHidden);
         }
@@ -847,6 +898,7 @@
     public void setInsets(Rect insets) {
         mInsets.set(insets);
         resetPaddingFromTaskSize();
+        mLiveTileTaskViewSimulator.setDp(mActivity.getDeviceProfile());
     }
 
     private void resetPaddingFromTaskSize() {
@@ -864,6 +916,7 @@
     public void getTaskSize(Rect outRect) {
         mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
                 mOrientationHandler);
+        mLastComputedTaskSize.set(outRect);
     }
 
     /** Gets the task size for modal state. */
@@ -888,6 +941,12 @@
 
         // Update the high res thumbnail loader state
         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
+
+        mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+                && mLiveTileParams.getTargetSet() != null) {
+            redrawLiveTile();
+        }
         return scrolling;
     }
 
@@ -905,8 +964,8 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            mScrollState.updateInterpolation(mOrientationHandler.getChildStartWithTranslation(page),
-                    mPageSpacing);
+            mScrollState.updateInterpolation(
+                    mOrientationHandler.getChildStartWithTranslation(page));
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
     }
@@ -995,7 +1054,7 @@
         mTaskListChangeId = -1;
 
         mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
+        mLiveTileParams.setTargetSet(null);
 
         unloadVisibleTaskData();
         setCurrentPage(0);
@@ -1124,9 +1183,9 @@
     }
 
     /**
-     * Returns true if we should add a dummy taskView for the running task id
+     * Returns true if we should add a stub taskView for the running task id
      */
-    protected boolean shouldAddDummyTaskView(RunningTaskInfo runningTaskInfo) {
+    protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
         return runningTaskInfo != null && getTaskView(runningTaskInfo.taskId) == null;
     }
 
@@ -1137,7 +1196,7 @@
      * is called.  Also scrolls the view to this task.
      */
     public void showCurrentTask(RunningTaskInfo runningTaskInfo) {
-        if (shouldAddDummyTaskView(runningTaskInfo)) {
+        if (shouldAddStubTaskView(runningTaskInfo)) {
             boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
@@ -1328,10 +1387,14 @@
         /**
          * Updates linearInterpolation for the provided child position
          */
-        public void updateInterpolation(float childStart, int pageSpacing) {
-            float pageCenter = childStart + halfPageSize;
+        public void updateInterpolation(float childStart) {
+            float scaledHalfPageSize = halfPageSize / pageParentScale;
+            float pageCenter = childStart + scaledHalfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
-            float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
+            // How far the page has to move from the center to be offscreen, taking into account
+            // the EDGE_SCALE_DOWN_FACTOR that will be applied at that position.
+            float distanceToReachEdge = halfScreenSize
+                    + scaledHalfPageSize * (1 - TaskView.EDGE_SCALE_DOWN_FACTOR);
             linearInterpolation = Math.min(1,
                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
         }
@@ -1446,6 +1509,13 @@
             anim.addOnFrameCallback(this::updateCurveProperties);
         }
 
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && getRunningTaskView() == taskView) {
+            anim.addOnFrameCallback(() -> {
+                mLiveTileTaskViewSimulator.setOffsetY(taskView.getTranslationY());
+                redrawLiveTile();
+            });
+        }
+
         // Add a tiny bit of translation Z, so that it draws on top of other views
         if (animateTaskView) {
             taskView.setTranslationZ(0.1f);
@@ -1657,9 +1727,11 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        if (mOrientationState.setActivityConfiguration(newConfig)) {
+        final int rotation = mActivity.getDisplay().getRotation();
+        if (mOrientationState.setRecentsRotation(rotation)) {
             updateOrientationHandler();
         }
+        mLiveTileTaskViewSimulator.setRecentsRotation(rotation);
     }
 
     public void setLayoutRotation(int touchRotation, int displayRotation) {
@@ -1688,6 +1760,8 @@
         requestLayout();
         // Reapply the current page to update page scrolls.
         setCurrentPage(mCurrentPage);
+        mLiveTileTaskViewSimulator.setLayoutRotation(getPagedViewOrientedState().getTouchRotation(),
+                getPagedViewOrientedState().getDisplayRotation());
     }
 
     public RecentsOrientedState getPagedViewOrientedState() {
@@ -1769,14 +1843,15 @@
         setPivotX(mTempPointF.x);
         setPivotY(mTempPointF.y);
         setTaskModalness(mTaskModalness);
+        mLastComputedTaskPushOutDistance = null;
         updatePageOffsets();
         setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
     }
 
     private void updatePageOffsets() {
-        float offset = mAdjacentPageOffset * getWidth();
-        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness) * getWidth();
+        float offset = mAdjacentPageOffset;
+        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
         if (mIsRtl) {
             offset = -offset;
             modalOffset = -modalOffset;
@@ -1785,19 +1860,90 @@
 
         TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden
                 ? null : getTaskView(mRunningTaskId);
-        int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
-        int currentPage = getCurrentPage();
+        int midpoint = runningTask == null ? -1 : indexOfChild(runningTask);
+        int modalMidpoint = getCurrentPage();
+
+        float midpointOffsetSize = 0;
+        float leftOffsetSize = midpoint - 1 >= 0
+                ? -getOffsetSize(midpoint - 1, midpoint, offset)
+                : 0;
+        float rightOffsetSize = midpoint + 1 < count
+                ? getOffsetSize(midpoint + 1, midpoint, offset)
+                : 0;
+
+        float modalMidpointOffsetSize = 0;
+        float modalLeftOffsetSize = modalMidpoint - 1 >= 0
+                ? -getOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
+                : 0;
+        float modalRightOffsetSize = modalMidpoint + 1 < count
+                ? getOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset)
+                : 0;
 
         for (int i = 0; i < count; i++) {
-            float translation = i == midPoint ? 0 : (i < midPoint ? -offset : offset);
-            float modalTranslation =
-                    i == currentPage ? 0 : (i < currentPage ? -modalOffset : modalOffset);
+            float translation = i == midpoint
+                    ? midpointOffsetSize
+                    : i < midpoint
+                            ? leftOffsetSize
+                            : rightOffsetSize;
+            float modalTranslation = i == modalMidpoint
+                    ? modalMidpointOffsetSize
+                    : i < modalMidpoint
+                            ? modalLeftOffsetSize
+                            : modalRightOffsetSize;
             getChildAt(i).setTranslationX(translation + modalTranslation);
         }
         updateCurveProperties();
     }
 
     /**
+     * Computes the distance to offset the given child such that it is completely offscreen when
+     * translating away from the given midpoint.
+     * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
+     */
+    private float getOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
+        if (offsetProgress == 0) {
+            // Don't bother calculating everything below if we won't offset anyway.
+            return 0;
+        }
+        // First, get the position of the task relative to the midpoint. If there is no midpoint
+        // then we just use the normal (centered) task position.
+        mTempRectF.set(mLastComputedTaskSize);
+        RectF taskPosition = mTempRectF;
+        float desiredLeft = getWidth();
+        float distanceToOffscreen = desiredLeft - taskPosition.left;
+        // Used to calculate the scale of the task view based on its new offset.
+        float centerToOffscreenProgress = Math.abs(offsetProgress);
+        if (midpointIndex > -1) {
+            // When there is a midpoint reference task, adjacent tasks have less distance to travel
+            // to reach offscreen. Offset the task position to the task's starting point.
+            View child = getChildAt(childIndex);
+            View midpointChild = getChildAt(midpointIndex);
+            int distanceFromMidpoint = Math.abs(mOrientationHandler.getChildStart(child)
+                    - mOrientationHandler.getChildStart(midpointChild)
+                    + getDisplacementFromScreenCenter(midpointIndex));
+            taskPosition.offset(distanceFromMidpoint, 0);
+            centerToOffscreenProgress = Utilities.mapRange(centerToOffscreenProgress,
+                    distanceFromMidpoint / distanceToOffscreen, 1);
+        }
+        // Find the task's scale based on its offscreen progress, then see how far it still needs to
+        // move to be completely offscreen.
+        Utilities.scaleRectFAboutCenter(taskPosition,
+                TaskView.getCurveScaleForInterpolation(centerToOffscreenProgress));
+        distanceToOffscreen = desiredLeft - taskPosition.left;
+        // Finally, we need to account for RecentsView scale, because it moves tasks based on its
+        // pivot. To do this, we move the task position to where it would be offscreen at scale = 1
+        // (computed above), then we apply the scale via getMatrix() to determine how much that
+        // moves the task from its desired position, and adjust the computed distance accordingly.
+        if (mLastComputedTaskPushOutDistance == null) {
+            taskPosition.offsetTo(desiredLeft, 0);
+            getMatrix().mapRect(taskPosition);
+            mLastComputedTaskPushOutDistance = (taskPosition.left - desiredLeft) / getScaleX();
+        }
+        distanceToOffscreen -= mLastComputedTaskPushOutDistance;
+        return distanceToOffscreen * offsetProgress;
+    }
+
+    /**
      * TODO: Do not assume motion across X axis for adjacent page
      */
     public float getPageOffsetScale() {
@@ -1895,7 +2041,7 @@
         float toScale = getMaxScaleForFullScreen();
         if (launchingCenterTask) {
             RecentsView recentsView = tv.getRecentsView();
-            anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
+            anim.play(ObjectAnimator.ofFloat(recentsView, RECENTS_SCALE_PROPERTY, toScale));
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -1945,8 +2091,6 @@
                             ? targetSysUiFlags
                             : 0);
 
-            onTaskLaunchAnimationUpdate(animator.getAnimatedFraction(), tv);
-
             // Passing the threshold from taskview to fullscreen app will vibrate
             final boolean passed = animator.getAnimatedFraction() >=
                     SUCCESS_TRANSITION_PROGRESS;
@@ -1970,6 +2114,10 @@
 
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
+            mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+        }
         mPendingAnimation.addEndListener((endState) -> {
             if (endState.isSuccess) {
                 Consumer<Boolean> onLaunchResult = (result) -> {
@@ -1995,9 +2143,6 @@
         return mPendingAnimation;
     }
 
-    protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
-    }
-
     protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             resetTaskVisuals();
@@ -2064,13 +2209,23 @@
         mEnableDrawingLiveTile = enableDrawingLiveTile;
     }
 
-    public void redrawLiveTile(boolean mightNeedToRefill) { }
+    public void redrawLiveTile() {
+        mLiveTileTaskViewSimulator.apply(mLiveTileParams);
+    }
+
+    public TaskViewSimulator getLiveTileTaskViewSimulator() {
+        return mLiveTileTaskViewSimulator;
+    }
 
     // TODO: To be removed in a follow up CL
     public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
             RecentsAnimationTargets recentsAnimationTargets) {
         mRecentsAnimationController = recentsAnimationController;
-        mRecentsAnimationTargets = recentsAnimationTargets;
+        if (recentsAnimationTargets != null) {
+            mLiveTileTaskViewSimulator.setPreview(
+                    recentsAnimationTargets.apps[recentsAnimationTargets.apps.length - 1]);
+            mLiveTileParams.setTargetSet(recentsAnimationTargets);
+        }
     }
 
     public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
@@ -2206,11 +2361,6 @@
         };
     }
 
-    public TransformParams getLiveTileParams(
-            boolean mightNeedToRefill) {
-        return null;
-    }
-
     private void updateEnabledOverlays() {
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
index 9b2048e..8b49f2c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
@@ -282,7 +282,6 @@
         mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
                 setVisibility(VISIBLE);
             }
 
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 37f6faf..7651dd8 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
@@ -59,7 +59,6 @@
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ConfigurationCompat;
 
 /**
  * A task in the Recents view.
@@ -126,7 +125,7 @@
         mDimmingPaintAfterClearing.setColor(Color.BLACK);
         mActivity = BaseActivity.fromContext(context);
         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
-        // Initialize with dummy value. It is overridden later by TaskView
+        // Initialize with placeholder value. It is overridden later by TaskView
         mFullscreenParams = TEMP_PARAMS.get(context);
     }
 
@@ -385,8 +384,8 @@
         if (mBitmapShader != null && mThumbnailData != null) {
             mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
                     mThumbnailData.thumbnail.getHeight());
-            int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
-                    mActivity.getResources().getConfiguration());
+            int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
+                    .getRecentsActivityRotation();
             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
                     getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
                     currentRotation);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 82fabac..6b7daec 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -22,10 +22,14 @@
 import static android.view.Gravity.END;
 import static android.view.Gravity.START;
 import static android.view.Gravity.TOP;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.comp;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
@@ -52,7 +56,9 @@
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -77,6 +83,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.TransformingTouchDelegate;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
@@ -121,6 +128,13 @@
 
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
+    /**
+     * This technically can be a vanilla {@link TouchDelegate} class, however that class requires
+     * setting the touch bounds at construction, so we'd repeatedly be created many instances
+     * unnecessarily as scrolling occurs, whereas {@link TransformingTouchDelegate} allows touch
+     * delegated bounds only to be updated.
+     */
+    private TransformingTouchDelegate mIconTouchDelegate;
 
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
@@ -185,6 +199,7 @@
     private int mStackHeight;
     private View mContextualChipWrapper;
     private View mContextualChip;
+    private final float[] mIconCenterCoords = new float[2];
 
     public TaskView(Context context) {
         this(context, null);
@@ -230,14 +245,14 @@
      */
     public WorkspaceItemInfo getItemInfo() {
         ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(getTask().key);
-        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
-        dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK;
-        dummyInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
-        dummyInfo.user = componentKey.user;
-        dummyInfo.intent = new Intent().setComponent(componentKey.componentName);
-        dummyInfo.title = TaskUtils.getTitle(getContext(), getTask());
-        dummyInfo.screenId = getRecentsView().indexOfChild(this);
-        return dummyInfo;
+        WorkspaceItemInfo stubInfo = new WorkspaceItemInfo();
+        stubInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK;
+        stubInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
+        stubInfo.user = componentKey.user;
+        stubInfo.intent = new Intent().setComponent(componentKey.componentName);
+        stubInfo.title = TaskUtils.getTitle(getContext(), getTask());
+        stubInfo.screenId = getRecentsView().indexOfChild(this);
+        return stubInfo;
     }
 
     @Override
@@ -245,6 +260,26 @@
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
         mIconView = findViewById(R.id.icon);
+        mIconTouchDelegate = new TransformingTouchDelegate(mIconView);
+    }
+
+    public TouchDelegate getIconTouchDelegate(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            computeAndSetIconTouchDelegate();
+        }
+        return mIconTouchDelegate;
+    }
+
+    private void computeAndSetIconTouchDelegate() {
+        float iconHalfSize = mIconView.getWidth() / 2f;
+        mIconCenterCoords[0] = mIconCenterCoords[1] = iconHalfSize;
+        getDescendantCoordRelativeToAncestor(mIconView, mActivity.getDragLayer(), mIconCenterCoords,
+                false);
+        mIconTouchDelegate.setBounds(
+                (int) (mIconCenterCoords[0] - iconHalfSize),
+                (int) (mIconCenterCoords[1] - iconHalfSize),
+                (int) (mIconCenterCoords[0] + iconHalfSize),
+                (int) (mIconCenterCoords[1] + iconHalfSize));
     }
 
     /**
@@ -466,18 +501,18 @@
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         switch (orientationHandler.getRotation()) {
-            case Surface.ROTATION_90:
+            case ROTATION_90:
                 iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
                 iconParams.rightMargin = -thumbnailPadding;
                 iconParams.leftMargin = 0;
                 iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
-            case Surface.ROTATION_180:
+            case ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
                 iconParams.bottomMargin = -thumbnailPadding;
                 iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
                 break;
-            case Surface.ROTATION_270:
+            case ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
                 iconParams.leftMargin = -thumbnailPadding;
                 iconParams.rightMargin = 0;
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index e05688e..258f24a 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -17,9 +17,7 @@
 <com.android.quickstep.views.OverviewActionsView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="@dimen/overview_actions_height"
-    android:layout_gravity="center_horizontal|bottom"
-    android:layout_marginLeft="@dimen/overview_actions_horizontal_margin"
-    android:layout_marginRight="@dimen/overview_actions_horizontal_margin">
+    android:layout_gravity="center_horizontal|bottom">
 
     <LinearLayout
         android:id="@+id/action_buttons"
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9d70316..6737c5f 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -76,6 +76,10 @@
     <dimen name="gestures_assistant_drag_threshold">55dp</dimen>
     <dimen name="gestures_assistant_fling_threshold">55dp</dimen>
 
+    <!-- One-Handed Mode -->
+    <!-- Threshold for draging distance to enable one-handed mode -->
+    <dimen name="gestures_onehanded_drag_threshold">20dp</dimen>
+
     <!-- Distance to move elements when swiping up to go home from launcher -->
     <dimen name="home_pullback_distance">28dp</dimen>
 
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
index c148a4b..7049af0 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
@@ -60,9 +60,9 @@
 
         FallbackRecentsView frv = activity.getOverviewPanel();
 
-        RunningTaskInfo dummyTask = new RunningTaskInfo();
-        dummyTask.taskId = 22;
-        frv.showCurrentTask(dummyTask);
+        RunningTaskInfo placeholderTask = new RunningTaskInfo();
+        placeholderTask.taskId = 22;
+        frv.showCurrentTask(placeholderTask);
         doLayout(activity);
 
         ThumbnailData thumbnailData = new ThumbnailData();
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 6b941be..235df42 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -111,6 +111,13 @@
     }
 
     @Override
+    protected void handleGestureContract(Intent intent) {
+        if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) {
+            super.handleGestureContract(intent);
+        }
+    }
+
+    @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
         RecentsModel.INSTANCE.get(this).onTrimMemory(level);
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index bd97f65..f3d5434 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -45,7 +45,6 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -55,7 +54,6 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Looper;
@@ -94,7 +92,6 @@
  * {@link LauncherAppTransitionManager} with Quickstep-specific app transitions for launching from
  * home and/or all-apps.  Not used for 3p launchers.
  */
-@TargetApi(Build.VERSION_CODES.O)
 @SuppressWarnings("unused")
 public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTransitionManager
         implements OnDeviceProfileChangeListener {
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index f42b124..cc7b712 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -43,9 +43,11 @@
 import android.util.Log;
 
 import androidx.annotation.MainThread;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.RemoteActionShortcut;
 import com.android.launcher3.popup.SystemShortcut;
@@ -57,6 +59,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * Data model for digital wellbeing status of apps.
@@ -72,6 +75,9 @@
     private static final int MSG_FULL_REFRESH = 3;
 
     // Welbeing contract
+    private static final String PATH_ACTIONS = "actions";
+    private static final String PATH_MINIMAL_DEVICE = "minimal_device";
+    private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config";
     private static final String METHOD_GET_ACTIONS = "get_actions";
     private static final String EXTRA_ACTIONS = "actions";
     private static final String EXTRA_ACTION = "action";
@@ -104,15 +110,22 @@
         mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
             @Override
             public void onChange(boolean selfChange, Uri uri) {
-                // Wellbeing reports that app actions have changed.
                 if (DEBUG || mIsInTest) {
-                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
-                            + "], uri = [" + uri + "]");
+                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = ["
+                            + selfChange + "], uri = [" + uri + "]");
                 }
                 Preconditions.assertUIThread();
-                updateWellbeingData();
+
+                if (uri.getPath().contains(PATH_ACTIONS)) {
+                    // Wellbeing reports that app actions have changed.
+                    updateWellbeingData();
+                } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) {
+                    // Wellbeing reports that minimal device state or config is changed.
+                    updateLauncherModel();
+                }
             }
         };
+        FeatureFlags.ENABLE_MINIMAL_DEVICE.addChangeListener(mContext, this::updateLauncherModel);
 
         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
             context.registerReceiver(
@@ -146,14 +159,18 @@
     private void restartObserver() {
         final ContentResolver resolver = mContext.getContentResolver();
         resolver.unregisterContentObserver(mContentObserver);
-        Uri actionsUri = apiBuilder().path("actions").build();
+        Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
+        Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build();
         try {
             resolver.registerContentObserver(
                     actionsUri, true /* notifyForDescendants */, mContentObserver);
+            resolver.registerContentObserver(
+                    minimalDeviceUri, true /* notifyForDescendants */, mContentObserver);
         } catch (Exception e) {
             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
             if (mIsInTest) throw new RuntimeException(e);
         }
+
         updateWellbeingData();
     }
 
@@ -191,12 +208,42 @@
         mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
     }
 
+    private void updateLauncherModel() {
+        if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) return;
+
+        // TODO: init Launcher in minimal device / normal mode
+    }
+
     private Uri.Builder apiBuilder() {
         return new Uri.Builder()
                 .scheme(SCHEME_CONTENT)
                 .authority(mWellbeingProviderPkg + ".api");
     }
 
+    /**
+     * Fetch most up-to-date minimal device config.
+     */
+    @WorkerThread
+    private void runWithMinimalDeviceConfigs(Consumer<Bundle> consumer) {
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "runWithMinimalDeviceConfigs() called");
+        }
+        Preconditions.assertNonUiThread();
+
+        final Uri contentUri = apiBuilder().build();
+        final Bundle remoteBundle;
+        try (ContentProviderClient client = mContext.getContentResolver()
+                .acquireUnstableContentProviderClient(contentUri)) {
+            remoteBundle = client.call(
+                    METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */);
+            consumer.accept(remoteBundle);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+        }
+        if (DEBUG || mIsInTest) Log.i(TAG, "runWithMinimalDeviceConfigs(): finished");
+    }
+
     private boolean updateActions(String... packageNames) {
         if (packageNames.length == 0) {
             return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index ec3a490..6e120e8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
@@ -29,6 +28,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.util.FloatProperty;
 
@@ -61,7 +61,7 @@
     @Override
     public void setState(@NonNull LauncherState state) {
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
-        SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+        RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
         ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
 
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
@@ -93,7 +93,7 @@
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index f4a394a..a9a4e95 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,12 +15,11 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.SysUINavigationMode.getMode;
 import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
@@ -28,6 +27,7 @@
 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.animation.Animator;
 import android.annotation.TargetApi;
@@ -52,6 +52,7 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.views.RecentsView;
@@ -106,7 +107,7 @@
     public abstract void onAssistantVisibilityChanged(float visibility);
 
     public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
-            boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
+            boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback);
 
     public abstract ActivityInitListener createActivityInitListener(
             Predicate<Boolean> onInitListener);
@@ -312,11 +313,11 @@
 
         protected final ACTIVITY_TYPE mActivity;
         private final STATE_TYPE mStartState;
-        private final Consumer<AnimatorPlaybackController> mCallback;
+        private final Consumer<AnimatorControllerWithResistance> mCallback;
 
         private boolean mIsAttachedToWindow;
 
-        DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
+        DefaultAnimationFactory(Consumer<AnimatorControllerWithResistance> callback) {
             mCallback = callback;
 
             mActivity = getCreatedActivity();
@@ -344,7 +345,13 @@
             controller.setEndAction(() -> mActivity.getStateManager().goToState(
                     controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
                     false));
-            mCallback.accept(controller);
+
+            RecentsView recentsView = mActivity.getOverviewPanel();
+            AnimatorControllerWithResistance controllerWithResistance =
+                    AnimatorControllerWithResistance.createForRecents(controller, mActivity,
+                            recentsView.getPagedViewOrientedState(), mActivity.getDeviceProfile(),
+                            recentsView, RECENTS_SCALE_PROPERTY);
+            mCallback.accept(controllerWithResistance);
 
             // Creating the activity controller animation sometimes reapplies the launcher state
             // (because we set the animation as the current state animation), so we reapply the
@@ -393,7 +400,7 @@
         protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
             //  Scale down recents from being full screen to being in overview.
             RecentsView recentsView = activity.getOverviewPanel();
-            pa.addFloat(recentsView, SCALE_PROPERTY,
+            pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
                     recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
             pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
         }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index ec720d5..67711c0 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -35,6 +35,7 @@
     int TYPE_RESET_GESTURE = 1 << 8;
     int TYPE_OVERSCROLL = 1 << 9;
     int TYPE_SYSUI_OVERLAY = 1 << 10;
+    int TYPE_ONE_HANDED = 1 << 11;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -47,7 +48,8 @@
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
             "TYPE_OVERSCROLL",              // 9
-            "TYPE_SYSUI_OVERLAY"         // 10
+            "TYPE_SYSUI_OVERLAY",           // 10
+            "TYPE_ONE_HANDED",              // 11
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 1081548..b2bce0c 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -63,7 +63,9 @@
     private SparseArray<OrientationRectF> mSwipeTouchRegions = new SparseArray<>(MAX_ORIENTATIONS);
     private final RectF mAssistantLeftRegion = new RectF();
     private final RectF mAssistantRightRegion = new RectF();
+    private final RectF mOneHandedModeRegion = new RectF();
     private int mCurrentDisplayRotation;
+    private int mNavBarGesturalHeight;
     private boolean mEnableMultipleRegions;
     private Resources mResources;
     private OrientationRectF mLastRectTouched;
@@ -103,18 +105,33 @@
         mResources = resources;
         mMode = mode;
         mContractInfo = contractInfo;
+        mNavBarGesturalHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
     }
 
-    void setNavigationMode(SysUINavigationMode.Mode newMode, DefaultDisplay.Info info) {
+    private void refreshTouchRegion(DefaultDisplay.Info info, Resources newRes) {
+        // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
+        // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
+        // It tries to cache and reuse swipe regions whenever possible based only on rotation
+        mResources = newRes;
+        mSwipeTouchRegions.clear();
+        resetSwipeRegions(info);
+    }
+
+    void setNavigationMode(SysUINavigationMode.Mode newMode, DefaultDisplay.Info info,
+            Resources newRes) {
         if (mMode == newMode) {
             return;
         }
         this.mMode = newMode;
-        // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
-        // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
-        // It tries to cache and reuse swipe regions whenever possible based only on rotation
-        mSwipeTouchRegions.clear();
-        resetSwipeRegions(info);
+        refreshTouchRegion(info, newRes);
+    }
+
+    void setGesturalHeight(int newGesturalHeight, DefaultDisplay.Info info, Resources newRes) {
+        if (mNavBarGesturalHeight == newGesturalHeight) {
+            return;
+        }
+        mNavBarGesturalHeight = newGesturalHeight;
+        refreshTouchRegion(info, newRes);
     }
 
     /**
@@ -216,10 +233,10 @@
 
         Point size = display.realSize;
         int rotation = display.rotation;
+        int touchHeight = mNavBarGesturalHeight;
         OrientationRectF orientationRectF =
                 new OrientationRectF(0, 0, size.x, size.y, rotation);
         if (mMode == SysUINavigationMode.Mode.NO_BUTTON) {
-            int touchHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
             orientationRectF.top = orientationRectF.bottom - touchHeight;
             updateAssistantRegions(orientationRectF);
         } else {
@@ -235,10 +252,11 @@
                             + getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
                     break;
                 default:
-                    orientationRectF.top = orientationRectF.bottom
-                            - getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
+                    orientationRectF.top = orientationRectF.bottom - touchHeight;
             }
         }
+        // One handed gestural only active on portrait mode
+        mOneHandedModeRegion.set(0, orientationRectF.bottom - touchHeight, size.x, size.y);
 
         return orientationRectF;
     }
@@ -264,6 +282,10 @@
 
     }
 
+    boolean touchInOneHandedModeRegion(MotionEvent ev) {
+        return mOneHandedModeRegion.contains(ev.getX(), ev.getY());
+    }
+
     private int getNavbarSize(String resName) {
         return ResourceUtils.getNavbarSize(resName, mResources);
     }
@@ -352,9 +374,11 @@
         StringBuilder regions = new StringBuilder("  currentTouchableRotations=");
         for(int i = 0; i < mSwipeTouchRegions.size(); i++) {
             OrientationRectF rectF = mSwipeTouchRegions.get(mSwipeTouchRegions.keyAt(i));
-            regions.append(rectF.mRotation).append(" ");
+            regions.append(rectF).append(" ");
         }
         pw.println(regions.toString());
+        pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
+        pw.println("  mOneHandedModeRegion=" + mOneHandedModeRegion);
     }
 
     private class OrientationRectF extends RectF {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 4e9aa61..51f5e5d 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -15,49 +15,30 @@
  */
 package com.android.quickstep;
 
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_UP;
-
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.util.Preconditions;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.function.Consumer;
-import java.util.function.Supplier;
 
 /**
  * Wrapper around RecentsAnimationControllerCompat to help with some synchronization
  */
 public class RecentsAnimationController {
 
-    private static final String TAG = "RecentsAnimationController";
-
     private final RecentsAnimationControllerCompat mController;
     private final Consumer<RecentsAnimationController> mOnFinishedListener;
     private final boolean mAllowMinimizeSplitScreen;
 
-    private InputConsumerController mInputConsumerController;
-    private Supplier<InputConsumer> mInputProxySupplier;
-    private InputConsumer mInputConsumer;
     private boolean mUseLauncherSysBarFlags = false;
     private boolean mSplitScreenMinimized = false;
-    private boolean mTouchInProgress;
-    private boolean mDisableInputProxyPending;
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
             boolean allowMinimizeSplitScreen,
@@ -136,12 +117,12 @@
 
     @UiThread
     public void finishAnimationToHome() {
-        finishAndDisableInputProxy(true /* toRecents */, null, false /* sendUserLeaveHint */);
+        finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
     }
 
     @UiThread
     public void finishAnimationToApp() {
-        finishAndDisableInputProxy(false /* toRecents */, null, false /* sendUserLeaveHint */);
+        finishController(false /* toRecents */, null, false /* sendUserLeaveHint */);
     }
 
     /** See {@link #finish(boolean, Runnable, boolean)} */
@@ -160,18 +141,6 @@
     @UiThread
     public void finish(boolean toRecents, Runnable onFinishComplete, boolean sendUserLeaveHint) {
         Preconditions.assertUIThread();
-        if (toRecents && mTouchInProgress) {
-            // Finish the controller as requested, but don't disable input proxy yet.
-            mDisableInputProxyPending = true;
-            finishController(toRecents, onFinishComplete, sendUserLeaveHint);
-        } else {
-            finishAndDisableInputProxy(toRecents, onFinishComplete, sendUserLeaveHint);
-        }
-    }
-
-    private void finishAndDisableInputProxy(boolean toRecents, Runnable onFinishComplete,
-            boolean sendUserLeaveHint) {
-        disableInputProxy();
         finishController(toRecents, onFinishComplete, sendUserLeaveHint);
     }
 
@@ -179,7 +148,6 @@
     public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
         mOnFinishedListener.accept(this);
         UI_HELPER_EXECUTOR.execute(() -> {
-            mController.setInputConsumerEnabled(false);
             mController.finish(toRecents, sendUserLeaveHint);
             if (callback != null) {
                 MAIN_EXECUTOR.execute(callback);
@@ -197,75 +165,8 @@
         });
     }
 
-    public void enableInputProxy(InputConsumerController inputConsumerController,
-            Supplier<InputConsumer> inputProxySupplier) {
-        mInputProxySupplier = inputProxySupplier;
-        mInputConsumerController = inputConsumerController;
-        mInputConsumerController.setInputListener(this::onInputConsumerEvent);
-    }
-
     /** @return wrapper controller. */
     public RecentsAnimationControllerCompat getController() {
         return mController;
     }
-
-    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();
-        }
-        if (mInputConsumerController != null) {
-            mInputConsumerController.setInputListener(null);
-        }
-        mInputProxySupplier = null;
-    }
-
-    private boolean onInputConsumerEvent(InputEvent ev) {
-        if (ev instanceof MotionEvent) {
-            onInputConsumerMotionEvent((MotionEvent) ev);
-        } else if (ev instanceof KeyEvent) {
-            if (mInputConsumer == null) {
-                mInputConsumer = mInputProxySupplier.get();
-            }
-            mInputConsumer.onKeyEvent((KeyEvent) ev);
-            return true;
-        }
-        return false;
-    }
-
-    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) {
-                mInputConsumer = mInputProxySupplier.get();
-            }
-        } else if (action == ACTION_CANCEL || action == ACTION_UP) {
-            // Finish any pending actions
-            mTouchInProgress = false;
-            if (mDisableInputProxyPending) {
-                mDisableInputProxyPending = false;
-                disableInputProxy();
-            }
-        }
-        if (mInputConsumer != null) {
-            mInputConsumer.onMotionEvent(ev);
-        }
-
-        return true;
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 7a6bbb4..fe152a0 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -29,6 +29,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
@@ -43,10 +44,13 @@
 import android.content.res.Resources;
 import android.graphics.Region;
 import android.os.Process;
+import android.os.SystemProperties;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.util.DisplayMetrics;
 import android.view.MotionEvent;
+import android.view.Surface;
 
 import androidx.annotation.BinderThread;
 
@@ -55,6 +59,7 @@
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.SysUINavigationMode.OneHandedModeChangeListener;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -71,7 +76,10 @@
  */
 public class RecentsAnimationDeviceState implements
         NavigationModeChangeListener,
-        DefaultDisplay.DisplayInfoChangeListener {
+        DefaultDisplay.DisplayInfoChangeListener,
+        OneHandedModeChangeListener {
+
+    static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
 
     private final Context mContext;
     private final SysUINavigationMode mSysUiNavMode;
@@ -88,6 +96,9 @@
     private final Region mDeferredGestureRegion = new Region();
     private boolean mAssistantAvailable;
     private float mAssistantVisibility;
+    private boolean mIsOneHandedModeEnabled;
+    private boolean mIsSwipeToNotificationEnabled;
+    private final boolean mIsOneHandedModeSupported;
 
     private boolean mIsUserUnlocked;
     private final ArrayList<Runnable> mUserUnlockedActions = new ArrayList<>();
@@ -113,6 +124,7 @@
         mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
         mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
         mDisplayId = mDefaultDisplay.getInfo().id;
+        mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
         runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
         mRotationTouchHelper = new RotationTouchHelper(context);
         runOnDestroy(mRotationTouchHelper::destroy);
@@ -157,6 +169,24 @@
             }
         }
 
+        if (mIsOneHandedModeSupported) {
+            SecureSettingsObserver oneHandedEnabledObserver =
+                    SecureSettingsObserver.newOneHandedSettingsObserver(
+                            mContext, enabled -> mIsOneHandedModeEnabled = enabled);
+            oneHandedEnabledObserver.register();
+            oneHandedEnabledObserver.dispatchOnChange();
+            runOnDestroy(oneHandedEnabledObserver::unregister);
+        } else {
+            mIsOneHandedModeEnabled = false;
+        }
+
+        SecureSettingsObserver swipeBottomEnabledObserver =
+                SecureSettingsObserver.newSwipeToNotificationSettingsObserver(
+                        mContext, enabled -> mIsSwipeToNotificationEnabled = enabled);
+        swipeBottomEnabledObserver.register();
+        swipeBottomEnabledObserver.dispatchOnChange();
+        runOnDestroy(swipeBottomEnabledObserver::unregister);
+
         SecureSettingsObserver userSetupObserver = new SecureSettingsObserver(
                 context.getContentResolver(),
                 e -> mIsUserSetupComplete = e,
@@ -191,6 +221,15 @@
         runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(listener));
     }
 
+    /**
+     * Adds a listener for the one handed mode change,
+     * guaranteed to be called after the device state's mode has changed.
+     */
+    public void addOneHandedModeChangedCallback(OneHandedModeChangeListener listener) {
+        listener.onOneHandedModeChanged(mSysUiNavMode.addOneHandedOverlayChangeListener(listener));
+        runOnDestroy(() -> mSysUiNavMode.removeOneHandedOverlayChangeListener(listener));
+    }
+
     @Override
     public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
         mDefaultDisplay.removeChangeListener(this);
@@ -204,7 +243,6 @@
         }
 
         mNavBarPosition = new NavBarPosition(newMode, mDefaultDisplay.getInfo());
-
         mMode = newMode;
     }
 
@@ -221,6 +259,11 @@
         mNavBarPosition = new NavBarPosition(mMode, info);
     }
 
+    @Override
+    public void onOneHandedModeChanged(int newGesturalHeight) {
+        mRotationTouchHelper.setGesturalHeight(newGesturalHeight);
+    }
+
     /**
      * @return the current navigation mode for the device.
      */
@@ -406,6 +449,13 @@
     }
 
     /**
+     * @return whether one-handed mode is enabled and active
+     */
+    public boolean isOneHandedModeActive() {
+        return (mSystemUiStateFlags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
+    }
+
+    /**
      * Sets the region in screen space where the gestures should be deferred (ie. due to specific
      * nav bar ui).
      */
@@ -467,6 +517,35 @@
                 && !isGestureBlockedActivity(task);
     }
 
+    /**
+     * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
+     *
+     * @param ev The touch screen motion event.
+     * @return whether the given motion event can trigger the one handed mode.
+     */
+    public boolean canTriggerOneHandedAction(MotionEvent ev) {
+        if (!mIsOneHandedModeSupported) {
+            return false;
+        }
+
+        if (mIsOneHandedModeEnabled || mIsSwipeToNotificationEnabled) {
+            final DefaultDisplay.Info displayInfo = mDefaultDisplay.getInfo();
+            return (mRotationTouchHelper.touchInOneHandedModeRegion(ev)
+                && displayInfo.rotation != Surface.ROTATION_90
+                && displayInfo.rotation != Surface.ROTATION_270
+                && displayInfo.metrics.densityDpi < DisplayMetrics.DENSITY_600);
+        }
+        return false;
+    }
+
+    public boolean isOneHandedModeEnabled() {
+        return mIsOneHandedModeEnabled;
+    }
+
+    public boolean isSwipeToNotificationEnabled() {
+        return mIsSwipeToNotificationEnabled;
+    }
+
     public RotationTouchHelper getRotationTouchHelper() {
         return mRotationTouchHelper;
     }
@@ -481,6 +560,8 @@
         pw.println("  assistantDisabled="
                 + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
         pw.println("  isUserUnlocked=" + mIsUserUnlocked);
+        pw.println("  isOneHandedModeEnabled=" + mIsOneHandedModeEnabled);
+        pw.println("  isSwipeToNotificationEnabled=" + mIsSwipeToNotificationEnabled);
         mRotationTouchHelper.dump(pw);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 6f54ba2..6c302ae 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -160,9 +160,9 @@
 
     @Override
     public void onTaskRemoved(int taskId) {
-        Task.TaskKey dummyKey = new Task.TaskKey(taskId, 0, null, null, 0, 0);
-        mThumbnailCache.remove(dummyKey);
-        mIconCache.onTaskRemoved(dummyKey);
+        Task.TaskKey stubKey = new Task.TaskKey(taskId, 0, null, null, 0, 0);
+        mThumbnailCache.remove(stubKey);
+        mIconCache.onTaskRemoved(stubKey);
     }
 
     public void onTrimMemory(int level) {
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 5f3c022..2b5e42a 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -188,6 +188,10 @@
         return mOrientationTouchTransformer.touchInAssistantRegion(ev);
     }
 
+    public boolean touchInOneHandedModeRegion(MotionEvent ev) {
+        return mOrientationTouchTransformer.touchInOneHandedModeRegion(ev);
+    }
+
     /**
      * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
      */
@@ -222,7 +226,8 @@
         mDefaultDisplay.addChangeListener(this);
         onDisplayInfoChanged(mDefaultDisplay.getInfo(), CHANGE_ALL);
 
-        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo());
+        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo(),
+            mContext.getResources());
         if (!mMode.hasGestures && newMode.hasGestures) {
             setupOrientationSwipeHandler();
         } else if (mMode.hasGestures && !newMode.hasGestures){
@@ -267,6 +272,14 @@
     }
 
     /**
+     * Sets the gestural height.
+     */
+    void setGesturalHeight(int newGesturalHeight) {
+        mOrientationTouchTransformer.setGesturalHeight(newGesturalHeight, mDefaultDisplay.getInfo(),
+            mContext.getResources());
+    }
+
+    /**
      * *May* apply a transform on the motion event if it lies in the nav bar region for another
      * orientation that is currently being tracked as a part of quickstep
      */
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 05ce2a2..6b50218 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -16,14 +16,19 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_2_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_3_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.util.Log;
 
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MainThreadInitializedObject;
 
@@ -37,16 +42,18 @@
 public class SysUINavigationMode {
 
     public enum Mode {
-        THREE_BUTTONS(false, 0),
-        TWO_BUTTONS(true, 1),
-        NO_BUTTON(true, 2);
+        THREE_BUTTONS(false, 0, LAUNCHER_NAVIGATION_MODE_3_BUTTON),
+        TWO_BUTTONS(true, 1, LAUNCHER_NAVIGATION_MODE_2_BUTTON),
+        NO_BUTTON(true, 2, LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON);
 
         public final boolean hasGestures;
         public final int resValue;
+        public final LauncherEvent launcherEvent;
 
-        Mode(boolean hasGestures, int resValue) {
+        Mode(boolean hasGestures, int resValue, LauncherEvent launcherEvent) {
             this.hasGestures = hasGestures;
             this.resValue = resValue;
+            this.launcherEvent = launcherEvent;
         }
     }
 
@@ -58,15 +65,19 @@
             new MainThreadInitializedObject<>(SysUINavigationMode::new);
 
     private static final String TAG = "SysUINavigationMode";
-
     private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
     private static final String NAV_BAR_INTERACTION_MODE_RES_NAME =
             "config_navBarInteractionMode";
+    private static final String TARGET_OVERLAY_PACKAGE = "android";
 
     private final Context mContext;
     private Mode mMode;
 
+    private int mNavBarGesturalHeight;
+
     private final List<NavigationModeChangeListener> mChangeListeners = new ArrayList<>();
+    private final List<OneHandedModeChangeListener> mOneHandedOverlayChangeListeners =
+            new ArrayList<>();
 
     public SysUINavigationMode(Context context) {
         mContext = context;
@@ -76,8 +87,9 @@
             @Override
             public void onReceive(Context context, Intent intent) {
                 updateMode();
+                updateGesturalHeight();
             }
-        }, getPackageFilter("android", ACTION_OVERLAY_CHANGED));
+        }, getPackageFilter(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED));
     }
 
     /** Updates navigation mode when needed. */
@@ -89,9 +101,35 @@
         }
     }
 
+    private void updateGesturalHeight() {
+        int newGesturalHeight = ResourceUtils.getDimenByName(
+                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(),
+                INVALID_RESOURCE_HANDLE);
+
+        if (newGesturalHeight == INVALID_RESOURCE_HANDLE) {
+            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+            return;
+        }
+
+        if (mNavBarGesturalHeight != newGesturalHeight) {
+            mNavBarGesturalHeight = newGesturalHeight;
+            dispatchOneHandedOverlayChange();
+        }
+    }
+
     private void initializeMode() {
-        int modeInt = getSystemIntegerRes(mContext, NAV_BAR_INTERACTION_MODE_RES_NAME);
-        for(Mode m : Mode.values()) {
+        int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME,
+                mContext.getResources(), INVALID_RESOURCE_HANDLE);
+        mNavBarGesturalHeight = ResourceUtils.getDimenByName(
+                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mContext.getResources(),
+                INVALID_RESOURCE_HANDLE);
+
+        if (modeInt == INVALID_RESOURCE_HANDLE) {
+            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
+            return;
+        }
+
+        for (Mode m : Mode.values()) {
             if (m.resValue == modeInt) {
                 mMode = m;
             }
@@ -104,6 +142,12 @@
         }
     }
 
+    private void dispatchOneHandedOverlayChange() {
+        for (OneHandedModeChangeListener listener : mOneHandedOverlayChangeListeners) {
+            listener.onOneHandedModeChanged(mNavBarGesturalHeight);
+        }
+    }
+
     public Mode addModeChangeListener(NavigationModeChangeListener listener) {
         mChangeListeners.add(listener);
         return mMode;
@@ -113,20 +157,17 @@
         mChangeListeners.remove(listener);
     }
 
-    public Mode getMode() {
-        return mMode;
+    public int addOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) {
+        mOneHandedOverlayChangeListeners.add(listener);
+        return mNavBarGesturalHeight;
     }
 
-    private static int getSystemIntegerRes(Context context, String resName) {
-        Resources res = context.getResources();
-        int resId = res.getIdentifier(resName, "integer", "android");
+    public void removeOneHandedOverlayChangeListener(OneHandedModeChangeListener listener) {
+        mOneHandedOverlayChangeListeners.remove(listener);
+    }
 
-        if (resId != 0) {
-            return res.getInteger(resId);
-        } else {
-            Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?");
-            return -1;
-        }
+    public Mode getMode() {
+        return mMode;
     }
 
     /** @return Whether we can remove the shelf from overview. */
@@ -144,10 +185,14 @@
     public void dump(PrintWriter pw) {
         pw.println("SysUINavigationMode:");
         pw.println("  mode=" + mMode.name());
+        pw.println("  mNavBarGesturalHeight=:" + mNavBarGesturalHeight);
     }
 
     public interface NavigationModeChangeListener {
-
         void onNavigationModeChanged(Mode newMode);
     }
+
+    public interface OneHandedModeChangeListener {
+        void onOneHandedModeChanged(int newGesturalHeight);
+    }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 299e9e5..e4b05ae 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -352,10 +352,43 @@
         if (mSystemUiProxy != null) {
             try {
                 mSystemUiProxy.handleImageBundleAsScreenshot(screenImageBundle, locationInScreen,
-                        visibleInsets, task);
+                    visibleInsets, task);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call handleImageBundleAsScreenshot");
             }
         }
     }
+
+    @Override
+    public void startOneHandedMode() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startOneHandedMode();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startOneHandedMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void stopOneHandedMode() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.stopOneHandedMode();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call stopOneHandedMode", e);
+            }
+        }
+    }
+
+    @Override
+    public void expandNotificationPanel() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.expandNotificationPanel();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call expandNotificationPanel", e);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 0e2312b..81c4d0c 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.interaction;
 
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
@@ -48,6 +49,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -74,6 +76,7 @@
     private final PointF mAssistantStartDragPos = new PointF();
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
+    private final MotionPauseDetector mMotionPauseDetector;
     private boolean mTouchCameFromAssistantCorner;
     private boolean mTouchCameFromNavBar;
     private boolean mPassedAssistantSlop;
@@ -100,6 +103,7 @@
                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
                         new NavBarPosition(Mode.NO_BUTTON, displayRotation),
                         null /*onInterceptTouch*/, this);
+        mMotionPauseDetector = new MotionPauseDetector(context);
 
         final Resources resources = context.getResources();
         mBottomGestureHeight =
@@ -177,12 +181,14 @@
                 }
                 mLaunchedAssistant = false;
                 mSwipeUpTouchTracker.init();
+                mMotionPauseDetector.clear();
+                mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
                 break;
             case MotionEvent.ACTION_MOVE:
+                mLastPos.set(event.getX(), event.getY());
                 if (!mAssistantGestureActive) {
                     break;
                 }
-                mLastPos.set(event.getX(), event.getY());
 
                 if (!mPassedAssistantSlop) {
                     // Normal gesture, ensure we pass the slop before we start tracking the gesture
@@ -213,6 +219,8 @@
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
+                mMotionPauseDetector.clear();
+                mMotionPauseDetector.setOnMotionPauseListener(null);
                 if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
                     mGestureCallback.onNavBarGestureAttempted(
                             HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
@@ -239,9 +247,17 @@
         }
         mSwipeUpTouchTracker.onMotionEvent(event);
         mAssistantGestureDetector.onTouchEvent(event);
+        mMotionPauseDetector.addPosition(event);
+        mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight);
         return intercepted;
     }
 
+    protected void onMotionPauseChanged(boolean isPaused) {
+        if (isPaused) {
+            VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+        }
+    }
+
     /**
      * Determine if angle is larger than threshold for assistant detection
      */
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 4110b33..60d8ef6 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -18,7 +18,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.MAX_SWIPE_DURATION;
+import static com.android.quickstep.AbsSwipeUpHandler.MAX_SWIPE_DURATION;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
 
@@ -184,8 +184,7 @@
 
         @Override
         public void updateFinalShift() {
-            float progress = mCurrentShift.value / mDragLengthFactor;
-            mWindowTransitionController.setPlayFraction(progress);
+            mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
             mTaskViewSimulator.apply(mTransformParams);
         }
 
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index ecbe734..059d158 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,6 +16,10 @@
 
 package com.android.quickstep.logging;
 
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.formatElapsedTime;
+
+import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
@@ -24,6 +28,8 @@
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW;
 
+import static java.lang.System.currentTimeMillis;
+
 import android.content.Context;
 import android.util.Log;
 
@@ -33,6 +39,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase;
 import com.android.launcher3.logger.LauncherAtom.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
@@ -52,6 +59,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -60,15 +68,17 @@
  * This class calls StatsLog compile time generated methods.
  *
  * To see if the logs are properly sent to statsd, execute following command.
+ * <ul>
  * $ wwdebug (to turn on the logcat printout)
  * $ wwlogcat (see logcat with grep filter on)
  * $ statsd_testdrive (see how ww is writing the proto to statsd buffer)
+ * </ul>
  */
 public class StatsLogCompatManager extends StatsLogManager {
 
     private static final String TAG = "StatsLog";
     private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
-
+    private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS";
     private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0);
     // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto migrates
     // from nano to lite, bake constant to prevent robo test failure.
@@ -91,23 +101,40 @@
     }
 
     /**
-     * Logs the workspace layout information on the model thread.
+     * Logs impression of the current workspace with additional launcher events.
      */
     @Override
-    public void logSnapshot() {
+    public void logSnapshot(List<EventEnum> extraEvents) {
         LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
-                new SnapshotWorker());
+                new SnapshotWorker(extraEvents));
     }
 
     private class SnapshotWorker extends BaseModelUpdateTask {
         private final InstanceId mInstanceId;
-        SnapshotWorker() {
-            mInstanceId = new InstanceIdSequence(
-                    1 << 20 /*InstanceId.INSTANCE_ID_MAX*/).newInstanceId();
+        private final List<EventEnum> mExtraEvents;
+
+        SnapshotWorker(List<EventEnum> extraEvents) {
+            mInstanceId = new InstanceIdSequence(1 << 20 /*InstanceId.INSTANCE_ID_MAX*/)
+                    .newInstanceId();
+            this.mExtraEvents = extraEvents;
         }
 
         @Override
         public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+            long lastSnapshotTimeMillis = getDevicePrefs(mContext)
+                    .getLong(LAST_SNAPSHOT_TIME_MILLIS, 0);
+            // Log snapshot only if previous snapshot was older than a day
+            if (currentTimeMillis() - lastSnapshotTimeMillis < DAY_IN_MILLIS) {
+                if (IS_VERBOSE) {
+                    String elapsedTime = formatElapsedTime(
+                            (currentTimeMillis() - lastSnapshotTimeMillis) / 1000);
+                    Log.d(TAG, String.format(
+                            "Skipped snapshot logging since previous snapshot was %s old.",
+                            elapsedTime));
+                }
+                return;
+            }
+
             IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone();
             ArrayList<ItemInfo> workspaceItems = (ArrayList) dataModel.workspaceItems.clone();
             ArrayList<LauncherAppWidgetInfo> appWidgets = (ArrayList) dataModel.appWidgets.clone();
@@ -123,16 +150,22 @@
                         LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
                         writeSnapshot(atomInfo, mInstanceId);
                     }
-                } catch (Exception e) { }
+                } catch (Exception e) {
+                }
             }
             for (ItemInfo info : appWidgets) {
                 LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
                 writeSnapshot(atomInfo, mInstanceId);
             }
+            mExtraEvents
+                    .forEach(eventName -> logger().withInstanceId(mInstanceId).log(eventName));
+
+            getDevicePrefs(mContext).edit()
+                    .putLong(LAST_SNAPSHOT_TIME_MILLIS, currentTimeMillis()).apply();
         }
     }
 
-    private static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
+    private void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
         if (IS_VERBOSE) {
             Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
         }
@@ -335,7 +368,7 @@
     }
 
     private static int getCardinality(LauncherAtom.ItemInfo info) {
-        switch (info.getContainerInfo().getContainerCase()){
+        switch (info.getContainerInfo().getContainerCase()) {
             case PREDICTED_HOTSEAT_CONTAINER:
                 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
             case SEARCH_RESULT_CONTAINER:
@@ -400,9 +433,16 @@
     }
 
     private static int getPageId(LauncherAtom.ItemInfo info) {
+        if (info.hasTask()) {
+            return info.getTask().getIndex();
+        }
         switch (info.getContainerInfo().getContainerCase()) {
             case FOLDER:
                 return info.getContainerInfo().getFolder().getPageIndex();
+            case HOTSEAT:
+                return info.getContainerInfo().getHotseat().getIndex();
+            case PREDICTED_HOTSEAT_CONTAINER:
+                return info.getContainerInfo().getPredictedHotseatContainer().getIndex();
             default:
                 return info.getContainerInfo().getWorkspace().getPageIndex();
         }
@@ -411,6 +451,10 @@
     private static int getParentPageId(LauncherAtom.ItemInfo info) {
         switch (info.getContainerInfo().getContainerCase()) {
             case FOLDER:
+                if (info.getContainerInfo().getFolder().getParentContainerCase()
+                        == ParentContainerCase.HOTSEAT) {
+                    return info.getContainerInfo().getFolder().getHotseat().getIndex();
+                }
                 return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
             case SEARCH_RESULT_CONTAINER:
                 return info.getContainerInfo().getSearchResultContainer().getWorkspace()
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
new file mode 100644
index 0000000..d58329a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.SysUINavigationMode;
+
+/**
+ * Controls an animation that can go beyond progress = 1, at which point resistance should be
+ * applied. Internally, this is a wrapper around 2 {@link AnimatorPlaybackController}s, one that
+ * runs from progress 0 to 1 like normal, then one that seamlessly continues that animation but
+ * starts applying resistance as well.
+ */
+public class AnimatorControllerWithResistance {
+
+    /**
+     * How much farther we can drag past overview in 2-button mode, as a factor of the distance
+     * it takes to drag from an app to overview.
+     */
+    public static final float TWO_BUTTON_EXTRA_DRAG_FACTOR = 0.25f;
+
+    /**
+     * Start slowing down the rate of scaling down when recents view is smaller than this scale.
+     */
+    private static final float RECENTS_SCALE_START_RESIST = 0.75f;
+
+    /**
+     * Recents view will reach this scale at the very end of the drag.
+     */
+    private static final float RECENTS_SCALE_MAX_RESIST = 0.5f;
+
+    private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DEACCEL;
+
+    private final AnimatorPlaybackController mNormalController;
+    private final AnimatorPlaybackController mResistanceController;
+
+    // Initialize to -1 so the first 0 gets applied.
+    private float mLastNormalProgress = -1;
+    private float mLastResistProgress;
+
+    public AnimatorControllerWithResistance(AnimatorPlaybackController normalController,
+            AnimatorPlaybackController resistanceController) {
+        mNormalController = normalController;
+        mResistanceController = resistanceController;
+    }
+
+    public AnimatorPlaybackController getNormalController() {
+        return mNormalController;
+    }
+
+    /**
+     * Applies the current progress of the animation.
+     * @param progress From 0 to maxProgress, where 1 is the target we are animating towards.
+     * @param maxProgress > 1, this is where the resistance will be applied.
+     */
+    public void setProgress(float progress, float maxProgress) {
+        float normalProgress = Utilities.boundToRange(progress, 0, 1);
+        if (normalProgress != mLastNormalProgress) {
+            mLastNormalProgress = normalProgress;
+            mNormalController.setPlayFraction(normalProgress);
+        }
+        if (maxProgress <= 1) {
+            return;
+        }
+        float resistProgress = progress <= 1 ? 0 : Utilities.getProgress(progress, 1, maxProgress);
+        if (resistProgress != mLastResistProgress) {
+            mLastResistProgress = resistProgress;
+            mResistanceController.setPlayFraction(resistProgress);
+        }
+    }
+
+    /**
+     * Applies resistance to recents when swiping up past its target position.
+     * @param normalController The controller to run from 0 to 1 before this resistance applies.
+     * @param context Used to compute start and end values.
+     * @param recentsOrientedState Used to compute start and end values.
+     * @param dp Used to compute start and end values.
+     * @param scaleTarget The target for the scaleProperty.
+     * @param scaleProperty Animate the value to change the scale of the window/recents view.
+     */
+    public static <SCALE> AnimatorControllerWithResistance createForRecents(
+            AnimatorPlaybackController normalController, Context context,
+            RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
+            FloatProperty<SCALE> scaleProperty) {
+        Rect startRect = new Rect();
+        LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, startRect,
+                recentsOrientedState.getOrientationHandler());
+        long distanceToCover = startRect.bottom;
+        boolean isTwoButtonMode = SysUINavigationMode.getMode(context) == TWO_BUTTONS;
+        if (isTwoButtonMode) {
+            // We can only drag a small distance past overview, not to the top of the screen.
+            distanceToCover = (long)
+                    ((dp.heightPx - startRect.bottom) * TWO_BUTTON_EXTRA_DRAG_FACTOR);
+        }
+        PendingAnimation resistAnim = new PendingAnimation(distanceToCover * 2);
+
+        PointF pivot = new PointF();
+        float fullscreenScale = recentsOrientedState.getFullScreenScaleAndPivot(
+                startRect, dp, pivot);
+        float startScale = 1;
+        float prevScaleRate = (fullscreenScale - startScale) / (dp.heightPx - startRect.bottom);
+        // This is what the scale would be at the end of the drag if we didn't apply resistance.
+        float endScale = startScale - prevScaleRate * distanceToCover;
+        final TimeInterpolator scaleInterpolator;
+        if (isTwoButtonMode) {
+            // We are bounded by the distance of the drag, so we don't need to apply resistance.
+            scaleInterpolator = LINEAR;
+        } else {
+            // Create an interpolator that resists the scale so the scale doesn't get smaller than
+            // RECENTS_SCALE_MAX_RESIST.
+            float startResist = Utilities.getProgress(RECENTS_SCALE_START_RESIST, startScale,
+                    endScale);
+            float maxResist = Utilities.getProgress(RECENTS_SCALE_MAX_RESIST, startScale, endScale);
+            scaleInterpolator = t -> {
+                if (t < startResist) {
+                    return t;
+                }
+                float resistProgress = Utilities.getProgress(t, startResist, 1);
+                resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
+                return startResist + resistProgress * (maxResist - startResist);
+            };
+        }
+        resistAnim.addFloat(scaleTarget, scaleProperty, startScale, endScale,
+                scaleInterpolator);
+
+        AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
+        return new AnimatorControllerWithResistance(normalController, resistanceController);
+    }
+
+}
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index a5d4568..969fa50 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -358,18 +358,23 @@
 
             if (count < 3) {
                 // Too few samples
-                if (count == 2) {
-                    int endPos = pointPos - 1;
-                    if (endPos < 0) {
-                        endPos += HISTORY_SIZE;
+                switch (count) {
+                    case 2: {
+                        int endPos = pointPos - 1;
+                        if (endPos < 0) {
+                            endPos += HISTORY_SIZE;
+                        }
+                        float denominator = eventTime - mHistoricTimes[endPos];
+                        if (denominator != 0) {
+                            return (mHistoricPos[pointPos] - mHistoricPos[endPos]) / denominator;
+                        }
                     }
-                    float denominator = eventTime - mHistoricTimes[endPos];
-                    if (denominator != 0) {
-                        return (eventTime - mHistoricPos[endPos]) / denominator;
-
-                    }
+                    // fall through
+                    case 1:
+                        return 0f;
+                    default:
+                        return null;
                 }
-                return null;
             }
 
             float Sxx = sxi2 - sxi * sxi / count;
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 9ed2bbe..c2e67c1 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -87,20 +87,6 @@
             });
         }
 
-        if (!hasReachedMaxCount(ALL_APPS_COUNT)) {
-            stateManager.addStateListener(new StateListener<LauncherState>() {
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    if (finalState == ALL_APPS) {
-                        if (incrementEventCount(ALL_APPS_COUNT)) {
-                            stateManager.removeStateListener(this);
-                            mLauncher.getScrimView().updateDragHandleVisibility();
-                        }
-                    }
-                }
-            });
-        }
-
         if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() && !hasReachedMaxCount(
                 HOTSEAT_DISCOVERY_TIP_COUNT)) {
             stateManager.addStateListener(new StateListener<LauncherState>() {
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index d822b6c..81d24d7 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -33,7 +33,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Matrix;
@@ -48,7 +47,6 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -58,7 +56,6 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.systemui.shared.system.ConfigurationCompat;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -91,6 +88,7 @@
     private @SurfaceRotation int mTouchRotation = ROTATION_0;
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
     private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
+    private @SurfaceRotation int mRecentsRotation = ROTATION_0 - 1;
 
     // Launcher activity supports multiple orientation, but fallback activity does not
     private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
@@ -133,8 +131,6 @@
     private int mFlags;
     private int mPreviousRotation = ROTATION_0;
 
-    @Nullable private Configuration mActivityConfiguration;
-
     /**
      * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
      *                              is enabled
@@ -170,11 +166,11 @@
     }
 
     /**
-     * Sets the configuration for the recents activity, which could affect the activity's rotation
+     * Sets the rotation for the recents activity, which could affect the appearance of task view.
      * @see #update(int, int)
      */
-    public boolean setActivityConfiguration(Configuration activityConfiguration) {
-        mActivityConfiguration = activityConfiguration;
+    public boolean setRecentsRotation(@SurfaceRotation int recentsRotation) {
+        mRecentsRotation = recentsRotation;
         return update(mTouchRotation, mDisplayRotation);
     }
 
@@ -231,9 +227,7 @@
     @SurfaceRotation
     private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
         if (isRecentsActivityRotationAllowed()) {
-            return mActivityConfiguration == null
-                    ? displayRotation
-                    : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
+            return mRecentsRotation < ROTATION_0 ? displayRotation : mRecentsRotation;
         } else {
             return ROTATION_0;
         }
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index 19e278b..f1ac6a5 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
@@ -77,15 +76,11 @@
     private final float mRadius;
     private final int mMaxScrimAlpha;
     private final Paint mPaint;
-    private final OnboardingPrefs mOnboardingPrefs;
 
     // Mid point where the alpha changes
     private int mMidAlpha;
     private float mMidProgress;
 
-    // The progress at which the drag handle starts moving up with the shelf.
-    private float mDragHandleProgress;
-
     private Interpolator mBeforeMidProgressColorInterpolator = ACCEL;
     private Interpolator mAfterMidProgressColorInterpolator = ACCEL;
 
@@ -103,7 +98,6 @@
     private boolean mRemainingScreenPathValid = false;
 
     private Mode mSysUINavigationMode;
-    private boolean mIsTwoZoneSwipeModel;
 
     public ShelfScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -112,7 +106,6 @@
         mEndAlpha = Color.alpha(mEndScrim);
         mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mOnboardingPrefs = mLauncher.getOnboardingPrefs();
 
         // Just assume the easiest UI for now, until we have the proper layout information.
         mDrawingFlatColor = true;
@@ -145,11 +138,9 @@
             // Show the shelf more quickly before reaching overview progress.
             mBeforeMidProgressColorInterpolator = ACCEL_2;
             mAfterMidProgressColorInterpolator = ACCEL;
-            mIsTwoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get();
         } else {
             mBeforeMidProgressColorInterpolator = ACCEL;
             mAfterMidProgressColorInterpolator = Interpolators.clampToProgress(ACCEL, 0.5f, 1f);
-            mIsTwoZoneSwipeModel = false;
         }
     }
 
@@ -164,7 +155,6 @@
 
             Context context = getContext();
             if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
-                mDragHandleProgress = 1;
                 if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()
                         && SysUINavigationMode.removeShelfFromOverview(context)) {
                     // Fade in all apps background quickly to distinguish from swiping from nav bar.
@@ -182,29 +172,22 @@
                         + hotseatPadding.bottom + hotseatPadding.top;
                 float dragHandleTop =
                         Math.min(hotseatSize, LayoutUtils.getDefaultSwipeHeight(context, dp));
-                mDragHandleProgress =  1 - (dragHandleTop / mShiftRange);
             }
-            mTopOffset = dp.getInsets().top - mDragHandleSize.y;
+            mTopOffset = dp.getInsets().top;
             mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
         }
         updateColors();
         updateSysUiColors();
-        updateDragHandleAlpha();
         invalidate();
     }
 
     @Override
     public void updateColors() {
         super.updateColors();
-        mDragHandleOffset = 0;
         if (mDrawingFlatColor) {
             return;
         }
 
-        if (mProgress < mDragHandleProgress) {
-            mDragHandleOffset = mShiftRange * (mDragHandleProgress - mProgress);
-        }
-
         if (mProgress >= SCRIM_CATCHUP_THRESHOLD) {
             mShelfTop = mShiftRange * mProgress + mTopOffset;
         } else {
@@ -259,19 +242,7 @@
     }
 
     @Override
-    protected boolean shouldDragHandleBeVisible() {
-        boolean needsAllAppsEdu = mIsTwoZoneSwipeModel
-                && !mOnboardingPrefs.hasReachedMaxCount(OnboardingPrefs.ALL_APPS_COUNT);
-        return needsAllAppsEdu || super.shouldDragHandleBeVisible();
-    }
-
-    @Override
     protected void onDraw(Canvas canvas) {
-        drawBackground(canvas);
-        drawDragHandle(canvas);
-    }
-
-    private void drawBackground(Canvas canvas) {
         if (mDrawingFlatColor) {
             if (mCurrentFlatColor != 0) {
                 canvas.drawColor(mCurrentFlatColor);
@@ -311,9 +282,4 @@
         mPaint.setColor(mShelfColor);
         canvas.drawRoundRect(0, mShelfTop, width, height + mRadius, mRadius, mRadius, mPaint);
     }
-
-    @Override
-    public float getVisualTop() {
-        return mShelfTop;
-    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
deleted file mode 100644
index 5904fcd..0000000
--- a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * 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 org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.app.prediction.AppTargetId;
-import android.content.ComponentName;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.os.Process;
-import android.view.View;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.appprediction.PredictionRowView;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.model.AppLaunchTracker;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class AppPredictionsUITests extends AbstractQuickStepTest {
-
-    private LauncherActivityInfo mSampleApp1;
-    private LauncherActivityInfo mSampleApp2;
-    private LauncherActivityInfo mSampleApp3;
-
-    private AppPredictor.Callback mCallback;
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        List<LauncherActivityInfo> activities = mTargetContext.getSystemService(LauncherApps.class)
-                .getActivityList(null, Process.myUserHandle());
-        mSampleApp1 = activities.get(0);
-        mSampleApp2 = activities.get(1);
-        mSampleApp3 = activities.get(2);
-
-        // Disable app tracker
-        AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
-        PredictionUiStateManager.INSTANCE.initializeForTesting(null);
-
-        mCallback = PredictionUiStateManager.INSTANCE.get(mTargetContext).appPredictorCallback(
-                Client.HOME);
-
-        mDevice.setOrientationNatural();
-    }
-
-    @After
-    public void tearDown() throws Throwable {
-        AppLaunchTracker.INSTANCE.initializeForTesting(null);
-        PredictionUiStateManager.INSTANCE.initializeForTesting(null);
-        mDevice.unfreezeRotation();
-    }
-
-    /**
-     * Test that prediction UI is updated as soon as we get predictions from the system
-     */
-    @Test
-    public void testPredictionExistsInAllApps() {
-        mLauncher.pressHome().switchToAllApps();
-
-        // Dispatch an update
-        sendPredictionUpdate(mSampleApp1, mSampleApp2);
-        // The first update should apply immediately.
-        waitForLauncherCondition("Predictions were not updated in loading state",
-                launcher -> getPredictedApp(launcher).size() == 2);
-    }
-
-    /**
-     * Test that prediction update is deferred if it is already visible
-     */
-    @Test
-    public void testPredictionsDeferredUntilHome() {
-        mDevice.pressHome();
-        sendPredictionUpdate(mSampleApp1, mSampleApp2);
-        mLauncher.pressHome().switchToAllApps();
-        waitForLauncherCondition("Predictions were not updated in loading state",
-                launcher -> getPredictedApp(launcher).size() == 2);
-
-        // Update predictions while all-apps is visible
-        sendPredictionUpdate(mSampleApp1, mSampleApp2, mSampleApp3);
-        assertEquals(2, getFromLauncher(this::getPredictedApp).size());
-
-        // Go home and go back to all-apps
-        mLauncher.pressHome().switchToAllApps();
-        assertEquals(3, getFromLauncher(this::getPredictedApp).size());
-    }
-
-    @Test
-    public void testPredictionsDisabled() {
-        mDevice.pressHome();
-        sendPredictionUpdate();
-        mLauncher.pressHome().switchToAllApps();
-
-        waitForLauncherCondition("Predictions were not updated in loading state",
-                launcher -> launcher.getAppsView().getFloatingHeaderView()
-                        .findFixedRowByType(PredictionRowView.class).getVisibility() == View.GONE);
-        assertFalse(PredictionUiStateManager.INSTANCE.get(mTargetContext)
-                .getCurrentState().isEnabled);
-    }
-
-    public ArrayList<BubbleTextView> getPredictedApp(Launcher launcher) {
-        PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
-                .findFixedRowByType(PredictionRowView.class);
-
-        ArrayList<BubbleTextView> predictedAppViews = new ArrayList<>();
-        for (int i = 0; i < container.getChildCount(); i++) {
-            View view = container.getChildAt(i);
-            if (view instanceof BubbleTextView && view.getVisibility() == View.VISIBLE) {
-                predictedAppViews.add((BubbleTextView) view);
-            }
-        }
-        return predictedAppViews;
-    }
-
-    private void sendPredictionUpdate(LauncherActivityInfo... activities) {
-        getOnUiThread(() -> {
-            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();
-                targets.add(target);
-            }
-            mCallback.onTargetsAvailable(targets);
-            return null;
-        });
-    }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index bf093fd..ecd4e2b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.tapl.Background;
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
+import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.ui.TaplTestsLauncher3;
@@ -68,11 +69,14 @@
         });
     }
 
-    private void startTestApps() throws Exception {
+    public static void startTestApps() throws Exception {
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
+    }
 
+    private void startTestAppsWithCheck() throws Exception {
+        startTestApps();
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
                         + "one",
@@ -105,7 +109,7 @@
     @Test
     @PortraitLandscape
     public void testOverview() throws Exception {
-        startTestApps();
+        startTestAppsWithCheck();
         // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
@@ -189,6 +193,22 @@
                         0, getTaskCount(launcher)));
     }
 
+    /**
+     * Smoke test for action buttons: Presses all the buttons and makes sure no crashes occur.
+     */
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testOverviewActions() throws Exception {
+        if (mLauncher.getNavigationModel() != NavigationModel.TWO_BUTTON) {
+            startTestAppsWithCheck();
+            OverviewActions actionsView =
+                    mLauncher.pressHome().switchToOverview().getOverviewActions();
+            actionsView.clickAndDismissScreenshot();
+            actionsView.clickAndDismissShare();
+        }
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index 115294a..72116eb 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -71,7 +71,7 @@
 
 /**
  * Test to verify view inflation does not happen during swipe up.
- * To verify view inflation, we setup a dummy ViewConfiguration and check if any call to that class
+ * To verify view inflation, we setup a stub ViewConfiguration and check if any call to that class
  * does from a View.init method or not.
  *
  * Alternative approaches considered:
@@ -138,13 +138,13 @@
     @Test
     @NavigationModeSwitch(mode = ZERO_BUTTON)
     public void testSwipeUpFromApp_widget_update() {
-        String dummyText = "Some random dummy text";
+        String stubText = "Some random stub text";
 
         executeSwipeUpTestWithWidget(
                 widgetId -> { },
                 widgetId -> AppWidgetManager.getInstance(getContext())
-                        .updateAppWidget(widgetId, createMainWidgetViews(dummyText)),
-                dummyText);
+                        .updateAppWidget(widgetId, createMainWidgetViews(stubText)),
+                stubText);
     }
 
     @Test
diff --git a/res/animator-v23/discovery_bounce.xml b/res/animator-v23/discovery_bounce.xml
deleted file mode 100644
index f554853..0000000
--- a/res/animator-v23/discovery_bounce.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 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.
-*/
--->
-<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:duration="2166"
-    android:repeatCount="5">
-    <propertyValuesHolder
-        android:propertyName="progress"
-        android:valueType="floatType">
-        <keyframe
-            android:fraction="0"
-            android:value="1f" />
-        <keyframe
-            android:fraction="0.246"
-            android:value="1f" />
-        <keyframe
-            android:fraction=".423"
-            android:interpolator="@interpolator/disco_bounce"
-            android:value="0.9738f" />
-        <keyframe
-            android:fraction="0.754"
-            android:interpolator="@interpolator/disco_bounce"
-            android:value="1f" />
-        <keyframe
-            android:fraction="1"
-            android:value="1f" />
-    </propertyValuesHolder>
-</objectAnimator>
diff --git a/res/animator/discovery_bounce.xml b/res/animator/discovery_bounce.xml
index f02ebdb..f554853 100644
--- a/res/animator/discovery_bounce.xml
+++ b/res/animator/discovery_bounce.xml
@@ -16,20 +16,28 @@
 ** limitations under the License.
 */
 -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:ordering="sequentially">
-    <objectAnimator
-        android:duration="166"
-        android:interpolator="@interpolator/disco_bounce"
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="2166"
+    android:repeatCount="5">
+    <propertyValuesHolder
         android:propertyName="progress"
-        android:startOffset="750"
-        android:valueFrom="1f"
-        android:valueTo="0.9438f"
-        android:valueType="floatType" />
-    <objectAnimator
-        android:duration="500"
-        android:interpolator="@interpolator/disco_bounce"
-        android:propertyName="progress"
-        android:valueTo="1f"
-        android:valueType="floatType" />
-</set>
+        android:valueType="floatType">
+        <keyframe
+            android:fraction="0"
+            android:value="1f" />
+        <keyframe
+            android:fraction="0.246"
+            android:value="1f" />
+        <keyframe
+            android:fraction=".423"
+            android:interpolator="@interpolator/disco_bounce"
+            android:value="0.9738f" />
+        <keyframe
+            android:fraction="0.754"
+            android:interpolator="@interpolator/disco_bounce"
+            android:value="1f" />
+        <keyframe
+            android:fraction="1"
+            android:value="1f" />
+    </propertyValuesHolder>
+</objectAnimator>
diff --git a/res/color-v24/all_apps_bg_hand_fill.xml b/res/color/all_apps_bg_hand_fill.xml
similarity index 100%
rename from res/color-v24/all_apps_bg_hand_fill.xml
rename to res/color/all_apps_bg_hand_fill.xml
diff --git a/res/color-v24/all_apps_bg_hand_fill_dark.xml b/res/color/all_apps_bg_hand_fill_dark.xml
similarity index 100%
rename from res/color-v24/all_apps_bg_hand_fill_dark.xml
rename to res/color/all_apps_bg_hand_fill_dark.xml
diff --git a/res/drawable-v26/ic_deepshortcut_placeholder.xml b/res/drawable-v26/ic_deepshortcut_placeholder.xml
deleted file mode 100644
index 3fa8506..0000000
--- a/res/drawable-v26/ic_deepshortcut_placeholder.xml
+++ /dev/null
@@ -1,20 +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.
--->
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="?attr/popupColorSecondary"/>
-    <foreground android:drawable="?attr/popupColorSecondary"/>
-</adaptive-icon>
diff --git a/res/drawable-v26/ic_launcher_home.xml b/res/drawable-v26/ic_launcher_home.xml
deleted file mode 100644
index 7038775..0000000
--- a/res/drawable-v26/ic_launcher_home.xml
+++ /dev/null
@@ -1,21 +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.
--->
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/icon_background" />
-    <foreground>
-        <bitmap android:src="@mipmap/ic_launcher_home_foreground"/>
-    </foreground>
-</adaptive-icon>
diff --git a/res/drawable/drag_handle_indicator_no_shadow.xml b/res/drawable/drag_handle_indicator_no_shadow.xml
deleted file mode 100644
index 341e60c..0000000
--- a/res/drawable/drag_handle_indicator_no_shadow.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/vertical_drag_handle_width"
-    android:height="@dimen/vertical_drag_handle_height"
-    android:viewportWidth="18.0"
-    android:viewportHeight="6.0"
-    android:tint="?attr/workspaceTextColor" >
-
-    <path
-        android:pathData="M17,6c-0.15,0-0.3-0.03-0.45-0.11L9,2.12L1.45,5.89c-0.5,0.25-1.09,
-        0.05-1.34-0.45S0.06,4.35,0.55,4.11l8-4c0.28-0.14,0.61-0.14,0.89,0l8,4c0.49,0.25,0.69,
-        0.85,0.45,1.34C17.72,5.8,17.37,6,17,6z"
-        android:fillColor="@android:color/white" />
-</vector>
diff --git a/res/drawable-v24/ic_block_shadow.xml b/res/drawable/ic_block_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_block_shadow.xml
rename to res/drawable/ic_block_shadow.xml
diff --git a/res/drawable/ic_deepshortcut_placeholder.xml b/res/drawable/ic_deepshortcut_placeholder.xml
index 85a9694..3fa8506 100644
--- a/res/drawable/ic_deepshortcut_placeholder.xml
+++ b/res/drawable/ic_deepshortcut_placeholder.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+     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.
@@ -13,10 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="oval">
-    <solid android:color="?attr/popupColorSecondary" />
-    <size
-        android:height="32dp"
-        android:width="32dp" />
-</shape>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="?attr/popupColorSecondary"/>
+    <foreground android:drawable="?attr/popupColorSecondary"/>
+</adaptive-icon>
diff --git a/res/drawable/ic_launcher_home.xml b/res/drawable/ic_launcher_home.xml
index a6f2519..7038775 100644
--- a/res/drawable/ic_launcher_home.xml
+++ b/res/drawable/ic_launcher_home.xml
@@ -13,6 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<bitmap
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:src="@mipmap/ic_launcher_home" />
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/icon_background" />
+    <foreground>
+        <bitmap android:src="@mipmap/ic_launcher_home_foreground"/>
+    </foreground>
+</adaptive-icon>
diff --git a/res/drawable-v24/ic_remove_shadow.xml b/res/drawable/ic_remove_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_remove_shadow.xml
rename to res/drawable/ic_remove_shadow.xml
diff --git a/res/drawable-v24/ic_setup_shadow.xml b/res/drawable/ic_setup_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_setup_shadow.xml
rename to res/drawable/ic_setup_shadow.xml
diff --git a/res/drawable-v24/ic_uninstall_shadow.xml b/res/drawable/ic_uninstall_shadow.xml
similarity index 100%
rename from res/drawable-v24/ic_uninstall_shadow.xml
rename to res/drawable/ic_uninstall_shadow.xml
diff --git a/res/drawable-v24/drag_handle_indicator_shadow.xml b/res/layout/floating_surface_view.xml
similarity index 74%
rename from res/drawable-v24/drag_handle_indicator_shadow.xml
rename to res/layout/floating_surface_view.xml
index 774bc38..434e84f 100644
--- a/res/drawable-v24/drag_handle_indicator_shadow.xml
+++ b/res/layout/floating_surface_view.xml
@@ -5,7 +5,7 @@
      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
+          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,
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.graphics.ShadowDrawable
+<com.android.launcher3.views.FloatingSurfaceView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:src="@drawable/drag_handle_indicator_no_shadow"
-    android:elevation="@dimen/vertical_drag_handle_elevation" />
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content" />
diff --git a/res/layout/home_settings.xml b/res/layout/home_settings.xml
new file mode 100644
index 0000000..0f2461a
--- /dev/null
+++ b/res/layout/home_settings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <EditText
+        android:id="@+id/filter_box"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/developer_options_filter_margins"
+        android:hint="@string/developer_options_filter_hint"
+        android:visibility="gone"
+        />
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@android:id/list_container"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index a137908..0c18c8a 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -28,6 +28,12 @@
         android:clipToPadding="false"
         android:importantForAccessibility="no">
 
+        <com.android.launcher3.views.AccessibilityActionsView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:contentDescription="@string/home_screen"
+            />
+
         <!-- The workspace contains 5 screens of cells -->
         <!-- DO NOT CHANGE THE ID -->
         <com.android.launcher3.Workspace
diff --git a/res/layout/search_section_title.xml b/res/layout/search_section_title.xml
new file mode 100644
index 0000000..c39a641
--- /dev/null
+++ b/res/layout/search_section_title.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/section_title"
+          android:textSize="14sp"
+          android:fontFamily="@style/TextHeadline"
+          android:layout_width="wrap_content"
+          android:textColor="?android:attr/textColorPrimary"
+          android:padding="4dp"
+          android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/res/mipmap-hdpi/ic_launcher_home.png b/res/mipmap-hdpi/ic_launcher_home.png
deleted file mode 100644
index d068d92..0000000
--- a/res/mipmap-hdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_home.png b/res/mipmap-mdpi/ic_launcher_home.png
deleted file mode 100644
index 16c8ec2..0000000
--- a/res/mipmap-mdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_home.png b/res/mipmap-xhdpi/ic_launcher_home.png
deleted file mode 100644
index 8b2671b..0000000
--- a/res/mipmap-xhdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_home.png b/res/mipmap-xxhdpi/ic_launcher_home.png
deleted file mode 100644
index 43d8b7d..0000000
--- a/res/mipmap-xxhdpi/ic_launcher_home.png
+++ /dev/null
Binary files differ
diff --git a/res/values-night-v26/styles.xml b/res/values-night/styles.xml
similarity index 100%
rename from res/values-night-v26/styles.xml
rename to res/values-night/styles.xml
diff --git a/res/values-v22/styles.xml b/res/values-v22/styles.xml
deleted file mode 100644
index f86db7a..0000000
--- a/res/values-v22/styles.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* 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.
-*/
--->
-
-<resources>
-
-    <style name="AppItemActivityTheme" parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
-        <item name="widgetsTheme">@style/WidgetContainerTheme</item>
-    </style>
-
-</resources>
\ No newline at end of file
diff --git a/res/values-v26/bools.xml b/res/values-v26/bools.xml
deleted file mode 100644
index ad8c7a1..0000000
--- a/res/values-v26/bools.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 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.
-*/
--->
-
-<resources>
-    <bool name="notification_dots_enabled">true</bool>
-
-    <bool name="enable_install_shortcut_api">false</bool>
-</resources>
\ No newline at end of file
diff --git a/res/values-v26/styles.xml b/res/values-v26/styles.xml
deleted file mode 100644
index d2f0802..0000000
--- a/res/values-v26/styles.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.
-*/
--->
-<resources>
-    <!-- Theme for the widget container. -->
-    <style name="WidgetContainerTheme" parent="@android:style/Theme.DeviceDefault.Settings">
-        <item name="android:colorPrimaryDark">#E8EAED</item>
-        <item name="android:textColorSecondary">?android:attr/textColorPrimary</item>
-        <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
-    </style>
-    <style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
-        <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
-        <item name="android:colorPrimaryDark">#616161</item> <!-- Gray 700 -->
-    </style>
-
-</resources>
diff --git a/res/values/bools.xml b/res/values/bools.xml
deleted file mode 100644
index bc2c678..0000000
--- a/res/values/bools.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 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.
-*/
--->
-
-<resources>
-    <bool name="notification_dots_enabled">false</bool>
-
-    <bool name="enable_install_shortcut_api">true</bool>
-</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 043ad9a..ad607a3 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -37,6 +37,7 @@
 
     <color name="all_apps_bg_hand_fill">#E5E5E5</color>
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
+    <color name="all_apps_section_fill">#327d7d7d</color>
 
     <color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
     <color name="gesture_tutorial_fake_task_view_color">#6DA1FF</color> <!-- Light Blue -->
diff --git a/res/values/config.xml b/res/values/config.xml
index 75fcc90..dc8bdff 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -70,6 +70,7 @@
     <string name="test_information_handler_class" translatable="false"></string>
     <string name="launcher_activity_logic_class" translatable="false"></string>
     <string name="prediction_model_class" translatable="false"></string>
+    <string name="model_delegate_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 947e635..f59f02f 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -40,14 +40,6 @@
     <dimen name="workspace_page_indicator_line_height">1dp</dimen>
     <dimen name="workspace_page_indicator_overlap_workspace">0dp</dimen>
 
-    <!-- Hotseat/all-apps scrim -->
-    <dimen name="all_apps_scrim_blur">4dp</dimen>
-    <dimen name="vertical_drag_handle_width">18dp</dimen>
-    <dimen name="vertical_drag_handle_height">6dp</dimen>
-    <dimen name="vertical_drag_handle_elevation">1dp</dimen>
-    <dimen name="vertical_drag_handle_touch_size">48dp</dimen>
-    <dimen name="vertical_drag_handle_padding_in_vertical_bar_layout">16dp</dimen>
-
 <!-- Drop target bar -->
     <dimen name="dynamic_grid_drop_target_size">48dp</dimen>
     <dimen name="drop_target_vertical_gap">20dp</dimen>
@@ -247,6 +239,9 @@
     <dimen name="snackbar_min_text_size">12sp</dimen>
     <dimen name="snackbar_max_text_size">14sp</dimen>
 
+<!-- Developer Options -->
+    <dimen name="developer_options_filter_margins">10dp</dimen>
+
 <!-- Theming related -->
     <dimen name="default_dialog_corner_radius">8dp</dimen>
 
diff --git a/res/values/drawables.xml b/res/values/drawables.xml
deleted file mode 100644
index 7d63142..0000000
--- a/res/values/drawables.xml
+++ /dev/null
@@ -1,22 +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.
--->
-<resources>
-    <drawable name="ic_setup_shadow">@drawable/ic_setting</drawable>
-    <drawable name="ic_remove_shadow">@drawable/ic_remove_no_shadow</drawable>
-    <drawable name="ic_uninstall_shadow">@drawable/ic_uninstall_no_shadow</drawable>
-    <drawable name="ic_block_shadow">@drawable/ic_block_no_shadow</drawable>
-    <drawable name="all_apps_arrow_shadow">@drawable/drag_handle_indicator_no_shadow</drawable>
-</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 935bb40..ef47eef 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -36,7 +36,7 @@
     <!-- Message shown when a shortcut is not available. It could have been temporarily disabled and may start working again after some time. -->
     <string name="shortcut_not_available">Shortcut isn\'t available</string>
     <!-- User visible name for the launcher/home screen. [CHAR_LIMIT=30] -->
-    <string name="home_screen">Home screen</string>
+    <string name="home_screen">Home</string>
     <!-- Label for showing custom action list of a shortcut or widget. [CHAR_LIMIT=30] -->
     <string name="custom_actions">Custom actions</string>
 
@@ -67,6 +67,10 @@
     <!-- Label for an icon representing any generic app. [CHAR_LIMIT=50] -->
     <string name="label_application">App</string>
 
+    <!--All apps Search-->
+    <!-- Section title for apps [CHAR_LIMIT=50] -->
+    <string name="search_corpus_apps">Apps</string>
+
     <!-- Popup items -->
     <!-- Text to display as the header above notifications. [CHAR_LIMIT=30] -->
     <string name="notifications_header">Notifications</string>
@@ -88,10 +92,6 @@
     <string name="all_apps_button_personal_label">Personal apps list</string>
     <string name="all_apps_button_work_label">Work apps list</string>
 
-    <!-- Label for button in all applications label to go back home (to the workspace / desktop)
-         for accessibilty (spoken when the button gets focus). -->
-    <string name="all_apps_home_button_label">Home</string>
-
     <!-- Label for remove drop target (from the homescreen only).
          May appear next to uninstall_drop_target_label [CHAR_LIMIT=20] -->
     <string name="remove_drop_target_label">Remove</string>
@@ -348,7 +348,8 @@
     <!-- content description for paused work apps list -->
     <string name="work_apps_paused_content_description">Work profile is paused. Work apps can\’t send you notifications, use your battery, or access your location</string>
 
-
+    <!-- A hint shown in launcher settings develop options filter box -->
+    <string name="developer_options_filter_hint">Filter</string>
 
     <!-- A tip shown pointing at work toggle -->
     <string name="work_switch_tip">Pause work apps and notifications</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 25f21f3..fd3d873 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -143,12 +143,21 @@
     <style name="AppTheme.Dark.DarkMainColor" parent="@style/LauncherTheme.Dark.DarkMainColor" />
     <style name="AppTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark.DarkText" />
 
-    <style name="AppItemActivityTheme" parent="@android:style/Theme.Material.Light.Dialog.Alert">
+    <style name="AppItemActivityTheme" parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
         <item name="widgetsTheme">@style/WidgetContainerTheme</item>
     </style>
 
     <style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="preferenceTheme">@style/HomeSettingsPreferenceTheme</item>
+    </style>
+
+    <style name="HomeSettingsPreferenceTheme" parent="@style/PreferenceThemeOverlay.v14.Material">
+        <item name="preferenceFragmentCompatStyle">@style/HomeSettingsFragmentCompatStyle</item>
+    </style>
+
+    <style name="HomeSettingsFragmentCompatStyle" parent="@style/PreferenceFragment.Material">
+        <item name="android:layout">@layout/home_settings</item>
     </style>
 
     <!--
@@ -162,14 +171,16 @@
         <item name="android:textColorSecondary">?attr/workspaceTextColor</item>
     </style>
 
-    <!-- Theme for the widget container. Overridden on API 26. -->
+    <!-- Theme for the widget container. -->
     <style name="WidgetContainerTheme" parent="@android:style/Theme.DeviceDefault.Settings">
-        <item name="android:colorEdgeEffect">?android:attr/textColorSecondaryInverse</item>
-        <item name="android:textColorPrimary">?android:attr/textColorPrimaryInverse</item>
-        <item name="android:textColorSecondary">?android:attr/textColorSecondaryInverse</item>
+        <item name="android:colorPrimaryDark">#E8EAED</item>
+        <item name="android:textColorSecondary">?android:attr/textColorPrimary</item>
+        <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
     </style>
-
-    <style name="WidgetContainerTheme.Dark" />
+    <style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
+        <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
+        <item name="android:colorPrimaryDark">#616161</item> <!-- Gray 700 -->
+    </style>
 
     <style name="FastScrollerPopup" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
         <item name="android:layout_width">wrap_content</item>
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 5610b0e..9ac3fe7 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -53,10 +53,10 @@
         mModelHelper = new LauncherModelHelper();
         mModelHelper.initializeData("/cache_data_updated_task_data.txt");
 
-        // Add dummy entries in the cache to simulate update
+        // Add placeholder entries in the cache to simulate update
         Context context = RuntimeEnvironment.application;
         IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
-        CachingLogic<ItemInfo> dummyLogic = new CachingLogic<ItemInfo>() {
+        CachingLogic<ItemInfo> placeholderLogic = new CachingLogic<ItemInfo>() {
             @Override
             public ComponentName getComponent(ItemInfo info) {
                 return info.getTargetComponent();
@@ -81,7 +81,7 @@
 
         UserManager um = context.getSystemService(UserManager.class);
         for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
-            iconCache.addIconToDBAndMemCache(info, dummyLogic, new PackageInfo(),
+            iconCache.addIconToDBAndMemCache(info, placeholderLogic, new PackageInfo(),
                     um.getSerialNumberForUser(info.user), true);
         }
     }
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index bbbe21e..be03c7d 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -165,7 +165,7 @@
             @Override
             public void onOpen(SQLiteDatabase db) { }
         };
-        // Insert dummy data
+        // Insert mock data
         for (int i = 0; i < 10; i++) {
             ContentValues values = new ContentValues();
             values.put(Favorites._ID, i);
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index 8f3a83e..655237d 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -109,7 +109,7 @@
     public void testCustomProfileLoaded_with_widget() throws Exception {
         String pendingAppPkg = "com.test.pending";
 
-        // Add a dummy session info so that the widget exists
+        // Add a placeholder session info so that the widget exists
         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
         params.setAppPackageName(pendingAppPkg);
 
@@ -120,7 +120,7 @@
         setField(sessionInfo, "appIcon", BitmapInfo.LOW_RES_ICON);
 
         writeLayoutAndLoad(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
-                .putWidget(pendingAppPkg, "DummyWidget", 2, 2));
+                .putWidget(pendingAppPkg, "PlaceholderWidget", 2, 2));
 
         // Verify widget
         assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 3a252dc..0e760f9 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -110,7 +110,7 @@
     public void getAppShortcutInfo_dontAllowMissing_invalidComponent() {
         initCursor(ITEM_TYPE_APPLICATION, "");
         assertTrue(mLoaderCursor.moveToNext());
-        ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
+        ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
         assertNull(mLoaderCursor.getAppShortcutInfo(
                 new Intent().setComponent(cn), false /* allowMissingTarget */, true));
     }
@@ -136,7 +136,7 @@
         initCursor(ITEM_TYPE_APPLICATION, "");
         assertTrue(mLoaderCursor.moveToNext());
 
-        ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
+        ComponentName cn = new ComponentName(mContext.getPackageName(), "placeholder-do");
         WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
                 mLoaderCursor.getAppShortcutInfo(
                         new Intent().setComponent(cn), true  /* allowMissingTarget */, true))
diff --git a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index ee73b82..4184d33 100644
--- a/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -44,7 +44,7 @@
     @Test
     public void testMigrateProfileId() throws Exception {
         SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
-        // Add some dummy data
+        // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
             values.put(Favorites._ID, i);
@@ -64,7 +64,7 @@
     @Test
     public void testChangeDefaultColumn() throws Exception {
         SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
-        // Add some dummy data
+        // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
             values.put(Favorites._ID, i);
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 0388087..f2b3071 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -81,7 +81,7 @@
     public static final int NO__ICON = -1;
     public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
 
-    // Authority for providing a dummy default-workspace-layout data.
+    // Authority for providing a test default-workspace-layout data.
     private static final String TEST_PROVIDER_AUTHORITY =
             LauncherModelHelper.class.getName().toLowerCase();
     private static final int DEFAULT_BITMAP_SIZE = 10;
@@ -252,7 +252,7 @@
     }
 
     /**
-     * Adds a dummy item in the DB.
+     * Adds a mock item in the DB.
      * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
      *             folder (where the type represents the number of items in the folder).
      */
@@ -310,7 +310,7 @@
     }
 
     /**
-     * Initializes the DB with dummy elements to represent the provided grid structure.
+     * Initializes the DB with mock elements to represent the provided grid structure.
      * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
      *                  type definitions. The first dimension represents the screens and the next
      *                  two represent the workspace grid.
@@ -347,7 +347,7 @@
     }
 
     /**
-     * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+     * Sets up a mock provider to load the provided layout by default, next time the layout loads
      */
     public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
             throws Exception {
@@ -360,7 +360,7 @@
                 "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
 
         shadowOf(context.getPackageManager())
-                .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+                .addProviderIfNotPresent(new ComponentName("com.test", "Mock")).authority =
                 TEST_PROVIDER_AUTHORITY;
 
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
diff --git a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index 84c65b1..5ab3106 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -129,7 +129,7 @@
         ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
 
         for (int i = 0; i < num; i++) {
-            ComponentName cn = new ComponentName("com.dummy.apk" + i, "DummyWidet");
+            ComponentName cn = new ComponentName("com.placeholder.apk" + i, "PlaceholderWidet");
 
             AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
             widgetInfo.provider = cn;
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index cd27a2d..3bfe379 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
@@ -62,7 +63,8 @@
             TYPE_ALL_APPS_EDU,
 
             TYPE_TASK_MENU,
-            TYPE_OPTIONS_POPUP
+            TYPE_OPTIONS_POPUP,
+            TYPE_ICON_SURFACE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -80,16 +82,18 @@
     // Popups related to quickstep UI
     public static final int TYPE_TASK_MENU = 1 << 10;
     public static final int TYPE_OPTIONS_POPUP = 1 << 11;
+    public static final int TYPE_ICON_SURFACE = 1 << 12;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
             | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
-            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU;
+            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU
+            | TYPE_ICON_SURFACE;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
-            | TYPE_ALL_APPS_EDU;
+            | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE;
 
     // 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
@@ -123,7 +127,7 @@
     }
 
     public final void close(boolean animate) {
-        animate &= Utilities.areAnimationsEnabled(getContext());
+        animate &= areAnimatorsEnabled();
         if (mIsOpen) {
             BaseActivity.fromContext(getContext()).getUserEventDispatcher()
                     .resetElapsedContainerMillis("container closed");
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 48819cb..198f13d 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -614,6 +614,9 @@
     @Override
     public void setIconVisible(boolean visible) {
         mIsIconVisible = visible;
+        if (!mIsIconVisible) {
+            resetIconScale();
+        }
         Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
         applyCompoundDrawables(icon);
     }
@@ -753,11 +756,14 @@
 
     @Override
     public SafeCloseable prepareDrawDragView() {
-        if (getIcon() instanceof FastBitmapDrawable) {
-            FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
-            icon.setScale(1f);
-        }
+        resetIconScale();
         setForceHideDot(true);
         return () -> { };
     }
+
+    private void resetIconScale() {
+        if (mIcon instanceof FastBitmapDrawable) {
+            ((FastBitmapDrawable) mIcon).setScale(1f);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 89d768c..1cd201f 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
 
 import android.animation.Animator;
@@ -2009,7 +2011,7 @@
             // Animations are disabled in power save mode, causing the repeated animation to jump
             // spastically between beginning and end states. Since this looks bad, we don't repeat
             // the animation in power save mode.
-            if (Utilities.areAnimationsEnabled(getContext())) {
+            if (areAnimatorsEnabled()) {
                 va.setRepeatMode(ValueAnimator.REVERSE);
                 va.setRepeatCount(ValueAnimator.INFINITE);
             }
diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java
new file mode 100644
index 0000000..2a7e629
--- /dev/null
+++ b/src/com/android/launcher3/GestureNavContract.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.SurfaceControl;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Class to encapsulate the handshake protocol between Launcher and gestureNav.
+ */
+public class GestureNavContract {
+
+    private static final String TAG = "GestureNavContract";
+
+    public static final String EXTRA_GESTURE_CONTRACT = "gesture_nav_contract_v1";
+    public static final String EXTRA_ICON_POSITION = "gesture_nav_contract_icon_position";
+    public static final String EXTRA_ICON_SURFACE = "gesture_nav_contract_surface_control";
+    public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
+
+    public final ComponentName componentName;
+    public final UserHandle user;
+
+    private final Message mCallback;
+
+    public GestureNavContract(ComponentName componentName, UserHandle user, Message callback) {
+        this.componentName = componentName;
+        this.user = user;
+        this.mCallback = callback;
+    }
+
+    /**
+     * Sends the position information to the receiver
+     */
+    @TargetApi(Build.VERSION_CODES.R)
+    public void sendEndPosition(RectF position, @Nullable SurfaceControl surfaceControl) {
+        Bundle result = new Bundle();
+        result.putParcelable(EXTRA_ICON_POSITION, position);
+        result.putParcelable(EXTRA_ICON_SURFACE, surfaceControl);
+
+        Message callback = Message.obtain();
+        callback.copyFrom(mCallback);
+        callback.setData(result);
+
+        try {
+            callback.replyTo.send(callback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending icon position", e);
+        }
+    }
+
+    /**
+     * Clears and returns the GestureNavContract if it was present in the intent.
+     */
+    public static GestureNavContract fromIntent(Intent intent) {
+        if (!Utilities.ATLEAST_R) {
+            return null;
+        }
+        Bundle extras = intent.getBundleExtra(EXTRA_GESTURE_CONTRACT);
+        if (extras == null) {
+            return null;
+        }
+        intent.removeExtra(EXTRA_GESTURE_CONTRACT);
+
+        ComponentName componentName = extras.getParcelable(EXTRA_COMPONENT_NAME);
+        UserHandle userHandle = extras.getParcelable(EXTRA_USER);
+        Message callback = extras.getParcelable(EXTRA_REMOTE_CALLBACK);
+
+        if (componentName != null && userHandle != null && callback != null
+                && callback.replyTo != null) {
+            return new GestureNavContract(componentName, userHandle, callback);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 62c9b4d..be6adc7 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -20,7 +20,6 @@
 
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -69,7 +68,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class InstallShortcutReceiver extends BroadcastReceiver {
+public class InstallShortcutReceiver {
 
     public static final int FLAG_ACTIVITY_PAUSED = 1;
     public static final int FLAG_LOADER_RUNNING = 2;
@@ -82,9 +81,6 @@
     private static final String TAG = "InstallShortcutReceiver";
     private static final boolean DBG = false;
 
-    private static final String ACTION_INSTALL_SHORTCUT =
-            "com.android.launcher.action.INSTALL_SHORTCUT";
-
     private static final String LAUNCH_INTENT_KEY = "intent.launch";
     private static final String NAME_KEY = "name";
     private static final String ICON_KEY = "icon";
@@ -188,25 +184,6 @@
         sp.edit().putStringSet(APPS_PENDING_INSTALL, newStrings).apply();
     }
 
-    public void onReceive(Context context, Intent data) {
-        if (!ACTION_INSTALL_SHORTCUT.equals(data.getAction())) {
-            return;
-        }
-        PendingInstallShortcutInfo info = createPendingInfo(context, data);
-        if (info != null) {
-            if (!info.isLauncherActivity()) {
-                // Since its a custom shortcut, verify that it is safe to launch.
-                if (!new PackageManagerHelper(context).hasPermissionForActivity(
-                        info.launchIntent, null)) {
-                    // Target cannot be launched, or requires some special permission to launch
-                    Log.e(TAG, "Ignoring malicious intent " + info.launchIntent.toUri(0));
-                    return;
-                }
-            }
-            queuePendingShortcutInfo(info, context);
-        }
-    }
-
     /**
      * @return true is the extra is either null or is of type {@param type}
      */
@@ -251,6 +228,10 @@
         queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, widgetId, context), context);
     }
 
+    public static void queueApplication(LauncherActivityInfo info, Context context) {
+        queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context);
+    }
+
     public static void queueApplication(Intent data, UserHandle user, Context context) {
         queuePendingShortcutInfo(new PendingInstallShortcutInfo(data, context, user),
                 context);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 377c580..1ed6356 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.InstallShortcutReceiver.FLAG_DRAG_AND_DROP;
@@ -118,7 +119,6 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.data.AppInfo;
@@ -168,6 +168,7 @@
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.FloatingSurfaceView;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -451,10 +452,10 @@
                     mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
                 } else if (finalState == OVERVIEW || finalState == OVERVIEW_PEEK) {
                     mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
-                    mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+                    mScrimView.setAlpha(alpha);
                 } else {
                     mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
-                    mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
+                    mScrimView.setAlpha(1f);
                 }
             }
         });
@@ -509,6 +510,7 @@
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
+        AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
     }
 
     @Override
@@ -550,7 +552,7 @@
             mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
         } else if (state == OVERVIEW || state == OVERVIEW_PEEK) {
             mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
-            mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+            mScrimView.setAlpha(alpha);
         }
     }
 
@@ -908,14 +910,12 @@
         logStopAndResume(Action.Command.RESUME);
         getUserEventDispatcher().startSession();
 
-        AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
-
         // Process any items that were added while Launcher was away.
         InstallShortcutReceiver.disableAndFlushInstallQueue(
                 InstallShortcutReceiver.FLAG_ACTIVITY_PAUSED, this);
 
         // Refresh shortcuts if the permission changed.
-        mModel.refreshShortcutsIfRequired();
+        mModel.validateModelDataOnResume();
 
         // Set the notification listener and fetch updated notifications when we resume
         NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
@@ -1196,11 +1196,8 @@
         int[] cellXY = mTmpAddItemCellCoordinates;
         CellLayout layout = getCellLayout(container, screenId);
 
-        WorkspaceItemInfo info = null;
-        if (Utilities.ATLEAST_OREO) {
-            info = PinRequestHelper.createWorkspaceItemFromPinItemRequest(
+        WorkspaceItemInfo info = PinRequestHelper.createWorkspaceItemFromPinItemRequest(
                     this, PinRequestHelper.getPinItemRequest(data), 0);
-        }
 
         if (info == null) {
             // Legacy shortcuts are only supported for primary profile.
@@ -1450,6 +1447,7 @@
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
+            handleGestureContract(intent);
         } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
             showAllAppsFromIntent(alreadyOnHome);
         }
@@ -1463,6 +1461,17 @@
     }
 
     /**
+     * Handles gesture nav contract
+     */
+    protected void handleGestureContract(Intent intent) {
+        GestureNavContract gnc = GestureNavContract.fromIntent(intent);
+        if (gnc != null) {
+            AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
+            FloatingSurfaceView.show(this, gnc);
+        }
+    }
+
+    /**
      * Hides the keyboard if visible
      */
     public void hideKeyboard() {
@@ -1540,7 +1549,6 @@
         mOverlayManager.onActivityDestroyed(this);
         mAppTransitionManager.unregisterRemoteAnimations();
         mUserChangedCallbackCloseable.close();
-        mAllAppsController.onActivityDestroyed();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1927,7 +1935,7 @@
         // Populate event with a fake title based on the current state.
         // TODO: When can workspace be null?
         text.add(mWorkspace == null
-                ? getString(R.string.all_apps_home_button_label)
+                ? getString(R.string.home_screen)
                 : mStateManager.getState().getDescription(this));
         return result;
     }
@@ -2485,7 +2493,7 @@
      * @param updated list of shortcuts which have changed.
      */
     @Override
-    public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated) {
+    public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
         if (!updated.isEmpty()) {
             mWorkspace.updateShortcuts(updated);
         }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 53e5274..782a869 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -109,15 +109,11 @@
         mInstallSessionTracker = InstallSessionHelper.INSTANCE.get(context)
                 .registerInstallTracker(mModel, MODEL_EXECUTOR);
 
-        if (!mContext.getResources().getBoolean(R.bool.notification_dots_enabled)) {
-            mNotificationDotsObserver = null;
-        } else {
-            // Register an observer to rebind the notification listener when dots are re-enabled.
-            mNotificationDotsObserver =
-                    newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged);
-            mNotificationDotsObserver.register();
-            mNotificationDotsObserver.dispatchOnChange();
-        }
+        // Register an observer to rebind the notification listener when dots are re-enabled.
+        mNotificationDotsObserver =
+                newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged);
+        mNotificationDotsObserver.register();
+        mNotificationDotsObserver.dispatchOnChange();
     }
 
     public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -128,7 +124,7 @@
         mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
         mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
         mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
-        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+        mModel = new LauncherModel(context, this, mIconCache, AppFilter.newInstance(mContext));
         mPredictionModel = PredictionModel.newInstance(mContext);
     }
 
@@ -157,6 +153,7 @@
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
     public void onTerminate() {
+        mModel.destroy();
         if (mModelChangeReceiver != null) {
             mContext.unregisterReceiver(mModelChangeReceiver);
         }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index ff4b545..e2568d5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.content.Context;
 import android.content.Intent;
@@ -46,6 +45,7 @@
 import com.android.launcher3.model.CacheDataUpdatedTask;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.PackageInstallStateChangedTask;
 import com.android.launcher3.model.PackageUpdatedTask;
@@ -112,20 +112,22 @@
      */
     private final BgDataModel mBgDataModel = new BgDataModel();
 
+    private final ModelDelegate mModelDelegate;
+
     // Runnable to check if the shortcuts permission has changed.
-    private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
+    private final Runnable mDataValidationCheck = new Runnable() {
         @Override
         public void run() {
-            if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
-                    != mBgAllAppsList.hasShortcutHostPermission()) {
-                forceReload();
+            if (mModelLoaded) {
+                mModelDelegate.validateData();
             }
         }
     };
 
-    LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
+    LauncherModel(Context context, LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
         mApp = app;
         mBgAllAppsList = new AllAppsList(iconCache, appFilter);
+        mModelDelegate = ModelDelegate.newInstance(context, app, mBgAllAppsList, mBgDataModel);
     }
 
     /**
@@ -217,6 +219,13 @@
         }
     }
 
+    /**
+     * Called when the model is destroyed
+     */
+    public void destroy() {
+        MODEL_EXECUTOR.execute(mModelDelegate::destroy);
+    }
+
     public void onBroadcastIntent(Intent intent) {
         if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent);
         final String action = intent.getAction();
@@ -372,7 +381,8 @@
     public void startLoaderForResults(LoaderResults results) {
         synchronized (mLock) {
             stopLoader();
-            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
+            mLoaderTask = new LoaderTask(
+                    mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, results);
 
             // Always post the loader task, instead of running directly (even on same thread) so
             // that we exit any nested synchronized blocks
@@ -491,9 +501,9 @@
      * Current implementation simply reloads the workspace, but it can be optimized to
      * use partial updates similar to {@link UserCache}
      */
-    public void refreshShortcutsIfRequired() {
-        MODEL_EXECUTOR.getHandler().removeCallbacks(mShortcutPermissionCheckRunnable);
-        MODEL_EXECUTOR.post(mShortcutPermissionCheckRunnable);
+    public void validateModelDataOnResume() {
+        MODEL_EXECUTOR.getHandler().removeCallbacks(mDataValidationCheck);
+        MODEL_EXECUTOR.post(mDataValidationCheck);
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e8b5568..fdbbf4e 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -872,7 +872,6 @@
          * Removes widgets which are registered to the Launcher's host, but are not present
          * in our model.
          */
-        @TargetApi(Build.VERSION_CODES.O)
         public void removeGhostWidgets(SQLiteDatabase db) {
             // Get all existing widget ids.
             final AppWidgetHost host = newLauncherWidgetHost();
diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java
index 5f6ecb5..f2a3de7 100644
--- a/src/com/android/launcher3/MainProcessInitializer.java
+++ b/src/com/android/launcher3/MainProcessInitializer.java
@@ -38,7 +38,6 @@
     protected void init(Context context) {
         FileLog.setDir(context.getApplicationContext().getFilesDir());
         FeatureFlags.initialize(context);
-        SessionCommitReceiver.applyDefaultUserPrefs(context);
         IconShape.init(context);
 
         if (BitmapCreationCheck.ENABLED) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index e29faac..4303dee 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -195,9 +195,7 @@
         mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * density);
         mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density);
 
-        if (Utilities.ATLEAST_OREO) {
-            setDefaultFocusHighlightEnabled(false);
-        }
+        setDefaultFocusHighlightEnabled(false);
     }
 
     protected void setDefaultInterpolator(Interpolator interpolator) {
@@ -1448,11 +1446,8 @@
         int minDistanceFromScreenCenterIndex = -1;
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; ++i) {
-            View layout = getPageAt(i);
-            int childSize = mOrientationHandler.getMeasuredSize(layout);
-            int halfChildSize = (childSize / 2);
-            int childCenter = getChildOffset(i) + halfChildSize;
-            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
+            int distanceFromScreenCenter = Math.abs(
+                    getDisplacementFromScreenCenter(i, screenCenter));
             if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
                 minDistanceFromScreenCenter = distanceFromScreenCenter;
                 minDistanceFromScreenCenterIndex = i;
@@ -1461,6 +1456,20 @@
         return minDistanceFromScreenCenterIndex;
     }
 
+    private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
+        View layout = getPageAt(childIndex);
+        int childSize = mOrientationHandler.getMeasuredSize(layout);
+        int halfChildSize = (childSize / 2);
+        int childCenter = getChildOffset(childIndex) + halfChildSize;
+        return childCenter - screenCenter;
+    }
+
+    protected int getDisplacementFromScreenCenter(int childIndex) {
+        int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
+        int screenCenter = mOrientationHandler.getPrimaryScroll(this) + (pageOrientationSize / 2);
+        return getDisplacementFromScreenCenter(childIndex, screenCenter);
+    }
+
     protected void snapToDestination() {
         snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration());
     }
diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index 403d779..c9fb75a 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -22,6 +22,7 @@
 
 public class ResourceUtils {
     public static final int DEFAULT_NAVBAR_VALUE = 48;
+    public static final int INVALID_RESOURCE_HANDLE = -1;
     public static final String NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE = "navigation_bar_width";
     public static final String NAVBAR_BOTTOM_GESTURE_SIZE = "navigation_bar_gesture_height";
 
@@ -51,7 +52,13 @@
         return val;
     }
 
+    public static int getIntegerByName(String resName, Resources res, int defaultValue) {
+        int resId = res.getIdentifier(resName, "integer", "android");
+        return resId != 0 ? res.getInteger(resId) : defaultValue;
+    }
+
     public static int pxFromDp(float size, DisplayMetrics metrics) {
-        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
+        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(
+                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
     }
 }
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index 89f0a3d..a8d6490 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -16,55 +16,32 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
-
-import android.annotation.TargetApi;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.util.Executors;
 
 import java.util.List;
 
 /**
  * BroadcastReceiver to handle session commit intent.
  */
-@TargetApi(Build.VERSION_CODES.O)
 public class SessionCommitReceiver extends BroadcastReceiver {
 
-    private static final String TAG = "SessionCommitReceiver";
-
-    // The content provider for the add to home screen setting. It should be of the format:
-    // <package name>.addtohomescreen
-    private static final String MARKER_PROVIDER_PREFIX = ".addtohomescreen";
-
     // Preference key for automatically adding icon to homescreen.
     public static final String ADD_ICON_PREFERENCE_KEY = "pref_add_icon_to_home";
-    public static final String ADD_ICON_PREFERENCE_INITIALIZED_KEY =
-            "pref_add_icon_to_home_initialized";
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        if (!isEnabled(context) || !Utilities.ATLEAST_OREO) {
+        if (!isEnabled(context)) {
             // User has decided to not add icons on homescreen.
             return;
         }
@@ -89,16 +66,6 @@
         queueAppIconAddition(context, info.getAppPackageName(), user);
     }
 
-    public static void queuePromiseAppIconAddition(Context context, SessionInfo sessionInfo) {
-        String packageName = sessionInfo.getAppPackageName();
-        if (context.getSystemService(LauncherApps.class)
-                .getActivityList(packageName, getUserHandle(sessionInfo)).isEmpty()) {
-            // Ensure application isn't already installed.
-            queueAppIconAddition(context, packageName, sessionInfo.getAppLabel(),
-                    sessionInfo.getAppIcon(), getUserHandle(sessionInfo));
-        }
-    }
-
     public static void queueAppIconAddition(Context context, String packageName, UserHandle user) {
         List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class)
                 .getActivityList(packageName, user);
@@ -106,85 +73,11 @@
             // no activity found
             return;
         }
-        queueAppIconAddition(context, packageName, activities.get(0).getLabel(), null, user);
-    }
 
-    private static void queueAppIconAddition(Context context, String packageName,
-            CharSequence label, Bitmap icon, UserHandle user) {
-        Intent data = new Intent();
-        data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
-                new ComponentName(packageName, "")).setPackage(packageName));
-        data.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
-        data.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
-
-        InstallShortcutReceiver.queueApplication(data, user, context);
+        InstallShortcutReceiver.queueApplication(activities.get(0), context);
     }
 
     public static boolean isEnabled(Context context) {
         return Utilities.getPrefs(context).getBoolean(ADD_ICON_PREFERENCE_KEY, true);
     }
-
-    public static void applyDefaultUserPrefs(final Context context) {
-        if (!Utilities.ATLEAST_OREO) {
-            return;
-        }
-        SharedPreferences prefs = Utilities.getPrefs(context);
-        if (prefs.getAll().isEmpty()) {
-            // This logic assumes that the code is the first thing that is executed (before any
-            // shared preference is written).
-            // TODO: Move this logic to DB upgrade once we have proper support for db downgrade
-            // If it is a fresh start, just apply the default value. We use prefs.isEmpty() to infer
-            // a fresh start as put preferences always contain some values corresponding to current
-            // grid.
-            prefs.edit().putBoolean(ADD_ICON_PREFERENCE_KEY, true).apply();
-        } else if (!prefs.contains(ADD_ICON_PREFERENCE_INITIALIZED_KEY)) {
-            new PrefInitTask(context).executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
-        }
-    }
-
-    private static class PrefInitTask extends AsyncTask<Void, Void, Void> {
-        private final Context mContext;
-
-        PrefInitTask(Context context) {
-            mContext = context;
-        }
-
-        @Override
-        protected Void doInBackground(Void... voids) {
-            boolean addIconToHomeScreenEnabled = readValueFromMarketApp();
-            Utilities.getPrefs(mContext).edit()
-                    .putBoolean(ADD_ICON_PREFERENCE_KEY, addIconToHomeScreenEnabled)
-                    .putBoolean(ADD_ICON_PREFERENCE_INITIALIZED_KEY, true)
-                    .apply();
-            return null;
-        }
-
-        public boolean readValueFromMarketApp() {
-            // Get the marget package
-            ResolveInfo ri = mContext.getPackageManager().resolveActivity(
-                    new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MARKET),
-                    PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_SYSTEM_ONLY);
-            if (ri == null) {
-                return true;
-            }
-
-            Cursor c = null;
-            try {
-                c = mContext.getContentResolver().query(
-                        Uri.parse("content://" + ri.activityInfo.packageName
-                                + MARKER_PROVIDER_PREFIX),
-                        null, null, null, null);
-                if (c.moveToNext()) {
-                    return c.getInt(c.getColumnIndexOrThrow(Settings.NameValueTable.VALUE)) != 0;
-                }
-            } catch (Exception e) {
-                Log.d(TAG, "Error reading add to homescreen preference", e);
-            } finally {
-                if (c != null) {
-                    c.close();
-                }
-            }
-            return true;
-        }
-    }
 }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index bf63788..068d0bc 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -18,7 +18,6 @@
 
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ICON_BADGED;
 
-import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.Person;
@@ -48,7 +47,6 @@
 import android.os.DeadObjectException;
 import android.os.Handler;
 import android.os.Message;
-import android.os.PowerManager;
 import android.os.TransactionTooLargeException;
 import android.provider.Settings;
 import android.text.Spannable;
@@ -65,7 +63,6 @@
 
 import androidx.core.os.BuildCompat;
 
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.graphics.GridOptionsProvider;
 import com.android.launcher3.graphics.TintedDrawableSpan;
@@ -113,12 +110,6 @@
     public static final boolean ATLEAST_P =
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
 
-    public static final boolean ATLEAST_OREO_MR1 =
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1;
-
-    public static final boolean ATLEAST_OREO =
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
-
     /**
      * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
      */
@@ -494,21 +485,6 @@
                 LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
     }
 
-    /**
-     * @return {@link SharedPreferences} that backs {@link FeatureFlags}
-     */
-    public static SharedPreferences getFeatureFlagsPrefs(Context context) {
-        // Use application context for shared preferences, so that we use a single cached instance
-        return context.getApplicationContext().getSharedPreferences(
-            FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
-    }
-
-    public static boolean areAnimationsEnabled(Context context) {
-        return ATLEAST_OREO
-                ? ValueAnimator.areAnimatorsEnabled()
-                : !context.getSystemService(PowerManager.class).isPowerSaveMode();
-    }
-
     public static boolean isWallpaperAllowed(Context context) {
         return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed();
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index a6283ff..3be9ac7 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -115,6 +115,7 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.function.Predicate;
 
 /**
@@ -3087,7 +3088,7 @@
         return false;
     }
 
-    void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
+    void updateShortcuts(List<WorkspaceItemInfo> shortcuts) {
         final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
         ItemOperator op = (info, v) -> {
             if (v instanceof BubbleTextView && updates.contains(info)) {
@@ -3262,7 +3263,7 @@
         }
         if (nScreens == 0) {
             // When the workspace is not loaded, we do not know how many screen will be bound.
-            return getContext().getString(R.string.all_apps_home_button_label);
+            return getContext().getString(R.string.home_screen);
         }
         return getContext().getString(R.string.workspace_scroll_format, page + 1, nScreens);
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 77b8a32..2d711e6 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -486,7 +487,7 @@
         if (mWorkModeSwitch != null) {
             mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK
                     && mAllAppsStore.hasModelFlag(
-                            FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
+                    FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
         }
     }
 
@@ -538,6 +539,10 @@
         int padding = mHeader.getMaxTranslation();
         for (int i = 0; i < mAH.length; i++) {
             mAH[i].padding.top = padding;
+            if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && mUsingTabs) {
+                //add extra space between tabs and recycler view
+                mAH[i].padding.top += mLauncher.getDeviceProfile().edgeMarginPx;
+            }
             mAH[i].applyPadding();
         }
     }
@@ -652,6 +657,9 @@
             applyVerticalFadingEdgeEnabled(verticalFadingEdge);
             applyPadding();
             setupOverlay();
+            if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+                recyclerView.addItemDecoration(new AllAppsSectionDecorator(getApps()));
+            }
         }
 
         void setupOverlay() {
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index d653160..dec92df 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -66,6 +66,8 @@
     // A divider that separates the apps list and the search market button
     public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4;
 
+    public static final int VIEW_TYPE_SEARCH_CORPUS_TITLE =  1 << 5;
+
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
@@ -274,6 +276,9 @@
             case VIEW_TYPE_ALL_APPS_DIVIDER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.all_apps_divider, parent, false));
+            case VIEW_TYPE_SEARCH_CORPUS_TITLE:
+                return new ViewHolder(
+                        mLayoutInflater.inflate(R.layout.search_section_title, parent, false));
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -302,6 +307,10 @@
                     searchView.setVisibility(View.GONE);
                 }
                 break;
+            case VIEW_TYPE_SEARCH_CORPUS_TITLE:
+                TextView titleView = (TextView) holder.itemView;
+                titleView.setText(mApps.getAdapterItems().get(position).searchSectionInfo.getTitle(
+                        titleView.getContext()));
             case VIEW_TYPE_ALL_APPS_DIVIDER:
                 // nothing to do
                 break;
diff --git a/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
new file mode 100644
index 0000000..d7af5f1
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.graphics.Insets;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowInsetsAnimationControlListener;
+import android.view.WindowInsetsAnimationController;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Handles IME over all apps to be synchronously transitioning along with the passed in
+ * root inset.
+ */
+public class AllAppsInsetTransitionController {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "AllAppsInsetTransitionController";
+    private static final Interpolator LINEAR = new LinearInterpolator();
+
+    private WindowInsetsAnimationController mAnimationController;
+    private WindowInsetsAnimationControlListener mCurrentRequest;
+
+    private float mAllAppsHeight;
+
+    private int mDownInsetBottom;
+    private boolean mShownAtDown;
+
+    private int mHiddenInsetBottom;
+    private int mShownInsetBottom;
+
+    private float mDown, mCurrent;
+    private View mApps;
+
+    public AllAppsInsetTransitionController(float allAppsHeight, View appsView) {
+        mAllAppsHeight = allAppsHeight;
+        mApps = appsView;
+    }
+
+    public void hide() {
+        if (!BuildCompat.isAtLeastR()) return;
+
+        WindowInsets insets = mApps.getRootWindowInsets();
+        if (insets == null) return;
+
+        if (insets.isVisible(WindowInsets.Type.ime())) {
+            mApps.getWindowInsetsController().hide(WindowInsets.Type.ime());
+        }
+    }
+
+    /**
+     * Initializes member variables and requests for the {@link WindowInsetsAnimationController}
+     * object.
+     *
+     * @param progress value between 0..1
+     */
+    @RequiresApi(api = Build.VERSION_CODES.R)
+    public void onDragStart(float progress) {
+        if (!BuildCompat.isAtLeastR()) return;
+        onAnimationEnd(progress);
+
+        mDown = progress * mAllAppsHeight;
+
+        // Below two values are sometimes incorrect. Possibly a platform bug
+        mDownInsetBottom = mApps.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom;
+        mShownAtDown = mApps.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
+
+        // override this value based on what it should actually be.
+        mShownAtDown = Float.compare(progress, 1f) == 0 ? false : true;
+        mDownInsetBottom = mShownAtDown ? mShownInsetBottom : mHiddenInsetBottom;
+        if (DEBUG) {
+            Log.d(TAG, "\nonDragStart mDownInsets=" + mDownInsetBottom
+                    + " mShownAtDown =" + mShownAtDown);
+        }
+
+        mApps.getWindowInsetsController().controlWindowInsetsAnimation(
+                WindowInsets.Type.ime(), -1 /* no predetermined duration */, LINEAR, null,
+                mCurrentRequest = new WindowInsetsAnimationControlListener() {
+
+                    @Override
+                    public void onReady(WindowInsetsAnimationController controller, int types) {
+                        if (DEBUG) {
+                            Log.d(TAG, "Listener.onReady " + (mCurrentRequest == this));
+                        }
+                        if (mCurrentRequest == this) {
+                            mAnimationController = controller;
+                        } else {
+                            controller.finish(mShownAtDown);
+                        }
+                    }
+
+                    @Override
+                    public void onFinished(WindowInsetsAnimationController controller) {
+                        // when screen lock happens, then this method get called
+                        mAnimationController.finish(false);
+                        mAnimationController = null;
+                        if (DEBUG) {
+                            Log.d(TAG, "Listener.onFinished ctrl=" + controller);
+                        }
+                    }
+
+                    @Override
+                    public void onCancelled(@Nullable WindowInsetsAnimationController controller) {
+                        mAnimationController = null;
+                        if (controller != null) {
+                            controller.finish(mShownAtDown);
+                        }
+                        if (DEBUG) {
+                            Log.d(TAG, "Listener.onCancelled ctrl=" + controller);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Handles the translation using the progress.
+     *
+     * @param progress value between 0..1
+     */
+    @RequiresApi(api = 30)
+    public void setProgress(float progress) {
+        if (!BuildCompat.isAtLeastR()) return;
+        // progress that equals to 0 or 1 is error prone. Do not use them.
+        // Instead use onDragStart and onAnimationEnd
+        if (mAnimationController == null || progress <= 0f || progress >= 1f) return;
+
+        mCurrent = progress * mAllAppsHeight;
+        mHiddenInsetBottom = mAnimationController.getHiddenStateInsets().bottom; // 0
+        mShownInsetBottom = mAnimationController.getShownStateInsets().bottom; // 1155
+
+        int shift = mShownAtDown ? 0 : (int) (mAllAppsHeight - mShownInsetBottom);
+
+        int inset = (int) (mDownInsetBottom + (mDown - mCurrent) - shift);
+
+        if (DEBUG) {
+            Log.d(TAG, "updateInset mCurrent=" + mCurrent + " mDown="
+                    + mDown + " hidden=" + mHiddenInsetBottom
+                    + " shown=" + mShownInsetBottom
+                    + " mDownInsets.bottom=" + mDownInsetBottom + " inset:" + inset
+                    + " shift: " + shift);
+        }
+        final int start = mShownAtDown ? mShownInsetBottom : mHiddenInsetBottom;
+        final int end = mShownAtDown ? mHiddenInsetBottom : mShownInsetBottom;
+        inset = Math.max(inset, mHiddenInsetBottom);
+        inset = Math.min(inset, mShownInsetBottom);
+        Log.d(TAG, "updateInset inset:" + inset);
+
+        mAnimationController.setInsetsAndAlpha(
+                Insets.of(0, 0, 0, inset),
+                1f, (inset - start) / (float) (end - start));
+    }
+
+    /**
+     * Report to the animation controller that we no longer plan to translate anymore.
+     *
+     * @param progress value between 0..1
+     */
+    @RequiresApi(api = 30)
+    public void onAnimationEnd(float progress) {
+        if (DEBUG) {
+            Log.d(TAG, "endTranslation progress=" + progress
+                    + " mAnimationController=" + mAnimationController);
+        }
+
+        if (mAnimationController == null) return;
+
+        if (Float.compare(progress, 1f) == 0 /* bottom */) {
+            mAnimationController.finish(false /* gone */);
+        }
+        if (Float.compare(progress, 0f) == 0 /* top */) {
+            mAnimationController.finish(true /* show */);
+        }
+        mAnimationController = null;
+        mCurrentRequest = null;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index cbf02b7..640ef01 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -107,6 +107,7 @@
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
+        pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_CORPUS_TITLE, 1);
 
         mViewHeights.clear();
         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx);
diff --git a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
new file mode 100644
index 0000000..ac55072
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.search.SearchSectionInfo;
+import com.android.launcher3.util.Themes;
+
+import java.util.List;
+
+/**
+ * ItemDecoration class that groups items in {@link AllAppsRecyclerView}
+ */
+public class AllAppsSectionDecorator extends RecyclerView.ItemDecoration {
+
+    private final AlphabeticalAppsList mApps;
+
+    AllAppsSectionDecorator(AlphabeticalAppsList appsList) {
+        mApps = appsList;
+    }
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        // Iterate through views in recylerview and draw bounds around views in the same section.
+        // Since views in the same section will follow each other, we can skip to a last view in
+        // a section to get the bounds of the section without having to iterate on evert item.
+        int itemCount = parent.getChildCount();
+        List<AlphabeticalAppsList.AdapterItem> adapterItems = mApps.getAdapterItems();
+        SectionDecorationHandler lastDecorationHandler = null;
+        int i = 0;
+        while (i < itemCount) {
+            View view = parent.getChildAt(i);
+            int position = parent.getChildAdapterPosition(view);
+            AlphabeticalAppsList.AdapterItem adapterItem = adapterItems.get(position);
+            if (adapterItem.searchSectionInfo != null) {
+                SearchSectionInfo sectionInfo = adapterItem.searchSectionInfo;
+                int endIndex = Math.min(i + sectionInfo.getPosEnd() - position, itemCount - 1);
+                SectionDecorationHandler decorationHandler = sectionInfo.getDecorationHandler();
+                if (decorationHandler != lastDecorationHandler && lastDecorationHandler != null) {
+                    drawDecoration(c, lastDecorationHandler, parent);
+                }
+                lastDecorationHandler = decorationHandler;
+                if (decorationHandler != null) {
+                    decorationHandler.extendBounds(view);
+                }
+
+                if (endIndex > i) {
+                    i = endIndex;
+                    continue;
+                }
+
+            }
+            i++;
+        }
+        if (lastDecorationHandler != null) {
+            drawDecoration(c, lastDecorationHandler, parent);
+        }
+    }
+
+    private void drawDecoration(Canvas c, SectionDecorationHandler decorationHandler, View parent) {
+        if (decorationHandler == null) return;
+        if (decorationHandler.mIsFullWidth) {
+            decorationHandler.mBounds.left = parent.getPaddingLeft();
+            decorationHandler.mBounds.right = parent.getWidth() - parent.getPaddingRight();
+        }
+        decorationHandler.onDraw(c);
+        decorationHandler.reset();
+    }
+
+    /**
+     * Handles grouping and drawing of items in the same all apps sections.
+     */
+    public static class SectionDecorationHandler {
+        protected RectF mBounds = new RectF();
+        private final boolean mIsFullWidth;
+        private final float mRadius;
+        private final int mFillcolor;
+        Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+
+        public SectionDecorationHandler(Context context, boolean isFullWidth) {
+            mIsFullWidth = isFullWidth;
+            mFillcolor = context.getColor(R.color.all_apps_section_fill);
+            mRadius = Themes.getDialogCornerRadius(context);
+        }
+
+        /**
+         * Extends current bounds to include view
+         */
+        public void extendBounds(View view) {
+            if (mBounds.isEmpty()) {
+                mBounds.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+            } else {
+                mBounds.set(
+                        Math.min(mBounds.left, view.getLeft()),
+                        Math.min(mBounds.top, view.getTop()),
+                        Math.max(mBounds.right, view.getRight()),
+                        Math.max(mBounds.bottom, view.getBottom())
+                );
+            }
+        }
+
+        /**
+         * Draw bounds onto canvas
+         */
+        public void onDraw(Canvas canvas) {
+            mPaint.setColor(mFillcolor);
+            canvas.drawRoundRect(mBounds, mRadius, mRadius, mPaint);
+        }
+
+        /**
+         * Reset view bounds to empty
+         */
+        public void reset() {
+            mBounds.setEmpty();
+        }
+    }
+
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index a9b030e..0268b96 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,10 +1,24 @@
+/*
+ * Copyright (C) 2015 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.allapps;
 
 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.APPS_VIEW_ITEM_MASK;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
@@ -14,31 +28,29 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_HEADER_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_ALLAPPS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
-import android.content.Context;
 import android.util.FloatProperty;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.animation.Interpolator;
 import android.widget.EditText;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.views.ScrimView;
-import com.android.systemui.plugins.AllAppsSearchPlugin;
-import com.android.systemui.plugins.PluginListener;
 
 /**
  * Handles AllApps view transition.
@@ -51,7 +63,7 @@
  * closer to top or closer to the page indicator.
  */
 public class AllAppsTransitionController implements StateHandler<LauncherState>,
-        OnDeviceProfileChangeListener, PluginListener<AllAppsSearchPlugin> {
+        OnDeviceProfileChangeListener {
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -85,10 +97,7 @@
     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
 
     private float mScrollRangeDelta = 0;
-
-    // plugin related variables
-    private AllAppsSearchPlugin mPlugin;
-    private View mPluginContent;
+    private AllAppsInsetTransitionController mInsetController;
 
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
@@ -103,6 +112,10 @@
         return mShiftRange;
     }
 
+    public AllAppsInsetTransitionController getInsetController() {
+        return mInsetController;
+    }
+
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
         mIsVerticalLayout = dp.isVerticalBarLayout();
@@ -130,8 +143,8 @@
         float shiftCurrent = progress * mShiftRange;
 
         mAppsView.setTranslationY(shiftCurrent);
-        if (mPlugin != null) {
-            mPlugin.setProgress(progress);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            mInsetController.setProgress(progress);
         }
     }
 
@@ -201,20 +214,13 @@
         Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
         Interpolator headerFade = config.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
 
-        if (mPlugin == null) {
-            setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
-            setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
-            mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra,
-                    hasAllAppsContent, setter, headerFade, allAppsFade);
-        } else {
-            setter.setViewAlpha(mPluginContent, hasAllAppsContent ? 1 : 0, allAppsFade);
-            setter.setViewAlpha(mAppsView.getContentView(), 0, allAppsFade);
-            setter.setViewAlpha(mAppsView.getScrollBar(), 0, allAppsFade);
-        }
-        mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
 
-        setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
-                (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade);
+        setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
+        setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
+        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra,
+                hasAllAppsContent, setter, headerFade, allAppsFade);
+
+        mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
 
         // Set visibility of the container at the very beginning or end of the transition.
         setter.setViewAlpha(mAppsView, hasAnyVisibleItem ? 1 : 0,
@@ -228,8 +234,12 @@
     public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
         mAppsView = appsView;
         mScrimView = scrimView;
-        PluginManagerWrapper.INSTANCE.get(mLauncher)
-                .addPluginListener(this, AllAppsSearchPlugin.class, false);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) {
+            mInsetController = new AllAppsInsetTransitionController(mShiftRange, mAppsView);
+            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALLAPPS,
+                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        }
     }
 
     /**
@@ -252,47 +262,19 @@
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.reset(false /* animate */);
         }
-        updatePluginAnimationEnd();
-    }
-
-    @Override
-    public void onPluginConnected(AllAppsSearchPlugin plugin, Context context) {
-        mPlugin = plugin;
-        mPluginContent = mLauncher.getLayoutInflater().inflate(
-                R.layout.all_apps_content_layout, mAppsView, false);
-        mAppsView.addView(mPluginContent);
-        mPluginContent.setAlpha(0f);
-        mPlugin.setup((ViewGroup) mPluginContent, mLauncher, mShiftRange);
-    }
-
-    @Override
-    public void onPluginDisconnected(AllAppsSearchPlugin plugin) {
-        mPlugin = null;
-        mAppsView.removeView(mPluginContent);
-    }
-
-    public void onActivityDestroyed() {
-        PluginManagerWrapper.INSTANCE.get(mLauncher).removePluginListener(this);
-    }
-
-    /** Used for the plugin to signal when drag starts happens
-     * @param toAllApps*/
-    public void onDragStart(boolean toAllApps) {
-        if (mPlugin == null) return;
-
-        if (toAllApps) {
-            EditText editText = mAppsView.getSearchUiManager().setTextSearchEnabled(true);
-            mPlugin.setEditText(editText);
-        }
-        mPlugin.onDragStart(toAllApps ? 1f : 0f);
-    }
-
-    private void updatePluginAnimationEnd() {
-        if (mPlugin == null) return;
-        mPlugin.onAnimationEnd(mProgress);
-        if (Float.compare(mProgress, 1f) == 0) {
-            mAppsView.getSearchUiManager().setTextSearchEnabled(false);
-            mPlugin.setEditText(null);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) {
+            mInsetController.onAnimationEnd(mProgress);
+            if (Float.compare(mProgress, 0f) == 0) {
+                EditText editText = mAppsView.getSearchUiManager().getEditText();
+                if (editText != null) {
+                    editText.requestFocus();
+                }
+            }
+            if (Float.compare(mProgress, 1f) == 0) {
+                // Called when home gesture closes all apps container.
+                // TODO: should make the controller hide synchronously
+                mInsetController.hide();
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 06209bb..0fc04a7 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -19,8 +19,9 @@
 import android.content.Context;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.allapps.search.SearchSectionInfo;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LabelComparator;
 
@@ -82,6 +83,8 @@
         public AppInfo appInfo = null;
         // The index of this app not including sections
         public int appIndex = -1;
+        // Search section associated to result
+        public SearchSectionInfo searchSectionInfo = null;
 
         public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
                 int appIndex) {
@@ -114,6 +117,21 @@
             item.position = pos;
             return item;
         }
+
+        /**
+         * Factory method for search section title AdapterItem
+         */
+        public static AdapterItem asSearchTitle(SearchSectionInfo sectionInfo, int pos) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_CORPUS_TITLE;
+            item.position = pos;
+            item.searchSectionInfo = sectionInfo;
+            return item;
+        }
+
+        boolean isCountedForAccessibility() {
+            return viewType == AllAppsGridAdapter.VIEW_TYPE_ICON;
+        }
     }
 
     private final BaseDraggingActivity mLauncher;
@@ -122,8 +140,8 @@
     private final List<AppInfo> mApps = new ArrayList<>();
     private final AllAppsStore mAllAppsStore;
 
-    // The set of filtered apps with the current filter
-    private final List<AppInfo> mFilteredApps = new ArrayList<>();
+    // The number of results in current adapter
+    private int mAccessibilityResultsCount = 0;
     // The current set of adapter items
     private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
     // The set of sections that we allow fast-scrolling to (includes non-merged sections)
@@ -132,7 +150,7 @@
     private final boolean mIsWork;
 
     // The of ordered component names as a result of a search query
-    private ArrayList<ComponentKey> mSearchResults;
+    private ArrayList<AdapterItem> mSearchResults;
     private AllAppsGridAdapter mAdapter;
     private AppInfoComparator mAppNameComparator;
     private final int mNumAppsPerRow;
@@ -192,7 +210,7 @@
      * Returns the number of applications in this list.
      */
     public int getNumFilteredApps() {
-        return mFilteredApps.size();
+        return mAccessibilityResultsCount;
     }
 
     /**
@@ -206,14 +224,14 @@
      * Returns whether there are no filtered results.
      */
     public boolean hasNoFilteredResults() {
-        return (mSearchResults != null) && mFilteredApps.isEmpty();
+        return (mSearchResults != null) && mAccessibilityResultsCount == 0;
     }
 
     /**
-     * Sets the sorted list of filtered components.
+     * Sets results list for search
      */
-    public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
-        if (mSearchResults != f) {
+    public boolean setSearchResults(ArrayList<AdapterItem> f) {
+        if (f == null || mSearchResults != f) {
             boolean same = mSearchResults != null && mSearchResults.equals(f);
             mSearchResults = f;
             onAppsUpdated();
@@ -292,41 +310,73 @@
         int appIndex = 0;
 
         // Prepare to update the list of sections, filtered apps, etc.
-        mFilteredApps.clear();
+        mAccessibilityResultsCount = 0;
         mFastScrollerSections.clear();
         mAdapterItems.clear();
 
+        SearchSectionInfo appSection = new SearchSectionInfo();
+        appSection.setDecorationHandler(
+                new AllAppsSectionDecorator.SectionDecorationHandler(mLauncher, true));
+
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
-        for (AppInfo info : getFiltersAppInfos()) {
-            String sectionName = info.sectionName;
 
-            // Create a new section if the section names do not match
-            if (!sectionName.equals(lastSectionName)) {
-                lastSectionName = sectionName;
-                lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
-                mFastScrollerSections.add(lastFastScrollerSectionInfo);
-            }
+        if (!hasFilter()) {
+            mAccessibilityResultsCount = mApps.size();
+            appSection.setPosStart(position);
+            for (AppInfo info : mApps) {
+                String sectionName = info.sectionName;
 
-            // Create an app item
-            AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
-            if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
-                lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+                // Create a new section if the section names do not match
+                if (!sectionName.equals(lastSectionName)) {
+                    lastSectionName = sectionName;
+                    lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
+                    mFastScrollerSections.add(lastFastScrollerSectionInfo);
+                }
+
+                // Create an app item
+                AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
+                if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
+                    lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+                }
+                if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+                    appItem.searchSectionInfo = appSection;
+                }
+                mAdapterItems.add(appItem);
             }
-            mAdapterItems.add(appItem);
-            mFilteredApps.add(info);
+            appSection.setPosEnd(mApps.isEmpty() ? appSection.getPosStart() : position - 1);
+        } else {
+            List<AppInfo> appInfos = new ArrayList<>();
+            SearchSectionInfo lastSection = null;
+            for (int i = 0; i < mSearchResults.size(); i++) {
+                AdapterItem adapterItem = mSearchResults.get(i);
+                adapterItem.position = i;
+                mAdapterItems.add(adapterItem);
+                if (adapterItem.searchSectionInfo != lastSection) {
+                    adapterItem.searchSectionInfo.setPosStart(i);
+                    if (lastSection != null) {
+                        lastSection.setPosEnd(i - 1);
+                    }
+                    lastSection = adapterItem.searchSectionInfo;
+                }
+                if (AllAppsGridAdapter.isIconViewType(adapterItem.viewType)) {
+                    appInfos.add(adapterItem.appInfo);
+                }
+                if (adapterItem.isCountedForAccessibility()) {
+                    mAccessibilityResultsCount++;
+                }
+            }
+            if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+                // Append the search market item
+                if (hasNoFilteredResults()) {
+                    mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+                } else {
+                    mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
+                }
+                mAdapterItems.add(AdapterItem.asMarketSearch(position++));
+
+            }
         }
-
-        if (hasFilter()) {
-            // Append the search market item
-            if (hasNoFilteredResults()) {
-                mAdapterItems.add(AdapterItem.asEmptySearch(position++));
-            } else {
-                mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
-            }
-            mAdapterItems.add(AdapterItem.asMarketSearch(position++));
-        }
-
         if (mNumAppsPerRow != 0) {
             // Update the number of rows in the adapter after we do all the merging (otherwise, we
             // would have to shift the values again)
@@ -381,18 +431,4 @@
             }
         }
     }
-
-    private List<AppInfo> getFiltersAppInfos() {
-        if (mSearchResults == null) {
-            return mApps;
-        }
-        ArrayList<AppInfo> result = new ArrayList<>();
-        for (ComponentKey key : mSearchResults) {
-            AppInfo match = mAllAppsStore.getApp(key);
-            if (match != null) {
-                result.add(match);
-            }
-        }
-        return result;
-    }
 }
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index 7d5363f..aa056a0 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -66,12 +66,8 @@
     }
 
     /**
-     * Called to control how the search UI result should be handled.
-     *
-     * @param isEnabled when {@code true}, the search is all handled inside AOSP
-     *                  and is not overlayable.
-     * @return the searchbox edit text object
+     * @return the edit text object
      */
     @Nullable
-    EditText setTextSearchEnabled(boolean isEnabled);
+    EditText getEditText();
 }
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index df1cd26..db94e8b 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -28,7 +28,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
@@ -50,6 +50,7 @@
     public void setVisibility(int visibility) {
         mInput.setVisibility(visibility);
     }
+
     /**
      * Sets the references to the apps model and the search result callback.
      */
@@ -164,9 +165,9 @@
         /**
          * Called when the search is complete.
          *
-         * @param apps sorted list of matching components or null if in case of failure.
+         * @param items sorted list of search result adapter items.
          */
-        void onSearchResult(String query, ArrayList<ComponentKey> apps);
+        void onSearchResult(String query, ArrayList<AlphabeticalAppsList.AdapterItem> items);
 
         /**
          * Called when the search results should be cleared.
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 356c52c..16a1efd 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -38,13 +38,13 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Insettable;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.util.ComponentKey;
 
 import java.util.ArrayList;
 
@@ -135,7 +135,8 @@
         mApps = appsView.getApps();
         mAppsView = appsView;
         mSearchBarController.initialize(
-                new DefaultAppSearchAlgorithm(mApps.getApps()), this, mLauncher, this);
+                new DefaultAppSearchAlgorithm(LauncherAppState.getInstance(mLauncher)), this,
+                mLauncher, this);
     }
 
     @Override
@@ -168,9 +169,9 @@
     }
 
     @Override
-    public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
-        if (apps != null) {
-            mApps.setOrderedFilter(apps);
+    public void onSearchResult(String query, ArrayList<AlphabeticalAppsList.AdapterItem> items) {
+        if (items != null) {
+            mApps.setSearchResults(items);
             notifyResultChanged();
             mAppsView.setLastSearchQuery(query);
         }
@@ -178,7 +179,7 @@
 
     @Override
     public void clearSearchResult() {
-        if (mApps.setOrderedFilter(null)) {
+        if (mApps.setSearchResults(null)) {
             notifyResultChanged();
         }
 
@@ -216,7 +217,7 @@
     }
 
     @Override
-    public EditText setTextSearchEnabled(boolean isEnabled) {
+    public EditText getEditText() {
         return this;
     }
 }
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
new file mode 100644
index 0000000..fb78651
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps.search;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsSectionDecorator.SectionDecorationHandler;
+import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.data.AppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A device search section for handling app searches
+ */
+public class AppsSearchPipeline implements SearchPipeline {
+
+    private static final int MAX_RESULTS_COUNT = 5;
+
+    private static final int SECTION_TYPE_HEADER = 0;
+    private static final int SECTION_TYPE_APPS = 1;
+
+    private final SearchSectionInfo[] mSearchSectionInfos;
+    private final LauncherAppState mLauncherAppState;
+
+
+    public AppsSearchPipeline(LauncherAppState launcherAppState) {
+        mLauncherAppState = launcherAppState;
+        mSearchSectionInfos = new SearchSectionInfo[]{
+                new SearchSectionInfo(R.string.search_corpus_apps),
+                new SearchSectionInfo()
+        };
+        mSearchSectionInfos[SECTION_TYPE_APPS].setDecorationHandler(
+                new SectionDecorationHandler(launcherAppState.getContext(), true));
+    }
+
+    @Override
+    @WorkerThread
+    public void performSearch(String query, Consumer<ArrayList<AdapterItem>> callback) {
+        mLauncherAppState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
+            @Override
+            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                callback.accept(getAdapterItems(getTitleMatchResult(apps.data, query)));
+            }
+        });
+    }
+
+    /**
+     * Filters {@link AppInfo}s matching specified query
+     */
+    public static ArrayList<AppInfo> getTitleMatchResult(List<AppInfo> apps, String query) {
+        // Do an intersection of the words in the query and each title, and filter out all the
+        // apps that don't match all of the words in the query.
+        final String queryTextLower = query.toLowerCase();
+        final ArrayList<AppInfo> result = new ArrayList<>();
+        DefaultAppSearchAlgorithm.StringMatcher matcher =
+                DefaultAppSearchAlgorithm.StringMatcher.getInstance();
+        for (AppInfo info : apps) {
+            if (DefaultAppSearchAlgorithm.matches(info, queryTextLower, matcher)) {
+                result.add(info);
+            }
+        }
+        return result;
+    }
+
+    private ArrayList<AdapterItem> getAdapterItems(List<AppInfo> matchingApps) {
+        ArrayList<AdapterItem> items = new ArrayList<>();
+        if (matchingApps.isEmpty()) {
+            return items;
+        }
+        items.add(AdapterItem.asSearchTitle(mSearchSectionInfos[SECTION_TYPE_HEADER], 0));
+        int existingItems = items.size();
+        int searchResultsCount = Math.min(matchingApps.size(), MAX_RESULTS_COUNT);
+        for (int i = 0; i < searchResultsCount; i++) {
+            AdapterItem appItem = AdapterItem.asApp(i + existingItems, "", matchingApps.get(i), i);
+            appItem.searchSectionInfo = mSearchSectionInfos[SECTION_TYPE_APPS];
+            items.add(appItem);
+        }
+
+        return items;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index f72a988..db10311 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -17,24 +17,22 @@
 
 import android.os.Handler;
 
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.util.ComponentKey;
 
 import java.text.Collator;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * The default search implementation.
  */
 public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
 
-    private final List<AppInfo> mApps;
     protected final Handler mResultHandler;
+    private final AppsSearchPipeline mAppsSearchPipeline;
 
-    public DefaultAppSearchAlgorithm(List<AppInfo> apps) {
-        mApps = apps;
+    public DefaultAppSearchAlgorithm(LauncherAppState launcherAppState) {
         mResultHandler = new Handler();
+        mAppsSearchPipeline = new AppsSearchPipeline(launcherAppState);
     }
 
     @Override
@@ -47,28 +45,8 @@
     @Override
     public void doSearch(final String query,
             final AllAppsSearchBarController.Callbacks callback) {
-        final ArrayList<ComponentKey> result = getTitleMatchResult(query);
-        mResultHandler.post(new Runnable() {
-
-            @Override
-            public void run() {
-                callback.onSearchResult(query, result);
-            }
-        });
-    }
-
-    private ArrayList<ComponentKey> getTitleMatchResult(String query) {
-        // Do an intersection of the words in the query and each title, and filter out all the
-        // apps that don't match all of the words in the query.
-        final String queryTextLower = query.toLowerCase();
-        final ArrayList<ComponentKey> result = new ArrayList<>();
-        StringMatcher matcher = StringMatcher.getInstance();
-        for (AppInfo info : mApps) {
-            if (matches(info, queryTextLower, matcher)) {
-                result.add(info.toComponentKey());
-            }
-        }
-        return result;
+        mAppsSearchPipeline.performSearch(query,
+                results -> mResultHandler.post(() -> callback.onSearchResult(query, results)));
     }
 
     public static boolean matches(AppInfo info, String query, StringMatcher matcher) {
diff --git a/src/com/android/launcher3/allapps/search/SearchPipeline.java b/src/com/android/launcher3/allapps/search/SearchPipeline.java
new file mode 100644
index 0000000..3216740
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchPipeline.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps.search;
+
+import com.android.launcher3.allapps.AlphabeticalAppsList;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * An interface for handling search within pipeline
+ */
+public interface SearchPipeline {
+
+    /**
+     * Perform query
+     */
+    void performSearch(String query, Consumer<ArrayList<AlphabeticalAppsList.AdapterItem>> cb);
+}
diff --git a/src/com/android/launcher3/allapps/search/SearchSectionInfo.java b/src/com/android/launcher3/allapps/search/SearchSectionInfo.java
new file mode 100644
index 0000000..dee0ffd
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchSectionInfo.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps.search;
+
+import android.content.Context;
+
+import com.android.launcher3.allapps.AllAppsSectionDecorator.SectionDecorationHandler;
+
+/**
+ * Info class for a search section
+ */
+public class SearchSectionInfo {
+
+    private final int mTitleResId;
+    private SectionDecorationHandler mDecorationHandler;
+
+    public int getPosStart() {
+        return mPosStart;
+    }
+
+    public void setPosStart(int posStart) {
+        mPosStart = posStart;
+    }
+
+    public int getPosEnd() {
+        return mPosEnd;
+    }
+
+    public void setPosEnd(int posEnd) {
+        mPosEnd = posEnd;
+    }
+
+    private int mPosStart;
+    private int mPosEnd;
+
+    public SearchSectionInfo() {
+        this(-1);
+    }
+
+    public SearchSectionInfo(int titleResId) {
+        mTitleResId = titleResId;
+    }
+
+    public void setDecorationHandler(SectionDecorationHandler sectionDecorationHandler) {
+        mDecorationHandler = sectionDecorationHandler;
+    }
+
+
+    public SectionDecorationHandler getDecorationHandler() {
+        return mDecorationHandler;
+    }
+
+    /**
+     * Returns the section's title
+     */
+    public String getTitle(Context context) {
+        if (mTitleResId == -1) {
+            return "";
+        }
+        return context.getString(mTitleResId);
+    }
+}
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
index 69716ea..eabd283 100644
--- a/src/com/android/launcher3/anim/AlphaUpdateListener.java
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -46,8 +46,7 @@
     }
 
     @Override
-    public void onAnimationStart(Animator animation) {
-        super.onAnimationStart(animation);
+    public void onAnimationStart(Animator arg0) {
         // We want the views to be visible for animation, so fade-in/out is visible
         mView.setVisibility(View.VISIBLE);
     }
diff --git a/src/com/android/launcher3/anim/AnimationSuccessListener.java b/src/com/android/launcher3/anim/AnimationSuccessListener.java
index b83417c..9905e81 100644
--- a/src/com/android/launcher3/anim/AnimationSuccessListener.java
+++ b/src/com/android/launcher3/anim/AnimationSuccessListener.java
@@ -19,8 +19,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 
-import androidx.annotation.CallSuper;
-
 /**
  * Extension of {@link AnimatorListenerAdapter} for listening for non-cancelled animations
  */
@@ -29,12 +27,6 @@
     protected boolean mCancelled = false;
 
     @Override
-    @CallSuper
-    public void onAnimationStart(Animator animation) {
-        mCancelled = false;
-    }
-
-    @Override
     public void onAnimationCancel(Animator animation) {
         mCancelled = true;
     }
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 31e0418..ea0ff8b 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -335,7 +335,6 @@
 
         @Override
         public void onAnimationStart(Animator animation) {
-            super.onAnimationStart(animation);
             mCancelled = false;
             mDispatched = false;
         }
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 860cceb..6ad43ea 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -198,6 +198,7 @@
         public OvershootParams(float startProgress, float overshootPastProgress,
                 float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) {
             velocityPxPerMs = Math.abs(velocityPxPerMs);
+            overshootPastProgress = Math.max(overshootPastProgress, startProgress);
             start = startProgress;
             int startPx = (int) (start * totalDistancePx);
             // Overshoot by about half a frame.
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index 4195933..5362575 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -149,7 +149,7 @@
             mProgressAnimator = null;
         }
         if (mAnimHolders.isEmpty()) {
-            // Add a dummy animation to that the duration is respected
+            // Add a placeholder animation to that the duration is respected
             add(ValueAnimator.ofFloat(0, 1).setDuration(mDuration));
         }
         return mAnim;
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 1d32d1d..30c3417 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -70,7 +70,8 @@
         final Bundle parcel = new Bundle();
         parcel.putInt(TestProtocol.STATE_FIELD, stateOrdinal);
 
-        sendEventToTest(accessibilityManager, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
+        sendEventToTest(
+                accessibilityManager, context, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
         Log.d(TestProtocol.PERMANENT_DIAG_TAG, "sendStateEventToTest: " + stateOrdinal);
     }
 
@@ -78,22 +79,24 @@
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
-        sendEventToTest(accessibilityManager, TestProtocol.SCROLL_FINISHED_MESSAGE, null);
+        sendEventToTest(accessibilityManager, context, TestProtocol.SCROLL_FINISHED_MESSAGE, null);
     }
 
     public static void sendPauseDetectedEventToTest(Context context) {
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
-        sendEventToTest(accessibilityManager, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
+        sendEventToTest(accessibilityManager, context, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
     }
 
     private static void sendEventToTest(
-            AccessibilityManager accessibilityManager, String eventTag, Bundle data) {
+            AccessibilityManager accessibilityManager,
+            Context context, String eventTag, Bundle data) {
         final AccessibilityEvent e = AccessibilityEvent.obtain(
                 AccessibilityEvent.TYPE_ANNOUNCEMENT);
         e.setClassName(eventTag);
         e.setParcelableData(data);
+        e.setPackageName(context.getApplicationContext().getPackageName());
         accessibilityManager.sendAccessibilityEvent(e);
     }
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 78e8b82..0ab74af 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -181,6 +181,10 @@
     public static final BooleanFlag USER_EVENT_DISPATCHER = new DeviceFlag(
             "USER_EVENT_DISPATCHER", true, "User event dispatcher collects logs.");
 
+    public static final BooleanFlag ENABLE_MINIMAL_DEVICE = new DeviceFlag(
+            "ENABLE_MINIMAL_DEVICE", false,
+            "Allow user to toggle minimal device mode in launcher.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 03028d3..ef666f0 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -463,10 +463,10 @@
     }
 
     public void forceTouchMove() {
-        int[] dummyCoordinates = mCoordinatesTemp;
-        DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, dummyCoordinates);
-        mDragObject.x = dummyCoordinates[0];
-        mDragObject.y = dummyCoordinates[1];
+        int[] placeholderCoordinates = mCoordinatesTemp;
+        DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, placeholderCoordinates);
+        mDragObject.x = placeholderCoordinates[0];
+        mDragObject.y = placeholderCoordinates[1];
         checkTouchMove(dropTarget);
     }
 
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index de0fa1a..86b93d0 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -187,9 +187,6 @@
      */
     @TargetApi(Build.VERSION_CODES.O)
     public void setItemInfo(final ItemInfo info) {
-        if (!Utilities.ATLEAST_OREO) {
-            return;
-        }
         if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
                 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
                 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index 9982b39..f543e47 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -49,14 +49,14 @@
 
     // Class name used in the target component, such that it will never represent an
     // actual existing class.
-    private static final String DUMMY_COMPONENT_CLASS = "pinned-shortcut";
+    private static final String STUB_COMPONENT_CLASS = "pinned-shortcut";
 
     private final PinItemRequest mRequest;
     private final ShortcutInfo mInfo;
     private final Context mContext;
 
     public PinShortcutRequestActivityInfo(PinItemRequest request, Context context) {
-        super(new ComponentName(request.getShortcutInfo().getPackage(), DUMMY_COMPONENT_CLASS),
+        super(new ComponentName(request.getShortcutInfo().getPackage(), STUB_COMPONENT_CLASS),
                 request.getShortcutInfo().getUserHandle());
         mRequest = request;
         mInfo = request.getShortcutInfo();
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index de2b5da..b91d1c3 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -1135,6 +1135,9 @@
      * Rearranges the children based on their rank.
      */
     public void rearrangeChildren() {
+        if (!mContent.areViewsBound()) {
+            return;
+        }
         mContent.arrangeChildren(getIconsInReadingOrder());
         mItemsInvalidated = true;
     }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 75275b2..32d061c 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -129,6 +129,8 @@
     private float mDotScale;
     private Animator mDotScaleAnim;
 
+    private Rect mTouchArea = new Rect();
+
     private final PointF mTranslationForReorderBounce = new PointF(0, 0);
     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
     private float mScaleForReorderBounce = 1f;
@@ -711,6 +713,11 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN
+                && shouldIgnoreTouchDown(event.getX(), event.getY())) {
+            return false;
+        }
+
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
         super.onTouchEvent(event);
@@ -719,6 +726,15 @@
         return true;
     }
 
+    /**
+     * Returns true if the touch down at the provided position be ignored
+     */
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
+                getHeight() - getPaddingBottom());
+        return !mTouchArea.contains((int) x, (int) y);
+    }
+
     @Override
     public void cancelLongPress() {
         super.cancelLongPress();
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
index 4369385..b208a40 100644
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ b/src/com/android/launcher3/graphics/IconShape.java
@@ -43,8 +43,9 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.IconNormalizer;
@@ -59,8 +60,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.annotation.Nullable;
-
 /**
  * Abstract representation of the shape of an icon shape
  */
@@ -381,9 +380,6 @@
      * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
      */
     public static void init(Context context) {
-        if (!Utilities.ATLEAST_OREO) {
-            return;
-        }
         pickBestShape(context);
     }
 
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index a424f84..d8eb838 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -74,6 +74,7 @@
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.AppInfo;
@@ -579,7 +580,7 @@
         private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
 
         WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
-            super(app, null, new BgDataModel(), null);
+            super(app, null, new BgDataModel(), new ModelDelegate(), null);
         }
 
         @Override
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index acf4482..0374009 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -30,11 +30,16 @@
 import com.android.launcher3.userevent.LauncherLogProto;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.util.List;
+
 /**
  * Handles the user event logging in R+.
+ *
+ * <pre>
  * All of the event ids are defined here.
- * Most of the methods are dummy methods for Launcher3
+ * Most of the methods are placeholder methods for Launcher3
  * Actual call happens only for Launcher variant that implements QuickStep.
+ * </pre>
  */
 public class StatsLogManager implements ResourceBasedOverride {
 
@@ -46,8 +51,8 @@
     public static final int LAUNCHER_STATE_UNCHANGED = 5;
 
     /**
-     * Returns proper launcher state enum for {@link StatsLogManager}
-     * (to be removed during UserEventDispatcher cleanup)
+     * Returns proper launcher state enum for {@link StatsLogManager}(to be removed during
+     * UserEventDispatcher cleanup)
      */
     public static int containerTypeToAtomState(int containerType) {
         switch (containerType) {
@@ -64,9 +69,8 @@
     }
 
     /**
-     * Returns event enum based on the two {@link ContainerType} transition information when
-     * swipe gesture happens.
-     * (to be removed during UserEventDispatcher cleanup)
+     * Returns event enum based on the two {@link ContainerType} transition information when swipe
+     * gesture happens(to be removed during UserEventDispatcher cleanup).
      */
     public static EventEnum getLauncherAtomEvent(int startContainerType,
             int targetContainerType, EventEnum fallbackEvent) {
@@ -270,7 +274,46 @@
         LAUNCHER_SELECT_MODE_CLOSE(583),
 
         @UiEvent(doc = "User tapped on the highlight items in select mode")
-        LAUNCHER_SELECT_MODE_ITEM(584);
+        LAUNCHER_SELECT_MODE_ITEM(584),
+
+        @UiEvent(doc = "Notification dot on app icon enabled.")
+        LAUNCHER_NOTIFICATION_DOT_ENABLED(611),
+
+        @UiEvent(doc = "Notification dot on app icon disabled.")
+        LAUNCHER_NOTIFICATION_DOT_DISABLED(612),
+
+        @UiEvent(doc = "For new apps, add app icons to home screen enabled.")
+        LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED(613),
+
+        @UiEvent(doc = "For new apps, add app icons to home screen disabled.")
+        LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_DISABLED(614),
+
+        @UiEvent(doc = "Home screen rotation is enabled when phone is rotated.")
+        LAUNCHER_HOME_SCREEN_ROTATION_ENABLED(615),
+
+        @UiEvent(doc = "Home screen rotation is disabled when phone is rotated.")
+        LAUNCHER_HOME_SCREEN_ROTATION_DISABLED(616),
+
+        @UiEvent(doc = "Suggestions in all apps list enabled.")
+        LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED(619),
+
+        @UiEvent(doc = "Suggestions in all apps list disabled.")
+        LAUNCHER_ALL_APPS_SUGGESTIONS_DISABLED(620),
+
+        @UiEvent(doc = "Suggestions on home screen is enabled.")
+        LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED(621),
+
+        @UiEvent(doc = "Suggestions on home screen is disabled.")
+        LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED(622),
+
+        @UiEvent(doc = "System navigation is 3 button mode.")
+        LAUNCHER_NAVIGATION_MODE_3_BUTTON(623),
+
+        @UiEvent(doc = "System navigation mode is 2 button mode.")
+        LAUNCHER_NAVIGATION_MODE_2_BUTTON(624),
+
+        @UiEvent(doc = "System navigation mode is 0 button mode/gesture navigation mode .")
+        LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON(625);
 
         // ADD MORE
 
@@ -401,8 +444,8 @@
     }
 
     /**
-     * Logs snapshot, or impression of the current workspace.
+     * Logs impression of the current workspace with additional launcher events.
      */
-    public void logSnapshot() {
+    public void logSnapshot(List<EventEnum> additionalEvents) {
     }
 }
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index e094cab..31a81b8 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -147,7 +147,7 @@
     }
 
     /**
-     * Dummy method.
+     * Placeholder method.
      */
     public void logActionTip(int actionType, int viewType) {
     }
diff --git a/src/com/android/launcher3/model/AppLaunchTracker.java b/src/com/android/launcher3/model/AppLaunchTracker.java
deleted file mode 100644
index a93c0dd..0000000
--- a/src/com/android/launcher3/model/AppLaunchTracker.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.model;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Callback for receiving various app launch events
- */
-public class AppLaunchTracker implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<AppLaunchTracker> INSTANCE =
-            forOverride(AppLaunchTracker.class, R.string.app_launch_tracker_class);
-
-    public void onReturnedToHome() { }
-}
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 9013cba..d1e5017 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -22,7 +22,9 @@
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -30,7 +32,10 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 /**
  * Extension of {@link ModelUpdateTask} with some utility methods
@@ -88,11 +93,27 @@
         return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */);
     }
 
-
-    public void bindUpdatedWorkspaceItems(final ArrayList<WorkspaceItemInfo> updatedShortcuts) {
-        if (!updatedShortcuts.isEmpty()) {
-            scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(updatedShortcuts));
+    public void bindUpdatedWorkspaceItems(List<WorkspaceItemInfo> allUpdates) {
+        // Bind workspace items
+        List<WorkspaceItemInfo> workspaceUpdates = allUpdates.stream()
+                .filter(info -> info.id != ItemInfo.NO_ID)
+                .collect(Collectors.toList());
+        if (!workspaceUpdates.isEmpty()) {
+            scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(workspaceUpdates));
         }
+
+        // Bind extra items if any
+        allUpdates.stream()
+                .mapToInt(info -> info.container)
+                .distinct()
+                .mapToObj(mDataModel.extraItems::get)
+                .filter(Objects::nonNull)
+                .forEach(this::bindExtraContainerItems);
+    }
+
+    public void bindExtraContainerItems(FixedContainerItems item) {
+        FixedContainerItems copy = item.clone();
+        scheduleCallbackTask(c -> c.bindExtraContainerItems(copy));
     }
 
     public void bindDeepShortcuts(BgDataModel dataModel) {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 7524920..fd8520d 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -15,19 +15,25 @@
  */
 package com.android.launcher3.model;
 
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY;
+
 import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
 import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
 
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
-import android.util.MutableInt;
 
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
@@ -36,8 +42,10 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.PromiseAppInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -50,14 +58,16 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.function.BiConsumer;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * All the data stored in-memory and managed by the LauncherModel
@@ -89,9 +99,9 @@
     public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
 
     /**
-     * Map of ShortcutKey to the number of times it is pinned.
+     * Extra container based items
      */
-    public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
+    public final IntSparseArrayMap<FixedContainerItems> extraItems = new IntSparseArrayMap<>();
 
     /**
      * List of all cached predicted items visible on home screen
@@ -99,13 +109,6 @@
     public final ArrayList<AppInfo> cachedPredictedItems = new ArrayList<>();
 
     /**
-     * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
-     * @see Callbacks#FLAG_QUIET_MODE_ENABLED
-     * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
-     */
-    public int flags;
-
-    /**
      * Maps all launcher activities to counts of their shortcuts.
      */
     public final HashMap<ComponentKey, Integer> deepShortcutMap = new HashMap<>();
@@ -128,8 +131,8 @@
         appWidgets.clear();
         folders.clear();
         itemsIdMap.clear();
-        pinnedShortcutCounts.clear();
         deepShortcutMap.clear();
+        extraItems.clear();
     }
 
     /**
@@ -182,6 +185,7 @@
     }
 
     public synchronized void removeItem(Context context, Iterable<? extends ItemInfo> items) {
+        ArraySet<UserHandle> updatedDeepShortcuts = new ArraySet<>();
         for (ItemInfo item : items) {
             switch (item.itemType) {
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
@@ -200,14 +204,7 @@
                     workspaceItems.remove(item);
                     break;
                 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
-                    // Decrement pinned shortcut count
-                    ShortcutKey pinnedShortcut = ShortcutKey.fromItemInfo(item);
-                    MutableInt count = pinnedShortcutCounts.get(pinnedShortcut);
-                    if ((count == null || --count.value == 0)
-                            && !InstallShortcutReceiver.getPendingShortcuts(context)
-                                .contains(pinnedShortcut)) {
-                        unpinShortcut(context, pinnedShortcut);
-                    }
+                    updatedDeepShortcuts.add(item.user);
                     // Fall through.
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
@@ -221,6 +218,7 @@
             }
             itemsIdMap.remove(item.id);
         }
+        updatedDeepShortcuts.forEach(user -> updateShortcutPinnedState(context, user));
     }
 
     public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
@@ -230,23 +228,7 @@
                 folders.put(item.id, (FolderInfo) item);
                 workspaceItems.add(item);
                 break;
-            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
-                // Increment the count for the given shortcut
-                ShortcutKey pinnedShortcut = ShortcutKey.fromItemInfo(item);
-                MutableInt count = pinnedShortcutCounts.get(pinnedShortcut);
-                if (count == null) {
-                    count = new MutableInt(1);
-                    pinnedShortcutCounts.put(pinnedShortcut, count);
-                } else {
-                    count.value++;
-                }
-
-                // Since this is a new item, pin the shortcut in the system server.
-                if (newItem && count.value == 1) {
-                    updatePinnedShortcuts(context, pinnedShortcut, List::add);
-                }
-                // Fall through
-            }
+            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
@@ -271,36 +253,87 @@
                 appWidgets.add((LauncherAppWidgetInfo) item);
                 break;
         }
+        if (newItem && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+            updateShortcutPinnedState(context, item.user);
+        }
     }
 
     /**
-     * Removes the given shortcut from the current list of pinned shortcuts.
-     * (Runs on background thread)
+     * Updates the deep shortucts state in system to match out internal model, pinning any missing
+     * shortcuts and unpinning any extra shortcuts.
      */
-    public void unpinShortcut(Context context, ShortcutKey key) {
-        updatePinnedShortcuts(context, key, List::remove);
+    public void updateShortcutPinnedState(Context context) {
+        for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
+            updateShortcutPinnedState(context, user);
+        }
     }
 
-    private void updatePinnedShortcuts(Context context, ShortcutKey key,
-            BiConsumer<List<String>, String> idOp) {
+    /**
+     * Updates the deep shortucts state in system to match out internal model, pinning any missing
+     * shortcuts and unpinning any extra shortcuts.
+     */
+    public synchronized void updateShortcutPinnedState(Context context, UserHandle user) {
         if (GO_DISABLE_WIDGETS) {
             return;
         }
-        String packageName = key.componentName.getPackageName();
-        String id = key.getId();
-        UserHandle user = key.user;
-        List<String> pinnedIds = new ShortcutRequest(context, user)
-                .forPackage(packageName)
-                .query(PINNED)
-                .stream()
-                .map(ShortcutInfo::getId)
-                .collect(Collectors.toCollection(ArrayList::new));
-        idOp.accept(pinnedIds, id);
-        try {
-            context.getSystemService(LauncherApps.class).pinShortcuts(packageName, pinnedIds, user);
-        } catch (SecurityException | IllegalStateException e) {
-            Log.w(TAG, "Failed to pin shortcut", e);
+
+        // Collect all system shortcuts
+        QueryResult result = new ShortcutRequest(context, user)
+                .query(PINNED | FLAG_GET_KEY_FIELDS_ONLY);
+        if (!result.wasSuccess()) {
+            return;
         }
+        // Map of packageName to shortcutIds that are currently in the system
+        Map<String, Set<String>> systemMap = result.stream()
+                .collect(groupingBy(ShortcutInfo::getPackage,
+                        mapping(ShortcutInfo::getId, Collectors.toSet())));
+
+        // Collect all model shortcuts
+        Stream.Builder<WorkspaceItemInfo> itemStream = Stream.builder();
+        forAllWorkspaceItemInfos(user, itemStream::accept);
+        // Map of packageName to shortcutIds that are currently in our model
+        Map<String, Set<String>> modelMap = Stream.concat(
+                    // Model shortcuts
+                    itemStream.build()
+                        .filter(wi -> wi.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                        .map(ShortcutKey::fromItemInfo),
+                    // Pending shortcuts
+                    InstallShortcutReceiver.getPendingShortcuts(context)
+                        .stream().filter(si -> si.user.equals(user)))
+                .collect(groupingBy(ShortcutKey::getPackageName,
+                        mapping(ShortcutKey::getId, Collectors.toSet())));
+
+        // Check for diff
+        for (Map.Entry<String, Set<String>> entry : modelMap.entrySet()) {
+            Set<String> modelShortcuts = entry.getValue();
+            Set<String> systemShortcuts = systemMap.remove(entry.getKey());
+            if (systemShortcuts == null) {
+                systemShortcuts = Collections.emptySet();
+            }
+
+            // Do not use .equals as it can vary based on the type of set
+            if (systemShortcuts.size() != modelShortcuts.size()
+                    || !systemShortcuts.containsAll(modelShortcuts)) {
+                // Update system state for this package
+                try {
+                    context.getSystemService(LauncherApps.class).pinShortcuts(
+                            entry.getKey(), new ArrayList<>(modelShortcuts), user);
+                } catch (SecurityException | IllegalStateException e) {
+                    Log.w(TAG, "Failed to pin shortcut", e);
+                }
+            }
+        }
+
+        // If there are any extra pinned shortcuts, remove them
+        systemMap.keySet().forEach(packageName -> {
+            // Update system state
+            try {
+                context.getSystemService(LauncherApps.class).pinShortcuts(
+                        packageName, Collections.emptyList(), user);
+            } catch (SecurityException | IllegalStateException e) {
+                Log.w(TAG, "Failed to unpin shortcut", e);
+            }
+        });
     }
 
     /**
@@ -360,8 +393,40 @@
                 op.accept((WorkspaceItemInfo) info);
             }
         }
+
+        for (int i = extraItems.size() - 1; i >= 0; i--) {
+            for (ItemInfo info : extraItems.valueAt(i).items) {
+                if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
+                    op.accept((WorkspaceItemInfo) info);
+                }
+            }
+        }
     }
 
+    /**
+     * An object containing items corresponding to a fixed container
+     */
+    public static class FixedContainerItems {
+
+        public final int containerId;
+        public final List<ItemInfo> items;
+
+        public FixedContainerItems(int containerId) {
+            this(containerId, new ArrayList<>());
+        }
+
+        public FixedContainerItems(int containerId, List<ItemInfo> items) {
+            this.containerId = containerId;
+            this.items = items;
+        }
+
+        @Override
+        public FixedContainerItems clone() {
+            return new FixedContainerItems(containerId, new ArrayList<>(items));
+        }
+    }
+
+
     public interface Callbacks {
         // If the launcher has permission to access deep shortcuts.
         int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
@@ -384,7 +449,7 @@
         void bindAppsAdded(IntArray newScreens,
                 ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
         void bindPromiseAppProgressUpdated(PromiseAppInfo app);
-        void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated);
+        void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated);
         void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
         void bindRestoreItemsChange(HashSet<ItemInfo> updates);
         void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
@@ -393,6 +458,11 @@
         void executeOnNextDraw(ViewOnDrawExecutor executor);
         void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
 
+        /**
+         * Binds extra item provided any external source
+         */
+        default void bindExtraContainerItems(FixedContainerItems item) { }
+
         void bindAllApplications(AppInfo[] apps, int flags);
 
         /**
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 4a64522..b067909 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -46,12 +46,10 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
-import android.util.MutableInt;
 import android.util.TimingLogger;
 
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
@@ -94,7 +92,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CancellationException;
@@ -112,6 +109,7 @@
     protected final LauncherAppState mApp;
     private final AllAppsList mBgAllAppsList;
     protected final BgDataModel mBgDataModel;
+    private final ModelDelegate mModelDelegate;
 
     private FirstScreenBroadcast mFirstScreenBroadcast;
 
@@ -131,10 +129,11 @@
     private boolean mStopped;
 
     public LoaderTask(LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel dataModel,
-            LoaderResults results) {
+            ModelDelegate modelDelegate, LoaderResults results) {
         mApp = app;
         mBgAllAppsList = bgAllAppsList;
         mBgDataModel = dataModel;
+        mModelDelegate = modelDelegate;
         mResults = results;
 
         mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
@@ -770,6 +769,9 @@
                 IOUtils.closeSilently(c);
             }
 
+            // Load delegate items
+            mModelDelegate.loadItems();
+
             // Break early if we've stopped loading
             if (mStopped) {
                 mBgDataModel.clear();
@@ -794,17 +796,8 @@
                         LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
             }
 
-            // Unpin shortcuts that don't exist on the workspace.
-            HashSet<ShortcutKey> pendingShortcuts =
-                    InstallShortcutReceiver.getPendingShortcuts(context);
-            for (ShortcutKey key : shortcutKeyToPinnedShortcuts.keySet()) {
-                MutableInt numTimesPinned = mBgDataModel.pinnedShortcutCounts.get(key);
-                if ((numTimesPinned == null || numTimesPinned.value == 0)
-                        && !pendingShortcuts.contains(key)) {
-                    // Shortcut is pinned but doesn't exist on the workspace; unpin it.
-                    mBgDataModel.unpinShortcut(context, key);
-                }
-            }
+            // Update pinned state of model shortcuts
+            mBgDataModel.updateShortcutPinnedState(context);
 
             // Sort the folder items, update ranks, and make sure all preview items are high res.
             FolderGridOrganizer verifier =
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
new file mode 100644
index 0000000..ce4eed5
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
+import android.content.Context;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+/**
+ * Class to extend LauncherModel functionality to provide extra data
+ */
+public class ModelDelegate implements ResourceBasedOverride {
+
+    /**
+     * Creates and initializes a new instance of the delegate
+     */
+    public static ModelDelegate newInstance(
+            Context context, LauncherAppState app, AllAppsList appsList, BgDataModel dataModel) {
+        ModelDelegate delegate = Overrides.getObject(
+                ModelDelegate.class, context, R.string.model_delegate_class);
+
+        delegate.mApp = app;
+        delegate.mAppsList = appsList;
+        delegate.mDataModel = dataModel;
+        return delegate;
+    }
+
+    protected LauncherAppState mApp;
+    protected AllAppsList mAppsList;
+    protected BgDataModel mDataModel;
+
+    public ModelDelegate() { }
+
+    /**
+     * Called periodically to validate and update any data
+     */
+    @WorkerThread
+    public void validateData() {
+        if (hasShortcutsPermission(mApp.getContext())
+                != mAppsList.hasShortcutHostPermission()) {
+            mApp.getModel().forceReload();
+        }
+    }
+
+    /**
+     * Load delegate items if any in the data model
+     */
+    @WorkerThread
+    public void loadItems() { }
+
+    /**
+     * Called when the delegate is no loner needed
+     */
+    @WorkerThread
+    public void destroy() { }
+}
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index dca4ec0..c0ae6f9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -24,7 +24,6 @@
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
-import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
@@ -32,8 +31,6 @@
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.SessionCommitReceiver;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
@@ -108,11 +105,6 @@
                         appsList.removePackage(packages[i], mUser);
                     }
                     appsList.addPackage(context, packages[i], mUser);
-
-                    // Automatically add homescreen icon for work profile apps for below O device.
-                    if (!Utilities.ATLEAST_OREO && !Process.myUserHandle().equals(mUser)) {
-                        SessionCommitReceiver.queueAppIconAddition(context, packages[i], mUser);
-                    }
                 }
                 flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
@@ -331,7 +323,7 @@
             InstallShortcutReceiver.removeFromInstallQueue(context, removedPackages, mUser);
         }
 
-        if (Utilities.ATLEAST_OREO && mOp == OP_ADD) {
+        if (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.
             for (int i = 0; i < N; i++) {
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index b17b062..aee1f2a 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -138,8 +138,7 @@
         info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
                 ? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES;
 
-        if (Utilities.ATLEAST_OREO
-                && appInfo.targetSdkVersion >= Build.VERSION_CODES.O
+        if (appInfo.targetSdkVersion >= Build.VERSION_CODES.O
                 && Process.myUserHandle().equals(lai.getUser())) {
             // The icon for a non-primary user is badged, hence it's not exactly an adaptive icon.
             info.runtimeStatusFlags |= FLAG_ADAPTIVE_ICON;
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index a7bf1f3..1e1d093 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -216,7 +216,7 @@
         if (cn == null && (itemType == Favorites.ITEM_TYPE_SHORTCUT || hasStatusFlag(
                 FLAG_SUPPORTS_WEB_UI | FLAG_AUTOINSTALL_ICON | FLAG_RESTORED_ICON))) {
             // Legacy shortcuts and promise icons with web UI may not have a componentName but just
-            // a packageName. In that case create a dummy componentName instead of adding additional
+            // a packageName. In that case create a empty componentName instead of adding additional
             // check everywhere.
             String pkg = intent.getPackage();
             return pkg == null ? null : new ComponentName(pkg, IconCache.EMPTY_CLASS_NAME);
diff --git a/src/com/android/launcher3/notification/NotificationKeyData.java b/src/com/android/launcher3/notification/NotificationKeyData.java
index a1917ec..1dda3df 100644
--- a/src/com/android/launcher3/notification/NotificationKeyData.java
+++ b/src/com/android/launcher3/notification/NotificationKeyData.java
@@ -30,9 +30,9 @@
 
 /**
  * The key data associated with the notification, used to determine what to include
- * in dots and dummy popup views before they are populated.
+ * in dots and stub popup views before they are populated.
  *
- * @see NotificationInfo for the full data used when populating the dummy views.
+ * @see NotificationInfo for the full data used when populating the stub views.
  */
 public class NotificationKeyData {
     public final String notificationKey;
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b03aa9c..32f060b 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -137,7 +137,7 @@
             setOnClickListener(mNotificationInfo);
         }
         setContentTranslation(0);
-        // Add a dummy ItemInfo so that logging populates the correct container and item types
+        // Add a stub ItemInfo so that logging populates the correct container and item types
         // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively.
         setTag(NOTIFICATION_ITEM_INFO);
         if (animate) {
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 901d27f..ce4644f 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -18,7 +18,9 @@
 
 import static com.android.launcher3.Utilities.getPrefs;
 
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
@@ -32,6 +34,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
+import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.SessionCommitReceiver;
 import com.android.launcher3.Utilities;
@@ -167,7 +170,7 @@
      * Attempt to restore workspace layout if the session is triggered due to device restore.
      */
     public boolean restoreDbIfApplicable(@NonNull final SessionInfo info) {
-        if (!Utilities.ATLEAST_OREO || !FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
+        if (!FeatureFlags.ENABLE_DATABASE_RESTORE.get()) {
             return false;
         }
         if (isRestore(info)) {
@@ -203,7 +206,7 @@
      * - A promise icon for the session has not already been created
      */
     void tryQueuePromiseAppIcon(PackageInstaller.SessionInfo sessionInfo) {
-        if (Utilities.ATLEAST_OREO && FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
+        if (FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
                 && SessionCommitReceiver.isEnabled(mAppContext)
                 && verify(sessionInfo) != null
                 && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
@@ -212,7 +215,21 @@
                 && !mPromiseIconIds.contains(sessionInfo.getSessionId())
                 && new PackageManagerHelper(mAppContext).getApplicationInfo(
                         sessionInfo.getAppPackageName(), getUserHandle(sessionInfo), 0) == null) {
-            SessionCommitReceiver.queuePromiseAppIconAddition(mAppContext, sessionInfo);
+
+            String packageName = sessionInfo.getAppPackageName();
+            if (mAppContext.getSystemService(LauncherApps.class)
+                    .getActivityList(packageName, getUserHandle(sessionInfo)).isEmpty()) {
+                // Ensure application isn't already installed.
+                Intent data = new Intent();
+                data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
+                        new ComponentName(packageName, "")).setPackage(packageName));
+                data.putExtra(Intent.EXTRA_SHORTCUT_NAME, sessionInfo.getAppLabel());
+                data.putExtra(Intent.EXTRA_SHORTCUT_ICON, sessionInfo.getAppIcon());
+
+                InstallShortcutReceiver.queueApplication(data, getUserHandle(sessionInfo),
+                        mAppContext);
+            }
+
             mPromiseIconIds.add(sessionInfo.getSessionId());
             updatePromiseIconPrefs();
         }
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index 40d7031..7af14c6 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -27,7 +27,6 @@
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Process;
@@ -39,13 +38,13 @@
 
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -175,37 +174,23 @@
         List<ShortcutConfigActivityInfo> result = new ArrayList<>();
         UserHandle myUser = Process.myUserHandle();
 
-        if (Utilities.ATLEAST_OREO) {
-            final List<UserHandle> users;
-            final String packageName;
-            if (packageUser == null) {
-                users = UserCache.INSTANCE.get(context).getUserProfiles();
-                packageName = null;
-            } else {
-                users = new ArrayList<>(1);
-                users.add(packageUser.mUser);
-                packageName = packageUser.mPackageName;
-            }
-            LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
-            for (UserHandle user : users) {
-                boolean ignoreTargetSdk = myUser.equals(user);
-                for (LauncherActivityInfo activityInfo :
-                        launcherApps.getShortcutConfigActivityList(packageName, user)) {
-                    if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion
-                            >= Build.VERSION_CODES.O) {
-                        result.add(new ShortcutConfigActivityInfoVO(activityInfo));
-                    }
-                }
-            }
+        final List<UserHandle> users;
+        final String packageName;
+        if (packageUser == null) {
+            users = UserCache.INSTANCE.get(context).getUserProfiles();
+            packageName = null;
         } else {
-            if (packageUser == null || packageUser.mUser.equals(myUser)) {
-                Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
-                if (packageUser != null) {
-                    intent.setPackage(packageUser.mPackageName);
-                }
-                for (ResolveInfo info :
-                        context.getPackageManager().queryIntentActivities(intent, 0)) {
-                    result.add(new ShortcutConfigActivityInfoVL(info.activityInfo));
+            users = Collections.singletonList(packageUser.mUser);
+            packageName = packageUser.mPackageName;
+        }
+        LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+        for (UserHandle user : users) {
+            boolean ignoreTargetSdk = myUser.equals(user);
+            for (LauncherActivityInfo activityInfo :
+                    launcherApps.getShortcutConfigActivityList(packageName, user)) {
+                if (ignoreTargetSdk || activityInfo.getApplicationInfo().targetSdkVersion
+                        >= Build.VERSION_CODES.O) {
+                    result.add(new ShortcutConfigActivityInfoVO(activityInfo));
                 }
             }
         }
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 5aab41a..2d7d6b0 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -21,8 +21,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.LongSparseArray;
 
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -102,6 +104,9 @@
                 mUsers = null;
                 mUserToSerialMap = null;
             }
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work profile removed", new Exception());
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 614cf14..896fb18 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -52,7 +52,6 @@
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
@@ -400,9 +399,7 @@
         } else if (view instanceof ImageView) {
             // Only the system shortcut icon shows on a gray background header.
             info.setIconAndContentDescriptionFor((ImageView) view);
-            if (Utilities.ATLEAST_OREO) {
-                view.setTooltipText(view.getContentDescription());
-            }
+            view.setTooltipText(view.getContentDescription());
         }
         view.setTag(info);
         view.setOnClickListener(info);
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index a013312..31c3014 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -20,6 +20,7 @@
 import android.app.ActivityOptions;
 import android.content.Intent;
 import android.os.Bundle;
+import android.view.Display;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewAnimationUtils;
@@ -168,7 +169,9 @@
 
     @Override
     public ActivityOptions getActivityLaunchOptions(View v) {
-        return null;
+        final Display display = getWindow().getDecorView().getDisplay();
+        return display != null ? ActivityOptions.makeBasic().setLaunchDisplayId(
+                       display.getDisplayId()) : null;
     }
 
     @Override
@@ -222,7 +225,7 @@
     }
 
     @Override
-    public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated) { }
+    public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
 
     @Override
     public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index b12d04f..4baecb7 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -34,17 +34,23 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.Settings;
+import android.text.Editable;
+import android.text.TextWatcher;
 import android.util.ArrayMap;
 import android.util.Pair;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.widget.EditText;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
 import androidx.preference.PreferenceDataStore;
 import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceScreen;
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.SwitchPreference;
@@ -81,6 +87,7 @@
     private PreferenceCategory mPluginsCategory;
     private FlagTogglerPrefUi mFlagTogglerPrefUi;
 
+
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
@@ -99,6 +106,50 @@
         maybeAddSandboxCategory();
     }
 
+    private void filterPreferences(String query, PreferenceGroup pg) {
+        int count = pg.getPreferenceCount();
+        int hidden = 0;
+        for (int i = 0; i < count; i++) {
+            Preference preference = pg.getPreference(i);
+            if (preference instanceof PreferenceGroup) {
+                filterPreferences(query, (PreferenceGroup) preference);
+            } else {
+                String title = preference.getTitle().toString().toLowerCase().replace("_", " ");
+                if (query.isEmpty() || title.contains(query)) {
+                    preference.setVisible(true);
+                } else {
+                    preference.setVisible(false);
+                    hidden++;
+                }
+            }
+        }
+        pg.setVisible(hidden != count);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        EditText filterBox = view.findViewById(R.id.filter_box);
+        filterBox.setVisibility(View.VISIBLE);
+        filterBox.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+            }
+
+            @Override
+            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+            }
+
+            @Override
+            public void afterTextChanged(Editable editable) {
+                String query = editable.toString().toLowerCase().replace("_", " ");
+                filterPreferences(query, mPreferenceScreen);
+            }
+        });
+    }
+
     @Override
     public void onDestroy() {
         super.onDestroy();
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index d3213a1..ec3a467 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,7 +18,6 @@
 
 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
 
-import static com.android.launcher3.SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
@@ -172,11 +171,6 @@
         protected boolean initPreference(Preference preference) {
             switch (preference.getKey()) {
                 case NOTIFICATION_DOTS_PREFERENCE_KEY:
-                    if (!Utilities.ATLEAST_OREO ||
-                            !getResources().getBoolean(R.bool.notification_dots_enabled)) {
-                        return false;
-                    }
-
                     // Listen to system notification dot settings while this UI is active.
                     mNotificationDotsObserver = newNotificationSettingsObserver(
                             getActivity(), (NotificationDotsPreference) preference);
@@ -188,9 +182,6 @@
                     mNotificationDotsObserver.dispatchOnChange();
                     return true;
 
-                case ADD_ICON_PREFERENCE_KEY:
-                    return Utilities.ATLEAST_OREO;
-
                 case ALLOW_ROTATION_PREFERENCE_KEY:
                     if (getResources().getBoolean(R.bool.allow_rotation)) {
                         // Launcher supports rotation by default. No need to show this setting.
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index 3ca9490..0c6d675 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -30,6 +30,10 @@
         return componentName.getClassName();
     }
 
+    public String getPackageName() {
+        return componentName.getPackageName();
+    }
+
     /**
      * Creates a {@link ShortcutRequest} for this key
      */
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index f1e7f16..6ff1254 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.statemanager;
 
+import static android.animation.ValueAnimator.areAnimatorsEnabled;
+
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
 
 import android.animation.Animator;
@@ -26,7 +28,6 @@
 import android.os.Looper;
 import android.util.Log;
 
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
@@ -179,7 +180,7 @@
 
     private void goToState(STATE_TYPE state, boolean animated, long delay,
             final Runnable onCompleteRunnable) {
-        animated &= Utilities.areAnimationsEnabled(mActivity);
+        animated &= areAnimatorsEnabled();
         if (mActivity.isInState(state)) {
             if (mConfig.currentAnimation == null) {
                 // Run any queued runnable
@@ -315,7 +316,6 @@
 
             @Override
             public void onAnimationStart(Animator animation) {
-                super.onAnimationStart(animation);
                 // Change the internal state only when the transition actually starts
                 onStateTransitionStart(state);
             }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 8165627..8616881 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -99,6 +99,7 @@
     public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
 
     public static final String REQUEST_OVERVIEW_ACTIONS_ENABLED = "overview-actions-enabled";
+    public static final String REQUEST_OVERVIEW_SHARE_ENABLED = "overview-share-enabled";
 
     public static boolean sDisableSensorRotation;
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
@@ -107,4 +108,5 @@
     public static final String PAUSE_NOT_DETECTED = "b/139891609";
     public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
+    public static final String WORK_PROFILE_REMOVED = "b/159671700";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 3c78b08..02ca926 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -37,6 +37,8 @@
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherState;
@@ -44,6 +46,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
@@ -265,8 +268,10 @@
         mCanBlockFling = mFromState == NORMAL;
         mFlingBlockCheck.unblockFling();
         // Must be called after all the animation controllers have been paused
-        if (mToState == ALL_APPS || mToState == NORMAL) {
-            mLauncher.getAllAppsController().onDragStart(mToState == ALL_APPS);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()
+                && (mToState == ALL_APPS || mToState == NORMAL)) {
+            mLauncher.getAllAppsController().getInsetController().onDragStart(
+                    mToState == ALL_APPS ? 0 : 1);
         }
     }
 
@@ -508,15 +513,7 @@
         if (mAtomicAnim == null) {
             return 0;
         }
-        if (Utilities.ATLEAST_OREO) {
-            return mAtomicAnim.getTotalDuration() - mAtomicAnim.getCurrentPlayTime();
-        } else {
-            long remainingDuration = 0;
-            for (Animator anim : mAtomicAnim.getChildAnimations()) {
-                remainingDuration = Math.max(remainingDuration, anim.getDuration());
-            }
-            return remainingDuration;
-        }
+        return mAtomicAnim.getTotalDuration() - mAtomicAnim.getCurrentPlayTime();
     }
 
     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index ac1ade2..1aaa608 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -72,6 +72,7 @@
         out.halfPageSize = view.getNormalChildHeight() / 2;
         out.halfScreenSize = view.getMeasuredHeight() / 2;
         out.screenCenter = insets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
+        out.pageParentScale = view.getScaleY();
     }
 
     @Override
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index d4f5cba..f88cdb3 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -109,6 +109,7 @@
         public int halfPageSize;
         public int screenCenter;
         public int halfScreenSize;
+        public float pageParentScale;
     }
 
     class ChildBounds {
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 3341996..f18b109 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -70,6 +70,7 @@
         out.halfPageSize = view.getNormalChildWidth() / 2;
         out.halfScreenSize = view.getMeasuredWidth() / 2;
         out.screenCenter = insets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
+        out.pageParentScale = view.getScaleX();
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index 26313e5..6e5e7d9 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -36,7 +36,6 @@
     public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
     public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
     public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
-    public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
     public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
     public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
 
@@ -56,7 +55,6 @@
     @StringDef(value = {
             HOME_BOUNCE_COUNT,
             SHELF_BOUNCE_COUNT,
-            ALL_APPS_COUNT,
             HOTSEAT_DISCOVERY_TIP_COUNT
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -67,7 +65,6 @@
         Map<String, Integer> maxCounts = new ArrayMap<>(4);
         maxCounts.put(HOME_BOUNCE_COUNT, 3);
         maxCounts.put(SHELF_BOUNCE_COUNT, 3);
-        maxCounts.put(ALL_APPS_COUNT, 5);
         maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
         MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
     }
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 86f3431..523545a 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -37,7 +37,6 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PatternMatcher;
-import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
@@ -46,7 +45,6 @@
 
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -96,36 +94,12 @@
      * Returns the application info for the provided package or null
      */
     public ApplicationInfo getApplicationInfo(String packageName, UserHandle user, int flags) {
-        if (Utilities.ATLEAST_OREO) {
-            try {
-                ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
-                return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
-                        ? null : info;
-            } catch (PackageManager.NameNotFoundException e) {
-                return null;
-            }
-        } else {
-            final boolean isPrimaryUser = Process.myUserHandle().equals(user);
-            if (!isPrimaryUser && (flags == 0)) {
-                // We are looking for an installed app on a secondary profile. Prior to O, the only
-                // entry point for work profiles is through the LauncherActivity.
-                List<LauncherActivityInfo> activityList =
-                        mLauncherApps.getActivityList(packageName, user);
-                return activityList.size() > 0 ? activityList.get(0).getApplicationInfo() : null;
-            }
-            try {
-                ApplicationInfo info = mPm.getApplicationInfo(packageName, flags);
-                // There is no way to check if the app is installed for managed profile. But for
-                // primary profile, we can still have this check.
-                if (isPrimaryUser && ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0)
-                        || !info.enabled) {
-                    return null;
-                }
-                return info;
-            } catch (PackageManager.NameNotFoundException e) {
-                // Package not found
-                return null;
-            }
+        try {
+            ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
+            return (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled
+                    ? null : info;
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
         }
     }
 
diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java
index 48aa02b..4b22429 100644
--- a/src/com/android/launcher3/util/SecureSettingsObserver.java
+++ b/src/com/android/launcher3/util/SecureSettingsObserver.java
@@ -29,6 +29,11 @@
 
     /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
     public static final String NOTIFICATION_BADGING = "notification_badging";
+    /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */
+    public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled";
+    /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */
+    public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED =
+            "swipe_bottom_to_notification_enabled";
 
     private final ContentResolver mResolver;
     private final String mKeySetting;
@@ -79,4 +84,21 @@
         return new SecureSettingsObserver(
                 context.getContentResolver(), listener, NOTIFICATION_BADGING, 1);
     }
+
+    public static SecureSettingsObserver newOneHandedSettingsObserver(Context context,
+            OnChangeListener listener) {
+        return new SecureSettingsObserver(
+                context.getContentResolver(), listener, ONE_HANDED_ENABLED, 1);
+    }
+
+    /**
+     * Constructs settings observer for {@link #ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED}
+     * preference.
+     */
+    public static SecureSettingsObserver newSwipeToNotificationSettingsObserver(Context context,
+            OnChangeListener listener) {
+        return new SecureSettingsObserver(
+                context.getContentResolver(), listener,
+                ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
+    }
 }
diff --git a/src/com/android/launcher3/util/SystemUiController.java b/src/com/android/launcher3/util/SystemUiController.java
index 275c024..50166c3 100644
--- a/src/com/android/launcher3/util/SystemUiController.java
+++ b/src/com/android/launcher3/util/SystemUiController.java
@@ -19,8 +19,6 @@
 import android.view.View;
 import android.view.Window;
 
-import com.android.launcher3.Utilities;
-
 import java.util.Arrays;
 
 /**
@@ -33,6 +31,7 @@
     public static final int UI_STATE_SCRIM_VIEW = 1;
     public static final int UI_STATE_WIDGET_BOTTOM_SHEET = 2;
     public static final int UI_STATE_OVERVIEW = 3;
+    public static final int UI_STATE_ALLAPPS = 4;
 
     public static final int FLAG_LIGHT_NAV = 1 << 0;
     public static final int FLAG_DARK_NAV = 1 << 1;
@@ -40,7 +39,7 @@
     public static final int FLAG_DARK_STATUS = 1 << 3;
 
     private final Window mWindow;
-    private final int[] mStates = new int[4];
+    private final int[] mStates = new int[5];
 
     public SystemUiController(Window window) {
         mWindow = window;
@@ -77,12 +76,10 @@
     }
 
     private int getSysUiVisibilityFlags(int stateFlag, int currentVisibility) {
-        if (Utilities.ATLEAST_OREO) {
-            if ((stateFlag & FLAG_LIGHT_NAV) != 0) {
-                currentVisibility |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
-            } else if ((stateFlag & FLAG_DARK_NAV) != 0) {
-                currentVisibility &= ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
-            }
+        if ((stateFlag & FLAG_LIGHT_NAV) != 0) {
+            currentVisibility |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+        } else if ((stateFlag & FLAG_DARK_NAV) != 0) {
+            currentVisibility &= ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
         }
 
         if ((stateFlag & FLAG_LIGHT_STATUS) != 0) {
diff --git a/src/com/android/launcher3/views/AccessibilityActionsView.java b/src/com/android/launcher3/views/AccessibilityActionsView.java
new file mode 100644
index 0000000..0eacaa3
--- /dev/null
+++ b/src/com/android/launcher3/views/AccessibilityActionsView.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.views;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.views.OptionsPopupView.OptionItem;
+
+/**
+ * Placeholder view to expose additional Launcher actions via accessibility actions
+ */
+public class AccessibilityActionsView extends View implements StateListener<LauncherState> {
+
+    public AccessibilityActionsView(Context context) {
+        this(context, null);
+    }
+
+    public AccessibilityActionsView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AccessibilityActionsView(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        Launcher.getLauncher(context).getStateManager().addStateListener(this);
+        setWillNotDraw(true);
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        setImportantForAccessibility(finalState == NORMAL
+                ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO);
+    }
+
+    @Override
+    public AccessibilityNodeInfo createAccessibilityNodeInfo() {
+        AccessibilityNodeInfo info = super.createAccessibilityNodeInfo();
+        Launcher l = Launcher.getLauncher(getContext());
+        info.addAction(new AccessibilityAction(
+                R.string.all_apps_button_label, l.getText(R.string.all_apps_button_label)));
+        for (OptionItem item : OptionsPopupView.getOptions(l)) {
+            info.addAction(new AccessibilityAction(item.labelRes, l.getText(item.labelRes)));
+        }
+        return info;
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (super.performAccessibilityAction(action, arguments)) {
+            return true;
+        }
+        Launcher l = Launcher.getLauncher(getContext());
+        if (action == R.string.all_apps_button_label) {
+            l.getStateManager().goToState(ALL_APPS);
+            return true;
+        }
+        for (OptionItem item : OptionsPopupView.getOptions(l)) {
+            if (item.labelRes == action) {
+                if (item.eventId.getId() > 0) {
+                    l.getStatsLogManager().logger().log(item.eventId);
+                }
+                if (item.clickListener.onLongClick(this)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 7cdde2e..52a82f8 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -196,13 +196,18 @@
         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
     }
 
+    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+            RectF outRect) {
+        getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect());
+    }
+
     /**
      * Gets the location bounds of a view and returns the overall rotation.
      * - For DeepShortcutView, we return the bounds of the icon view.
      * - For BubbleTextView, we return the icon bounds.
      */
-    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
-            RectF outRect) {
+    public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+            RectF outRect, Rect outViewBounds) {
         boolean ignoreTransform = !isOpening;
         if (v instanceof DeepShortcutView) {
             v = ((DeepShortcutView) v).getBubbleText();
@@ -215,17 +220,16 @@
             return;
         }
 
-        Rect iconBounds = new Rect();
         if (v instanceof BubbleTextView) {
-            ((BubbleTextView) v).getIconBounds(iconBounds);
+            ((BubbleTextView) v).getIconBounds(outViewBounds);
         } else if (v instanceof FolderIcon) {
-            ((FolderIcon) v).getPreviewBounds(iconBounds);
+            ((FolderIcon) v).getPreviewBounds(outViewBounds);
         } else {
-            iconBounds.set(0, 0, v.getWidth(), v.getHeight());
+            outViewBounds.set(0, 0, v.getWidth(), v.getHeight());
         }
 
-        float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right,
-                iconBounds.bottom};
+        float[] points = new float[] {outViewBounds.left, outViewBounds.top, outViewBounds.right,
+                outViewBounds.bottom};
         Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
                 false, ignoreTransform);
         outRect.set(
@@ -252,7 +256,6 @@
         Drawable drawable = null;
         Drawable badge = null;
         boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get()
-                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
                 && !info.isDisabled(); // Use original icon for disabled icons.
         Drawable btvIcon = originalView instanceof BubbleTextView
                 ? ((BubbleTextView) originalView).getIcon() : null;
@@ -379,8 +382,7 @@
     @WorkerThread
     @SuppressWarnings("WrongThread")
     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
-                || !(drawable instanceof AdaptiveIconDrawable)
+        if (!(drawable instanceof AdaptiveIconDrawable)
                 || (drawable instanceof FolderAdaptiveIcon)) {
             return 0;
         }
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
new file mode 100644
index 0000000..040619e
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.GestureNavContract;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes
+ * the surfaceHandle to the {@link GestureNavContract}.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class FloatingSurfaceView extends AbstractFloatingView implements
+        OnGlobalLayoutListener, Insettable, SurfaceHolder.Callback2 {
+
+    private final RectF mTmpPosition = new RectF();
+
+    private final Launcher mLauncher;
+    private final RectF mIconPosition = new RectF();
+
+    private final Rect mIconBounds = new Rect();
+    private final Picture mPicture = new Picture();
+    private final Runnable mRemoveViewRunnable = this::removeViewFromParent;
+
+    private final SurfaceView mSurfaceView;
+
+
+    private View mIcon;
+    private GestureNavContract mContract;
+
+    public FloatingSurfaceView(Context context) {
+        this(context, null);
+    }
+
+    public FloatingSurfaceView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FloatingSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+
+        mSurfaceView = new SurfaceView(context);
+        mSurfaceView.setZOrderOnTop(true);
+
+        mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+        mSurfaceView.getHolder().addCallback(this);
+        mIsOpen = true;
+        addView(mSurfaceView);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        setCurrentIconVisible(true);
+        mLauncher.getViewCache().recycleView(R.layout.floating_surface_view, this);
+        mContract = null;
+        mIcon = null;
+        mIsOpen = false;
+
+        // Remove after some time, to avoid flickering
+        Executors.MAIN_EXECUTOR.getHandler().postDelayed(mRemoveViewRunnable,
+                DefaultDisplay.INSTANCE.get(mLauncher).getInfo().singleFrameMs);
+    }
+
+    private void removeViewFromParent() {
+        mPicture.beginRecording(1, 1);
+        mPicture.endRecording();
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    /**
+     * Shows the surfaceView for the provided contract
+     */
+    public static void show(Launcher launcher, GestureNavContract contract) {
+        FloatingSurfaceView view = launcher.getViewCache().getView(R.layout.floating_surface_view,
+                launcher, launcher.getDragLayer());
+        view.mContract = contract;
+        view.mIsOpen = true;
+
+        // Cancel any pending remove
+        Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(view.mRemoveViewRunnable);
+        view.removeViewFromParent();
+        launcher.getDragLayer().addView(view);
+    }
+
+    @Override
+    public void logActionCommand(int command) { }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ICON_SURFACE) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        close(false);
+        return false;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnGlobalLayoutListener(this);
+        updateIconLocation();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getViewTreeObserver().removeOnGlobalLayoutListener(this);
+        setCurrentIconVisible(true);
+    }
+
+    @Override
+    public void onGlobalLayout() {
+        updateIconLocation();
+    }
+
+    @Override
+    public void setInsets(Rect insets) { }
+
+    private void updateIconLocation() {
+        if (mContract == null) {
+            return;
+        }
+        View icon = mLauncher.getWorkspace().getFirstMatchForAppClose(
+                mContract.componentName.getPackageName(), mContract.user);
+
+        boolean iconChanged = mIcon != icon;
+        if (iconChanged) {
+            setCurrentIconVisible(true);
+            mIcon = icon;
+            setCurrentIconVisible(false);
+        }
+
+        if (icon != null && icon.isAttachedToWindow()) {
+            getLocationBoundsForView(mLauncher, icon, false, mTmpPosition, mIconBounds);
+
+            if (!mTmpPosition.equals(mIconPosition)) {
+                mIconPosition.set(mTmpPosition);
+                sendIconInfo();
+
+                LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+                lp.width = Math.round(mIconPosition.width());
+                lp.height = Math.round(mIconPosition.height());
+                lp.leftMargin = Math.round(mIconPosition.left);
+                lp.topMargin = Math.round(mIconPosition.top);
+            }
+        }
+        if (iconChanged && !mIconBounds.isEmpty()) {
+            // Record the icon display
+            setCurrentIconVisible(true);
+            Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height());
+            c.translate(-mIconBounds.left, -mIconBounds.top);
+            mIcon.draw(c);
+            mPicture.endRecording();
+            setCurrentIconVisible(false);
+            drawOnSurface();
+        }
+    }
+
+    private void sendIconInfo() {
+        if (mContract != null && !mIconPosition.isEmpty()) {
+            mContract.sendEndPosition(mIconPosition, mSurfaceView.getSurfaceControl());
+        }
+    }
+
+    @Override
+    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
+        drawOnSurface();
+        sendIconInfo();
+    }
+
+    @Override
+    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder,
+            int format, int width, int height) {
+        drawOnSurface();
+    }
+
+    @Override
+    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {}
+
+    @Override
+    public void surfaceRedrawNeeded(@NonNull SurfaceHolder surfaceHolder) {
+        drawOnSurface();
+    }
+
+    private void drawOnSurface() {
+        SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
+
+        Canvas c = surfaceHolder.lockHardwareCanvas();
+        if (c != null) {
+            mPicture.draw(c);
+            surfaceHolder.unlockCanvasAndPost(c);
+        }
+    }
+
+    private void setCurrentIconVisible(boolean isVisible) {
+        if (mIcon != null) {
+            setIconAndDotVisible(mIcon, isVisible);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 049a1ac..9ad2331 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -85,10 +85,10 @@
         if (item == null) {
             return false;
         }
-        if (item.mEventId.getId() > 0) {
-            mLauncher.getStatsLogManager().logger().log(item.mEventId);
+        if (item.eventId.getId() > 0) {
+            mLauncher.getStatsLogManager().logger().log(item.eventId);
         }
-        if (item.mClickListener.onLongClick(view)) {
+        if (item.clickListener.onLongClick(view)) {
             close(true);
             return true;
         }
@@ -130,8 +130,8 @@
         for (OptionItem item : items) {
             DeepShortcutView view =
                     (DeepShortcutView) popup.inflateAndAdd(R.layout.system_shortcut, popup);
-            view.getIconView().setBackgroundResource(item.mIconRes);
-            view.getBubbleText().setText(item.mLabelRes);
+            view.getIconView().setBackgroundResource(item.iconRes);
+            view.getBubbleText().setText(item.labelRes);
             view.setDividerVisibility(View.INVISIBLE);
             view.setOnClickListener(popup);
             view.setOnLongClickListener(popup);
@@ -152,7 +152,13 @@
             y = launcher.getDragLayer().getHeight() / 2;
         }
         RectF target = new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
+        show(launcher, target, getOptions(launcher));
+    }
 
+    /**
+     * Returns the list of supported actions
+     */
+    public static ArrayList<OptionItem> getOptions(Launcher launcher) {
         ArrayList<OptionItem> options = new ArrayList<>();
         int resString = Utilities.existsStyleWallpapers(launcher) ?
                 R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
@@ -170,10 +176,10 @@
                 LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
                 OptionsPopupView::startSettings));
 
-        show(launcher, target, options);
+        return options;
     }
 
-    public static boolean onWidgetsClicked(View view) {
+    private static boolean onWidgetsClicked(View view) {
         return openWidgets(Launcher.getLauncher(view.getContext())) != null;
     }
 
@@ -188,7 +194,7 @@
         }
     }
 
-    public static boolean startSettings(View view) {
+    private static boolean startSettings(View view) {
         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startSettings");
         Launcher launcher = Launcher.getLauncher(view.getContext());
         launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
@@ -201,7 +207,7 @@
      * Event handler for the wallpaper picker button that appears after a long press
      * on the home screen.
      */
-    public static boolean startWallpaperPicker(View v) {
+    private static boolean startWallpaperPicker(View v) {
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!Utilities.isWallpaperAllowed(launcher)) {
             Toast.makeText(launcher, R.string.msg_disabled_by_admin, Toast.LENGTH_SHORT).show();
@@ -220,30 +226,30 @@
         if (!TextUtils.isEmpty(pickerPackage)) {
             intent.setPackage(pickerPackage);
         }
-        return launcher.startActivitySafely(v, intent, dummyInfo(intent));
+        return launcher.startActivitySafely(v, intent, placeholderInfo(intent));
     }
 
-    static WorkspaceItemInfo dummyInfo(Intent intent) {
-        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
-        dummyInfo.intent = intent;
-        dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-        dummyInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
-        return dummyInfo;
+    static WorkspaceItemInfo placeholderInfo(Intent intent) {
+        WorkspaceItemInfo placeholderInfo = new WorkspaceItemInfo();
+        placeholderInfo.intent = intent;
+        placeholderInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+        placeholderInfo.container = LauncherSettings.Favorites.CONTAINER_SETTINGS;
+        return placeholderInfo;
     }
 
     public static class OptionItem {
 
-        private final int mLabelRes;
-        private final int mIconRes;
-        private final EventEnum mEventId;
-        private final OnLongClickListener mClickListener;
+        public final int labelRes;
+        public final int iconRes;
+        public final EventEnum eventId;
+        public final OnLongClickListener clickListener;
 
         public OptionItem(int labelRes, int iconRes, EventEnum eventId,
                 OnLongClickListener clickListener) {
-            mLabelRes = labelRes;
-            mIconRes = iconRes;
-            mEventId = eventId;
-            mClickListener = clickListener;
+            this.labelRes = labelRes;
+            this.iconRes = iconRes;
+            this.eventId = eventId;
+            this.clickListener = clickListener;
         }
     }
 }
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 22faf97..7f0765b 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -15,119 +15,37 @@
  */
 package com.android.launcher3.views;
 
-import static android.content.Context.ACCESSIBILITY_SERVICE;
-import static android.view.MotionEvent.ACTION_DOWN;
-
 import static androidx.core.graphics.ColorUtils.compositeColors;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.Keyframe;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.RectEvaluator;
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Point;
 import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
 import android.util.AttributeSet;
-import android.util.IntProperty;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
 import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.core.graphics.ColorUtils;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
-import androidx.customview.widget.ExploreByTouchHelper;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.Themes;
-import com.android.launcher3.widget.WidgetsFullSheet;
-
-import java.util.List;
 
 /**
  * Simple scrim which draws a flat color
  */
-public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
-        AccessibilityStateChangeListener {
-
-    public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
-            new IntProperty<ScrimView>("dragHandleAlpha") {
-
-                @Override
-                public Integer get(ScrimView scrimView) {
-                    return scrimView.mDragHandleAlpha;
-                }
-
-                @Override
-                public void setValue(ScrimView scrimView, int value) {
-                    scrimView.setDragHandleAlpha(value);
-                }
-            };
-    private static final int WALLPAPERS = R.string.wallpaper_button_text;
-    private static final int WIDGETS = R.string.widget_button_text;
-    private static final int SETTINGS = R.string.settings_button_text;
-    private static final int ALPHA_CHANNEL_COUNT = 1;
-
-    private static final long DRAG_HANDLE_BOUNCE_DURATION_MS = 300;
-    // How much to delay before repeating the bounce.
-    private static final long DRAG_HANDLE_BOUNCE_DELAY_MS = 200;
-    // Repeat this many times (i.e. total number of bounces is 1 + this).
-    private static final int DRAG_HANDLE_BOUNCE_REPEAT_COUNT = 2;
-
-    private final Rect mTempRect = new Rect();
-    private final int[] mTempPos = new int[2];
+public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener {
 
     protected final T mLauncher;
     private final WallpaperColorInfo mWallpaperColorInfo;
-    private final AccessibilityManager mAM;
     protected final int mEndScrim;
     protected final boolean mIsScrimDark;
 
-    private final StateListener<LauncherState> mAccessibilityLauncherStateListener =
-            new StateListener<LauncherState>() {
-        @Override
-        public void onStateTransitionComplete(LauncherState finalState) {
-            setImportantForAccessibility(finalState == ALL_APPS
-                    ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                    : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        }
-    };
-
     protected float mMaxScrimAlpha;
 
     protected float mProgress = 1;
@@ -137,22 +55,6 @@
     protected int mEndFlatColor;
     protected int mEndFlatColorAlpha;
 
-    protected final Point mDragHandleSize;
-    private final int mDragHandleTouchSize;
-    private final int mDragHandlePaddingInVerticalBarLayout;
-    protected float mDragHandleOffset;
-    private final Rect mDragHandleBounds;
-    private final RectF mHitRect = new RectF();
-    private ObjectAnimator mDragHandleAnim;
-
-    private final MultiValueAlpha mMultiValueAlpha;
-
-    private final AccessibilityHelper mAccessibilityHelper;
-    @Nullable
-    protected Drawable mDragHandle;
-
-    private int mDragHandleAlpha = 255;
-
     public ScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mLauncher = Launcher.cast(Launcher.getLauncher(context));
@@ -161,59 +63,24 @@
         mIsScrimDark = ColorUtils.calculateLuminance(mEndScrim) < 0.5f;
 
         mMaxScrimAlpha = 0.7f;
-
-        Resources res = context.getResources();
-        mDragHandleSize = new Point(res.getDimensionPixelSize(R.dimen.vertical_drag_handle_width),
-                res.getDimensionPixelSize(R.dimen.vertical_drag_handle_height));
-        mDragHandleBounds = new Rect(0, 0, mDragHandleSize.x, mDragHandleSize.y);
-        mDragHandleTouchSize = res.getDimensionPixelSize(R.dimen.vertical_drag_handle_touch_size);
-        mDragHandlePaddingInVerticalBarLayout = context.getResources()
-                .getDimensionPixelSize(R.dimen.vertical_drag_handle_padding_in_vertical_bar_layout);
-
-        mAccessibilityHelper = createAccessibilityHelper();
-        ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
-
-        mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
         setFocusable(false);
-        mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
-    }
-
-    public AlphaProperty getAlphaProperty(int index) {
-        return mMultiValueAlpha.getProperty(index);
-    }
-
-    @NonNull
-    protected AccessibilityHelper createAccessibilityHelper() {
-        return new AccessibilityHelper();
     }
 
     @Override
-    public void setInsets(Rect insets) {
-        updateDragHandleBounds();
-        updateDragHandleVisibility();
-    }
+    public void setInsets(Rect insets) { }
 
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        updateDragHandleBounds();
-    }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mWallpaperColorInfo.addOnChangeListener(this);
         onExtractedColorsChanged(mWallpaperColorInfo);
-
-        mAM.addAccessibilityStateChangeListener(this);
-        onAccessibilityStateChanged(mAM.isEnabled());
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mWallpaperColorInfo.removeOnChangeListener(this);
-        mAM.removeAccessibilityStateChangeListener(this);
     }
 
     @Override
@@ -234,10 +101,8 @@
     public void setProgress(float progress) {
         if (mProgress != progress) {
             mProgress = progress;
-            stopDragHandleEducationAnim();
             updateColors();
             updateSysUiColors();
-            updateDragHandleAlpha();
             invalidate();
         }
     }
@@ -260,286 +125,10 @@
         }
     }
 
-    protected void updateDragHandleAlpha() {
-        if (mDragHandle != null) {
-            mDragHandle.setAlpha(mDragHandleAlpha);
-        }
-    }
-
-    private void setDragHandleAlpha(int alpha) {
-        if (alpha != mDragHandleAlpha) {
-            mDragHandleAlpha = alpha;
-            if (mDragHandle != null) {
-                mDragHandle.setAlpha(mDragHandleAlpha);
-                invalidate();
-            }
-        }
-    }
-
     @Override
     protected void onDraw(Canvas canvas) {
         if (mCurrentFlatColor != 0) {
             canvas.drawColor(mCurrentFlatColor);
         }
-        drawDragHandle(canvas);
-    }
-
-    protected void drawDragHandle(Canvas canvas) {
-        if (mDragHandle != null) {
-            canvas.translate(0, -mDragHandleOffset);
-            mDragHandle.draw(canvas);
-            canvas.translate(0, mDragHandleOffset);
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        boolean superHandledTouch = super.onTouchEvent(event);
-        if (event.getAction() == ACTION_DOWN) {
-            if (!superHandledTouch && mHitRect.contains(event.getX(), event.getY())) {
-                if (startDragHandleEducationAnim()) {
-                    return true;
-                }
-            }
-            stopDragHandleEducationAnim();
-        }
-        return superHandledTouch;
-    }
-
-    /**
-     * Animates the drag handle to demonstrate how to get to all apps.
-     * @return Whether the animation was started (false if drag handle is invisible).
-     */
-    public boolean startDragHandleEducationAnim() {
-        stopDragHandleEducationAnim();
-
-        if (mDragHandle == null || mDragHandle.getAlpha() != 255) {
-            return false;
-        }
-
-        final Drawable drawable = mDragHandle;
-        mDragHandle = null;
-
-        Rect bounds = new Rect(mDragHandleBounds);
-        bounds.offset(0, -(int) mDragHandleOffset);
-        drawable.setBounds(bounds);
-
-        Rect topBounds = new Rect(bounds);
-        topBounds.offset(0, -bounds.height());
-
-        Rect invalidateRegion = new Rect(bounds);
-        invalidateRegion.top = topBounds.top;
-
-        final float progressToReachTop = 0.6f;
-        Keyframe frameTop = Keyframe.ofObject(progressToReachTop, topBounds);
-        frameTop.setInterpolator(DEACCEL);
-        Keyframe frameBot = Keyframe.ofObject(1, bounds);
-        frameBot.setInterpolator(ACCEL_DEACCEL);
-        PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("bounds",
-                Keyframe.ofObject(0, bounds), frameTop, frameBot);
-        holder.setEvaluator(new RectEvaluator());
-
-        mDragHandleAnim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
-        long totalBounceDuration = DRAG_HANDLE_BOUNCE_DURATION_MS + DRAG_HANDLE_BOUNCE_DELAY_MS;
-        // The bounce finishes by this progress, the rest of the duration just delays next bounce.
-        float delayStartProgress = 1f - (float) DRAG_HANDLE_BOUNCE_DELAY_MS / totalBounceDuration;
-        mDragHandleAnim.addUpdateListener((v) -> invalidate(invalidateRegion));
-        mDragHandleAnim.setDuration(totalBounceDuration);
-        mDragHandleAnim.setInterpolator(clampToProgress(LINEAR, 0, delayStartProgress));
-        mDragHandleAnim.setRepeatCount(DRAG_HANDLE_BOUNCE_REPEAT_COUNT);
-        getOverlay().add(drawable);
-
-        mDragHandleAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mDragHandleAnim = null;
-                getOverlay().remove(drawable);
-                updateDragHandleVisibility(drawable);
-            }
-        });
-        mDragHandleAnim.start();
-        return true;
-    }
-
-    private void stopDragHandleEducationAnim() {
-        if (mDragHandleAnim != null) {
-            mDragHandleAnim.end();
-        }
-    }
-
-    protected void updateDragHandleBounds() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        final int left;
-        final int width = getMeasuredWidth();
-        final int top = getMeasuredHeight() - mDragHandleSize.y - grid.getInsets().bottom;
-        final int topMargin;
-
-        if (grid.isVerticalBarLayout()) {
-            topMargin = grid.workspacePadding.bottom + mDragHandlePaddingInVerticalBarLayout;
-            if (grid.isSeascape()) {
-                left = width - grid.getInsets().right - mDragHandleSize.x
-                        - mDragHandlePaddingInVerticalBarLayout;
-            } else {
-                left = grid.getInsets().left + mDragHandlePaddingInVerticalBarLayout;
-            }
-        } else {
-            left = Math.round((width - mDragHandleSize.x) / 2f);
-            topMargin = grid.hotseatBarSizePx;
-        }
-        mDragHandleBounds.offsetTo(left, top - topMargin);
-        mHitRect.set(mDragHandleBounds);
-        // Inset outwards to increase touch size.
-        mHitRect.inset((mDragHandleSize.x - mDragHandleTouchSize) / 2f,
-                (mDragHandleSize.y - mDragHandleTouchSize) / 2f);
-
-        if (mDragHandle != null) {
-            mDragHandle.setBounds(mDragHandleBounds);
-        }
-    }
-
-    @Override
-    public void onAccessibilityStateChanged(boolean enabled) {
-        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
-        stateManager.removeStateListener(mAccessibilityLauncherStateListener);
-
-        if (enabled) {
-            stateManager.addStateListener(mAccessibilityLauncherStateListener);
-            mAccessibilityLauncherStateListener.onStateTransitionComplete(stateManager.getState());
-        } else {
-            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-        }
-        updateDragHandleVisibility();
-    }
-
-    public void updateDragHandleVisibility() {
-        updateDragHandleVisibility(null);
-    }
-
-    private void updateDragHandleVisibility(@Nullable Drawable recycle) {
-        boolean visible = shouldDragHandleBeVisible();
-        boolean wasVisible = mDragHandle != null;
-        if (visible != wasVisible) {
-            if (visible) {
-                mDragHandle = recycle != null ? recycle :
-                        mLauncher.getDrawable(R.drawable.drag_handle_indicator_shadow);
-                mDragHandle.setBounds(mDragHandleBounds);
-
-                updateDragHandleAlpha();
-            } else {
-                mDragHandle = null;
-            }
-            invalidate();
-        }
-    }
-
-    protected boolean shouldDragHandleBeVisible() {
-        return mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
-    }
-
-    @Override
-    public boolean dispatchHoverEvent(MotionEvent event) {
-        return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(KeyEvent event) {
-        return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
-    }
-
-    @Override
-    public void onFocusChanged(boolean gainFocus, int direction,
-            Rect previouslyFocusedRect) {
-        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-        mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-    }
-
-    protected class AccessibilityHelper extends ExploreByTouchHelper {
-
-        private static final int DRAG_HANDLE_ID = 1;
-
-        public AccessibilityHelper() {
-            super(ScrimView.this);
-        }
-
-        @Override
-        protected int getVirtualViewAt(float x, float y) {
-            return  mHitRect.contains((int) x, (int) y)
-                    ? DRAG_HANDLE_ID : INVALID_ID;
-        }
-
-        @Override
-        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
-            virtualViewIds.add(DRAG_HANDLE_ID);
-        }
-
-        @Override
-        protected void onPopulateNodeForVirtualView(int virtualViewId,
-                AccessibilityNodeInfoCompat node) {
-            node.setContentDescription(getContext().getString(R.string.all_apps_button_label));
-            node.setBoundsInParent(mDragHandleBounds);
-
-            getLocationOnScreen(mTempPos);
-            mTempRect.set(mDragHandleBounds);
-            mTempRect.offset(mTempPos[0], mTempPos[1]);
-            node.setBoundsInScreen(mTempRect);
-
-            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
-            node.setClickable(true);
-            node.setFocusable(true);
-
-            if (mLauncher.isInState(NORMAL)) {
-                Context context = getContext();
-                if (Utilities.isWallpaperAllowed(context)) {
-                    node.addAction(
-                            new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS)));
-                }
-                node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS)));
-                node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS)));
-            }
-        }
-
-        @Override
-        protected boolean onPerformActionForVirtualView(
-                int virtualViewId, int action, Bundle arguments) {
-            if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
-                mLauncher.getUserEventDispatcher().logActionOnControl(
-                        Action.Touch.TAP, ControlType.ALL_APPS_BUTTON,
-                        mLauncher.getStateManager().getState().containerType);
-                mLauncher.getStateManager().goToState(ALL_APPS);
-                return true;
-            } else if (action == WALLPAPERS) {
-                return OptionsPopupView.startWallpaperPicker(ScrimView.this);
-            } else if (action == WIDGETS) {
-                int originalImportanceForAccessibility = getImportantForAccessibility();
-                setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-                WidgetsFullSheet widgetsFullSheet = OptionsPopupView.openWidgets(mLauncher);
-                if (widgetsFullSheet == null) {
-                    setImportantForAccessibility(originalImportanceForAccessibility);
-                    return false;
-                }
-                widgetsFullSheet.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
-                    @Override
-                    public void onViewAttachedToWindow(View view) {}
-
-                    @Override
-                    public void onViewDetachedFromWindow(View view) {
-                        setImportantForAccessibility(originalImportanceForAccessibility);
-                        widgetsFullSheet.removeOnAttachStateChangeListener(this);
-                    }
-                });
-                return true;
-            } else if (action == SETTINGS) {
-                return OptionsPopupView.startSettings(ScrimView.this);
-            }
-
-            return false;
-        }
-    }
-
-    /**
-     * @return The top of this scrim view, or {@link Float#MAX_VALUE} if there's no distinct top.
-     */
-    public float getVisualTop() {
-        return Float.MAX_VALUE;
     }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 6f2e179..780a1a1 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -80,9 +80,7 @@
         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
         setBackgroundResource(R.drawable.widget_internal_focus_bg);
 
-        if (Utilities.ATLEAST_OREO) {
-            setExecutor(Executors.THREAD_POOL_EXECUTOR);
-        }
+        setExecutor(Executors.THREAD_POOL_EXECUTOR);
         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
         }
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 9021d9e..ca47728 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -52,7 +52,6 @@
     private static final float MIN_SATUNATION = 0.7f;
 
     private final Rect mRect = new Rect();
-    private View mDefaultView;
     private OnClickListener mClickListener;
     private final LauncherAppWidgetInfo mInfo;
     private final int mStartState;
@@ -111,12 +110,11 @@
 
     @Override
     protected View getDefaultView() {
-        if (mDefaultView == null) {
-            mDefaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
-            mDefaultView.setOnClickListener(this);
-            applyState();
-        }
-        return mDefaultView;
+        View defaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
+        defaultView.setOnClickListener(this);
+        applyState();
+        invalidate();
+        return defaultView;
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index 4b6c569..c0c5c48 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -23,13 +23,11 @@
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Process;
 import android.os.UserHandle;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.pm.UserCache;
@@ -84,22 +82,8 @@
             return allWidgetsSteam(mContext).collect(Collectors.toList());
         }
 
-        if (Utilities.ATLEAST_OREO) {
-            return mAppWidgetManager.getInstalledProvidersForPackage(
-                    packageUser.mPackageName, packageUser.mUser);
-        }
-
-        String pkg = packageUser.mPackageName;
-        return Stream.concat(
-                // Only get providers for the given package/user.
-                mAppWidgetManager.getInstalledProvidersForProfile(packageUser.mUser)
-                        .stream()
-                        .filter(w -> w.provider.equals(pkg)),
-                Process.myUserHandle().equals(packageUser.mUser)
-                        && mContext.getPackageName().equals(pkg)
-                        ? CustomWidgetManager.INSTANCE.get(mContext).stream()
-                        : Stream.empty())
-                .collect(Collectors.toList());
+        return mAppWidgetManager.getInstalledProvidersForPackage(
+                packageUser.mPackageName, packageUser.mUser);
     }
 
     /**
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
index 5a1f9ca..780a0f0 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/ColorExtractionAlgorithm.java
@@ -21,16 +21,16 @@
 import android.util.Pair;
 import android.util.Range;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.ColorUtils;
+
 import com.android.launcher3.Utilities;
 
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.ColorUtils;
-
 /**
  * Implementation of tonal color extraction
  **/
@@ -69,7 +69,7 @@
         // palettes. The best fit is tweaked to be closer to the source color
         // and replaces the original palette
 
-        // Get the most preeminent, non-blacklisted color.
+        // Get the most preeminent, non-disallowed color.
         Integer bestColor = 0;
         final float[] hsl = new float[3];
         for (int i = 0; i < mainColorsSize; i++) {
@@ -78,7 +78,7 @@
                     Color.blue(colorValue), hsl);
 
             // Stop when we find a color that meets our criteria
-            if (!isBlacklisted(hsl)) {
+            if (!isDisallowed(hsl)) {
                 bestColor = colorValue;
                 break;
             }
@@ -167,12 +167,12 @@
     }
 
     /**
-     * Checks if a given color exists in the blacklist
+     * Checks if a given color exists in the disallowed_colors list.
      * @param hsl float array with 3 components (H 0..360, S 0..1 and L 0..1)
      * @return true if color should be avoided
      */
-    private boolean isBlacklisted(float[] hsl) {
-        for (ColorRange badRange: BLACKLISTED_COLORS) {
+    private boolean isDisallowed(float[] hsl) {
+        for (ColorRange badRange: DISALLOWED_COLORS) {
             if (badRange.containsColor(hsl[0], hsl[1], hsl[2])) {
                 return true;
             }
@@ -592,7 +592,7 @@
     );
 
     @SuppressWarnings("WeakerAccess")
-    static final ColorRange[] BLACKLISTED_COLORS = new ColorRange[] {
+    static final ColorRange[] DISALLOWED_COLORS = new ColorRange[] {
 
             // Red
             new ColorRange(
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
index 0fd0a35..9dbe47c 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/dynamicui/WallpaperManagerCompat.java
@@ -17,8 +17,7 @@
 package com.android.launcher3.uioverrides.dynamicui;
 
 import android.content.Context;
-
-import com.android.launcher3.Utilities;
+import android.os.Build;
 
 import androidx.annotation.Nullable;
 
@@ -32,7 +31,7 @@
             if (sInstance == null) {
                 context = context.getApplicationContext();
 
-                if (Utilities.ATLEAST_OREO_MR1) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
                     try {
                         sInstance = new WallpaperManagerCompatVOMR1(context);
                     } catch (Throwable e) {
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index f243f27..bc6356f 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -98,7 +98,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
             android:clearTaskOnLaunch="true"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:enabled="false"
             android:label="Test launcher"
             android:launchMode="singleTask"
diff --git a/tests/src/com/android/launcher3/testcomponent/ListViewService.java b/tests/src/com/android/launcher3/testcomponent/ListViewService.java
index 3da20e0..9e3a492 100644
--- a/tests/src/com/android/launcher3/testcomponent/ListViewService.java
+++ b/tests/src/com/android/launcher3/testcomponent/ListViewService.java
@@ -89,7 +89,7 @@
                 public RemoteViewsFactory onGetViewFactory(Intent intent) {
                     return SimpleViewsFactory.this;
                 }
-            }.onBind(new Intent("dummy_intent"));
+            }.onBind(new Intent("stub_intent"));
         }
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 858e183..5e42d9b 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -53,7 +53,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.common.WidgetUtils;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
@@ -116,8 +115,7 @@
         if (TestHelpers.isInLauncherProcess()) {
             StrictMode.VmPolicy.Builder builder =
                     new StrictMode.VmPolicy.Builder()
-// b/154772063
-//                            .detectActivityLeaks()
+                            .detectActivityLeaks()
                             .penaltyLog()
                             .penaltyListener(Runnable::run, violation -> {
                                 if (sStrictmodeDetectedActivityLeak == null) {
@@ -272,8 +270,6 @@
         }
 
         mLauncherPid = 0;
-        // Disable app tracker
-        AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
 
         mTargetContext = InstrumentationRegistry.getTargetContext();
         mTargetPackage = mTargetContext.getPackageName();
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index 8d571ff..f5f93c4 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -24,6 +24,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
 import android.widget.TextView;
 
 import androidx.test.filters.LargeTest;
@@ -34,6 +35,7 @@
 import com.android.launcher3.allapps.AllAppsPagedView;
 import com.android.launcher3.allapps.WorkModeSwitch;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.WorkEduView;
 
 import org.junit.After;
@@ -132,6 +134,10 @@
                     l.getResources().getString(R.string.work_profile_edu_personal_apps));
             workEduView.findViewById(R.id.proceed).callOnClick();
         });
+
+        executeOnLauncher(launcher -> Log.d(TestProtocol.WORK_PROFILE_REMOVED,
+                "Work profile status: " + launcher.getAppsView().isPersonalTabVisible()));
+
         // verify work edu is seen next
         waitForLauncherCondition("Launcher did not show the next edu screen", l ->
                 ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage() == WORK_PAGE
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index a4aa9f2..fa495f5 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -192,7 +192,7 @@
                         WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
         executeOnLauncher(l -> l.getAppWidgetHost().startListening());
         verifyWidgetPresent(info);
-        assertNull(mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT));
+        assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100));
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 0e43d81..822fefc 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -30,7 +30,6 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace.ItemOperator;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -126,9 +125,6 @@
 
     private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
             Intent... commandIntents) throws Throwable {
-        if (!Utilities.ATLEAST_OREO) {
-            return;
-        }
         clearHomescreen();
         mDevice.pressHome();
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 0c4e5a9..c9c846f 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,8 +16,9 @@
 
 package com.android.launcher3.tapl;
 
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
+
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
-import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
 import android.graphics.Point;
@@ -93,7 +94,7 @@
 
                 mLauncher.sendPointer(
                         downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
-                mLauncher.executeAndWaitForEvent(
+                mLauncher.executeAndWaitForLauncherEvent(
                         () -> mLauncher.movePointer(
                                 downTime,
                                 downTime,
@@ -150,67 +151,68 @@
     /**
      * Swipes right or double presses the square button to switch to the previous app.
      */
+    @NonNull
     public Background quickSwitchToPreviousApp() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "want to quick switch to the previous app")) {
             verifyActiveContainer();
-            quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
+            final boolean launcherWasVisible = mLauncher.isLauncherVisible();
+            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;
+                    }
+                    final boolean isZeroButton = mLauncher.getNavigationModel()
+                            == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
+                    LauncherInstrumentation.GestureScope gestureScope =
+                            launcherWasVisible && isZeroButton
+                                    ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+                                    : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
+                    mLauncher.executeAndWaitForEvent(
+                            () -> mLauncher.linearGesture(
+                                    startX, startY, endX, endY, 20, false, gestureScope),
+                            event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                            () -> "Quick switch gesture didn't change window state");
+                    break;
+                }
+
+                case THREE_BUTTON:
+                    // Double press the recents button.
+                    UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                    mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
+                    mLauncher.getOverview();
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                    mLauncher.executeAndWaitForEvent(
+                            () -> recentsButton.click(),
+                            event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                            () -> "Pressing recents button didn't change window state");
+                    break;
+            }
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
             return new Background(mLauncher);
         }
     }
 
-    protected int getExpectedStateForQuickSwitch() {
-        return BACKGROUND_APP_STATE_ORDINAL;
-    }
-
-    protected void quickSwitchToPreviousApp(int expectedState) {
-        final boolean launcherWasVisible = mLauncher.isLauncherVisible();
-        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;
-                }
-                final boolean isZeroButton = mLauncher.getNavigationModel()
-                        == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
-                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
-                        launcherWasVisible && isZeroButton
-                                ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                                : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
-                break;
-            }
-
-            case THREE_BUTTON:
-                // Double press the recents button.
-                UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
-                mLauncher.getOverview();
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                recentsButton.click();
-                break;
-        }
-        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
-    }
-
     protected String getSwipeHeightRequestName() {
         return TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 223ae29..588b6b8 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -27,7 +27,7 @@
 import java.util.List;
 
 /**
- * Common overview pane for both Launcher and fallback recents
+ * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
@@ -135,4 +135,19 @@
     public boolean hasTasks() {
         return getTasks().size() > 0;
     }
+
+    /**
+     * Gets Overview Actions.
+     *
+     * @return The Overview Actions
+     */
+    @NonNull
+    public OverviewActions getOverviewActions() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get overview actions")) {
+            verifyActiveContainer();
+            UiObject2 overviewActions = mLauncher.waitForLauncherObject("action_buttons");
+            return new OverviewActions(overviewActions, mLauncher);
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index c06e254..0060844 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
-
 import androidx.annotation.NonNull;
 
 /**
@@ -65,8 +63,4 @@
         return true;
     }
 
-    @Override
-    protected int getExpectedStateForQuickSwitch() {
-        return QUICK_SWITCH_STATE_ORDINAL;
-    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 13ecfb8..093c024 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -56,15 +56,18 @@
     private Background launch(BySelector selector) {
         LauncherInstrumentation.log("Launchable.launch before click " +
                 mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
+        final String label = mObject.getText();
 
         mLauncher.executeAndWaitForEvent(
-                () -> mLauncher.clickLauncherObject(mObject),
+                () -> {
+                    mLauncher.clickLauncherObject(mObject);
+                    expectActivityStartEvents();
+                },
                 event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                () -> "Launching an app didn't open a new window: " + mObject.getText());
-        expectActivityStartEvents();
+                () -> "Launching an app didn't open a new window: " + label);
 
         mLauncher.assertTrue(
-                "App didn't start: " + selector,
+                "App didn't start: " + label,
                 mLauncher.getDevice().wait(Until.hasObject(selector),
                         LauncherInstrumentation.WAIT_TIME_MS));
         return new Background(mLauncher);
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index c74dea9..a77c1c6 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -103,6 +103,7 @@
 
     static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
     static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
+    private final String mLauncherPackage;
 
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
@@ -155,6 +156,7 @@
     public static final int WAIT_TIME_MS = 10000;
     public static final int LONG_WAIT_TIME_MS = 60000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final String ANDROID_PACKAGE = "android";
 
     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
 
@@ -173,8 +175,7 @@
     private Runnable mOnLauncherCrashed;
 
     private static Pattern getTouchEventPattern(String prefix, String action) {
-        // The pattern includes sanity checks that we don't get a multi-touch events or other
-        // surprises.
+        // The pattern includes checks that we don't get a multi-touch events or other surprises.
         return Pattern.compile(
                 prefix + ": MotionEvent.*?action=" + action + ".*?id\\[0\\]=0"
                         + ".*?toolType\\[0\\]=TOOL_TYPE_FINGER.*?buttonState=0.*?pointerCount=1");
@@ -216,11 +217,11 @@
         // Launcher package. As during inproc tests the tested launcher may not be selected as the
         // current launcher, choosing target package for inproc. For out-of-proc, use the installed
         // launcher package.
-        final String authorityPackage = testPackage.equals(targetPackage) ?
-                getLauncherPackageName() :
-                targetPackage;
+        mLauncherPackage = testPackage.equals(targetPackage)
+                ? getLauncherPackageName()
+                : targetPackage;
 
-        String testProviderAuthority = authorityPackage + ".TestInfo";
+        String testProviderAuthority = mLauncherPackage + ".TestInfo";
         mTestProviderUri = new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(testProviderAuthority)
@@ -450,7 +451,7 @@
             }
         }
 
-        dumpDiagnostics();
+        dumpDiagnostics(message);
 
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
@@ -458,10 +459,11 @@
         return message;
     }
 
-    private void dumpDiagnostics() {
-        Log.e("b/156287114", "Input:");
+    private void dumpDiagnostics(String message) {
+        log("Diagnostics for failure: " + message);
+        log("Input:");
         logShellCommand("dumpsys input");
-        Log.e("b/156287114", "TIS:");
+        log("TIS:");
         logShellCommand("dumpsys activity service TouchInteractionService");
     }
 
@@ -469,10 +471,10 @@
         try {
             for (String line : mDevice.executeShellCommand(command).split("\\n")) {
                 SystemClock.sleep(10);
-                Log.d("b/156287114", line);
+                log(line);
             }
         } catch (IOException e) {
-            Log.d("b/156287114", "Failed to execute " + command);
+            log("Failed to execute " + command);
         }
     }
 
@@ -628,6 +630,14 @@
         fail("Launcher didn't initialize");
     }
 
+    Parcelable executeAndWaitForLauncherEvent(Runnable command,
+            UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
+        return executeAndWaitForEvent(
+                command,
+                e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
+                message);
+    }
+
     Parcelable executeAndWaitForEvent(Runnable command,
             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
         try {
@@ -921,6 +931,14 @@
         return waitForObjectBySelector(getOverviewObjectSelector(resName));
     }
 
+    @NonNull
+    UiObject2 waitForAndroidObject(String resId) {
+        final UiObject2 object = mDevice.wait(
+                Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
+        assertNotNull("Can't find a android object with id: " + resId, object);
+        return object;
+    }
+
     private UiObject2 waitForObjectBySelector(BySelector selector) {
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
@@ -972,7 +990,7 @@
 
     void runToState(Runnable command, int expectedState) {
         final List<Integer> actualEvents = new ArrayList<>();
-        executeAndWaitForEvent(
+        executeAndWaitForLauncherEvent(
                 command,
                 event -> isSwitchToStateEvent(event, expectedState, actualEvents),
                 () -> "Failed to receive an event for the state change: expected ["
@@ -1087,7 +1105,7 @@
                 return;
         }
 
-        executeAndWaitForEvent(
+        executeAndWaitForLauncherEvent(
                 () -> linearGesture(
                         startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
                 event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
@@ -1297,6 +1315,11 @@
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    boolean overviewShareEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     private void disableSensorRotation() {
         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
     }
@@ -1362,7 +1385,7 @@
                 if (mCheckEventsForSuccessfulGestures) {
                     final String message = eventChecker.verify(WAIT_TIME_MS, true);
                     if (message != null) {
-                        dumpDiagnostics();
+                        dumpDiagnostics(message);
                         checkForAnomaly();
                         Assert.fail(formatSystemHealthMessage(
                                 "http://go/tapl : successful gesture produced " + message));
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
new file mode 100644
index 0000000..a30a404
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.TestProtocol;
+
+/**
+ * View containing overview actions
+ */
+public class OverviewActions {
+    private final UiObject2 mOverviewActions;
+    private final LauncherInstrumentation mLauncher;
+
+    OverviewActions(UiObject2 overviewActions, LauncherInstrumentation launcherInstrumentation) {
+        this.mOverviewActions = overviewActions;
+        this.mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Clicks screenshot button and closes screenshot ui.
+     */
+    @NonNull
+    public Overview clickAndDismissScreenshot() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to click screenshot button and exit screenshot ui")) {
+            UiObject2 screenshot = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_screenshot");
+            mLauncher.clickLauncherObject(screenshot);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked screenshot button")) {
+                UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject(
+                        "global_screenshot_dismiss_image");
+                if (mLauncher.getNavigationModel()
+                        != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+                }
+                closeScreenshot.click();
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "dismissed screenshot")) {
+                    return new Overview(mLauncher);
+                }
+            }
+        }
+    }
+
+    /**
+     * Click share button, then drags sharesheet down to remove it.
+     *
+     * Share is currently hidden behind flag, test is kept in case share becomes a default feature.
+     * If share is completely removed then remove this test as well.
+     */
+    @NonNull
+    public Overview clickAndDismissShare() {
+        if (mLauncher.overviewShareEnabled()) {
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+                 LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                         "want to click share button and dismiss sharesheet")) {
+                UiObject2 share = mLauncher.waitForObjectInContainer(mOverviewActions,
+                        "action_share");
+                mLauncher.clickLauncherObject(share);
+                try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                        "clicked share button")) {
+                    mLauncher.waitForAndroidObject("contentPanel");
+                    mLauncher.getDevice().pressBack();
+                    try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                            "dismissed sharesheet")) {
+                        return new Overview(mLauncher);
+                    }
+                }
+            }
+        }
+        return new Overview(mLauncher);
+    }
+
+    /**
+     * Click select button
+     *
+     * @return The select mode buttons that are now shown instead of action buttons.
+     */
+    @NonNull
+    public SelectModeButtons clickSelect() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click select button")) {
+            UiObject2 select = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_select");
+            mLauncher.clickLauncherObject(select);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked select button")) {
+                return getSelectModeButtons();
+            }
+
+        }
+    }
+
+    /**
+     * Gets the Select Mode Buttons.
+     *
+     * @return The Select Mode Buttons.
+     */
+    @NonNull
+    private SelectModeButtons getSelectModeButtons() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get select mode buttons")) {
+            UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons");
+            return new SelectModeButtons(selectModeButtons, mLauncher);
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
new file mode 100644
index 0000000..3507418
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * View containing select mode buttons
+ */
+public class SelectModeButtons {
+    private final UiObject2 mSelectModeButtons;
+    private final LauncherInstrumentation mLauncher;
+
+    SelectModeButtons(UiObject2 selectModeButtons,
+            LauncherInstrumentation launcherInstrumentation) {
+        mSelectModeButtons = selectModeButtons;
+        mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Click close button.
+     */
+    @NonNull
+    public Overview clickClose() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click close button")) {
+            UiObject2 close = mLauncher.waitForObjectInContainer(mSelectModeButtons, "close");
+            mLauncher.clickLauncherObject(close);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked close button")) {
+                return new Overview(mLauncher);
+            }
+        }
+    }
+
+    /**
+     * Click feedback button.
+     */
+    @NonNull
+    public Background clickFeedback() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click feedback button")) {
+            UiObject2 feedback = mLauncher.waitForObjectInContainer(mSelectModeButtons, "feedback");
+            mLauncher.clickLauncherObject(feedback);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked feedback button")) {
+                return new Background(mLauncher);
+            }
+        }
+    }
+}