Merge "Render preview layout in full widgets sheet and bottom widgets sheet" into sc-dev
diff --git a/Android.bp b/Android.bp
index a720658..9d675a4 100644
--- a/Android.bp
+++ b/Android.bp
@@ -24,7 +24,6 @@
     ],
     srcs: [
         "tests/tapl/**/*.java",
-        "src/com/android/launcher3/util/SecureSettingsObserver.java",
         "src/com/android/launcher3/ResourceUtils.java",
         "src/com/android/launcher3/testing/TestProtocol.java",
     ],
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 68c3851..a0e87cf 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -130,4 +130,5 @@
     <dimen name="taskbar_icon_spacing">14dp</dimen>
     <dimen name="taskbar_divider_thickness">1dp</dimen>
     <dimen name="taskbar_divider_height">24dp</dimen>
+    <dimen name="taskbar_folder_margin">16dp</dimen>
 </resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 5a353f0..df089f6 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -89,6 +89,5 @@
     <!-- Icon displayed on the taskbar -->
     <style name="BaseIcon.Workspace.Taskbar" >
         <item name="iconDisplay">taskbar</item>
-        <item name="iconSizeOverride">@dimen/taskbar_icon_size</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index edcd0a2..d1fa2fd 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -30,7 +30,6 @@
 import android.content.IntentSender;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.view.LayoutInflater;
 import android.view.View;
 
 import androidx.annotation.Nullable;
@@ -43,7 +42,7 @@
 import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.taskbar.TaskbarContainerView;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarController;
 import com.android.launcher3.taskbar.TaskbarStateHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
@@ -207,6 +206,7 @@
         mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
 
         addTaskbarIfNecessary();
+        addOnDeviceProfileChangeListener(newDp -> addTaskbarIfNecessary());
     }
 
     @Override
@@ -223,9 +223,9 @@
             mTaskbarController = null;
         }
         if (FeatureFlags.ENABLE_TASKBAR.get() && mDeviceProfile.isTablet) {
-            TaskbarContainerView taskbarContainer = (TaskbarContainerView) LayoutInflater.from(this)
-                    .inflate(R.layout.taskbar, null, false);
-            mTaskbarController = new TaskbarController(this, taskbarContainer);
+            TaskbarActivityContext taskbarActivityContext = new TaskbarActivityContext(this);
+            mTaskbarController = new TaskbarController(this,
+                    taskbarActivityContext.getTaskbarContainerView());
             mTaskbarController.init();
         }
     }
diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
new file mode 100644
index 0000000..96559cb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+
+import java.util.List;
+
+public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate {
+
+    public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) {
+        super(launcher);
+        mActions.put(PIN_PREDICTION, new LauncherAction(
+                PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P));
+    }
+
+    @Override
+    protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
+        if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) {
+            out.add(new LauncherAction(PIN_PREDICTION, R.string.pin_prediction,
+                    KeyEvent.KEYCODE_P));
+        }
+        super.getSupportedActions(host, item, out);
+    }
+
+    @Override
+    protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
+        QuickstepLauncher launcher = (QuickstepLauncher) mLauncher;
+        if (action == PIN_PREDICTION) {
+            if (launcher.getHotseatPredictionController() == null) {
+                return false;
+            }
+            launcher.getHotseatPredictionController().pinPrediction(item);
+            return true;
+        }
+        return super.performAction(host, item, action, fromKeyboard);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
new file mode 100644
index 0000000..06372fe
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import android.content.ContextWrapper;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+
+/**
+ * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements
+ * that are used by both Launcher and Taskbar (such as Folder) to reference a generic
+ * ActivityContext and BaseDragLayer instead of the Launcher activity and its DragLayer.
+ */
+public class TaskbarActivityContext extends ContextWrapper implements ActivityContext {
+
+    private final DeviceProfile mDeviceProfile;
+    private final LayoutInflater mLayoutInflater;
+    private final TaskbarContainerView mTaskbarContainerView;
+
+    public TaskbarActivityContext(BaseQuickstepLauncher launcher) {
+        super(launcher);
+        mDeviceProfile = launcher.getDeviceProfile().copy(this);
+        float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
+        float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
+        mDeviceProfile.updateIconSize(iconScale, getResources());
+
+        mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
+
+        mTaskbarContainerView = (TaskbarContainerView) mLayoutInflater
+                .inflate(R.layout.taskbar, null, false);
+    }
+
+    public TaskbarContainerView getTaskbarContainerView() {
+        return mTaskbarContainerView;
+    }
+
+    /**
+     * @return A LayoutInflater to use in this Context. Views inflated with this LayoutInflater will
+     * be able to access this TaskbarActivityContext via ActivityContext.lookupContext().
+     */
+    public LayoutInflater getLayoutInflater() {
+        return mLayoutInflater;
+    }
+
+    @Override
+    public BaseDragLayer<TaskbarActivityContext> getDragLayer() {
+        return mTaskbarContainerView;
+    }
+
+    @Override
+    public DeviceProfile getDeviceProfile() {
+        return mDeviceProfile;
+    }
+
+    @Override
+    public Rect getFolderBoundingBox() {
+        return mTaskbarContainerView.getFolderBoundingBox();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
index 3b361c4..ddd0d15 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
@@ -19,19 +19,29 @@
 import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.widget.FrameLayout;
+import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.R;
 import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
 import com.android.systemui.shared.system.ViewTreeObserverWrapper;
 
 /**
  * Top-level ViewGroup that hosts the TaskbarView as well as Views created by it such as Folder.
  */
-public class TaskbarContainerView extends FrameLayout {
+public class TaskbarContainerView extends BaseDragLayer<TaskbarActivityContext> {
+
+    private final int[] mTempLoc = new int[2];
+    private final int mFolderMargin;
+
+    // Initialized in TaskbarController constructor.
+    private TaskbarController.TaskbarContainerViewCallbacks mControllerCallbacks;
 
     // Initialized in init.
     private TaskbarView mTaskbarView;
@@ -52,12 +62,23 @@
 
     public TaskbarContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+        super(context, attrs, 1 /* alphaChannelCount */);
+        mFolderMargin = getResources().getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+    }
+
+    protected void construct(TaskbarController.TaskbarContainerViewCallbacks callbacks) {
+        mControllerCallbacks = callbacks;
     }
 
     protected void init(TaskbarView taskbarView) {
         mTaskbarView = taskbarView;
         mTaskbarInsetsComputer = createTaskbarInsetsComputer();
+        recreateControllers();
+    }
+
+    @Override
+    public void recreateControllers() {
+        mControllers = new TouchController[0];
     }
 
     private ViewTreeObserverWrapper.OnComputeInsetsListener createTaskbarInsetsComputer() {
@@ -70,6 +91,17 @@
                  // We're visible again, accept touches anywhere in our bounds.
                 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
             }
+
+            // TaskbarContainerView provides insets to other apps based on contentInsets. These
+            // insets should stay consistent even if we expand TaskbarContainerView's bounds, e.g.
+            // to show a floating view like Folder. Thus, we set the contentInsets to be where
+            // mTaskbarView is, since its position never changes and insets rather than overlays.
+            int[] loc = mTempLoc;
+            mTaskbarView.getLocationInWindow(loc);
+            insetsInfo.contentInsets.left = loc[0];
+            insetsInfo.contentInsets.top = loc[1];
+            insetsInfo.contentInsets.right = getWidth() - (loc[0] + mTaskbarView.getWidth());
+            insetsInfo.contentInsets.bottom = getHeight() - (loc[1] + mTaskbarView.getHeight());
         };
     }
 
@@ -91,4 +123,30 @@
 
         cleanup();
     }
+
+    @Override
+    protected boolean canFindActiveController() {
+        // Unlike super class, we want to be able to find controllers when touches occur in the
+        // gesture area. For example, this allows Folder to close itself when touching the Taskbar.
+        return true;
+    }
+
+    @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        mControllerCallbacks.onViewRemoved();
+    }
+
+    /**
+     * @return Bounds (in our coordinates) where an opened Folder can display.
+     */
+    protected Rect getFolderBoundingBox() {
+        Rect boundingBox = new Rect(0, 0, getWidth(), getHeight() - mTaskbarView.getHeight());
+        boundingBox.inset(mFolderMargin, mFolderMargin);
+        return boundingBox;
+    }
+
+    protected TaskbarActivityContext getTaskbarActivityContext() {
+        return mActivity;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index 260428d..ab05fbf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -34,11 +34,15 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.ItemClickHandler;
@@ -81,8 +85,9 @@
             TaskbarContainerView taskbarContainerView) {
         mLauncher = launcher;
         mTaskbarContainerView = taskbarContainerView;
+        mTaskbarContainerView.construct(createTaskbarContainerViewCallbacks());
         mTaskbarView = mTaskbarContainerView.findViewById(R.id.taskbar_view);
-        mTaskbarView.setCallbacks(createTaskbarViewCallbacks());
+        mTaskbarView.construct(createTaskbarViewCallbacks());
         mWindowManager = mLauncher.getWindowManager();
         mTaskbarSize = new Point(MATCH_PARENT,
                 mLauncher.getResources().getDimensionPixelSize(R.dimen.taskbar_size));
@@ -110,6 +115,18 @@
         };
     }
 
+    private TaskbarContainerViewCallbacks createTaskbarContainerViewCallbacks() {
+        return new TaskbarContainerViewCallbacks() {
+            @Override
+            public void onViewRemoved() {
+                if (mTaskbarContainerView.getChildCount() == 1) {
+                    // Only TaskbarView remains.
+                    setTaskbarWindowFullscreen(false);
+                }
+            }
+        };
+    }
+
     private TaskbarViewCallbacks createTaskbarViewCallbacks() {
         return new TaskbarViewCallbacks() {
             @Override
@@ -120,9 +137,29 @@
                         Task task = (Task) tag;
                         ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
                                 ActivityOptions.makeBasic());
+                    } else if (tag instanceof FolderInfo) {
+                        FolderIcon folderIcon = (FolderIcon) view;
+                        Folder folder = folderIcon.getFolder();
+
+                        setTaskbarWindowFullscreen(true);
+
+                        mTaskbarContainerView.post(() -> {
+                            folder.animateOpen();
+
+                            folder.iterateOverItems((itemInfo, itemView) -> {
+                                itemView.setOnClickListener(getItemOnClickListener());
+                                itemView.setOnLongClickListener(getItemOnLongClickListener());
+                                // To play haptic when dragging, like other Taskbar items do.
+                                itemView.setHapticFeedbackEnabled(true);
+                                return false;
+                            });
+                        });
                     } else {
                         ItemClickHandler.INSTANCE.onClick(view);
                     }
+
+                    AbstractFloatingView.closeAllOpenViews(
+                            mTaskbarContainerView.getTaskbarActivityContext());
                 };
             }
 
@@ -345,6 +382,20 @@
     }
 
     /**
+     * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
+     */
+    private void setTaskbarWindowFullscreen(boolean fullscreen) {
+        if (fullscreen) {
+            mWindowLayoutParams.width = MATCH_PARENT;
+            mWindowLayoutParams.height = MATCH_PARENT;
+        } else {
+            mWindowLayoutParams.width = mTaskbarSize.x;
+            mWindowLayoutParams.height = mTaskbarSize.y;
+        }
+        mWindowManager.updateViewLayout(mTaskbarContainerView, mWindowLayoutParams);
+    }
+
+    /**
      * Contains methods that TaskbarStateHandler can call to interface with TaskbarController.
      */
     protected interface TaskbarStateHandlerCallbacks {
@@ -361,6 +412,13 @@
     }
 
     /**
+     * Contains methods that TaskbarContainerView can call to interface with TaskbarController.
+     */
+    protected interface TaskbarContainerViewCallbacks {
+        void onViewRemoved();
+    }
+
+    /**
      * Contains methods that TaskbarView can call to interface with TaskbarController.
      */
     protected interface TaskbarViewCallbacks {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index d8f3bb5..7a13b89 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -17,12 +17,12 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Canvas;
 import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.DragEvent;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -32,16 +32,20 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
 import com.android.systemui.shared.recents.model.Task;
 
 /**
  * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
  */
-public class TaskbarView extends LinearLayout {
+public class TaskbarView extends LinearLayout implements FolderIcon.FolderIconParent {
 
     private final ColorDrawable mBackgroundDrawable;
     private final int mItemMarginLeftRight;
@@ -51,6 +55,9 @@
     private final RectF mDelegateSlopBounds = new RectF();
     private final int[] mTempOutLocation = new int[2];
 
+    // Initialized in TaskbarController constructor.
+    private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
+
     // Initialized in init().
     private int mHotseatStartIndex;
     private int mHotseatEndIndex;
@@ -58,13 +65,13 @@
     private int mRecentsStartIndex;
     private int mRecentsEndIndex;
 
-    private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
-
     // Delegate touches to the closest view if within mIconTouchSize.
     private boolean mDelegateTargeted;
     private View mDelegateView;
 
     private boolean mIsDraggingItem;
+    // Only non-null when the corresponding Folder is open.
+    private @Nullable FolderIcon mLeaveBehindFolderIcon;
 
     public TaskbarView(@NonNull Context context) {
         this(context, null);
@@ -90,7 +97,7 @@
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     }
 
-    protected void setCallbacks(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
+    protected void construct(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
         mControllerCallbacks = taskbarViewCallbacks;
     }
 
@@ -130,17 +137,37 @@
 
             // Replace any Hotseat views with the appropriate type if it's not already that type.
             final int expectedLayoutResId;
+            boolean isFolder = false;
+            boolean needsReinflate = false;
             if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
                 expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
+            } else if (hotseatItemInfo instanceof FolderInfo) {
+                expectedLayoutResId = R.layout.folder_icon;
+                isFolder = true;
+                // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation, so
+                // if the info changes we need to reinflate. This should only happen if a new folder
+                // is dragged to the position that another folder previously existed.
+                needsReinflate = hotseatView != null && hotseatView.getTag() != hotseatItemInfo;
             } else {
                 expectedLayoutResId = R.layout.taskbar_app_icon;
             }
-            if (hotseatView == null || hotseatView.getSourceLayoutResId() != expectedLayoutResId) {
+            if (hotseatView == null || hotseatView.getSourceLayoutResId() != expectedLayoutResId
+                    || needsReinflate) {
                 removeView(hotseatView);
-                BubbleTextView btv = (BubbleTextView) inflate(expectedLayoutResId);
-                LayoutParams lp = new LayoutParams(btv.getIconSize(), btv.getIconSize());
+                TaskbarActivityContext activityContext =
+                        ActivityContext.lookupContext(getContext());
+                if (isFolder) {
+                    FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
+                    FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
+                            activityContext, this, folderInfo);
+                    folderIcon.setTextVisible(false);
+                    hotseatView = folderIcon;
+                } else {
+                    hotseatView = inflate(expectedLayoutResId);
+                }
+                int iconSize = activityContext.getDeviceProfile().iconSizePx;
+                LayoutParams lp = new LayoutParams(iconSize, iconSize);
                 lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
-                hotseatView = btv;
                 addView(hotseatView, hotseatIndex, lp);
             }
 
@@ -153,6 +180,11 @@
                 hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
                 hotseatView.setOnLongClickListener(
                         mControllerCallbacks.getItemOnLongClickListener());
+            } else if (isFolder) {
+                hotseatView.setVisibility(VISIBLE);
+                hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+                hotseatView.setOnLongClickListener(
+                        mControllerCallbacks.getItemOnLongClickListener());
             } else {
                 hotseatView.setVisibility(GONE);
                 hotseatView.setOnClickListener(null);
@@ -345,6 +377,7 @@
         switch (event.getAction()) {
             case DragEvent.ACTION_DRAG_STARTED:
                 mIsDraggingItem = true;
+                AbstractFloatingView.closeAllOpenViews(ActivityContext.lookupContext(getContext()));
                 return true;
             case DragEvent.ACTION_DRAG_ENDED:
                 mIsDraggingItem = false;
@@ -357,7 +390,35 @@
         return mIsDraggingItem;
     }
 
+    // FolderIconParent implemented methods.
+
+    @Override
+    public void drawFolderLeaveBehindForIcon(FolderIcon child) {
+        mLeaveBehindFolderIcon = child;
+        invalidate();
+    }
+
+    @Override
+    public void clearFolderLeaveBehind(FolderIcon child) {
+        mLeaveBehindFolderIcon = null;
+        invalidate();
+    }
+
+    // End FolderIconParent implemented methods.
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mLeaveBehindFolderIcon != null) {
+            canvas.save();
+            canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
+            mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas);
+            canvas.restore();
+        }
+    }
+
     private View inflate(@LayoutRes int layoutResId) {
-        return LayoutInflater.from(getContext()).inflate(layoutResId, this, false);
+        TaskbarActivityContext taskbarActivityContext = ActivityContext.lookupContext(getContext());
+        return taskbarActivityContext.getLayoutInflater().inflate(layoutResId, this, false);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 2d50125..98551fb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION;
 import static com.android.launcher3.graphics.IconShape.getShape;
 
 import android.content.Context;
@@ -29,7 +28,6 @@
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.core.graphics.ColorUtils;
 
@@ -37,9 +35,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
@@ -53,8 +49,7 @@
 /**
  * A BubbleTextView with a ring around it's drawable
  */
-public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
-        LauncherAccessibilityDelegate.AccessibilityActionHandler {
+public class PredictedAppIcon extends DoubleShadowBubbleTextView {
 
     private static final int RING_SHADOW_COLOR = 0x99000000;
     private static final float RING_EFFECT_RATIO = 0.095f;
@@ -148,29 +143,6 @@
     }
 
     @Override
-    public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) {
-        if (!mIsPinned) {
-            accessibilityNodeInfo.addAction(
-                    new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
-                            getContext().getText(R.string.pin_prediction)));
-        }
-    }
-
-    @Override
-    public boolean performAccessibilityAction(int action, ItemInfo info) {
-        QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext()));
-        if (action == PIN_PREDICTION) {
-            if (launcher == null) {
-                return false;
-            }
-            HotseatPredictionController controller = launcher.getHotseatPredictionController();
-            controller.pinPrediction(info);
-            return true;
-        }
-        return false;
-    }
-
-    @Override
     public void getIconBounds(Rect outBounds) {
         super.getIconBounds(outBounds);
         if (!mIsPinned && !mIsDrawingDot) {
@@ -179,6 +151,10 @@
         }
     }
 
+    public boolean isPinned() {
+        return mIsPinned;
+    }
+
     private int getOutlineOffsetX() {
         return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index d98e792..55dde45 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -43,7 +43,9 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepAccessibilityDelegate;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
@@ -134,6 +136,11 @@
         mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
     }
 
+    @Override
+    protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+        return new QuickstepAccessibilityDelegate(this);
+    }
+
     /**
      * Returns Prediction controller for hybrid hotseat
      */
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 70b4f20..1c5dc4c 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -191,6 +191,7 @@
             } else {
                 task = new Task(taskKey);
             }
+            task.setLastSnapshotData(rawTask);
             allTasks.add(task);
         }
 
@@ -200,9 +201,7 @@
     private ArrayList<Task> copyOf(ArrayList<Task> tasks) {
         ArrayList<Task> newTasks = new ArrayList<>();
         for (int i = 0; i < tasks.size(); i++) {
-            Task t = tasks.get(i);
-            newTasks.add(new Task(t.key, t.colorPrimary, t.colorBackground, t.isDockable,
-                    t.isLocked, t.taskDescription, t.topActivity));
+            newTasks.add(new Task(tasks.get(i)));
         }
         return newTasks;
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 4301377..f99b7e6 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -17,14 +17,16 @@
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 
+import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED;
+import static com.android.launcher3.util.SettingsCache.ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED;
 import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_FRAME_DELAY;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
@@ -44,6 +46,7 @@
 import android.content.IntentFilter;
 import android.content.res.Resources;
 import android.graphics.Region;
+import android.net.Uri;
 import android.os.Process;
 import android.os.SystemProperties;
 import android.os.UserManager;
@@ -57,11 +60,11 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayHolder;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.SysUINavigationMode.OneHandedModeChangeListener;
 import com.android.quickstep.util.NavBarPosition;
@@ -177,33 +180,33 @@
             }
         }
 
+        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
         if (mIsOneHandedModeSupported) {
-            SecureSettingsObserver oneHandedEnabledObserver =
-                    SecureSettingsObserver.newOneHandedSettingsObserver(
-                            mContext, enabled -> mIsOneHandedModeEnabled = enabled);
-            oneHandedEnabledObserver.register();
-            oneHandedEnabledObserver.dispatchOnChange();
-            runOnDestroy(oneHandedEnabledObserver::unregister);
+            Uri oneHandedUri = Settings.Secure.getUriFor(ONE_HANDED_ENABLED);
+            SettingsCache.OnChangeListener onChangeListener =
+                    enabled -> mIsOneHandedModeEnabled = enabled;
+            settingsCache.register(oneHandedUri, onChangeListener);
+            settingsCache.dispatchOnChange(oneHandedUri);
+            runOnDestroy(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
         } 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,
-                Settings.Secure.USER_SETUP_COMPLETE,
-                0);
-        mIsUserSetupComplete = userSetupObserver.getValue();
+        Uri swipeBottomNotificationUri =
+                Settings.Secure.getUriFor(ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED);
+        SettingsCache.OnChangeListener onChangeListener =
+                enabled -> mIsSwipeToNotificationEnabled = enabled;
+        settingsCache.register(swipeBottomNotificationUri, onChangeListener);
+        settingsCache.dispatchOnChange(swipeBottomNotificationUri);
+        runOnDestroy(() -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener));
+
+        Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+        mIsUserSetupComplete = settingsCache.getValue(setupCompleteUri, 0);
         if (!mIsUserSetupComplete) {
-            userSetupObserver.register();
-            runOnDestroy(userSetupObserver::unregister);
+            SettingsCache.OnChangeListener userSetupChangeListener = e -> mIsUserSetupComplete = e;
+            settingsCache.register(setupCompleteUri, userSetupChangeListener);
+            runOnDestroy(() -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 3157865..f336bf5 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -28,7 +28,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED;
 import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -43,7 +43,7 @@
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
@@ -77,11 +77,10 @@
         getPrefs(context).registerOnSharedPreferenceChangeListener(this);
         getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
 
-        SecureSettingsObserver dotsObserver =
-                newNotificationSettingsObserver(context, this::onNotificationDotsChanged);
-        mNotificationDotsEnabled = dotsObserver.getValue();
-        dispatchUserEvent();
-
+        SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context);
+        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                this::onNotificationDotsChanged);
+        mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
     }
 
     private static ArrayMap<String, LoggablePref> loadPrefKeys(Context context) {
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index a3ee912..215f05a 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -23,22 +23,18 @@
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
-import static com.android.launcher3.Utilities.newContentObserver;
+import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.Handler;
-import android.provider.Settings;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.OrientationEventListener;
@@ -51,6 +47,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.WindowBounds;
@@ -72,9 +69,6 @@
     private static final String TAG = "RecentsOrientedState";
     private static final boolean DEBUG = false;
 
-    private ContentObserver mSystemAutoRotateObserver =
-            newContentObserver(new Handler(), t -> updateAutoRotateSetting());
-
     @Retention(SOURCE)
     @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
     public @interface SurfaceRotation {}
@@ -118,9 +112,11 @@
                     | FLAG_SWIPE_UP_NOT_RUNNING;
 
     private final Context mContext;
-    private final ContentResolver mContentResolver;
     private final SharedPreferences mSharedPrefs;
     private final OrientationEventListener mOrientationListener;
+    private final SettingsCache mSettingsCache;
+    private final SettingsCache.OnChangeListener mRotationChangeListener =
+            isEnabled -> updateAutoRotateSetting();
 
     private final Matrix mTmpMatrix = new Matrix();
 
@@ -138,7 +134,6 @@
     public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
             IntConsumer rotationChangeListener) {
         mContext = context;
-        mContentResolver = context.getContentResolver();
         mSharedPrefs = Utilities.getPrefs(context);
         mOrientationListener = new OrientationEventListener(context) {
             @Override
@@ -162,6 +157,7 @@
             mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
         }
         mFlags |= FLAG_SWIPE_UP_NOT_RUNNING;
+        mSettingsCache = SettingsCache.INSTANCE.get(mContext);
         initFlags();
     }
 
@@ -271,8 +267,8 @@
     }
 
     private void updateAutoRotateSetting() {
-        setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver,
-                Settings.System.ACCELEROMETER_ROTATION, 1) == 1);
+        setFlag(FLAG_SYSTEM_ROTATION_ALLOWED,
+                mSettingsCache.getValue(ROTATION_SETTING_URI, 1));
     }
 
     private void updateHomeRotationSetting() {
@@ -295,9 +291,7 @@
     public void initListeners() {
         if (isMultipleOrientationSupportedByDevice()) {
             mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
-            mContentResolver.registerContentObserver(
-                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
-                    false, mSystemAutoRotateObserver);
+            mSettingsCache.register(ROTATION_SETTING_URI, mRotationChangeListener);
         }
         initFlags();
     }
@@ -308,7 +302,7 @@
     public void destroyListeners() {
         if (isMultipleOrientationSupportedByDevice()) {
             mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
-            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+            mSettingsCache.unregister(ROTATION_SETTING_URI, mRotationChangeListener);
         }
         setRotationWatcherEnabled(false);
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5d492ac..deb1388 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -646,9 +646,9 @@
 
     public TaskView getTaskView(int taskId) {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = getTaskViewAt(i);
-            if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) {
-                return tv;
+            TaskView taskView = getTaskViewAt(i);
+            if (taskView.hasTaskId(taskId)) {
+                return taskView;
             }
         }
         return null;
@@ -808,6 +808,7 @@
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
             taskView.bind(task, mOrientationState);
+            taskView.updateTaskSize(!taskView.hasTaskId(mRunningTaskId));
         }
 
         if (mNextPage == INVALID_PAGE) {
@@ -942,7 +943,8 @@
         // Force TaskView to update size from thumbnail
         final int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).updateTaskSize();
+            TaskView taskView = getTaskViewAt(i);
+            taskView.updateTaskSize(!taskView.hasTaskId(mRunningTaskId));
         }
     }
 
@@ -1252,6 +1254,7 @@
             // gesture and the task list is loaded and applied
             mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
             taskView.bind(mTmpRunningTask, mOrientationState);
+            taskView.updateTaskSize(false);
 
             // Measure and layout immediately so that the scroll values is updated instantly
             // as the user might be quick-switching
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 3bd883d..f2f4bc1 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -111,9 +111,6 @@
     private boolean mOverlayEnabled;
     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
 
-    // TODO(b/179466077): Remove when proper API is ready.
-    private Float mThumbnailRatio = null;
-
     public TaskThumbnailView(Context context) {
         this(context, null);
     }
@@ -454,31 +451,6 @@
         return mThumbnailData.isRealSnapshot;
     }
 
-    // TODO(b/179466077): Remove when proper API is ready.
-    public float getThumbnailRatio() {
-        // API is ready.
-        if (mThumbnailRatio != null) {
-            return mThumbnailRatio;
-        }
-
-        if (mThumbnailData == null || mThumbnailData.thumbnail == null) {
-            final float[] thumbnailRatios =
-                    new float[]{0.8882452f, 1.2834098f, 0.5558415f, 2.15625f};
-            // Use key's hash code to return a deterministic thumbnail ratio.
-            mThumbnailRatio = thumbnailRatios[mTask.key.hashCode() % thumbnailRatios.length];
-            return mThumbnailRatio;
-        }
-
-        float surfaceWidth = mThumbnailData.thumbnail.getWidth() / mThumbnailData.scale;
-        float surfaceHeight = mThumbnailData.thumbnail.getHeight() / mThumbnailData.scale;
-        float availableWidth = surfaceWidth
-                - (mThumbnailData.insets.left + mThumbnailData.insets.right);
-        float availableHeight = surfaceHeight
-                - (mThumbnailData.insets.top + mThumbnailData.insets.bottom);
-        mThumbnailRatio = availableWidth / availableHeight;
-        return mThumbnailRatio;
-    }
-
     /**
      * Utility class to position the thumbnail in the TaskView
      */
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index a1b5533..e891c95 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -48,6 +48,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Outline;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
@@ -449,7 +450,6 @@
         cancelPendingLoadTasks();
         mTask = task;
         mSnapshotView.bind(task);
-        updateTaskSize();
         setOrientationState(orientedState);
     }
 
@@ -457,6 +457,10 @@
         return mTask;
     }
 
+    public boolean hasTaskId(int taskId) {
+        return mTask != null && mTask.key != null && mTask.key.id == taskId;
+    }
+
     public TaskThumbnailView getThumbnail() {
         return mSnapshotView;
     }
@@ -1077,9 +1081,11 @@
                 previewPositionHelper);
     }
 
-    void updateTaskSize() {
+    void updateTaskSize(boolean variableWidth) {
         ViewGroup.LayoutParams params = getLayoutParams();
-        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+        float thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio() : 0f;
+        if (variableWidth && mActivity.getDeviceProfile().isTablet
+                && FeatureFlags.ENABLE_OVERVIEW_GRID.get() && thumbnailRatio != 0f) {
             final int thumbnailPadding = (int) getResources().getDimension(
                     R.dimen.task_thumbnail_top_margin);
 
@@ -1087,7 +1093,6 @@
             int taskWidth = lastComputedTaskSize.width();
             int taskHeight = lastComputedTaskSize.height();
             int boxLength = Math.max(taskWidth, taskHeight);
-            float thumbnailRatio = mSnapshotView.getThumbnailRatio();
 
             int expectedWidth;
             int expectedHeight;
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 0f6671d..35383d2 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -168,7 +168,7 @@
 
         Log.d(TAG, "setActiveOverlay: " + overlayPackage + "...");
         UiDevice.getInstance(getInstrumentation()).executeShellCommand(
-                "cmd overlay enable-exclusive " + overlayPackage);
+                "cmd overlay enable-exclusive --category " + overlayPackage);
 
         if (currentSysUiNavigationMode() != expectedMode) {
             final CountDownLatch latch = new CountDownLatch(1);
diff --git a/res/drawable/ic_widget_height_decrease.xml b/res/drawable/ic_widget_height_decrease.xml
new file mode 100644
index 0000000..df704ba
--- /dev/null
+++ b/res/drawable/ic_widget_height_decrease.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2021 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="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary">
+  <path
+      android:fillColor="#FFFFFF"
+      android:pathData="M8,19h3v3h2v-3h3l-4,-4 -4,4zM16,4h-3L13,1h-2v3L8,4l4,4 4,-4zM4,9v2h16L20,9L4,9zM4,12h16v2H4z"/>
+</vector>
diff --git a/res/drawable/ic_widget_height_increase.xml b/res/drawable/ic_widget_height_increase.xml
new file mode 100644
index 0000000..c263a4b
--- /dev/null
+++ b/res/drawable/ic_widget_height_increase.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2021 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="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary">
+  <path
+      android:fillColor="#FFFFFF"
+      android:pathData="M4,20h16v2L4,22zM4,2h16v2L4,4zM13,9h3l-4,-4 -4,4h3v6L8,15l4,4 4,-4h-3z"/>
+</vector>
diff --git a/res/drawable/ic_widget_width_decrease.xml b/res/drawable/ic_widget_width_decrease.xml
new file mode 100644
index 0000000..2a2fad7
--- /dev/null
+++ b/res/drawable/ic_widget_width_decrease.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2021 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.
+-->
+<rotate
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:drawable="@drawable/ic_widget_height_decrease"
+  android:pivotX="50%"
+  android:pivotY="50%"
+  android:fromDegrees="90"
+  android:toDegrees="90" />
diff --git a/res/drawable/ic_widget_width_increase.xml b/res/drawable/ic_widget_width_increase.xml
new file mode 100644
index 0000000..89b9f40
--- /dev/null
+++ b/res/drawable/ic_widget_width_increase.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2021 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.
+-->
+<rotate
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/ic_widget_height_increase"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:fromDegrees="90"
+    android:toDegrees="90" />
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 923352e..c230dad 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -27,8 +27,8 @@
         android:clipToPadding="false"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:paddingLeft="8dp"
-        android:paddingRight="8dp"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
         android:paddingTop="16dp"
         launcher:pageIndicator="@+id/folder_page_indicator" />
 
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index b8600a6..6baf39e 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -17,17 +17,17 @@
 */
 -->
 <resources>
-    <color name="popup_color_primary_light">@android:color/system_main_50</color>
-    <color name="popup_color_secondary_light">@android:color/system_main_100</color>
-    <color name="popup_color_tertiary_light">@android:color/system_main_300</color>
-    <color name="popup_color_primary_dark">@android:color/system_main_800</color>
-    <color name="popup_color_secondary_dark">@android:color/system_main_900</color>
-    <color name="popup_color_tertiary_dark">@android:color/system_main_700</color>
+    <color name="popup_color_primary_light">@android:color/system_primary_50</color>
+    <color name="popup_color_secondary_light">@android:color/system_primary_100</color>
+    <color name="popup_color_tertiary_light">@android:color/system_primary_300</color>
+    <color name="popup_color_primary_dark">@android:color/system_primary_800</color>
+    <color name="popup_color_secondary_dark">@android:color/system_primary_900</color>
+    <color name="popup_color_tertiary_dark">@android:color/system_primary_700</color>
 
-    <color name="workspace_text_color_light">@android:color/system_main_50</color>
-    <color name="workspace_text_color_dark">@android:color/system_main_900</color>
+    <color name="workspace_text_color_light">@android:color/system_primary_50</color>
+    <color name="workspace_text_color_dark">@android:color/system_primary_900</color>
 
-    <color name="text_color_primary_dark">@android:color/system_main_50</color>
-    <color name="text_color_secondary_dark">@android:color/system_main_200</color>
-    <color name="text_color_tertiary_dark">@android:color/system_main_400</color>
+    <color name="text_color_primary_dark">@android:color/system_primary_50</color>
+    <color name="text_color_secondary_dark">@android:color/system_primary_200</color>
+    <color name="text_color_tertiary_dark">@android:color/system_primary_400</color>
 </resources>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index b19ea22..587df6d 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -135,6 +135,16 @@
         <attr name="demoModeLayoutId" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="DevicePadding">
+        <attr name="maxEmptySpace" format="dimension" />
+    </declare-styleable>
+
+    <declare-styleable name="DevicePaddingFormula">
+        <attr name="a" format="float|dimension" />
+        <attr name="b" format="float|dimension" />
+        <attr name="c" format="float|dimension" />
+    </declare-styleable>
+
     <declare-styleable name="ProfileDisplayOption">
         <attr name="name" />
         <attr name="minWidthDps" format="float" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index eaf7a5f..84dfca5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,12 +20,14 @@
 
     <!-- Dynamic Grid -->
     <dimen name="dynamic_grid_edge_margin">8dp</dimen>
-    <dimen name="dynamic_grid_icon_drawable_padding">8dp</dimen>
+    <dimen name="dynamic_grid_icon_drawable_padding">7dp</dimen>
     <!-- Minimum space between workspace and hotseat in spring loaded mode -->
     <dimen name="dynamic_grid_min_spring_loaded_space">8dp</dimen>
 
+    <dimen name="dynamic_grid_cell_border_spacing">16dp</dimen>
     <dimen name="dynamic_grid_cell_layout_padding">5.5dp</dimen>
     <dimen name="dynamic_grid_cell_padding_x">8dp</dimen>
+    <dimen name="dynamic_grid_cell_padding_y">7dp</dimen>
 
     <!-- Hotseat -->
     <dimen name="dynamic_grid_hotseat_top_padding">8dp</dimen>
@@ -147,9 +149,9 @@
     <dimen name="folder_cell_x_padding">9dp</dimen>
     <dimen name="folder_cell_y_padding">6dp</dimen>
     <dimen name="folder_child_text_size">13sp</dimen>
-    <dimen name="folder_label_padding_top">4dp</dimen>
+    <dimen name="folder_label_padding_top">12dp</dimen>
     <dimen name="folder_label_padding_bottom">12dp</dimen>
-    <dimen name="folder_label_text_size">14sp</dimen>
+    <dimen name="folder_label_text_size">16sp</dimen>
 
 <!-- Sizes for managed profile badges -->
     <dimen name="profile_badge_size">24dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c30019b..5a9def7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -37,8 +37,6 @@
     <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</string>
-    <!-- Label for showing custom action list of a shortcut or widget. [CHAR_LIMIT=30] -->
-    <string name="custom_actions">Custom actions</string>
 
     <!-- Widgets -->
     <!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
diff --git a/res/xml/size_limits.xml b/res/xml/size_limits.xml
new file mode 100644
index 0000000..ba57014
--- /dev/null
+++ b/res/xml/size_limits.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 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.
+-->
+
+<device-paddings xmlns:launcher="http://schemas.android.com/apk/res-auto" >
+
+    <device-padding
+        launcher:maxEmptySpace="88dp">
+        <workspaceTopPadding
+            launcher:a="0"
+            launcher:b="0"/>
+        <workspaceBottomPadding
+            launcher:a="0.52"
+            launcher:b="0"/>
+        <hotseatBottomPadding
+            launcher:a="0.48"
+            launcher:b="0"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="97dp">
+        <workspaceTopPadding
+            launcher:a="0"
+            launcher:b="16dp"/>
+        <workspaceBottomPadding
+            launcher:a="0.50"
+            launcher:b="0"
+            launcher:c="-16dp"/>
+        <hotseatBottomPadding
+            launcher:a="0.50"
+            launcher:b="0"
+            launcher:c="16dp"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="107dp">
+        <workspaceTopPadding
+            launcher:a="0"
+            launcher:b="16dp"/>
+        <workspaceBottomPadding
+            launcher:a="0"
+            launcher:b="36dp"/>
+        <hotseatBottomPadding
+            launcher:a="1"
+            launcher:b="0"
+            launcher:c="52dp"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="9999dp">
+        <workspaceTopPadding
+            launcher:a="0.38"
+            launcher:c="36dp"/>
+        <workspaceBottomPadding
+            launcher:a="0.62"
+            launcher:c="36dp"/>
+        <hotseatBottomPadding
+            launcher:a="0"
+            launcher:b="36dp"/>
+    </device-padding>
+
+</device-paddings>
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/launcher3/util/SettingsCacheTest.java b/robolectric_tests/src/com/android/launcher3/util/SettingsCacheTest.java
new file mode 100644
index 0000000..fbf4c63
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/SettingsCacheTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class SettingsCacheTest {
+
+    public static final Uri KEY_SYSTEM_URI_TEST1 = Uri.parse("content://settings/system/test1");
+    public static final Uri KEY_SYSTEM_URI_TEST2 = Uri.parse("content://settings/system/test2");;
+
+    private SettingsCache.OnChangeListener mChangeListener;
+    private SettingsCache mSettingsCache;
+
+    @Before
+    public void setup() {
+        mChangeListener = mock(SettingsCache.OnChangeListener.class);
+        Context targetContext = RuntimeEnvironment.application;
+        mSettingsCache = SettingsCache.INSTANCE.get(targetContext);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST1, mChangeListener);
+    }
+
+    @Test
+    public void listenerCalledOnChange() {
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        verify(mChangeListener, times(1)).onSettingsChanged(true);
+    }
+
+    @Test
+    public void getValueRespectsDefaultValue() {
+        // Case of key not found
+        boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertFalse(val);
+    }
+
+    @Test
+    public void getValueHitsCache() {
+        mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true));
+        boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertTrue(val);
+    }
+
+    @Test
+    public void getValueUpdatedCache() {
+        // First ensure there's nothing in cache
+        boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertFalse(val);
+
+        mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true));
+        val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertTrue(val);
+    }
+
+    @Test
+    public void multipleListenersSingleKey() {
+        SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST1, secondListener);
+
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        verify(mChangeListener, times(1)).onSettingsChanged(true);
+        verify(secondListener, times(1)).onSettingsChanged(true);
+    }
+
+    @Test
+    public void singleListenerMultipleKeys() {
+        SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST2, secondListener);
+
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2);
+        verify(mChangeListener, times(1)).onSettingsChanged(true);
+        verify(secondListener, times(1)).onSettingsChanged(true);
+    }
+
+    @Test
+    public void sameListenerMultipleKeys() {
+        SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST2, mChangeListener);
+
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2);
+        verify(mChangeListener, times(2)).onSettingsChanged(true);
+        verify(secondListener, times(0)).onSettingsChanged(true);
+    }
+}
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 6037c96..95cdbdd 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -59,7 +59,7 @@
             TYPE_SNACKBAR,
             TYPE_LISTENER,
             TYPE_ALL_APPS_EDU,
-
+            TYPE_DRAG_DROP_POPUP,
             TYPE_TASK_MENU,
             TYPE_OPTIONS_POPUP,
             TYPE_ICON_SURFACE
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index bc3e341..1330ed4 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -279,8 +279,9 @@
      *  Based on the current deltas, we determine if and how to resize the widget.
      */
     private void resizeWidgetIfNeeded(boolean onDismiss) {
-        float xThreshold = mCellLayout.getCellWidth();
-        float yThreshold = mCellLayout.getCellHeight();
+        DeviceProfile dp = mLauncher.getDeviceProfile();
+        float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacingPx;
+        float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacingPx;
 
         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
@@ -364,13 +365,18 @@
         final float density = context.getResources().getDisplayMetrics().density;
         final Point[] cellSize = CELL_SIZE.get(context);
 
+        final int borderSpacing = context.getResources()
+                .getDimensionPixelSize(R.dimen.dynamic_grid_cell_border_spacing);
+        final float hBorderSpacing = (spanX - 1) * borderSpacing;
+        final float vBorderSpacing = (spanY - 1) * borderSpacing;
+
         // Compute landscape size
-        int landWidth = (int) ((spanX * cellSize[0].x) / density);
-        int landHeight = (int) ((spanY * cellSize[0].y) / density);
+        int landWidth = (int) (((spanX * cellSize[0].x) + hBorderSpacing) / density);
+        int landHeight = (int) (((spanY * cellSize[0].y) + vBorderSpacing) / density);
 
         // Compute portrait size
-        int portWidth = (int) ((spanX * cellSize[1].x) / density);
-        int portHeight = (int) ((spanY * cellSize[1].y) / density);
+        int portWidth = (int) (((spanX * cellSize[1].x) + hBorderSpacing) / density);
+        int portHeight = (int) (((spanY * cellSize[1].y) + vBorderSpacing) / density);
         rect.set(portWidth, landHeight, landWidth, portHeight);
         return rect;
     }
@@ -384,8 +390,9 @@
     }
 
     private void onTouchUp() {
-        int xThreshold = mCellLayout.getCellWidth();
-        int yThreshold = mCellLayout.getCellHeight();
+        DeviceProfile dp = mLauncher.getDeviceProfile();
+        int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacingPx;
+        int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacingPx;
 
         mDeltaXAddOn = mRunningHInc * xThreshold;
         mDeltaYAddOn = mRunningVInc * yThreshold;
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 8a03fac..062ab71 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -31,7 +31,6 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.util.Log;
-import android.view.ContextThemeWrapper;
 
 import androidx.annotation.IntDef;
 
@@ -333,7 +332,7 @@
     public static <T extends BaseActivity> T fromContext(Context context) {
         if (context instanceof BaseActivity) {
             return (T) context;
-        } else if (context instanceof ContextThemeWrapper) {
+        } else if (context instanceof ContextWrapper) {
             return fromContext(((ContextWrapper) context).getBaseContext());
         } else {
             throw new IllegalArgumentException("Cannot find BaseActivity in parent tree");
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index cea8cd6..e5a4335 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.graphics.IconShape.getShape;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -194,6 +195,7 @@
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
+            setCenterVertically(ENABLE_FOUR_COLUMNS.get());
         } else if (mDisplay == DISPLAY_ALL_APPS) {
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
@@ -228,7 +230,6 @@
         int shadowSize = context.getResources().getDimensionPixelSize(
                 R.dimen.blur_size_click_shadow);
         mHighlightShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.INNER);
-
     }
 
     @Override
@@ -504,7 +505,7 @@
      * @param canvas The canvas to draw to.
      */
     protected void drawDotIfNecessary(Canvas canvas) {
-        if (mDisplay == DISPLAY_TASKBAR) {
+        if (mActivity instanceof Launcher && ((Launcher) mActivity).isViewInTaskbar(this)) {
             // TODO: support notification dots in Taskbar
             return;
         }
@@ -550,6 +551,14 @@
         outBounds.set(left, top, right, bottom);
     }
 
+
+    /**
+     * Sets whether to vertically center the content.
+     */
+    public void setCenterVertically(boolean centerVertically) {
+        mCenterVertically = centerVertically;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         if (mCenterVertically) {
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 452207d..947388b 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -19,6 +19,7 @@
 import static android.animation.ValueAnimator.areAnimatorsEnabled;
 
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -88,6 +89,8 @@
     @Thunk int mCellHeight;
     private int mFixedCellWidth;
     private int mFixedCellHeight;
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private final int mBorderSpacing;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mCountX;
@@ -208,6 +211,7 @@
 
         DeviceProfile grid = mActivity.getDeviceProfile();
 
+        mBorderSpacing = grid.cellLayoutBorderSpacingPx;
         mCellWidth = mCellHeight = -1;
         mFixedCellWidth = mFixedCellHeight = -1;
 
@@ -288,7 +292,8 @@
         }
 
         mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
-        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                mBorderSpacing);
         addView(mShortcutsAndWidgets);
     }
 
@@ -305,6 +310,8 @@
         setImportantForAccessibility(accessibilityFlag);
         getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
 
+        // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared.
+        setFocusable(delegate != null);
         // Invalidate the accessibility hierarchy
         if (getParent() != null) {
             getParent().notifySubtreeAccessibilityStateChanged(
@@ -345,7 +352,8 @@
     public void setCellDimensions(int width, int height) {
         mFixedCellWidth = mCellWidth = width;
         mFixedCellHeight = mCellHeight = height;
-        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                mBorderSpacing);
     }
 
     public void setGridSize(int x, int y) {
@@ -354,7 +362,8 @@
         mOccupied = new GridOccupancy(mCountX, mCountY);
         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
         mTempRectStack.clear();
-        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                mBorderSpacing);
         requestLayout();
     }
 
@@ -475,8 +484,8 @@
             for (int j = 0; j < mCountY; j++) {
                 canvas.save();
 
-                int transX = i * mCellWidth;
-                int transY = j * mCellHeight;
+                int transX = i * mCellWidth + (i * mBorderSpacing);
+                int transY = j * mCellHeight + (j * mBorderSpacing);
 
                 canvas.translate(getPaddingLeft() + transX, getPaddingTop() + transY);
 
@@ -591,6 +600,9 @@
         if (child instanceof BubbleTextView) {
             BubbleTextView bubbleChild = (BubbleTextView) child;
             bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
+            if (ENABLE_FOUR_COLUMNS.get()) {
+                bubbleChild.setCenterVertically(mContainerType != HOTSEAT);
+            }
         }
 
         child.setScaleX(mChildScale);
@@ -706,11 +718,9 @@
      * @param result Array of 2 ints to hold the x and y coordinate of the point
      */
     void cellToPoint(int cellX, int cellY, int[] result) {
-        final int hStartPadding = getPaddingLeft();
-        final int vStartPadding = getPaddingTop();
-
-        result[0] = hStartPadding + cellX * mCellWidth;
-        result[1] = vStartPadding + cellY * mCellHeight;
+        cellToRect(cellX, cellY, 1, 1, mTempRect);
+        result[0] = mTempRect.left;
+        result[1] = mTempRect.top;
     }
 
     /**
@@ -734,25 +744,9 @@
      * @param result Array of 2 ints to hold the x and y coordinate of the point
      */
     void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
-        final int hStartPadding = getPaddingLeft();
-        final int vStartPadding = getPaddingTop();
-        result[0] = hStartPadding + cellX * mCellWidth + (spanX * mCellWidth) / 2;
-        result[1] = vStartPadding + cellY * mCellHeight + (spanY * mCellHeight) / 2;
-    }
-
-     /**
-     * Given a cell coordinate and span fills out a corresponding pixel rect
-     *
-     * @param cellX X coordinate of the cell
-     * @param cellY Y coordinate of the cell
-     * @param result Rect in which to write the result
-     */
-     void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) {
-        final int hStartPadding = getPaddingLeft();
-        final int vStartPadding = getPaddingTop();
-        final int left = hStartPadding + cellX * mCellWidth;
-        final int top = vStartPadding + cellY * mCellHeight;
-        result.set(left, top, left + (spanX * mCellWidth), top + (spanY * mCellHeight));
+        cellToRect(cellX, cellY, spanX, spanY, mTempRect);
+        result[0] = mTempRect.centerX();
+        result[1] = mTempRect.centerY();
     }
 
     public float getDistanceFromCell(float x, float y, int[] cell) {
@@ -783,12 +777,15 @@
         int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
 
         if (mFixedCellWidth < 0 || mFixedCellHeight < 0) {
-            int cw = DeviceProfile.calculateCellWidth(childWidthSize, mCountX);
-            int ch = DeviceProfile.calculateCellHeight(childHeightSize, mCountY);
+            int cw = DeviceProfile.calculateCellWidth(childWidthSize, mBorderSpacing,
+                    mCountX);
+            int ch = DeviceProfile.calculateCellHeight(childHeightSize, mBorderSpacing,
+                    mCountY);
             if (cw != mCellWidth || ch != mCellHeight) {
                 mCellWidth = cw;
                 mCellHeight = ch;
-                mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+                mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                        mBorderSpacing);
             }
         }
 
@@ -838,10 +835,11 @@
     /**
      * Returns the amount of space left over after subtracting padding and cells. This space will be
      * very small, a few pixels at most, and is a result of rounding down when calculating the cell
-     * width in {@link DeviceProfile#calculateCellWidth(int, int)}.
+     * width in {@link DeviceProfile#calculateCellWidth(int, int, int)}.
      */
     public int getUnusedHorizontalSpace() {
-        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth);
+        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth)
+                - ((mCountX - 1) * mBorderSpacing);
     }
 
     public Drawable getScrimBackground() {
@@ -857,8 +855,8 @@
         return mShortcutsAndWidgets;
     }
 
-    public View getChildAt(int x, int y) {
-        return mShortcutsAndWidgets.getChildAt(x, y);
+    public View getChildAt(int cellX, int cellY) {
+        return mShortcutsAndWidgets.getChildAt(cellX, cellY);
     }
 
     public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
@@ -989,11 +987,11 @@
             }
 
             // Center horizontaly
-            left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
+            left += (r.width() - dragOutline.getWidth()) / 2;
 
             if (v != null && v.getViewType() == DraggableView.DRAGGABLE_WIDGET) {
                 // Center vertically
-                top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
+                top += (r.height() - dragOutline.getHeight()) / 2;
             } else if (v != null && v.getViewType() == DraggableView.DRAGGABLE_ICON) {
                 int cHeight = getShortcutsAndWidgets().getCellContentHeight();
                 int cellPaddingY = (int) Math.max(0, ((mCellHeight - cHeight) / 2f));
@@ -2153,7 +2151,7 @@
 
         findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
         Rect dragRect = new Rect();
-        regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+        cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
         dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
 
         Rect dropRegionRect = new Rect();
@@ -2163,7 +2161,7 @@
         int dropRegionSpanX = dropRegionRect.width();
         int dropRegionSpanY = dropRegionRect.height();
 
-        regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+        cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
                 dropRegionRect.height(), dropRegionRect);
 
         int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
@@ -2521,10 +2519,11 @@
         final int hStartPadding = getPaddingLeft();
         final int vStartPadding = getPaddingTop();
 
-        int width = cellHSpan * cellWidth;
-        int height = cellVSpan * cellHeight;
-        int x = hStartPadding + cellX * cellWidth;
-        int y = vStartPadding + cellY * cellHeight;
+        int x = hStartPadding + (cellX * mBorderSpacing) + (cellX * cellWidth);
+        int y = vStartPadding + (cellY * mBorderSpacing) + (cellY * cellHeight);
+
+        int width = cellHSpan * cellWidth + ((cellHSpan - 1) * mBorderSpacing);
+        int height = cellVSpan * cellHeight + ((cellVSpan - 1) * mBorderSpacing);
 
         resultRect.set(x, y, x + width, y + height);
     }
@@ -2542,11 +2541,13 @@
     }
 
     public int getDesiredWidth() {
-        return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth);
+        return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth)
+                + ((mCountX - 1) * mBorderSpacing);
     }
 
     public int getDesiredHeight()  {
-        return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight);
+        return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight)
+                + ((mCountY - 1) * mBorderSpacing);
     }
 
     public boolean isOccupied(int x, int y) {
@@ -2661,19 +2662,21 @@
             this.cellVSpan = cellVSpan;
         }
 
-        public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount) {
-            setup(cellWidth, cellHeight, invertHorizontally, colCount, 1.0f, 1.0f);
+        public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
+                int rowCount, int borderSpacing) {
+            setup(cellWidth, cellHeight, invertHorizontally, colCount, rowCount, 1.0f, 1.0f,
+                    borderSpacing);
         }
 
         /**
-         * Use this method, as opposed to {@link #setup(int, int, boolean, int)}, if the view needs
-         * to be scaled.
+         * Use this method, as opposed to {@link #setup(int, int, boolean, int, int, int)},
+         * if the view needs to be scaled.
          *
          * ie. In multi-window mode, we setup widgets so that they are measured and laid out
          * using their full/invariant device profile sizes.
          */
         public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
-                float cellScaleX, float cellScaleY) {
+                int rowCount, float cellScaleX, float cellScaleY, int borderSpacing) {
             if (isLockedToGrid) {
                 final int myCellHSpan = cellHSpan;
                 final int myCellVSpan = cellVSpan;
@@ -2684,17 +2687,23 @@
                     myCellX = colCount - myCellX - cellHSpan;
                 }
 
-                width = (int) (myCellHSpan * cellWidth / cellScaleX - leftMargin - rightMargin);
-                height = (int) (myCellVSpan * cellHeight / cellScaleY - topMargin - bottomMargin);
-                x = (myCellX * cellWidth + leftMargin);
-                y = (myCellY * cellHeight + topMargin);
+                int hBorderSpacing = (myCellHSpan - 1) * borderSpacing;
+                int vBorderSpacing = (myCellVSpan - 1) * borderSpacing;
+
+                float myCellWidth = ((myCellHSpan * cellWidth) + hBorderSpacing) / cellScaleX;
+                float myCellHeight = ((myCellVSpan * cellHeight) + vBorderSpacing) / cellScaleY;
+
+                width = Math.round(myCellWidth) - leftMargin - rightMargin;
+                height = Math.round(myCellHeight) - topMargin - bottomMargin;
+                x = leftMargin + (myCellX * cellWidth) + (myCellX * borderSpacing);
+                y = topMargin + (myCellY * cellHeight) + (myCellY * borderSpacing);
             }
         }
 
         /**
          * Sets the position to the provided point
          */
-        public void setXY(Point point) {
+        public void setCellXY(Point point) {
             cellX = point.x;
             cellY = point.y;
         }
diff --git a/src/com/android/launcher3/DevicePaddings.java b/src/com/android/launcher3/DevicePaddings.java
new file mode 100644
index 0000000..4827f36
--- /dev/null
+++ b/src/com/android/launcher3/DevicePaddings.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Workspace items have a fixed height, so we need a way to distribute any unused workspace height.
+ *
+ * The unused or "extra" height is allocated to three different variable heights:
+ * - The space above the workspace
+ * - The space between the workspace and hotseat
+ * - The espace below the hotseat
+ */
+public class DevicePaddings {
+
+    private static final String DEVICE_PADDING = "device-paddings";
+    private static final String DEVICE_PADDINGS = "device-padding";
+
+    private static final String WORKSPACE_TOP_PADDING = "workspaceTopPadding";
+    private static final String WORKSPACE_BOTTOM_PADDING = "workspaceBottomPadding";
+    private static final String HOTSEAT_BOTTOM_PADDING = "hotseatBottomPadding";
+
+    private static final String TAG = DevicePaddings.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    ArrayList<DevicePadding> mDevicePaddings = new ArrayList<>();
+
+    public DevicePaddings(Context context) {
+        try (XmlResourceParser parser = context.getResources().getXml(R.xml.size_limits)) {
+            final int depth = parser.getDepth();
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if ((type == XmlPullParser.START_TAG) && DEVICE_PADDING.equals(parser.getName())) {
+                    final int displayDepth = parser.getDepth();
+                    while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                            parser.getDepth() > displayDepth)
+                            && type != XmlPullParser.END_DOCUMENT) {
+                        if ((type == XmlPullParser.START_TAG)
+                                && DEVICE_PADDINGS.equals(parser.getName())) {
+                            TypedArray a = context.obtainStyledAttributes(
+                                    Xml.asAttributeSet(parser), R.styleable.DevicePadding);
+                            int maxWidthPx = a.getDimensionPixelSize(
+                                    R.styleable.DevicePadding_maxEmptySpace, 0);
+                            a.recycle();
+
+                            PaddingFormula workspaceTopPadding = null;
+                            PaddingFormula workspaceBottomPadding = null;
+                            PaddingFormula hotseatBottomPadding = null;
+
+                            final int limitDepth = parser.getDepth();
+                            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                                    parser.getDepth() > limitDepth)
+                                    && type != XmlPullParser.END_DOCUMENT) {
+                                AttributeSet attr = Xml.asAttributeSet(parser);
+                                if ((type == XmlPullParser.START_TAG)) {
+                                    if (WORKSPACE_TOP_PADDING.equals(parser.getName())) {
+                                        workspaceTopPadding = new PaddingFormula(context, attr);
+                                    } else if (WORKSPACE_BOTTOM_PADDING.equals(parser.getName())) {
+                                        workspaceBottomPadding = new PaddingFormula(context, attr);
+                                    } else if (HOTSEAT_BOTTOM_PADDING.equals(parser.getName())) {
+                                        hotseatBottomPadding = new PaddingFormula(context, attr);
+                                    }
+                                }
+                            }
+
+                            if (workspaceTopPadding == null
+                                    || workspaceBottomPadding == null
+                                    || hotseatBottomPadding == null) {
+                                throw new RuntimeException("DevicePadding missing padding.");
+                            }
+
+                            mDevicePaddings.add(new DevicePadding(maxWidthPx, workspaceTopPadding,
+                                    workspaceBottomPadding, hotseatBottomPadding));
+                        }
+                    }
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+
+        // Sort ascending by maxEmptySpacePx
+        mDevicePaddings.sort((sl1, sl2) -> Integer.compare(sl1.maxEmptySpacePx,
+                sl2.maxEmptySpacePx));
+    }
+
+    public DevicePadding getDevicePadding(int extraSpacePx) {
+        for (DevicePadding limit : mDevicePaddings) {
+            if (extraSpacePx <= limit.maxEmptySpacePx) {
+                return limit;
+            }
+        }
+
+        return mDevicePaddings.get(mDevicePaddings.size() - 1);
+    }
+
+    /**
+     * Holds all the formulas to calculate the padding for a particular device based on the
+     * amount of extra space.
+     */
+    public static final class DevicePadding {
+
+        private final int maxEmptySpacePx;
+        private final PaddingFormula workspaceTopPadding;
+        private final PaddingFormula workspaceBottomPadding;
+        private final PaddingFormula hotseatBottomPadding;
+
+        public DevicePadding(int maxEmptySpacePx,
+                PaddingFormula workspaceTopPadding,
+                PaddingFormula workspaceBottomPadding,
+                PaddingFormula hotseatBottomPadding) {
+            this.maxEmptySpacePx = maxEmptySpacePx;
+            this.workspaceTopPadding = workspaceTopPadding;
+            this.workspaceBottomPadding = workspaceBottomPadding;
+            this.hotseatBottomPadding = hotseatBottomPadding;
+        }
+
+        public int getWorkspaceTopPadding(int extraSpacePx) {
+            return workspaceTopPadding.calculate(extraSpacePx);
+        }
+
+        public int getWorkspaceBottomPadding(int extraSpacePx) {
+            return workspaceBottomPadding.calculate(extraSpacePx);
+        }
+
+        public int getHotseatBottomPadding(int extraSpacePx) {
+            return hotseatBottomPadding.calculate(extraSpacePx);
+        }
+    }
+
+    /**
+     * Used to calculate a padding based on three variables: a, b, and c.
+     *
+     * Calculation: a * (extraSpace - c) + b
+     */
+    private static final class PaddingFormula {
+
+        private final float a;
+        private final float b;
+        private final float c;
+
+        public PaddingFormula(Context context, AttributeSet attrs) {
+            TypedArray t = context.obtainStyledAttributes(attrs,
+                    R.styleable.DevicePaddingFormula);
+
+            a = getValue(t, R.styleable.DevicePaddingFormula_a);
+            b = getValue(t, R.styleable.DevicePaddingFormula_b);
+            c = getValue(t, R.styleable.DevicePaddingFormula_c);
+
+            t.recycle();
+        }
+
+        public int calculate(int extraSpacePx) {
+            if (DEBUG) {
+                Log.d(TAG, "a=" + a + " * (" + extraSpacePx + " - " + c + ") + b=" + b);
+            }
+            return Math.round(a * (extraSpacePx - c) + b);
+        }
+
+        private static float getValue(TypedArray a, int index) {
+            if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
+                return a.getDimensionPixelSize(index, 0);
+            } else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
+                return a.getFloat(index, 0);
+            }
+            return 0;
+        }
+
+        @Override
+        public String toString() {
+            return "a=" + a + ", b=" + b + ", c=" + c;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 4d5bd5d..9992fde 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
+
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -25,6 +27,7 @@
 import android.view.Surface;
 
 import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.DevicePaddings.DevicePadding;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
@@ -74,12 +77,16 @@
 
     // Workspace
     public final int desiredWorkspaceLeftRightMarginPx;
+    public final int cellLayoutBorderSpacingPx;
     public final int cellLayoutPaddingLeftRightPx;
     public final int cellLayoutBottomPaddingPx;
     public final int edgeMarginPx;
     public float workspaceSpringLoadShrinkFactor;
     public final int workspaceSpringLoadedBottomSpace;
 
+    public int workspaceTopPadding;
+    public int workspaceBottomPadding;
+
     // Workspace page indicator
     public final int workspacePageIndicatorHeight;
     private final int mWorkspacePageIndicatorOverlapWorkspace;
@@ -92,6 +99,7 @@
 
     public int cellWidthPx;
     public int cellHeightPx;
+    public int cellYPaddingPx;
     public int workspaceCellPaddingXPx;
 
     // Folder
@@ -187,6 +195,11 @@
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
         desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
 
+        cellYPaddingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_y);
+        cellLayoutBorderSpacingPx = isVerticalBarLayout()
+                || isMultiWindowMode
+                || !ENABLE_FOUR_COLUMNS.get()
+                ? 0 : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_border_spacing);
         int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet
                 ? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1;
         int cellLayoutPadding = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
@@ -220,22 +233,31 @@
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
         // Add a bit of space between nav bar and hotseat in vertical bar layout.
         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
+        int hotseatExtraVerticalSize =
+                res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size);
         hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
                 + (isVerticalBarLayout()
                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
-                : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
-                        + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
+                : (hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx
+                        + (ENABLE_FOUR_COLUMNS.get() ? 0 : hotseatExtraVerticalSize)));
 
         // Calculate all of the remaining variables.
-        updateAvailableDimensions(res);
-
+        int extraSpace = updateAvailableDimensions(res);
         // Now that we have all of the variables calculated, we can tune certain sizes.
-        if (!isVerticalBarLayout() && isPhone && isTallDevice) {
+        if (ENABLE_FOUR_COLUMNS.get()) {
+            DevicePadding padding = inv.devicePaddings.getDevicePadding(extraSpace);
+            workspaceTopPadding = padding.getWorkspaceTopPadding(extraSpace);
+            workspaceBottomPadding = padding.getWorkspaceBottomPadding(extraSpace);
+
+            float hotseatBarBottomPadding = padding.getHotseatBottomPadding(extraSpace);
+            hotseatBarSizePx += hotseatBarBottomPadding;
+            hotseatBarBottomPaddingPx += hotseatBarBottomPadding;
+        } else if (!isVerticalBarLayout() && isPhone && isTallDevice) {
             // We increase the hotseat size when there is extra space.
             // ie. For a display with a large aspect ratio, we can keep the icons on the workspace
             // in portrait mode closer together by adding more height to the hotseat.
             // Note: This calculation was created after noticing a pattern in the design spec.
-            int extraSpace = getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2
+            extraSpace = getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2
                     - workspacePageIndicatorHeight;
             hotseatBarSizePx += extraSpace;
             hotseatBarBottomPaddingPx += extraSpace;
@@ -328,17 +350,24 @@
                 + topBottomPadding * 2;
     }
 
-    private void updateAvailableDimensions(Resources res) {
+    /**
+     * Returns the amount of extra (or unused) vertical space.
+     */
+    private int updateAvailableDimensions(Resources res) {
         updateIconSize(1f, res);
 
         // Check to see if the icons fit within the available height.  If not, then scale down.
-        float usedHeight = (cellHeightPx * inv.numRows);
+        float usedHeight = (cellHeightPx * inv.numRows)
+                + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
         int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
+        float extraHeight = Math.max(0, maxHeight - usedHeight);
         if (usedHeight > maxHeight) {
             float scale = maxHeight / usedHeight;
             updateIconSize(scale, res);
+            extraHeight = 0;
         }
         updateAvailableFolderCellDimensions(res);
+        return Math.round(extraHeight);
     }
 
     /**
@@ -346,7 +375,7 @@
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
      */
-    private void updateIconSize(float scale, Resources res) {
+    public void updateIconSize(float scale, Resources res) {
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
@@ -355,16 +384,23 @@
         iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
-        cellHeightPx = iconSizePx + iconDrawablePaddingPx
-                + Utilities.calculateTextHeight(iconTextSizePx);
-        int cellYPadding = (getCellSize().y - cellHeightPx) / 2;
-        if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout
-                && !isMultiWindowMode) {
-            // Ensures that the label is closer to its corresponding icon. This is not an issue
-            // with vertical bar layout or multi-window mode since the issue is handled separately
-            // with their calls to {@link #adjustToHideWorkspaceLabels}.
-            cellHeightPx -= (iconDrawablePaddingPx - cellYPadding);
-            iconDrawablePaddingPx = cellYPadding;
+        if (ENABLE_FOUR_COLUMNS.get()) {
+            cellHeightPx = iconSizePx + iconDrawablePaddingPx
+                    + Utilities.calculateTextHeight(iconTextSizePx)
+                    + (cellYPaddingPx * 2);
+        } else {
+            cellYPaddingPx = 0;
+            cellHeightPx = iconSizePx + iconDrawablePaddingPx
+                    + Utilities.calculateTextHeight(iconTextSizePx);
+            int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
+            if (iconDrawablePaddingPx > cellPaddingY && !isVerticalLayout
+                    && !isMultiWindowMode) {
+                // Ensures that the label is closer to its corresponding icon. This is not an issue
+                // with vertical bar layout or multi-window mode since the issue is handled
+                // separately with their calls to {@link #adjustToHideWorkspaceLabels}.
+                cellHeightPx -= (iconDrawablePaddingPx - cellPaddingY);
+                iconDrawablePaddingPx = cellPaddingY;
+            }
         }
         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
@@ -425,13 +461,15 @@
         Point totalWorkspacePadding = getTotalWorkspacePadding();
 
         // Check if the icons fit within the available height.
-        float contentUsedHeight = folderCellHeightPx * inv.numFolderRows;
+        float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
+                + ((inv.numFolderRows - 1) * cellLayoutBorderSpacingPx);
         int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
                 - folderMargin;
         float scaleY = contentMaxHeight / contentUsedHeight;
 
         // Check if the icons fit within the available width.
-        float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns;
+        float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
+                + ((inv.numFolderColumns - 1) * cellLayoutBorderSpacingPx);
         int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
         float scaleX = contentMaxWidth / contentUsedWidth;
 
@@ -479,9 +517,9 @@
         // not matter.
         Point padding = getTotalWorkspacePadding();
         result.x = calculateCellWidth(availableWidthPx - padding.x
-                - cellLayoutPaddingLeftRightPx * 2, numColumns);
+                - cellLayoutPaddingLeftRightPx * 2, cellLayoutBorderSpacingPx, numColumns);
         result.y = calculateCellHeight(availableHeightPx - padding.y
-                - cellLayoutBottomPaddingPx, numRows);
+                - cellLayoutBottomPaddingPx, cellLayoutBorderSpacingPx, numRows);
         return result;
     }
 
@@ -509,7 +547,7 @@
             }
         } else {
             int paddingBottom = hotseatBarSizePx + workspacePageIndicatorHeight
-                    - mWorkspacePageIndicatorOverlapWorkspace;
+                    + workspaceBottomPadding - mWorkspacePageIndicatorOverlapWorkspace;
             if (isTablet) {
                 // Pad the left and right of the workspace to ensure consistent spacing
                 // between all icons
@@ -526,7 +564,7 @@
             } else {
                 // Pad the top and bottom of the workspace with search/hotseat bar sizes
                 padding.set(desiredWorkspaceLeftRightMarginPx,
-                        edgeMarginPx,
+                        workspaceTopPadding + edgeMarginPx,
                         desiredWorkspaceLeftRightMarginPx,
                         paddingBottom);
             }
@@ -581,11 +619,11 @@
         }
     }
 
-    public static int calculateCellWidth(int width, int countX) {
-        return width / countX;
+    public static int calculateCellWidth(int width, int borderSpacing, int countX) {
+        return (width - ((countX - 1) * borderSpacing)) / countX;
     }
-    public static int calculateCellHeight(int height, int countY) {
-        return height / countY;
+    public static int calculateCellHeight(int height, int borderSpacing, int countY) {
+        return (height - ((countY - 1) * borderSpacing)) / countY;
     }
 
     /**
@@ -610,8 +648,13 @@
     public boolean updateIsSeascape(Context context) {
         if (isVerticalBarLayout()) {
             // Check an up-to-date info.
-            boolean isSeascape = DisplayController.getDefaultDisplay(context)
-                    .createInfoForContext(context).rotation == Surface.ROTATION_270;
+            DisplayController.Info displayInfo = DisplayController.getDefaultDisplay(context)
+                    .createInfoForContext(context);
+            if (displayInfo == null) {
+                return false;
+            }
+
+            boolean isSeascape = displayInfo.rotation == Surface.ROTATION_270;
             if (mIsSeascape != isSeascape) {
                 mIsSeascape = isSeascape;
                 return true;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index aa3ef9b..2a08c50 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -130,6 +130,8 @@
     public DeviceProfile landscapeProfile;
     public DeviceProfile portraitProfile;
 
+    public DevicePaddings devicePaddings;
+
     public Point defaultWallpaperSize;
     public Rect defaultWidgetPadding;
 
@@ -159,6 +161,7 @@
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
         mOverlayMonitor = p.mOverlayMonitor;
+        devicePaddings = p.devicePaddings;
     }
 
     @TargetApi(23)
@@ -210,6 +213,8 @@
         result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
         result.allAppsIconSize = Math.min(
                 defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+
+        devicePaddings = new DevicePaddings(context);
         initGrid(context, myInfo, result);
     }
 
@@ -237,6 +242,7 @@
         ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
 
         DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
+        devicePaddings = new DevicePaddings(context);
         initGrid(context, displayInfo, displayOption);
         return displayOption.grid.name;
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 51e7c7d..49adf1f 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -37,6 +37,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
@@ -74,6 +75,7 @@
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Bitmap;
+import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -106,6 +108,7 @@
 
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.LauncherAction;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AllAppsTransitionController;
@@ -122,7 +125,6 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.keyboard.CustomActionsPopup;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.FileLog;
@@ -167,7 +169,6 @@
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
@@ -394,7 +395,7 @@
         idp.addOnChangeListener(this);
         mSharedPrefs = Utilities.getPrefs(this);
         mIconCache = app.getIconCache();
-        mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
+        mAccessibilityDelegate = createAccessibilityDelegate();
 
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
@@ -1797,6 +1798,43 @@
         return newFolder;
     }
 
+    @Override
+    public Rect getFolderBoundingBox() {
+        // We need to bound the folder to the currently visible workspace area
+        Rect folderBoundingBox = new Rect();
+        getWorkspace().getPageAreaRelativeToDragLayer(folderBoundingBox);
+        return folderBoundingBox;
+    }
+
+    @Override
+    public void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
+        int left = inOutPosition[0];
+        int top = inOutPosition[1];
+        DeviceProfile grid = getDeviceProfile();
+        int distFromEdgeOfScreen = getWorkspace().getPaddingLeft();
+        if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) {
+            // Center the folder if it is very close to being centered anyway, by virtue of
+            // filling the majority of the viewport. ie. remove it from the uncanny valley
+            // of centeredness.
+            left = (grid.availableWidthPx - width) / 2;
+        } else if (width >= bounds.width()) {
+            // If the folder doesn't fit within the bounds, center it about the desired bounds
+            left = bounds.left + (bounds.width() - width) / 2;
+        }
+        if (height >= bounds.height()) {
+            // Folder height is greater than page height, center on page
+            top = bounds.top + (bounds.height() - height) / 2;
+        } else {
+            // Folder height is less than page height, so bound it to the absolute open folder
+            // bounds if necessary
+            Rect folderBounds = grid.getAbsoluteOpenFolderBounds();
+            left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width));
+            top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height));
+        }
+        inOutPosition[0] = left;
+        inOutPosition[1] = top;
+    }
+
     /**
      * Unbinds the view for the specified item, and removes the item and all its children.
      *
@@ -2656,19 +2694,9 @@
             shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.widget_button_text),
                     KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON));
         }
-        final View currentFocus = getCurrentFocus();
-        if (currentFocus != null) {
-            if (new CustomActionsPopup(this, currentFocus).canShow()) {
-                shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.custom_actions),
-                        KeyEvent.KEYCODE_O, KeyEvent.META_CTRL_ON));
-            }
-            if (currentFocus.getTag() instanceof ItemInfo
-                    && ShortcutUtil.supportsShortcuts((ItemInfo) currentFocus.getTag())) {
+        getSupportedActions(this,  getCurrentFocus()).forEach(la ->
                 shortcutInfos.add(new KeyboardShortcutInfo(
-                        getString(R.string.shortcuts_menu_with_notifications_description),
-                        KeyEvent.KEYCODE_S, KeyEvent.META_CTRL_ON));
-            }
-        }
+                        la.accessibilityAction.getLabel(), la.keyCode, KeyEvent.META_CTRL_ON)));
         if (!shortcutInfos.isEmpty()) {
             data.add(new KeyboardShortcutGroup(getString(R.string.home_screen), shortcutInfos));
         }
@@ -2686,30 +2714,18 @@
                         return true;
                     }
                     break;
-                case KeyEvent.KEYCODE_S: {
-                    View focusedView = getCurrentFocus();
-                    if (focusedView instanceof BubbleTextView
-                            && focusedView.getTag() instanceof ItemInfo
-                            && mAccessibilityDelegate.performAction(focusedView,
-                            (ItemInfo) focusedView.getTag(),
-                            LauncherAccessibilityDelegate.DEEP_SHORTCUTS,
-                            true)) {
-                        PopupContainerWithArrow.getOpen(this).requestFocus();
-                        return true;
-                    }
-                    break;
-                }
-                case KeyEvent.KEYCODE_O:
-                    if (new CustomActionsPopup(this, getCurrentFocus()).show()) {
-                        return true;
-                    }
-                    break;
                 case KeyEvent.KEYCODE_W:
                     if (isInState(NORMAL)) {
                         OptionsPopupView.openWidgets(this);
                         return true;
                     }
                     break;
+                default:
+                    for (LauncherAction la : getSupportedActions(this, getCurrentFocus())) {
+                        if (la.keyCode == keyCode) {
+                            return la.invokeFromKeyboard(getCurrentFocus());
+                        }
+                    }
             }
         }
         return super.onKeyShortcut(keyCode, event);
@@ -2767,6 +2783,9 @@
         return Stream.of(APP_INFO, WIDGETS, INSTALL);
     }
 
+    protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+        return new LauncherAccessibilityDelegate(this);
+    }
 
     /**
      * @see LauncherState#getOverviewScaleAndOffset(Launcher)
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index a4181c5..57d7600 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,8 +17,8 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -37,10 +37,10 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
@@ -57,8 +57,9 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
+    private SettingsCache.OnChangeListener mNotificationSettingsChangedListener;
 
-    private SecureSettingsObserver mNotificationDotsObserver;
+    private SettingsCache mSettingsCache;
     private InstallSessionTracker mInstallSessionTracker;
     private SimpleBroadcastReceiver mModelChangeReceiver;
     private SafeCloseable mCalendarChangeTracker;
@@ -108,10 +109,11 @@
                 .registerInstallTracker(mModel);
 
         // Register an observer to rebind the notification listener when dots are re-enabled.
-        mNotificationDotsObserver =
-                newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged);
-        mNotificationDotsObserver.register();
-        mNotificationDotsObserver.dispatchOnChange();
+        mSettingsCache = SettingsCache.INSTANCE.get(mContext);
+        mNotificationSettingsChangedListener = this::onNotificationSettingsChanged;
+        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                mNotificationSettingsChangedListener);
+        mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
     }
 
     public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -166,8 +168,9 @@
         }
         CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
 
-        if (mNotificationDotsObserver != null) {
-            mNotificationDotsObserver.unregister();
+        if (mSettingsCache != null) {
+            mSettingsCache.unregister(NOTIFICATION_BADGING_URI,
+                    mNotificationSettingsChangedListener);
         }
     }
 
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 92b88e6..7276887 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -37,6 +37,8 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.ItemInfo;
@@ -203,9 +205,13 @@
         d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
 
         super.onDrop(d, options);
-        StatsLogger logger = mStatsLogManager.logger().withInstanceId(d.logInstanceId);
-        if (d.originalDragInfo != null) {
-            logger.withItemInfo(d.originalDragInfo);
+        doLog(d.logInstanceId, d.originalDragInfo);
+    }
+
+    private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) {
+        StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId);
+        if (itemInfo != null) {
+            logger.withItemInfo(itemInfo);
         }
         if (mCurrentAccessibilityAction == UNINSTALL) {
             logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
@@ -296,6 +302,7 @@
 
     @Override
     public void onAccessibilityDrop(View view, ItemInfo item) {
+        doLog(new InstanceIdSequence().newInstanceId(), item);
         performDropAction(view, item);
     }
 
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index ee0c7bb..eab8272 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -26,10 +26,11 @@
 import android.view.ViewGroup;
 
 import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
-public class ShortcutAndWidgetContainer extends ViewGroup {
+public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.FolderIconParent {
     static final String TAG = "ShortcutAndWidgetContainer";
 
     // These are temporary variables to prevent having to allocate a new object just to
@@ -42,8 +43,10 @@
 
     private int mCellWidth;
     private int mCellHeight;
+    private int mBorderSpacing;
 
     private int mCountX;
+    private int mCountY;
 
     private final ActivityContext mActivity;
     private boolean mInvertIfRtl = false;
@@ -55,20 +58,23 @@
         mContainerType = containerType;
     }
 
-    public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY) {
+    public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY,
+            int borderSpacing) {
         mCellWidth = cellWidth;
         mCellHeight = cellHeight;
         mCountX = countX;
+        mCountY = countY;
+        mBorderSpacing = borderSpacing;
     }
 
-    public View getChildAt(int x, int y) {
+    public View getChildAt(int cellX, int cellY) {
         final int count = getChildCount();
         for (int i = 0; i < count; i++) {
             View child = getChildAt(i);
             CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
 
-            if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) &&
-                    (lp.cellY <= y) && (y < lp.cellY + lp.cellVSpan)) {
+            if ((lp.cellX <= cellX) && (cellX < lp.cellX + lp.cellHSpan)
+                    && (lp.cellY <= cellY) && (cellY < lp.cellY + lp.cellVSpan)) {
                 return child;
             }
         }
@@ -95,10 +101,11 @@
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
         if (child instanceof LauncherAppWidgetHostView) {
             DeviceProfile profile = mActivity.getDeviceProfile();
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
-                    profile.appWidgetScale.x, profile.appWidgetScale.y);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing);
         } else {
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    mBorderSpacing);
         }
     }
 
@@ -117,11 +124,12 @@
         final DeviceProfile profile = mActivity.getDeviceProfile();
 
         if (child instanceof LauncherAppWidgetHostView) {
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
-                    profile.appWidgetScale.x, profile.appWidgetScale.y);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing);
             // Widgets have their own padding
         } else {
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    mBorderSpacing);
             // Center the icon/folder
             int cHeight = getCellContentHeight();
             int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
@@ -221,4 +229,24 @@
             child.cancelLongPress();
         }
     }
+
+    @Override
+    public void drawFolderLeaveBehindForIcon(FolderIcon child) {
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+        // While the folder is open, the position of the icon cannot change.
+        lp.canReorder = false;
+        if (mContainerType == CellLayout.HOTSEAT) {
+            CellLayout cl = (CellLayout) getParent();
+            cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+        }
+    }
+
+    @Override
+    public void clearFolderLeaveBehind(FolderIcon child) {
+        ((CellLayout.LayoutParams) child.getLayoutParams()).canReorder = true;
+        if (mContainerType == CellLayout.HOTSEAT) {
+            CellLayout cl = (CellLayout) getParent();
+            cl.clearFolderLeaveBehind();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 51d8e66..c440303 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -605,7 +605,6 @@
                 outObj[0] = activityInfo;
                 return activityInfo.getFullResIcon(appState.getIconCache());
             }
-            if (info.getIntent() == null || info.getIntent().getPackage() == null) return null;
             List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
                     .buildRequest(launcher)
                     .query(ShortcutRequest.ALL);
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 6fac79a..cd4616a 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -3,17 +3,18 @@
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
 
-import android.app.AlertDialog;
 import android.appwidget.AppWidgetProviderInfo;
-import android.content.DialogInterface;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Bundle;
 import android.os.Handler;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -25,7 +26,6 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
@@ -33,7 +33,6 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.keyboard.CustomActionsPopup;
 import com.android.launcher3.keyboard.KeyboardDragAndDropView;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
@@ -41,14 +40,19 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.views.OptionsPopupView.OptionItem;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener {
 
@@ -78,89 +82,105 @@
         public View item;
     }
 
-    protected final SparseArray<AccessibilityAction> mActions = new SparseArray<>();
-    @Thunk final Launcher mLauncher;
+    protected final SparseArray<LauncherAction> mActions = new SparseArray<>();
+    protected final Launcher mLauncher;
 
     private DragInfo mDragInfo = null;
 
     public LauncherAccessibilityDelegate(Launcher launcher) {
         mLauncher = launcher;
 
-        mActions.put(REMOVE, new AccessibilityAction(REMOVE,
-                launcher.getText(R.string.remove_drop_target_label)));
-        mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL,
-                launcher.getText(R.string.uninstall_drop_target_label)));
-        mActions.put(DISMISS_PREDICTION, new AccessibilityAction(DISMISS_PREDICTION,
-                launcher.getText(R.string.dismiss_prediction_label)));
-        mActions.put(RECONFIGURE, new AccessibilityAction(RECONFIGURE,
-                launcher.getText(R.string.gadget_setup_text)));
-        mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE,
-                launcher.getText(R.string.action_add_to_workspace)));
-        mActions.put(MOVE, new AccessibilityAction(MOVE,
-                launcher.getText(R.string.action_move)));
-        mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE,
-                launcher.getText(R.string.action_move_to_workspace)));
-        mActions.put(RESIZE, new AccessibilityAction(RESIZE,
-                        launcher.getText(R.string.action_resize)));
-        mActions.put(DEEP_SHORTCUTS, new AccessibilityAction(DEEP_SHORTCUTS,
-                launcher.getText(R.string.action_deep_shortcut)));
-        mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new AccessibilityAction(DEEP_SHORTCUTS,
-                launcher.getText(R.string.shortcuts_menu_with_notifications_description)));
+        mActions.put(REMOVE, new LauncherAction(
+                REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X));
+        mActions.put(UNINSTALL, new LauncherAction(
+                UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U));
+        mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION,
+                R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X));
+        mActions.put(RECONFIGURE, new LauncherAction(
+                RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E));
+        mActions.put(ADD_TO_WORKSPACE, new LauncherAction(
+                ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P));
+        mActions.put(MOVE, new LauncherAction(
+                MOVE, R.string.action_move, KeyEvent.KEYCODE_M));
+        mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE,
+                R.string.action_move_to_workspace, KeyEvent.KEYCODE_P));
+        mActions.put(RESIZE, new LauncherAction(
+                RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R));
+        mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS,
+                R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
+        mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS,
+                R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S));
     }
 
     @Override
     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(host, info);
-        addSupportedActions(host, info, false);
+        if (host.getTag() instanceof ItemInfo) {
+            ItemInfo item = (ItemInfo) host.getTag();
+
+            List<LauncherAction> actions = new ArrayList<>();
+            getSupportedActions(host, item, actions);
+            actions.forEach(la -> info.addAction(la.accessibilityAction));
+
+            if (!itemSupportsLongClick(host, item)) {
+                info.setLongClickable(false);
+                info.removeAction(AccessibilityAction.ACTION_LONG_CLICK);
+            }
+        }
     }
 
-    public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
-        if (!(host.getTag() instanceof ItemInfo)) return;
-        ItemInfo item = (ItemInfo) host.getTag();
-
-        if (host instanceof AccessibilityActionHandler) {
-            ((AccessibilityActionHandler) host).addSupportedAccessibilityActions(info);
-        }
-
+    /**
+     * Adds all the accessibility actions that can be handled.
+     */
+    protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
         // If the request came from keyboard, do not add custom shortcuts as that is already
         // exposed as a direct shortcut
-        if (!fromKeyboard && ShortcutUtil.supportsShortcuts(item)) {
-            info.addAction(mActions.get(NotificationListener.getInstanceIfConnected() != null
+        if (ShortcutUtil.supportsShortcuts(item)) {
+            out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null
                     ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS));
         }
 
         for (ButtonDropTarget target : mLauncher.getDropTargetBar().getDropTargets()) {
             if (target.supportsAccessibilityDrop(item, host)) {
-                info.addAction(mActions.get(target.getAccessibilityAction()));
+                out.add(mActions.get(target.getAccessibilityAction()));
             }
         }
 
         // Do not add move actions for keyboard request as this uses virtual nodes.
         if (itemSupportsAccessibleDrag(item)) {
-            info.addAction(mActions.get(MOVE));
+            out.add(mActions.get(MOVE));
 
             if (item.container >= 0) {
-                info.addAction(mActions.get(MOVE_TO_WORKSPACE));
+                out.add(mActions.get(MOVE_TO_WORKSPACE));
             } else if (item instanceof LauncherAppWidgetInfo) {
                 if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) {
-                    info.addAction(mActions.get(RESIZE));
+                    out.add(mActions.get(RESIZE));
                 }
             }
         }
 
-        if (!fromKeyboard && !itemSupportsLongClick(host, item)) {
-            info.setLongClickable(false);
-            info.removeAction(AccessibilityAction.ACTION_LONG_CLICK);
-        }
-
         if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) {
-            info.addAction(mActions.get(ADD_TO_WORKSPACE));
+            out.add(mActions.get(ADD_TO_WORKSPACE));
         }
     }
 
+    /**
+     * Returns all the accessibility actions that can be handled by the host.
+     */
+    public static List<LauncherAction> getSupportedActions(Launcher launcher, View host) {
+        if (host == null || !(host.getTag() instanceof  ItemInfo)) {
+            return Collections.emptyList();
+        }
+        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
+        LauncherAccessibilityDelegate delegate = container != null
+                ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate();
+        List<LauncherAction> result = new ArrayList<>();
+        delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result);
+        return result;
+    }
+
     private boolean itemSupportsLongClick(View host, ItemInfo info) {
-        return PopupContainerWithArrow.canShow(host, info)
-                || new CustomActionsPopup(mLauncher, host).canShow();
+        return PopupContainerWithArrow.canShow(host, info);
     }
 
     private boolean itemSupportsAccessibleDrag(ItemInfo item) {
@@ -184,7 +204,7 @@
     /**
      * Performs the provided action on the host
      */
-    public boolean performAction(final View host, final ItemInfo item, int action,
+    protected boolean performAction(final View host, final ItemInfo item, int action,
             boolean fromKeyboard) {
         if (action == ACTION_LONG_CLICK) {
             if (PopupContainerWithArrow.canShow(host, item)) {
@@ -193,19 +213,8 @@
                 // standard long press path does.
                 PopupContainerWithArrow.showForIcon((BubbleTextView) host);
                 return true;
-            } else {
-                CustomActionsPopup popup = new CustomActionsPopup(mLauncher, host);
-                if (popup.canShow()) {
-                    popup.show();
-                    return true;
-                }
             }
-        }
-        if (host instanceof AccessibilityActionHandler
-                && ((AccessibilityActionHandler) host).performAccessibilityAction(action, item)) {
-            return true;
-        }
-        if (action == MOVE) {
+        } else if (action == MOVE) {
             return beginAccessibleDrag(host, item, fromKeyboard);
         } else if (action == ADD_TO_WORKSPACE) {
             final int[] coordinates = new int[2];
@@ -220,9 +229,7 @@
                                 Favorites.CONTAINER_DESKTOP,
                                 screenId, coordinates[0], coordinates[1]);
 
-                        ArrayList<ItemInfo> itemList = new ArrayList<>();
-                        itemList.add(info);
-                        mLauncher.bindItems(itemList, true);
+                        mLauncher.bindItems(Collections.singletonList(info), true);
                         announceConfirmation(R.string.item_added_to_workspace);
                     } else if (item instanceof PendingAddItemInfo) {
                         PendingAddItemInfo info = (PendingAddItemInfo) item;
@@ -243,47 +250,31 @@
             final int[] coordinates = new int[2];
             final int screenId = findSpaceOnWorkspace(item, coordinates);
             mLauncher.getModelWriter().moveItemInDatabase(info,
-                    LauncherSettings.Favorites.CONTAINER_DESKTOP,
+                    Favorites.CONTAINER_DESKTOP,
                     screenId, coordinates[0], coordinates[1]);
 
             // Bind the item in next frame so that if a new workspace page was created,
             // it will get laid out.
-            new Handler().post(new Runnable() {
-
-                @Override
-                public void run() {
-                    ArrayList<ItemInfo> itemList = new ArrayList<>();
-                    itemList.add(item);
-                    mLauncher.bindItems(itemList, true);
-                    announceConfirmation(R.string.item_moved);
-                }
+            new Handler().post(() -> {
+                mLauncher.bindItems(Collections.singletonList(item), true);
+                announceConfirmation(R.string.item_moved);
             });
+            return true;
         } else if (action == RESIZE) {
             final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
-            final IntArray actions = getSupportedResizeActions(host, info);
-            CharSequence[] labels = new CharSequence[actions.size()];
-            for (int i = 0; i < actions.size(); i++) {
-                labels[i] = mLauncher.getText(actions.get(i));
-            }
-
-            new AlertDialog.Builder(mLauncher)
-                .setTitle(R.string.action_resize)
-                .setItems(labels, new DialogInterface.OnClickListener() {
-
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                        performResizeAction(actions.get(which), host, info);
-                        dialog.dismiss();
-                    }
-                })
-                .show();
+            List<OptionItem> actions = getSupportedResizeActions(host, info);
+            Rect pos = new Rect();
+            mLauncher.getDragLayer().getDescendantRectRelativeToSelf(host, pos);
+            ArrowPopup popup = OptionsPopupView.show(mLauncher, new RectF(pos), actions);
+            popup.requestFocus();
+            popup.setOnCloseCallback(host::requestFocus);
             return true;
-        } else if (action == DEEP_SHORTCUTS) {
+        } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) {
             return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null;
         } else {
             for (ButtonDropTarget dropTarget : mLauncher.getDropTargetBar().getDropTargets()) {
-                if (dropTarget.supportsAccessibilityDrop(item, host) &&
-                        action == dropTarget.getAccessibilityAction()) {
+                if (dropTarget.supportsAccessibilityDrop(item, host)
+                        && action == dropTarget.getAccessibilityAction()) {
                     dropTarget.onAccessibilityDrop(host, item);
                     return true;
                 }
@@ -292,9 +283,8 @@
         return false;
     }
 
-    private IntArray getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
-        IntArray actions = new IntArray();
-
+    private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
+        List<OptionItem> actions = new ArrayList<>();
         AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
         if (providerInfo == null) {
             return actions;
@@ -304,28 +294,40 @@
         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) {
             if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) ||
                     layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) {
-                actions.add(R.string.action_increase_width);
+                actions.add(new OptionItem(
+                        R.string.action_increase_width, R.drawable.ic_widget_width_increase,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_increase_width, host, info)));
             }
 
             if (info.spanX > info.minSpanX && info.spanX > 1) {
-                actions.add(R.string.action_decrease_width);
+                actions.add(new OptionItem(
+                        R.string.action_decrease_width, R.drawable.ic_widget_width_decrease,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_decrease_width, host, info)));
             }
         }
 
         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) {
             if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) ||
                     layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) {
-                actions.add(R.string.action_increase_height);
+                actions.add(new OptionItem(
+                        R.string.action_increase_height, R.drawable.ic_widget_height_increase,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_increase_height, host, info)));
             }
 
             if (info.spanY > info.minSpanY && info.spanY > 1) {
-                actions.add(R.string.action_decrease_height);
+                actions.add(new OptionItem(
+                        R.string.action_decrease_height, R.drawable.ic_widget_height_decrease,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_decrease_height, host, info)));
             }
         }
         return actions;
     }
 
-    @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
+    private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams();
         CellLayout layout = (CellLayout) host.getParent().getParent();
         layout.markCellsAsUnoccupiedForView(host);
@@ -362,6 +364,7 @@
         host.requestLayout();
         mLauncher.getModelWriter().updateItemInDatabase(info);
         announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY));
+        return true;
     }
 
     @Thunk void announceConfirmation(int resId) {
@@ -489,19 +492,28 @@
         return screenId;
     }
 
-    /**
-     * An interface allowing views to handle their own action.
-     */
-    public interface AccessibilityActionHandler {
+    public class LauncherAction {
+        public final int keyCode;
+        public final AccessibilityAction accessibilityAction;
+
+        private final LauncherAccessibilityDelegate mDelegate;
+
+        public LauncherAction(int id, int labelRes, int keyCode) {
+            this.keyCode = keyCode;
+            accessibilityAction = new AccessibilityAction(id, mLauncher.getString(labelRes));
+            mDelegate = LauncherAccessibilityDelegate.this;
+        }
 
         /**
-         * performs accessibility action and returns true on success
+         * Invokes the action for the provided host
          */
-        boolean performAccessibilityAction(int action, ItemInfo itemInfo);
-
-        /**
-         * adds all the accessibility actions that can be handled.
-         */
-        void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo);
+        public boolean invokeFromKeyboard(View host) {
+            if (host != null && host.getTag() instanceof ItemInfo) {
+                return mDelegate.performAction(
+                        host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true);
+            } else {
+                return false;
+            }
+        }
     }
 }
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index aaaff98..1733e5d 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -18,9 +18,8 @@
 
 import static com.android.launcher3.LauncherState.NORMAL;
 
+import android.view.KeyEvent;
 import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
@@ -31,7 +30,8 @@
 import com.android.launcher3.notification.NotificationMainView;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Extension of {@link LauncherAccessibilityDelegate} with actions specific to shortcuts in
@@ -43,23 +43,23 @@
 
     public ShortcutMenuAccessibilityDelegate(Launcher launcher) {
         super(launcher);
-        mActions.put(DISMISS_NOTIFICATION, new AccessibilityAction(DISMISS_NOTIFICATION,
-                launcher.getText(R.string.action_dismiss_notification)));
+        mActions.put(DISMISS_NOTIFICATION, new LauncherAction(DISMISS_NOTIFICATION,
+                R.string.action_dismiss_notification, KeyEvent.KEYCODE_X));
     }
 
     @Override
-    public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
+    protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
         if ((host.getParent() instanceof DeepShortcutView)) {
-            info.addAction(mActions.get(ADD_TO_WORKSPACE));
+            out.add(mActions.get(ADD_TO_WORKSPACE));
         } else if (host instanceof NotificationMainView) {
             if (((NotificationMainView) host).canChildBeDismissed()) {
-                info.addAction(mActions.get(DISMISS_NOTIFICATION));
+                out.add(mActions.get(DISMISS_NOTIFICATION));
             }
         }
     }
 
     @Override
-    public boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
+    protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
         if (action == ADD_TO_WORKSPACE) {
             if (!(host.getParent() instanceof DeepShortcutView)) {
                 return false;
@@ -73,9 +73,7 @@
                     mLauncher.getModelWriter().addItemToDatabase(info,
                             LauncherSettings.Favorites.CONTAINER_DESKTOP,
                             screenId, coordinates[0], coordinates[1]);
-                    ArrayList<ItemInfo> itemList = new ArrayList<>();
-                    itemList.add(info);
-                    mLauncher.bindItems(itemList, true);
+                    mLauncher.bindItems(Collections.singletonList(info), true);
                     AbstractFloatingView.closeAllOpenViews(mLauncher);
                     announceConfirmation(R.string.item_added_to_workspace);
                 }
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 769cb5e..355ccad 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -21,6 +21,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -78,6 +80,11 @@
         return (mModelFlags & mask) != 0;
     }
 
+    /**
+     * Returns {@link AppInfo} if any apps matches with provided {@link ComponentKey}, otherwise
+     * null.
+     */
+    @Nullable
     public AppInfo getApp(ComponentKey key) {
         mTempInfo.componentName = key.componentName;
         mTempInfo.user = key.user;
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 4f79fb8..aef32d7 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -109,7 +109,8 @@
         int rowWidth = myRequestedWidth - mAppsView.getActiveRecyclerView().getPaddingLeft()
                 - mAppsView.getActiveRecyclerView().getPaddingRight();
 
-        int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.inv.numHotseatIcons);
+        int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.cellLayoutBorderSpacingPx,
+                dp.inv.numHotseatIcons);
         int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * dp.iconSizePx);
         int iconPadding = cellWidth - iconVisibleSize;
 
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 74d8dca..504b29e 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -46,6 +46,7 @@
 import android.util.Pair;
 import android.view.FocusFinder;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewDebug;
@@ -82,7 +83,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
@@ -96,6 +96,8 @@
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.views.ClipPathView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
@@ -165,7 +167,11 @@
     private AnimatorSet mCurrentAnimator;
     private boolean mIsAnimatingClosed = false;
 
+    // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
+    // Anything specific to Launcher should use mLauncher, otherwise should use mActivityContext.
     protected final Launcher mLauncher;
+    protected final ActivityContext mActivityContext;
+
     protected DragController mDragController;
     public FolderInfo mInfo;
     private CharSequence mFromTitle;
@@ -228,6 +234,7 @@
         setAlwaysDrawnWithCacheEnabled(false);
 
         mLauncher = Launcher.getLauncher(context);
+        mActivityContext = ActivityContext.lookupContext(context);
         mStatsLogManager = StatsLogManager.newInstance(context);
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
@@ -457,9 +464,9 @@
         Collections.sort(children, ITEM_POS_COMPARATOR);
         updateItemLocationsInDatabaseBatch(true);
 
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
         if (lp == null) {
-            lp = new DragLayer.LayoutParams(0, 0);
+            lp = new BaseDragLayer.LayoutParams(0, 0);
             lp.customPosition = true;
             setLayoutParams(lp);
         }
@@ -513,13 +520,14 @@
     /**
      * Creates a new UserFolder, inflated from R.layout.user_folder.
      *
-     * @param launcher The main activity.
+     * @param activityContext The main ActivityContext in which to inflate this Folder. It must also
+     *                        be an instance or ContextWrapper around the Launcher activity context.
      *
      * @return A new UserFolder.
      */
     @SuppressLint("InflateParams")
-    static Folder fromXml(Launcher launcher) {
-        return (Folder) launcher.getLayoutInflater()
+    static <T extends Context & ActivityContext> Folder fromXml(T activityContext) {
+        return (Folder) LayoutInflater.from(activityContext).cloneInContext(activityContext)
                 .inflate(R.layout.user_folder_icon_normalized, null);
     }
 
@@ -597,7 +605,7 @@
      * is played.
      */
     private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
-        Folder openFolder = getOpen(mLauncher);
+        Folder openFolder = getOpen(mActivityContext);
         if (openFolder != null && openFolder != this) {
             // Close any open folder before opening a folder.
             openFolder.close(true);
@@ -610,7 +618,7 @@
 
         mIsOpen = true;
 
-        DragLayer dragLayer = mLauncher.getDragLayer();
+        BaseDragLayer dragLayer = mActivityContext.getDragLayer();
         // Just verify that the folder hasn't already been added to the DragLayer.
         // There was a one-off crash where the folder had a parent already.
         if (getParent() == null) {
@@ -724,7 +732,7 @@
 
         // Notify the accessibility manager that this folder "window" has disappeared and no
         // longer occludes the workspace items
-        mLauncher.getDragLayer().sendAccessibilityEvent(
+        mActivityContext.getDragLayer().sendAccessibilityEvent(
                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
     }
 
@@ -772,7 +780,7 @@
 
     private void closeComplete(boolean wasAnimated) {
         // TODO: Clear all active animations.
-        DragLayer parent = (DragLayer) getParent();
+        BaseDragLayer parent = (BaseDragLayer) getParent();
         if (parent != null) {
             parent.removeView(this);
         }
@@ -1011,7 +1019,7 @@
 
     private void updateItemLocationsInDatabaseBatch(boolean isBind) {
         FolderGridOrganizer verifier = new FolderGridOrganizer(
-                mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+                mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
 
         ArrayList<ItemInfo> items = new ArrayList<>();
         int total = mInfo.contents.size();
@@ -1048,10 +1056,8 @@
     }
 
     private void centerAboutIcon() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
-        DragLayer parent = mLauncher.getDragLayer();
+        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
+        BaseDragLayer parent = mActivityContext.getDragLayer();
         int width = getFolderWidth();
         int height = getFolderHeight();
 
@@ -1061,38 +1067,13 @@
         int centeredLeft = centerX - width / 2;
         int centeredTop = centerY - height / 2;
 
-        // We need to bound the folder to the currently visible workspace area
-        if (mLauncher.getStateManager().getState().overviewUi) {
-            parent.getDescendantRectRelativeToSelf(mLauncher.getOverviewPanel(), sTempRect);
-        } else {
-            mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
-        }
-        int left = Math.min(Math.max(sTempRect.left, centeredLeft),
-                sTempRect.right- width);
-        int top = Math.min(Math.max(sTempRect.top, centeredTop),
-                sTempRect.bottom - height);
-
-        int distFromEdgeOfScreen = mLauncher.getWorkspace().getPaddingLeft() + getPaddingLeft();
-
-        if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) {
-            // Center the folder if it is very close to being centered anyway, by virtue of
-            // filling the majority of the viewport. ie. remove it from the uncanny valley
-            // of centeredness.
-            left = (grid.availableWidthPx - width) / 2;
-        } else if (width >= sTempRect.width()) {
-            // If the folder doesn't fit within the bounds, center it about the desired bounds
-            left = sTempRect.left + (sTempRect.width() - width) / 2;
-        }
-        if (height >= sTempRect.height()) {
-            // Folder height is greater than page height, center on page
-            top = sTempRect.top + (sTempRect.height() - height) / 2;
-        } else {
-            // Folder height is less than page height, so bound it to the absolute open folder
-            // bounds if necessary
-            Rect folderBounds = grid.getAbsoluteOpenFolderBounds();
-            left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width));
-            top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height));
-        }
+        sTempRect.set(mActivityContext.getFolderBoundingBox());
+        int left = Utilities.boundToRange(centeredLeft, sTempRect.left, sTempRect.right - width);
+        int top = Utilities.boundToRange(centeredTop, sTempRect.top, sTempRect.bottom - height);
+        int[] inOutPosition = new int[] {left, top};
+        mActivityContext.updateOpenFolderPosition(inOutPosition, sTempRect, width, height);
+        left = inOutPosition[0];
+        top = inOutPosition[1];
 
         int folderPivotX = width / 2 + (centeredLeft - left);
         int folderPivotY = height / 2 + (centeredTop - top);
@@ -1106,7 +1087,7 @@
     }
 
     protected int getContentAreaHeight() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
         int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
                 - mFooterHeight;
         int height = Math.min(maxContentAreaHeight,
@@ -1384,7 +1365,7 @@
     @Override
     public void onAdd(WorkspaceItemInfo item, int rank) {
         FolderGridOrganizer verifier = new FolderGridOrganizer(
-                mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+                mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
         verifier.updateRankAndPos(item, rank);
         mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
                 item.cellY);
@@ -1591,8 +1572,8 @@
     /**
      * Returns a folder which is already open or null
      */
-    public static Folder getOpen(Launcher launcher) {
-        return getOpenView(launcher, TYPE_FOLDER);
+    public static Folder getOpen(ActivityContext activityContext) {
+        return getOpenView(activityContext, TYPE_FOLDER);
     }
 
     /**
@@ -1611,7 +1592,7 @@
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            DragLayer dl = mLauncher.getDragLayer();
+            BaseDragLayer dl = (BaseDragLayer) getParent();
 
             if (isEditingName()) {
                 if (!dl.isEventOverView(mFolderName, ev)) {
@@ -1635,6 +1616,11 @@
         return false;
     }
 
+    @Override
+    public boolean canInterceptEventsInSystemGestureRegion() {
+        return true;
+    }
+
     /**
      * Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
      * rounded rect.
@@ -1663,9 +1649,9 @@
 
     /** Returns the height of the current folder's bottom edge from the bottom of the screen. */
     private int getHeightFromBottom() {
-        DragLayer.LayoutParams layoutParams = (DragLayer.LayoutParams) getLayoutParams();
+        BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
         int folderBottomPx = layoutParams.y + layoutParams.height;
-        int windowBottomPx = mLauncher.getDeviceProfile().heightPx;
+        int windowBottomPx = mActivityContext.getDeviceProfile().heightPx;
 
         return windowBottomPx - folderBottomPx;
     }
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 3d72b49..1cac31e 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -39,14 +39,13 @@
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.BaseDragLayer;
 
 import java.util.List;
 
@@ -69,7 +68,6 @@
     private PreviewBackground mPreviewBackground;
 
     private Context mContext;
-    private Launcher mLauncher;
 
     private final boolean mIsOpening;
 
@@ -92,8 +90,7 @@
         mPreviewBackground = mFolderIcon.mBackground;
 
         mContext = folder.getContext();
-        mLauncher = folder.mLauncher;
-        mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
+        mPreviewVerifier = new FolderGridOrganizer(folder.mActivityContext.getDeviceProfile().inv);
 
         mIsOpening = isOpening;
 
@@ -114,14 +111,15 @@
      * Prepares the Folder for animating between open / closed states.
      */
     public AnimatorSet getAnimator() {
-        final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
+        final BaseDragLayer.LayoutParams lp =
+                (BaseDragLayer.LayoutParams) mFolder.getLayoutParams();
         mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams();
         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
         final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0);
 
         // Match position of the FolderIcon
         final Rect folderIconPos = new Rect();
-        float scaleRelativeToDragLayer = mLauncher.getDragLayer()
+        float scaleRelativeToDragLayer = mFolder.mActivityContext.getDragLayer()
                 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
         int scaledRadius = mPreviewBackground.getScaledRadius();
         float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer;
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index fe310f6..6b02021 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -166,17 +166,19 @@
         mDotParams = new DotRenderer.DrawParams();
     }
 
-    public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
-            FolderInfo folderInfo) {
-        Folder folder = Folder.fromXml(launcher);
-        folder.setDragController(launcher.getDragController());
+    public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
+            T activityContext, ViewGroup group, FolderInfo folderInfo) {
+        Folder folder = Folder.fromXml(activityContext);
+        folder.setDragController(folder.mLauncher.getDragController());
 
-        FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+        FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
         folder.setFolderIcon(icon);
         folder.bind(folderInfo);
         icon.setFolder(folder);
 
-        icon.setOnFocusChangeListener(launcher.getFocusHandler());
+        icon.setOnFocusChangeListener(folder.mLauncher.getFocusHandler());
+        icon.mBackground.paddingY = icon.isInHotseat()
+                ? 0 : activityContext.getDeviceProfile().cellYPaddingPx;
         return icon;
     }
 
@@ -199,7 +201,7 @@
         icon.mFolderName.setText(folderInfo.title);
         icon.mFolderName.setCompoundDrawablePadding(0);
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
-        lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
+        lp.topMargin = grid.cellYPaddingPx + grid.iconSizePx + grid.iconDrawablePaddingPx;
 
         icon.setTag(folderInfo);
         icon.setOnClickListener(ItemClickHandler.INSTANCE);
@@ -218,6 +220,7 @@
 
         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
 
+        icon.mBackground.paddingY = icon.isInHotseat() ? 0 : grid.cellYPaddingPx;
         icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
         icon.mPreviewVerifier.setFolderInfo(folderInfo);
         icon.updatePreviewItems(false);
@@ -579,6 +582,7 @@
     public void setFolderBackground(PreviewBackground bg) {
         mBackground = bg;
         mBackground.setInvalidateDelegate(this);
+        mBackground.paddingY = isInHotseat() ? 0 : mActivity.getDeviceProfile().cellYPaddingPx;
     }
 
     @Override
@@ -745,21 +749,19 @@
         mInfo.removeListener(mFolder);
     }
 
+    private boolean isInHotseat() {
+        return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+    }
+
     public void clearLeaveBehindIfExists() {
-        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
-        if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-            CellLayout cl = (CellLayout) getParent().getParent();
-            cl.clearFolderLeaveBehind();
+        if (getParent() instanceof FolderIconParent) {
+            ((FolderIconParent) getParent()).clearFolderLeaveBehind(this);
         }
     }
 
     public void drawLeaveBehindIfExists() {
-        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
-        // While the folder is open, the position of the icon cannot change.
-        lp.canReorder = false;
-        if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-            CellLayout cl = (CellLayout) getParent().getParent();
-            cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+        if (getParent() instanceof FolderIconParent) {
+            ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this);
         }
     }
 
@@ -828,4 +830,19 @@
                     MAX_NUM_ITEMS_IN_PREVIEW);
         }
     }
+
+    /**
+     * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
+     */
+    public interface FolderIconParent {
+        /**
+         * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a
+         * gap where the FolderIcon would be when the Folder is closed.
+         */
+        void drawFolderLeaveBehindForIcon(FolderIcon child);
+        /**
+         * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed.
+         */
+        void clearFolderLeaveBehind(FolderIcon child);
+    }
 }
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index a08dd30..0235dfa 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
@@ -193,7 +192,7 @@
         int pageNo = rank / mOrganizer.getMaxItemsPerPage();
 
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
-        lp.setXY(mOrganizer.getPosForRank(rank));
+        lp.setCellXY(mOrganizer.getPosForRank(rank));
         getPageAt(pageNo).addViewToCellLayout(view, -1, item.getViewId(), lp, true);
     }
 
@@ -230,7 +229,7 @@
     }
 
     private CellLayout createAndAddNewPage() {
-        DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
+        DeviceProfile grid = mFolder.mActivityContext.getDeviceProfile();
         CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this);
         page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
         page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
@@ -306,7 +305,7 @@
             if (v != null) {
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
                 ItemInfo info = (ItemInfo) v.getTag();
-                lp.setXY(mOrganizer.getPosForRank(rank));
+                lp.setCellXY(mOrganizer.getPosForRank(rank));
                 currentPage.addViewToCellLayout(v, -1, info.getViewId(), lp, true);
 
                 if (mOrganizer.isItemInPreview(rank) && v instanceof BubbleTextView) {
@@ -624,7 +623,7 @@
 
     @Override
     protected boolean canScroll(float absVScroll, float absHScroll) {
-        return AbstractFloatingView.getTopOpenViewWithType(mFolder.mLauncher,
+        return AbstractFloatingView.getTopOpenViewWithType(mFolder.mActivityContext,
                 TYPE_ALL & ~TYPE_FOLDER) == null;
     }
 
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 27b906b..767fffe 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -74,6 +74,7 @@
     int previewSize;
     int basePreviewOffsetX;
     int basePreviewOffsetY;
+    int paddingY;
 
     private CellLayout mDrawingDelegate;
 
@@ -157,7 +158,7 @@
         previewSize = grid.folderIconSizePx;
 
         basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
-        basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
+        basePreviewOffsetY = paddingY + topPadding + grid.folderIconOffsetYPx;
 
         // Stroke width is 1dp
         mStrokeWidth = context.getResources().getDisplayMetrics().density;
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
deleted file mode 100644
index 77ce4a8..0000000
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2016 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.keyboard;
-
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.widget.PopupMenu;
-import android.widget.PopupMenu.OnMenuItemClickListener;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Handles showing a popup menu with available custom actions for a launcher icon.
- * This allows exposing various custom actions using keyboard shortcuts.
- */
-public class CustomActionsPopup implements OnMenuItemClickListener {
-
-    private final Launcher mLauncher;
-    private final LauncherAccessibilityDelegate mDelegate;
-    private final View mIcon;
-
-    public CustomActionsPopup(Launcher launcher, View icon) {
-        mLauncher = launcher;
-        mIcon = icon;
-        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
-        if (container != null) {
-            mDelegate = container.getAccessibilityDelegate();
-        } else {
-            mDelegate = launcher.getAccessibilityDelegate();
-        }
-    }
-
-    private List<AccessibilityAction> getActionList() {
-        if (mIcon == null || !(mIcon.getTag() instanceof ItemInfo)) {
-            return Collections.EMPTY_LIST;
-        }
-
-        AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
-        mDelegate.addSupportedActions(mIcon, info, true);
-        List<AccessibilityAction> result = new ArrayList<>(info.getActionList());
-        info.recycle();
-        return result;
-    }
-
-    public boolean canShow() {
-        return !getActionList().isEmpty();
-    }
-
-    public boolean show() {
-        List<AccessibilityAction> actions = getActionList();
-        if (actions.isEmpty()) {
-            return false;
-        }
-
-        PopupMenu popup = new PopupMenu(mLauncher, mIcon);
-        popup.setOnMenuItemClickListener(this);
-        Menu menu = popup.getMenu();
-        for (AccessibilityAction action : actions) {
-            menu.add(Menu.NONE, action.getId(), Menu.NONE, action.getLabel());
-        }
-        popup.show();
-        return true;
-    }
-
-    @Override
-    public boolean onMenuItemClick(MenuItem menuItem) {
-        return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId(),
-                true);
-    }
-}
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 1266bb4..0802f8a 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -31,8 +31,6 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.ResourceBasedOverride;
 
-import java.util.Optional;
-
 /**
  * Handles the user event logging in R+.
  *
@@ -486,7 +484,9 @@
      */
     public StatsLogger logger() {
         StatsLogger logger = createLogger();
-        Optional.ofNullable(mInstanceId).ifPresent(logger::withInstanceId);
+        if (mInstanceId != null) {
+            return logger.withInstanceId(mInstanceId);
+        }
         return logger;
     }
 
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index e3e4b69..92b5885 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -22,7 +22,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.os.LocaleList;
@@ -137,7 +136,7 @@
         if (findAppInfo(info.componentName, info.user) != null) {
             return;
         }
-        mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);
+        mIconCache.getTitleAndIcon(info, activityInfo, false /* useLowResIcon */);
         info.sectionName = mIndex.computeSectionName(info.title);
 
         data.add(info);
@@ -145,10 +144,9 @@
     }
 
     public void addPromiseApp(Context context, PackageInstallInfo installInfo) {
-        ApplicationInfo applicationInfo = new PackageManagerHelper(context)
-                .getApplicationInfo(installInfo.packageName, installInfo.user, 0);
         // only if not yet installed
-        if (applicationInfo == null) {
+        if (!new PackageManagerHelper(context)
+                .isAppInstalled(installInfo.packageName, installInfo.user)) {
             AppInfo info = new AppInfo(installInfo);
             mIconCache.getTitleAndIcon(info, info.usingLowResIcon());
             info.sectionName = mIndex.computeSectionName(info.title);
@@ -282,7 +280,7 @@
                 } else {
                     Intent launchIntent = AppInfo.makeLaunchIntent(info);
 
-                    mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */);
+                    mIconCache.getTitleAndIcon(applicationInfo, info, false /* useLowResIcon */);
                     applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title);
                     applicationInfo.setProgressLevel(
                             PackageManagerHelper.getLoadingProgress(info),
diff --git a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
index e3e8769..434776c 100644
--- a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.model;
 
-import android.content.ComponentName;
+import android.content.Intent;
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherAppState;
@@ -66,8 +66,8 @@
         final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>();
         synchronized (dataModel) {
             dataModel.forAllWorkspaceItemInfos(mUser, si -> {
-                ComponentName cn = si.getTargetComponent();
-                if ((cn != null) && cn.getPackageName().equals(mPackageName)) {
+                Intent intent = si.getIntent();
+                if ((intent != null) && mPackageName.equals(intent.getPackage())) {
                     si.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
                     si.setProgressLevel(downloadInfo);
                     updatedWorkspaceItems.add(si);
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 8215edd..1380e9e 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.model;
 
-import android.content.ComponentName;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
@@ -72,9 +72,9 @@
         synchronized (dataModel) {
             final HashSet<ItemInfo> updates = new HashSet<>();
             dataModel.forAllWorkspaceItemInfos(mInstallInfo.user, si -> {
-                ComponentName cn = si.getTargetComponent();
-                if (si.hasPromiseIconUi() && (cn != null)
-                        && cn.getPackageName().equals(mInstallInfo.packageName)) {
+                Intent intent = si.getIntent();
+                if (si.hasPromiseIconUi() && (intent != null)
+                        && mInstallInfo.packageName.equals(intent.getPackage())) {
                     int installProgress = mInstallInfo.progress;
 
                     si.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING);
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index f13a109..7bfa3ef 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -30,6 +30,7 @@
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
@@ -228,7 +229,8 @@
                                 isTargetValid = context.getSystemService(LauncherApps.class)
                                         .isActivityEnabled(cn, mUser);
                             }
-                            if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
+                            if (!isTargetValid && si.hasStatusFlag(
+                                    FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
                                 if (updateWorkspaceItemIntent(context, si, packageName)) {
                                     infoUpdated = true;
                                 } else if (si.hasPromiseIconUi()) {
@@ -250,8 +252,7 @@
                             }
                         }
 
-                        if (isNewApkAvailable
-                                && si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                        if (isNewApkAvailable) {
                             List<LauncherActivityInfo> activities = activitiesLists.get(
                                     packageName);
                             si.setProgressLevel(
@@ -260,8 +261,10 @@
                                             : PackageManagerHelper.getLoadingProgress(
                                                     activities.get(0)),
                                     PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
-                            iconCache.getTitleAndIcon(si, si.usingLowResIcon());
-                            infoUpdated = true;
+                            if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                                iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                                infoUpdated = true;
+                            }
                         }
 
                         int oldRuntimeFlags = si.runtimeStatusFlags;
@@ -353,6 +356,11 @@
      */
     private boolean updateWorkspaceItemIntent(Context context,
             WorkspaceItemInfo si, String packageName) {
+        if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+            // Do not update intent for deep shortcuts as they contain additional information
+            // about the shortcut.
+            return false;
+        }
         // Try to find the best match activity.
         Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser);
         if (intent != null) {
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 6fedad1..4296d32 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -66,6 +67,14 @@
         }
 
         if (!matchingWorkspaceItems.isEmpty()) {
+            if (mShortcuts.isEmpty()) {
+                // Verify that the app is indeed installed.
+                if (!new PackageManagerHelper(app.getContext())
+                        .isAppInstalled(mPackageName, mUser)) {
+                    // App is not installed, ignoring package events
+                    return;
+                }
+            }
             // Update the workspace to reflect the changes to updated shortcuts residing on it.
             List<String> allLauncherKnownIds = matchingWorkspaceItems.stream()
                     .map(WorkspaceItemInfo::getDeepShortcutId)
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 059ad18..2905dc3 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -16,9 +16,9 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.annotation.TargetApi;
 import android.app.Notification;
@@ -37,8 +37,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.SecureSettingsObserver;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -81,7 +81,8 @@
     /** The last notification key that was dismissed from launcher UI */
     private String mLastKeyDismissedByLauncher;
 
-    private SecureSettingsObserver mNotificationDotsObserver;
+    private SettingsCache mSettingsCache;
+    private SettingsCache.OnChangeListener mNotificationSettingsChangedListener;
 
     public NotificationListener() {
         mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
@@ -207,10 +208,12 @@
         super.onListenerConnected();
         sIsConnected = true;
 
-        mNotificationDotsObserver =
-                newNotificationSettingsObserver(this, this::onNotificationSettingsChanged);
-        mNotificationDotsObserver.register();
-        mNotificationDotsObserver.dispatchOnChange();
+        // Register an observer to rebind the notification listener when dots are re-enabled.
+        mSettingsCache = SettingsCache.INSTANCE.get(this);
+        mNotificationSettingsChangedListener = this::onNotificationSettingsChanged;
+        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                mNotificationSettingsChangedListener);
+        mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
 
         onNotificationFullRefresh();
     }
@@ -229,7 +232,7 @@
     public void onListenerDisconnected() {
         super.onListenerDisconnected();
         sIsConnected = false;
-        mNotificationDotsObserver.unregister();
+        mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener);
         onNotificationFullRefresh();
     }
 
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index fa25114..0091af1 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -217,8 +217,8 @@
                 && sessionInfo.getAppIcon() != null
                 && !TextUtils.isEmpty(sessionInfo.getAppLabel())
                 && !promiseIconAddedForId(sessionInfo.getSessionId())
-                && new PackageManagerHelper(mAppContext).getApplicationInfo(
-                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo), 0) == null) {
+                && !new PackageManagerHelper(mAppContext).isAppInstalled(
+                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo))) {
             ItemInstallQueue.INSTANCE.get(mAppContext)
                     .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
 
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 90285c4..56438d0 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -40,6 +40,8 @@
 import android.view.ViewOutlineProvider;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.InsettableFrameLayout;
@@ -82,6 +84,8 @@
     private final Rect mStartRect = new Rect();
     private final Rect mEndRect = new Rect();
 
+    private Runnable mOnCloseCallback = () -> { };
+
     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mInflater = LayoutInflater.from(context);
@@ -555,6 +559,14 @@
         mDeferContainerRemoval = false;
         getPopupContainer().removeView(this);
         getPopupContainer().removeView(mArrow);
+        mOnCloseCallback.run();
+    }
+
+    /**
+     * Callback to be called when the popup is closed
+     */
+    public void setOnCloseCallback(@NonNull Runnable callback) {
+        mOnCloseCallback = callback;
     }
 
     protected BaseDragLayer getPopupContainer() {
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 59930ff..a1ba747 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -206,6 +206,7 @@
                         .filter(Objects::nonNull)
                         .collect(Collectors.toList()));
         launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
+        container.requestFocus();
         return container;
     }
 
diff --git a/src/com/android/launcher3/settings/NotificationDotsPreference.java b/src/com/android/launcher3/settings/NotificationDotsPreference.java
index a91303a..a354169 100644
--- a/src/com/android/launcher3/settings/NotificationDotsPreference.java
+++ b/src/com/android/launcher3/settings/NotificationDotsPreference.java
@@ -35,14 +35,14 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
 
 /**
  * A {@link Preference} for indicating notification dots status.
  * Also has utility methods for updating UI based on dots status changes.
  */
 public class NotificationDotsPreference extends Preference
-        implements SecureSettingsObserver.OnChangeListener {
+        implements SettingsCache.OnChangeListener {
 
     private boolean mWidgetFrameVisible = false;
 
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 922425f..ac8dac5 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,13 +18,13 @@
 
 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
 
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_ENABLED_LISTENERS;
 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;
 
 import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.provider.Settings;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -45,8 +45,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.util.SecureSettingsObserver;
 
 /**
  * Settings activity for Launcher. Currently implements the following setting: Allow rotation
@@ -59,8 +59,6 @@
     private static final String FLAGS_PREFERENCE_KEY = "flag_toggler";
 
     private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging";
-    /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
-    private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners";
 
     public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
     public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
@@ -126,10 +124,11 @@
      */
     public static class LauncherSettingsFragment extends PreferenceFragmentCompat {
 
-        private SecureSettingsObserver mNotificationDotsObserver;
+        private SettingsCache mSettingsCache;
 
         private String mHighLightKey;
         private boolean mPreferenceHighlighted = false;
+        private NotificationDotsPreference mNotificationSettingsChangedListener;
 
         @Override
         public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -177,14 +176,16 @@
                     }
 
                     // Listen to system notification dot settings while this UI is active.
-                    mNotificationDotsObserver = newNotificationSettingsObserver(
-                            getActivity(), (NotificationDotsPreference) preference);
-                    mNotificationDotsObserver.register();
+                    mSettingsCache = SettingsCache.INSTANCE.get(getActivity());
+                    mNotificationSettingsChangedListener =
+                            ((NotificationDotsPreference) preference);
+                    mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                            (NotificationDotsPreference) mNotificationSettingsChangedListener);
                     // Also listen if notification permission changes
-                    mNotificationDotsObserver.getResolver().registerContentObserver(
-                            Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false,
-                            mNotificationDotsObserver);
-                    mNotificationDotsObserver.dispatchOnChange();
+                    mSettingsCache.register(NOTIFICATION_ENABLED_LISTENERS,
+                            mNotificationSettingsChangedListener);
+                    mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
+                    mSettingsCache.dispatchOnChange(NOTIFICATION_ENABLED_LISTENERS);
                     return true;
 
                 case ALLOW_ROTATION_PREFERENCE_KEY:
@@ -251,9 +252,11 @@
 
         @Override
         public void onDestroy() {
-            if (mNotificationDotsObserver != null) {
-                mNotificationDotsObserver.unregister();
-                mNotificationDotsObserver = null;
+            if (mSettingsCache != null) {
+                mSettingsCache.unregister(NOTIFICATION_BADGING_URI,
+                        mNotificationSettingsChangedListener);
+                mSettingsCache.unregister(NOTIFICATION_ENABLED_LISTENERS,
+                        mNotificationSettingsChangedListener);
             }
             super.onDestroy();
         }
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 601e117..7abb653 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -150,8 +150,8 @@
 
     private void handleDeferredResume() {
         if (hasBeenResumed() && !getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)) {
-            onDeferredResumed();
             addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
+            onDeferredResumed();
 
             mDeferredResumePending = false;
         } else {
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 3b7bcc2..a8a7e94 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -29,6 +29,7 @@
 import android.util.SparseArray;
 import android.view.Display;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
@@ -182,20 +183,26 @@
         }
 
         /** Creates and up-to-date DisplayController.Info for the given context. */
+        @Nullable
         public Info createInfoForContext(Context context) {
-            Display display = Utilities.ATLEAST_R
-                    ? context.getDisplay()
-                    : context
-                        .getSystemService(DisplayManager.class)
-                        .getDisplay(mId);
-            return display == null
-                    ? new Info(context)
-                    : new Info(context, display);
+            Display display = Utilities.ATLEAST_R ? context.getDisplay() : null;
+            if (display == null) {
+                display = context.getSystemService(DisplayManager.class).getDisplay(mId);
+            }
+            if (display == null) {
+                return null;
+            }
+            // Refresh the Context the prevent stale DisplayMetrics.
+            Context displayContext = context.getApplicationContext().createDisplayContext(display);
+            return new Info(displayContext, display);
         }
 
         protected void handleOnChange() {
             Info oldInfo = mInfo;
             Info newInfo = createInfoForContext(mDisplayContext);
+            if (newInfo == null) {
+                return;
+            }
 
             int change = 0;
             if (newInfo.hasDifferentSize(oldInfo)) {
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 7b26427..08ec591 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -92,6 +92,14 @@
     }
 
     /**
+     * Returns whether the target app is installed for a given user
+     */
+    public boolean isAppInstalled(String packageName, UserHandle user) {
+        ApplicationInfo info = getApplicationInfo(packageName, user, 0);
+        return info != null;
+    }
+
+    /**
      * Returns the application info for the provided package or null
      */
     public ApplicationInfo getApplicationInfo(String packageName, UserHandle user, int flags) {
@@ -105,7 +113,7 @@
     }
 
     public boolean isSafeMode() {
-        return mContext.getPackageManager().isSafeMode();
+        return mPm.isSafeMode();
     }
 
     public Intent getAppLaunchIntent(String pkg, UserHandle user) {
diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java
deleted file mode 100644
index 9fe72ad..0000000
--- a/src/com/android/launcher3/util/SecureSettingsObserver.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.provider.Settings;
-
-/**
- * Utility class to listen for secure settings changes
- */
-public class SecureSettingsObserver extends ContentObserver {
-
-    /** 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;
-    private final int mDefaultValue;
-    private final OnChangeListener mOnChangeListener;
-
-    public SecureSettingsObserver(ContentResolver resolver, OnChangeListener listener,
-            String keySetting, int defaultValue) {
-        super(new Handler());
-
-        mResolver = resolver;
-        mOnChangeListener = listener;
-        mKeySetting = keySetting;
-        mDefaultValue = defaultValue;
-    }
-
-    @Override
-    public void onChange(boolean selfChange) {
-        mOnChangeListener.onSettingsChanged(getValue());
-    }
-
-    public boolean getValue() {
-        return Settings.Secure.getInt(mResolver, mKeySetting, mDefaultValue) == 1;
-    }
-
-    public void register() {
-        mResolver.registerContentObserver(Settings.Secure.getUriFor(mKeySetting), false, this);
-    }
-
-    public ContentResolver getResolver() {
-        return mResolver;
-    }
-
-    public void dispatchOnChange() {
-        onChange(true);
-    }
-
-    public void unregister() {
-        mResolver.unregisterContentObserver(this);
-    }
-
-    public interface OnChangeListener {
-        void onSettingsChanged(boolean isEnabled);
-    }
-
-    public static SecureSettingsObserver newNotificationSettingsObserver(Context context,
-            OnChangeListener listener) {
-        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, 0);
-    }
-
-    /**
-     * 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/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
new file mode 100644
index 0000000..22b4d38
--- /dev/null
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static android.provider.Settings.System.ACCELEROMETER_ROTATION;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * ContentObserver over Settings keys that also has a caching layer.
+ * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and
+ * {@link #unregister(Uri, OnChangeListener)} methods.
+ *
+ * This can be used as a normal cache without any listeners as well via the
+ * {@link #getValue(Uri, int)} and {@link #dispatchOnChange(Uri)} to update (and subsequently call
+ * get)
+ *
+ * The cache will be invalidated/updated through the normal
+ * {@link ContentObserver#onChange(boolean)} calls
+ * or can be force updated by calling {@link #dispatchOnChange(Uri)}.
+ *
+ * Cache will also be updated if a key queried is missing (even if it has no listeners registered).
+ */
+public class SettingsCache extends ContentObserver {
+
+    /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
+    public static final Uri NOTIFICATION_BADGING_URI =
+            Settings.Secure.getUriFor("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";
+    /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
+    public static final Uri NOTIFICATION_ENABLED_LISTENERS =
+            Settings.Secure.getUriFor("enabled_notification_listeners");
+    public static final Uri ROTATION_SETTING_URI =
+            Settings.System.getUriFor(ACCELEROMETER_ROTATION);
+
+    private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString();
+
+    /**
+     * Caches the last seen value for registered keys.
+     */
+    private Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>();
+    private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>();
+    protected final ContentResolver mResolver;
+
+
+    /**
+     * Singleton instance
+     */
+    public static MainThreadInitializedObject<SettingsCache> INSTANCE =
+            new MainThreadInitializedObject<>(SettingsCache::new);
+
+    private SettingsCache(Context context) {
+        super(new Handler());
+        mResolver = context.getContentResolver();
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        // We use default of 1, but if we're getting an onChange call, can assume a non-default
+        // value will exist
+        boolean newVal = updateValue(uri, 1 /* Effectively Unused */);
+        if (!mListenerMap.containsKey(uri)) {
+            return;
+        }
+
+        for (OnChangeListener listener : mListenerMap.get(uri)) {
+            listener.onSettingsChanged(newVal);
+        }
+    }
+
+    /**
+     * Returns the value for this classes key from the cache. If not in cache, will call
+     * {@link #updateValue(Uri, int)} to fetch.
+     */
+    public boolean getValue(Uri keySetting, int defaultValue) {
+        if (mKeyCache.containsKey(keySetting)) {
+            return mKeyCache.get(keySetting);
+        } else {
+            return updateValue(keySetting, defaultValue);
+        }
+    }
+
+    /**
+     * Does not de-dupe if you add same listeners for the same key multiple times.
+     * Unregister once complete using {@link #unregister(Uri, OnChangeListener)}
+     */
+    public void register(Uri uri, OnChangeListener changeListener) {
+        if (mListenerMap.containsKey(uri)) {
+            mListenerMap.get(uri).add(changeListener);
+        } else {
+            CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>();
+            l.add(changeListener);
+            mListenerMap.put(uri, l);
+            mResolver.registerContentObserver(uri, false, this);
+        }
+    }
+
+    private boolean updateValue(Uri keyUri, int defaultValue) {
+        String key = keyUri.getLastPathSegment();
+        boolean newVal;
+        if (keyUri.toString().startsWith(SYSTEM_URI_PREFIX)) {
+            newVal = Settings.System.getInt(mResolver, key, defaultValue) == 1;
+        } else { // SETTING_SECURE
+            newVal = Settings.Secure.getInt(mResolver, key, defaultValue) == 1;
+        }
+
+        mKeyCache.put(keyUri, newVal);
+        return newVal;
+    }
+
+    /**
+     * Force update a change for a given URI and have all listeners for that URI receive callbacks
+     * even if the value is unchanged.
+     */
+    public void dispatchOnChange(Uri uri) {
+        onChange(true, uri);
+    }
+
+    /**
+     * Call to stop receiving updates on the given {@param listener}.
+     * This Uri/Listener pair must correspond to the same pair called with for
+     * {@link #register(Uri, OnChangeListener)}
+     */
+    public void unregister(Uri uri, OnChangeListener listener) {
+        List<OnChangeListener> listenersToRemoveFrom = mListenerMap.get(uri);
+        if (!listenersToRemoveFrom.contains(listener)) {
+            return;
+        }
+
+        listenersToRemoveFrom.remove(listener);
+        if (listenersToRemoveFrom.isEmpty()) {
+            mListenerMap.remove(uri);
+        }
+    }
+
+    /**
+     * Don't use this. Ever.
+     * @param keyCache Cache to replace {@link #mKeyCache}
+     */
+    @VisibleForTesting
+    void setKeyCache(Map<Uri, Boolean> keyCache) {
+        mKeyCache = keyCache;
+    }
+
+    public interface OnChangeListener {
+        void onSettingsChanged(boolean isEnabled);
+    }
+}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index ae459e1..505c6ce 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -17,7 +17,7 @@
 
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.view.ContextThemeWrapper;
+import android.graphics.Rect;
 import android.view.View.AccessibilityDelegate;
 
 import com.android.launcher3.DeviceProfile;
@@ -49,6 +49,23 @@
         return null;
     }
 
+    default Rect getFolderBoundingBox() {
+        return getDeviceProfile().getAbsoluteOpenFolderBounds();
+    }
+
+    /**
+     * After calling {@link #getFolderBoundingBox()}, we calculate a (left, top) position for a
+     * Folder of size width x height to be within those bounds. However, the chosen position may
+     * not be visually ideal (e.g. uncanny valley of centeredness), so here's a chance to update it.
+     * @param inOutPosition A 2-size array where the first element is the left position of the open
+     *     folder and the second element is the top position. Should be updated in place if desired.
+     * @param bounds The bounds that the open folder should fit inside.
+     * @param width The width of the open folder.
+     * @param height The height of the open folder.
+     */
+    default void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
+    }
+
     /**
      * The root view to support drag-and-drop and popup support.
      */
@@ -56,10 +73,10 @@
 
     DeviceProfile getDeviceProfile();
 
-    static ActivityContext lookupContext(Context context) {
+    static <T extends ActivityContext> T lookupContext(Context context) {
         if (context instanceof ActivityContext) {
-            return (ActivityContext) context;
-        } else if (context instanceof ContextThemeWrapper) {
+            return (T) context;
+        } else if (context instanceof ContextWrapper) {
             return lookupContext(((ContextWrapper) context).getBaseContext());
         } else {
             throw new IllegalArgumentException("Cannot find ActivityContext in parent tree");
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 15f7730..1939d15 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -206,15 +206,19 @@
 
     protected boolean findActiveController(MotionEvent ev) {
         mActiveController = null;
-        if ((mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
-                | TOUCH_DISPATCHING_FROM_PROXY)) == 0) {
-            // Only look for controllers if we are not dispatching from gesture area and proxy is
-            // not active
+        if (canFindActiveController()) {
             mActiveController = findControllerToHandleTouch(ev);
         }
         return mActiveController != null;
     }
 
+    protected boolean canFindActiveController() {
+        // Only look for controllers if we are not dispatching from gesture area and proxy is
+        // not active
+        return (mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
+                | TOUCH_DISPATCHING_FROM_PROXY)) == 0;
+    }
+
     @Override
     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
         // Shortcuts can appear above folder
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index addaf9c..0cb8c1e 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -118,7 +118,8 @@
         mTargetRect.roundOut(outPos);
     }
 
-    public static void show(Launcher launcher, RectF targetRect, List<OptionItem> items) {
+    public static OptionsPopupView show(
+            Launcher launcher, RectF targetRect, List<OptionItem> items) {
         OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
                 .inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
         popup.mTargetRect = targetRect;
@@ -134,6 +135,7 @@
             popup.mItemMap.put(view, item);
         }
         popup.show();
+        return popup;
     }
 
     @VisibleForTesting
diff --git a/tests/Android.mk b/tests/Android.mk
index 3d9077d..43d51fc 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -32,7 +32,6 @@
 
     LOCAL_SRC_FILES := $(call all-java-files-under, tapl) \
         ../src/com/android/launcher3/ResourceUtils.java \
-        ../src/com/android/launcher3/util/SecureSettingsObserver.java \
         ../src/com/android/launcher3/testing/TestProtocol.java
 endif