Adding support for keyboard based drag and drop

For keyboard DnD, we use Accessible DnD implementation.
A placeholder FloatingView draws the focus indicator
for the virtual views

Test: Visible
Bug: 178781566
Change-Id: I632fc7377dffa1e05e3f0a9c3ad18641deb5a1a4
diff --git a/res/layout/keyboard_drag_and_drop.xml b/res/layout/keyboard_drag_and_drop.xml
new file mode 100644
index 0000000..e9463c4
--- /dev/null
+++ b/res/layout/keyboard_drag_and_drop.xml
@@ -0,0 +1,34 @@
+<?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.
+-->
+<com.android.launcher3.keyboard.KeyboardDragAndDropView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:orientation="vertical"
+    android:elevation="6dp">
+
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:background="?attr/folderFillColor"
+        android:padding="8dp"
+        android:textColor="?attr/folderTextColor"
+        />
+
+</com.android.launcher3.keyboard.KeyboardDragAndDropView>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index cf51f77..eaf7a5f 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -139,6 +139,7 @@
     <dimen name="drag_flingToDeleteMinVelocity">-1500dp</dimen>
 
     <dimen name="spring_loaded_panel_border">1dp</dimen>
+    <dimen name="keyboard_drag_stroke_width">4dp</dimen>
 
 <!-- Folders -->
     <dimen name="page_indicator_dot_size">8dp</dimen>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 3c34444..6037c96 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -76,17 +76,18 @@
     public static final int TYPE_SNACKBAR = 1 << 7;
     public static final int TYPE_LISTENER = 1 << 8;
     public static final int TYPE_ALL_APPS_EDU = 1 << 9;
+    public static final int TYPE_DRAG_DROP_POPUP = 1 << 10;
 
     // Popups related to quickstep UI
-    public static final int TYPE_TASK_MENU = 1 << 10;
-    public static final int TYPE_OPTIONS_POPUP = 1 << 11;
-    public static final int TYPE_ICON_SURFACE = 1 << 12;
+    public static final int TYPE_TASK_MENU = 1 << 11;
+    public static final int TYPE_OPTIONS_POPUP = 1 << 12;
+    public static final int TYPE_ICON_SURFACE = 1 << 13;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
             | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
             | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU
-            | TYPE_ICON_SURFACE;
+            | TYPE_ICON_SURFACE | TYPE_DRAG_DROP_POPUP;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
@@ -103,7 +104,7 @@
     // These view all have particular operation associated with swipe down interaction.
     public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
             TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_ON_BOARD_POPUP |
-            TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU ;
+            TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_DRAG_DROP_POPUP;
 
     protected boolean mIsOpen;
 
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index df005e6..459b9a8 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -240,7 +240,7 @@
 
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        mActive = supportsDrop(dragObject.dragInfo);
+        mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo);
         mDrawable.setColorFilter(null);
         if (mCurrentColorAnim != null) {
             mCurrentColorAnim.cancel();
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 2809bd5..452207d 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -312,6 +312,13 @@
         }
     }
 
+    /**
+     * Returns the currently set accessibility delegate
+     */
+    public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() {
+        return mTouchHelper;
+    }
+
     @Override
     public boolean dispatchHoverEvent(MotionEvent event) {
         // Always attempt to dispatch hover events to accessibility first.
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index ca001a3..c768493 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -131,7 +131,10 @@
         int width = MeasureSpec.getSize(widthMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
 
-        if (mIsVertical) {
+        int visibleCount = getVisibleButtonsCount();
+        if (visibleCount == 0) {
+            // do nothing
+        } else if (mIsVertical) {
             int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
             int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
 
@@ -142,7 +145,6 @@
                 }
             }
         } else {
-            int visibleCount = getVisibleButtonsCount();
             int availableWidth = width / visibleCount;
             boolean textVisible = true;
             for (ButtonDropTarget buttons : mDropTargets) {
@@ -165,7 +167,10 @@
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        if (mIsVertical) {
+        int visibleCount = getVisibleButtonsCount();
+        if (visibleCount == 0) {
+            // do nothing
+        } else if (mIsVertical) {
             int gap = getResources().getDimensionPixelSize(R.dimen.drop_target_vertical_gap);
             int start = gap;
             int end;
@@ -178,7 +183,6 @@
                 }
             }
         } else {
-            int visibleCount = getVisibleButtonsCount();
             int frameSize = (right - left) / visibleCount;
 
             int start = frameSize / 2;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0274775..70a0be9 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2681,7 +2681,8 @@
                             && focusedView.getTag() instanceof ItemInfo
                             && mAccessibilityDelegate.performAction(focusedView,
                             (ItemInfo) focusedView.getTag(),
-                            LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
+                            LauncherAccessibilityDelegate.DEEP_SHORTCUTS,
+                            true)) {
                         PopupContainerWithArrow.getOpen(this).requestFocus();
                         return true;
                     }
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index ddb547f..d0fc175 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DragLayer;
 
 import java.util.List;
 
@@ -41,30 +42,32 @@
         implements OnClickListener, OnHoverListener {
     protected static final int INVALID_POSITION = -1;
 
-    private static final int[] sTempArray = new int[2];
+    protected final Rect mTempRect = new Rect();
+    protected final int[] mTempCords = new int[2];
 
     protected final CellLayout mView;
     protected final Context mContext;
     protected final LauncherAccessibilityDelegate mDelegate;
-
-    private final Rect mTempRect = new Rect();
+    protected final DragLayer mDragLayer;
 
     public DragAndDropAccessibilityDelegate(CellLayout forView) {
         super(forView);
         mView = forView;
         mContext = mView.getContext();
-        mDelegate = Launcher.getLauncher(mContext).getAccessibilityDelegate();
+        Launcher launcher = Launcher.getLauncher(mContext);
+        mDelegate = launcher.getAccessibilityDelegate();
+        mDragLayer = launcher.getDragLayer();
     }
 
     @Override
-    protected int getVirtualViewAt(float x, float y) {
+    public int getVirtualViewAt(float x, float y) {
         if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) {
             return INVALID_ID;
         }
-        mView.pointToCellExact((int) x, (int) y, sTempArray);
+        mView.pointToCellExact((int) x, (int) y, mTempCords);
 
         // Map cell to id
-        int id = sTempArray[0] + sTempArray[1] * mView.getCountX();
+        int id = mTempCords[0] + mTempCords[1] * mView.getCountX();
         return intersectsValidDropTarget(id);
     }
 
@@ -75,7 +78,7 @@
     protected abstract int intersectsValidDropTarget(int id);
 
     @Override
-    protected void getVisibleVirtualViews(List<Integer> virtualViews) {
+    public void getVisibleVirtualViews(List<Integer> virtualViews) {
         // We create a virtual view for each cell of the grid
         // The cell ids correspond to cells in reading order.
         int nCells = mView.getCountX() * mView.getCountY();
@@ -88,7 +91,7 @@
     }
 
     @Override
-    protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
+    public boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
         if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) {
             String confirmation = getConfirmationForIconDrop(viewId);
             mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation);
@@ -112,13 +115,25 @@
     }
 
     @Override
-    protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
+    public void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
         if (id == INVALID_ID) {
             throw new IllegalArgumentException("Invalid virtual view id");
         }
 
         node.setContentDescription(getLocationDescriptionForIconDrop(id));
-        node.setBoundsInParent(getItemBounds(id));
+
+        Rect itemBounds = getItemBounds(id);
+        node.setBoundsInParent(itemBounds);
+
+        // ExploreByTouchHelper does not currently handle view scale.
+        // Update BoundsInScreen to appropriate value.
+        mTempCords[0] = mTempCords[1] = 0;
+        float scale = mDragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
+        mTempRect.left = mTempCords[0] + (int) (itemBounds.left * scale);
+        mTempRect.right = mTempCords[0] + (int) (itemBounds.right * scale);
+        mTempRect.top = mTempCords[1] + (int) (itemBounds.top * scale);
+        mTempRect.bottom = mTempCords[1] + (int) (itemBounds.bottom * scale);
+        node.setBoundsInScreen(mTempRect);
 
         node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
         node.setClickable(true);
@@ -130,6 +145,13 @@
         return dispatchHoverEvent(motionEvent);
     }
 
+    /**
+     * Returns the target host container
+     */
+    public View getHost() {
+        return mView;
+    }
+
     protected abstract String getLocationDescriptionForIconDrop(int id);
 
     protected abstract String getConfirmationForIconDrop(int id);
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 136d43e..6fac79a 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -34,6 +34,7 @@
 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;
 import com.android.launcher3.model.data.ItemInfo;
@@ -107,10 +108,6 @@
                 launcher.getText(R.string.shortcuts_menu_with_notifications_description)));
     }
 
-    public void addAccessibilityAction(int action, int actionLabel) {
-        mActions.put(action, new AccessibilityAction(action, mLauncher.getText(actionLabel)));
-    }
-
     @Override
     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(host, info);
@@ -139,7 +136,7 @@
         }
 
         // Do not add move actions for keyboard request as this uses virtual nodes.
-        if (!fromKeyboard && itemSupportsAccessibleDrag(item)) {
+        if (itemSupportsAccessibleDrag(item)) {
             info.addAction(mActions.get(MOVE));
 
             if (item.container >= 0) {
@@ -178,13 +175,17 @@
     @Override
     public boolean performAccessibilityAction(View host, int action, Bundle args) {
         if ((host.getTag() instanceof ItemInfo)
-                && performAction(host, (ItemInfo) host.getTag(), action)) {
+                && performAction(host, (ItemInfo) host.getTag(), action, false)) {
             return true;
         }
         return super.performAccessibilityAction(host, action, args);
     }
 
-    public boolean performAction(final View host, final ItemInfo item, int action) {
+    /**
+     * Performs the provided action on the host
+     */
+    public boolean performAction(final View host, final ItemInfo item, int action,
+            boolean fromKeyboard) {
         if (action == ACTION_LONG_CLICK) {
             if (PopupContainerWithArrow.canShow(host, item)) {
                 // Long press should be consumed for workspace items, and it should invoke the
@@ -205,7 +206,7 @@
             return true;
         }
         if (action == MOVE) {
-            beginAccessibleDrag(host, item);
+            return beginAccessibleDrag(host, item, fromKeyboard);
         } else if (action == ADD_TO_WORKSPACE) {
             final int[] coordinates = new int[2];
             final int screenId = findSpaceOnWorkspace(item, coordinates);
@@ -406,7 +407,11 @@
         }
     }
 
-    public void beginAccessibleDrag(View item, ItemInfo info) {
+    private boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) {
+        if (!itemSupportsAccessibleDrag(info)) {
+            return false;
+        }
+
         mDragInfo = new DragInfo();
         mDragInfo.info = info;
         mDragInfo.item = item;
@@ -423,8 +428,17 @@
 
         DragOptions options = new DragOptions();
         options.isAccessibleDrag = true;
+        options.isKeyboardDrag = fromKeyboard;
         options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
-        ItemLongClickListener.beginDrag(item, mLauncher, info, options);
+
+        if (fromKeyboard) {
+            KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mLauncher.getLayoutInflater()
+                    .inflate(R.layout.keyboard_drag_and_drop, mLauncher.getDragLayer(), false);
+            popup.showForIcon(item, info, options);
+        } else {
+            ItemLongClickListener.beginDrag(item, mLauncher, info, options);
+        }
+        return true;
     }
 
     @Override
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index d4ba11e..aaaff98 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -59,7 +59,7 @@
     }
 
     @Override
-    public boolean performAction(View host, ItemInfo item, int action) {
+    public boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
         if (action == ADD_TO_WORKSPACE) {
             if (!(host.getParent() instanceof DeepShortcutView)) {
                 return false;
diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
index 65a261d..a331924 100644
--- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
+++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
@@ -17,17 +17,12 @@
 package com.android.launcher3.accessibility;
 
 import android.content.Context;
-import android.graphics.Rect;
 import android.text.TextUtils;
 import android.view.View;
 
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -38,9 +33,6 @@
  */
 public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelegate {
 
-    private final Rect mTempRect = new Rect();
-    private final int[] mTempCords = new int[2];
-
     public WorkspaceAccessibilityHelper(CellLayout layout) {
         super(layout);
     }
@@ -134,26 +126,6 @@
         }
         return "";
     }
-
-    @Override
-    protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
-        super.onPopulateNodeForVirtualView(id, node);
-
-
-        // ExploreByTouchHelper does not currently handle view scale.
-        // Update BoundsInScreen to appropriate value.
-        DragLayer dragLayer = Launcher.getLauncher(mView.getContext()).getDragLayer();
-        mTempCords[0] = mTempCords[1] = 0;
-        float scale = dragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
-
-        node.getBoundsInParent(mTempRect);
-        mTempRect.left = mTempCords[0] + (int) (mTempRect.left * scale);
-        mTempRect.right = mTempCords[0] + (int) (mTempRect.right * scale);
-        mTempRect.top = mTempCords[1] + (int) (mTempRect.top * scale);
-        mTempRect.bottom = mTempCords[1] + (int) (mTempRect.bottom * scale);
-        node.setBoundsInScreen(mTempRect);
-    }
-
     @Override
     protected String getLocationDescriptionForIconDrop(int id) {
         int x = id % mView.getCountX();
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index ddf44ca..e71c12d 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -85,9 +85,6 @@
     private final WorkspaceAndHotseatScrim mWorkspaceScrim;
     private final OverviewScrim mOverviewScrim;
 
-    // View that should handle move events
-    private View mMoveTarget;
-
     /**
      * Used to create a new DragLayer from XML.
      *
@@ -109,7 +106,6 @@
     public void setup(DragController dragController, Workspace workspace) {
         mDragController = dragController;
         mWorkspaceScrim.setWorkspace(workspace);
-        mMoveTarget = workspace;
         recreateControllers();
     }
 
@@ -215,12 +211,6 @@
     }
 
     @Override
-    public boolean dispatchUnhandledMove(View focused, int direction) {
-        return super.dispatchUnhandledMove(focused, direction)
-                || mMoveTarget.dispatchUnhandledMove(focused, direction);
-    }
-
-    @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         ev.offsetLocation(getTranslationX(), 0);
         try {
diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java
index 959602b..e8ff8da 100644
--- a/src/com/android/launcher3/dragndrop/DragOptions.java
+++ b/src/com/android/launcher3/dragndrop/DragOptions.java
@@ -28,6 +28,9 @@
     /** Whether or not an accessible drag operation is in progress. */
     public boolean isAccessibleDrag = false;
 
+    /** Whether or not the drag operation is controlled by keyboard. */
+    public boolean isKeyboardDrag = false;
+
     /**
      * Specifies the start location for a simulated DnD (like system drag or accessibility drag),
      * null when using internal DnD
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
index 800598e..77ce4a8 100644
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
@@ -88,6 +88,7 @@
 
     @Override
     public boolean onMenuItemClick(MenuItem menuItem) {
-        return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId());
+        return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId(),
+                true);
     }
 }
diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
index ae7ad10..83003ff 100644
--- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
@@ -16,233 +16,30 @@
 
 package com.android.launcher3.keyboard;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
 import android.graphics.Rect;
-import android.util.Property;
 import android.view.View;
 import android.view.View.OnFocusChangeListener;
 
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.Themes;
 
 /**
  * A helper class to draw background of a focused view.
  */
-public abstract class FocusIndicatorHelper implements
-        OnFocusChangeListener, AnimatorUpdateListener {
-
-    private static final float MIN_VISIBLE_ALPHA = 0.2f;
-    private static final long ANIM_DURATION = 150;
-
-    public static final Property<FocusIndicatorHelper, Float> ALPHA =
-            new Property<FocusIndicatorHelper, Float>(Float.TYPE, "alpha") {
-                @Override
-                public void set(FocusIndicatorHelper object, Float value) {
-                    object.setAlpha(value);
-                }
-
-                @Override
-                public Float get(FocusIndicatorHelper object) {
-                    return object.mAlpha;
-                }
-            };
-
-    public static final Property<FocusIndicatorHelper, Float> SHIFT =
-            new Property<FocusIndicatorHelper, Float>(
-                    Float.TYPE, "shift") {
-
-                @Override
-                public void set(FocusIndicatorHelper object, Float value) {
-                    object.mShift = value;
-                }
-
-                @Override
-                public Float get(FocusIndicatorHelper object) {
-                    return object.mShift;
-                }
-            };
-
-    private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
-    private static final Rect sTempRect1 = new Rect();
-    private static final Rect sTempRect2 = new Rect();
-
-    private final View mContainer;
-    private final Paint mPaint;
-    private final int mMaxAlpha;
-
-    private final Rect mDirtyRect = new Rect();
-    private boolean mIsDirty = false;
-
-    private View mLastFocusedView;
-
-    private View mCurrentView;
-    private View mTargetView;
-    /**
-     * The fraction indicating the position of the focusRect between {@link #mCurrentView}
-     * & {@link #mTargetView}
-     */
-    private float mShift;
-
-    private ObjectAnimator mCurrentAnimation;
-    private float mAlpha;
-    private float mRadius;
+public abstract class FocusIndicatorHelper extends ItemFocusIndicatorHelper<View>
+        implements OnFocusChangeListener {
 
     public FocusIndicatorHelper(View container) {
-        mContainer = container;
-
-        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        int color = container.getResources().getColor(R.color.focused_background);
-        mMaxAlpha = Color.alpha(color);
-        mPaint.setColor(0xFF000000 | color);
-
-        setAlpha(0);
-        mShift = 0;
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-            mRadius = Themes.getDialogCornerRadius(container.getContext());
-        }
-    }
-
-    protected void setAlpha(float alpha) {
-        mAlpha = alpha;
-        mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
-    }
-
-    @Override
-    public void onAnimationUpdate(ValueAnimator animation) {
-        invalidateDirty();
-    }
-
-    protected void invalidateDirty() {
-        if (mIsDirty) {
-            mContainer.invalidate(mDirtyRect);
-            mIsDirty = false;
-        }
-
-        Rect newRect = getDrawRect();
-        if (newRect != null) {
-            mContainer.invalidate(newRect);
-        }
-    }
-
-    public void draw(Canvas c) {
-        if (mAlpha <= 0) return;
-
-        Rect newRect = getDrawRect();
-        if (newRect != null) {
-            mDirtyRect.set(newRect);
-            c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
-                    (float) mDirtyRect.right, (float) mDirtyRect.bottom,
-                    mRadius, mRadius, mPaint);
-            mIsDirty = true;
-        }
-    }
-
-    private Rect getDrawRect() {
-        if (mCurrentView != null && mCurrentView.isAttachedToWindow()) {
-            viewToRect(mCurrentView, sTempRect1);
-
-            if (mShift > 0 && mTargetView != null) {
-                viewToRect(mTargetView, sTempRect2);
-                return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
-            } else {
-                return sTempRect1;
-            }
-        }
-        return null;
+        super(container, container.getResources().getColor(R.color.focused_background));
     }
 
     @Override
     public void onFocusChange(View v, boolean hasFocus) {
-        if (hasFocus) {
-            endCurrentAnimation();
-
-            if (mAlpha > MIN_VISIBLE_ALPHA) {
-                mTargetView = v;
-
-                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
-                        PropertyValuesHolder.ofFloat(ALPHA, 1),
-                        PropertyValuesHolder.ofFloat(SHIFT, 1));
-                mCurrentAnimation.addListener(new ViewSetListener(v, true));
-            } else {
-                setCurrentView(v);
-
-                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
-                        PropertyValuesHolder.ofFloat(ALPHA, 1));
-            }
-
-            mLastFocusedView = v;
-        } else {
-            if (mLastFocusedView == v) {
-                mLastFocusedView = null;
-                endCurrentAnimation();
-                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
-                        PropertyValuesHolder.ofFloat(ALPHA, 0));
-                mCurrentAnimation.addListener(new ViewSetListener(null, false));
-            }
-        }
-
-        // invalidate once
-        invalidateDirty();
-
-        mLastFocusedView = hasFocus ? v : null;
-        if (mCurrentAnimation != null) {
-            mCurrentAnimation.addUpdateListener(this);
-            mCurrentAnimation.setDuration(ANIM_DURATION).start();
-        }
+        changeFocus(v, hasFocus);
     }
 
-    protected void endCurrentAnimation() {
-        if (mCurrentAnimation != null) {
-            mCurrentAnimation.cancel();
-            mCurrentAnimation = null;
-        }
-    }
-
-    protected void setCurrentView(View v) {
-        mCurrentView = v;
-        mShift = 0;
-        mTargetView = null;
-    }
-
-    /**
-     * Gets the position of {@param v} relative to {@link #mContainer}.
-     */
-    public abstract void viewToRect(View v, Rect outRect);
-
-    private class ViewSetListener extends AnimatorListenerAdapter {
-        private final View mViewToSet;
-        private final boolean mCallOnCancel;
-        private boolean mCalled = false;
-
-        public ViewSetListener(View v, boolean callOnCancel) {
-            mViewToSet = v;
-            mCallOnCancel = callOnCancel;
-        }
-
-        @Override
-        public void onAnimationCancel(Animator animation) {
-            if (!mCallOnCancel) {
-                mCalled = true;
-            }
-        }
-
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            if (!mCalled) {
-                setCurrentView(mViewToSet);
-                mCalled = true;
-            }
-        }
+    @Override
+    protected boolean shouldDraw(View item) {
+        return item.isAttachedToWindow();
     }
 
     /**
diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
new file mode 100644
index 0000000..57fab2d
--- /dev/null
+++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
@@ -0,0 +1,252 @@
+/*
+ * 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.keyboard;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Themes;
+
+/**
+ * A helper class to draw background of a focused item.
+ * @param <T> Item type
+ */
+public abstract class ItemFocusIndicatorHelper<T> implements AnimatorUpdateListener {
+
+    private static final float MIN_VISIBLE_ALPHA = 0.2f;
+    private static final long ANIM_DURATION = 150;
+
+    public static final FloatProperty<ItemFocusIndicatorHelper> ALPHA =
+            new FloatProperty<ItemFocusIndicatorHelper>("alpha") {
+
+                @Override
+                public void setValue(ItemFocusIndicatorHelper object, float value) {
+                    object.setAlpha(value);
+                }
+
+                @Override
+                public Float get(ItemFocusIndicatorHelper object) {
+                    return object.mAlpha;
+                }
+            };
+
+    public static final FloatProperty<ItemFocusIndicatorHelper> SHIFT =
+            new FloatProperty<ItemFocusIndicatorHelper>("shift") {
+
+                @Override
+                public void setValue(ItemFocusIndicatorHelper object, float value) {
+                    object.mShift = value;
+                }
+
+                @Override
+                public Float get(ItemFocusIndicatorHelper object) {
+                    return object.mShift;
+                }
+            };
+
+    private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final Rect sTempRect1 = new Rect();
+    private static final Rect sTempRect2 = new Rect();
+
+    private final View mContainer;
+    protected final Paint mPaint;
+    private final int mMaxAlpha;
+
+    private final Rect mDirtyRect = new Rect();
+    private boolean mIsDirty = false;
+
+    private T mLastFocusedItem;
+
+    private T mCurrentItem;
+    private T mTargetItem;
+    /**
+     * The fraction indicating the position of the focusRect between {@link #mCurrentItem}
+     * & {@link #mTargetItem}
+     */
+    private float mShift;
+
+    private ObjectAnimator mCurrentAnimation;
+    private float mAlpha;
+    private float mRadius;
+
+    public ItemFocusIndicatorHelper(View container, int color) {
+        mContainer = container;
+
+        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mMaxAlpha = Color.alpha(color);
+        mPaint.setColor(0xFF000000 | color);
+
+        setAlpha(0);
+        mShift = 0;
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            mRadius = Themes.getDialogCornerRadius(container.getContext());
+        }
+    }
+
+    protected void setAlpha(float alpha) {
+        mAlpha = alpha;
+        mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
+    }
+
+    @Override
+    public void onAnimationUpdate(ValueAnimator animation) {
+        invalidateDirty();
+    }
+
+    protected void invalidateDirty() {
+        if (mIsDirty) {
+            mContainer.invalidate(mDirtyRect);
+            mIsDirty = false;
+        }
+
+        Rect newRect = getDrawRect();
+        if (newRect != null) {
+            mContainer.invalidate(newRect);
+        }
+    }
+
+    /**
+     * Draws the indicator on the canvas
+     */
+    public void draw(Canvas c) {
+        if (mAlpha <= 0) return;
+
+        Rect newRect = getDrawRect();
+        if (newRect != null) {
+            mDirtyRect.set(newRect);
+            c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
+                    (float) mDirtyRect.right, (float) mDirtyRect.bottom,
+                    mRadius, mRadius, mPaint);
+            mIsDirty = true;
+        }
+    }
+
+    private Rect getDrawRect() {
+        if (mCurrentItem != null && shouldDraw(mCurrentItem)) {
+            viewToRect(mCurrentItem, sTempRect1);
+
+            if (mShift > 0 && mTargetItem != null) {
+                viewToRect(mTargetItem, sTempRect2);
+                return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
+            } else {
+                return sTempRect1;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the provided item is valid
+     */
+    protected boolean shouldDraw(T item) {
+        return true;
+    }
+
+    protected void changeFocus(T item, boolean hasFocus) {
+        if (hasFocus) {
+            endCurrentAnimation();
+
+            if (mAlpha > MIN_VISIBLE_ALPHA) {
+                mTargetItem = item;
+
+                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofFloat(ALPHA, 1),
+                        PropertyValuesHolder.ofFloat(SHIFT, 1));
+                mCurrentAnimation.addListener(new ViewSetListener(item, true));
+            } else {
+                setCurrentItem(item);
+
+                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofFloat(ALPHA, 1));
+            }
+
+            mLastFocusedItem = item;
+        } else {
+            if (mLastFocusedItem == item) {
+                mLastFocusedItem = null;
+                endCurrentAnimation();
+                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofFloat(ALPHA, 0));
+                mCurrentAnimation.addListener(new ViewSetListener(null, false));
+            }
+        }
+
+        // invalidate once
+        invalidateDirty();
+
+        mLastFocusedItem = hasFocus ? item : null;
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.addUpdateListener(this);
+            mCurrentAnimation.setDuration(ANIM_DURATION).start();
+        }
+    }
+
+    protected void endCurrentAnimation() {
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.cancel();
+            mCurrentAnimation = null;
+        }
+    }
+
+    protected void setCurrentItem(T item) {
+        mCurrentItem = item;
+        mShift = 0;
+        mTargetItem = null;
+    }
+
+    /**
+     * Gets the position of the item relative to {@link #mContainer}.
+     */
+    public abstract void viewToRect(T item, Rect outRect);
+
+    private class ViewSetListener extends AnimatorListenerAdapter {
+        private final T mItemToSet;
+        private final boolean mCallOnCancel;
+        private boolean mCalled = false;
+
+        ViewSetListener(T item, boolean callOnCancel) {
+            mItemToSet = item;
+            mCallOnCancel = callOnCancel;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            if (!mCallOnCancel) {
+                mCalled = true;
+            }
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (!mCalled) {
+                setCurrentItem(mItemToSet);
+                mCalled = true;
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java
new file mode 100644
index 0000000..a6c897f
--- /dev/null
+++ b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java
@@ -0,0 +1,342 @@
+/*
+ * 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.keyboard;
+
+import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL;
+
+import static com.android.launcher3.LauncherState.SPRING_LOADED;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.TextView;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.util.Themes;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.ToIntBiFunction;
+import java.util.function.ToIntFunction;
+
+/**
+ * A floating view to allow keyboard navigation across virtual nodes
+ */
+public class KeyboardDragAndDropView extends AbstractFloatingView
+        implements Insettable, StateListener<LauncherState> {
+
+    private static final long MINOR_AXIS_WEIGHT = 13;
+
+    private final ArrayList<Integer> mIntList = new ArrayList<>();
+    private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>();
+    private final ArrayList<VirtualNodeInfo> mNodes = new ArrayList<>();
+
+    private final Rect mTempRect = new Rect();
+    private final Rect mTempRect2 = new Rect();
+    private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+    private final RectFocusIndicator mFocusIndicator;
+
+    private final Launcher mLauncher;
+    private VirtualNodeInfo mCurrentSelection;
+
+
+    public KeyboardDragAndDropView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+        mFocusIndicator = new RectFocusIndicator(this);
+        setWillNotDraw(false);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        mLauncher.getDragLayer().removeView(this);
+        mLauncher.getStateManager().removeStateListener(this);
+        mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+        mIsOpen = false;
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_DRAG_DROP_POPUP) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        // Consume all touch
+        return true;
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        setPadding(insets.left, insets.top, insets.right, insets.bottom);
+    }
+
+    @Override
+    public void onStateTransitionStart(LauncherState toState) {
+        if (toState != SPRING_LOADED) {
+            close(false);
+        }
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        if (mCurrentSelection != null) {
+            setCurrentSelection(mCurrentSelection);
+        }
+    }
+
+    private void setCurrentSelection(VirtualNodeInfo nodeInfo) {
+        mCurrentSelection = nodeInfo;
+        ((TextView) findViewById(R.id.label))
+                .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription());
+
+        Rect bounds = new Rect();
+        mTempNodeInfo.getBoundsInParent(bounds);
+        View host = nodeInfo.delegate.getHost();
+        ViewParent parent = host.getParent();
+        if (parent instanceof PagedView) {
+            PagedView pv = (PagedView) parent;
+            int pageIndex = pv.indexOfChild(host);
+
+            pv.setCurrentPage(pageIndex);
+            bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0);
+        }
+        float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom};
+        Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true);
+
+        new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds);
+        mFocusIndicator.changeFocus(bounds, true);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        mFocusIndicator.draw(canvas);
+    }
+
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        VirtualNodeInfo nodeInfo = getNextSelection(direction);
+        if (nodeInfo == null) {
+            return false;
+        }
+        setCurrentSelection(nodeInfo);
+        return true;
+    }
+
+    /**
+     * Focus finding logic:
+     * Collect all virtual nodes in reading order (used for forward and backwards).
+     * Then find the closest view by comparing the distances spatially. Since it is a move
+     * operation. consider all cell sizes to be approximately of the same size.
+     */
+    private VirtualNodeInfo getNextSelection(int direction) {
+        // Collect all virtual nodes
+        mDelegates.clear();
+        mNodes.clear();
+
+        Folder openFolder = Folder.getOpen(mLauncher);
+        PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent();
+        int count = pv.getPageCount();
+        for (int i = 0; i < count; i++) {
+            mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate());
+        }
+        if (openFolder == null) {
+            mDelegates.add(pv.getNextPage() + 1,
+                    mLauncher.getHotseat().getDragAndDropAccessibilityDelegate());
+        }
+        mDelegates.forEach(delegate -> {
+            mIntList.clear();
+            delegate.getVisibleVirtualViews(mIntList);
+            mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id)));
+        });
+
+        if (mNodes.isEmpty()) {
+            return null;
+        }
+        int index = mNodes.indexOf(mCurrentSelection);
+        if (mCurrentSelection == null || index < 0) {
+            return null;
+        }
+        int totalNodes = mNodes.size();
+
+        final ToIntBiFunction<Rect, Rect> majorAxis;
+        final ToIntFunction<Rect> minorAxis;
+
+        switch (direction) {
+            case View.FOCUS_RIGHT:
+                majorAxis = (source, dest) -> dest.left - source.left;
+                minorAxis = Rect::centerY;
+                break;
+            case View.FOCUS_LEFT:
+                majorAxis = (source, dest) -> source.left - dest.left;
+                minorAxis = Rect::centerY;
+                break;
+            case View.FOCUS_UP:
+                majorAxis = (source, dest) -> source.top - dest.top;
+                minorAxis = Rect::centerX;
+                break;
+            case View.FOCUS_DOWN:
+                majorAxis = (source, dest) -> dest.top - source.top;
+                minorAxis = Rect::centerX;
+                break;
+            case View.FOCUS_FORWARD:
+                return mNodes.get((index + 1) % totalNodes);
+            case View.FOCUS_BACKWARD:
+                return mNodes.get((index + totalNodes - 1) % totalNodes);
+            default:
+                // Unknown direction
+                return null;
+        }
+        mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect);
+
+        float minWeight = Float.MAX_VALUE;
+        VirtualNodeInfo match = null;
+        for (int i = 0; i < totalNodes; i++) {
+            VirtualNodeInfo node = mNodes.get(i);
+            node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2);
+
+            int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2);
+            if (majorAxisWeight <= 0) {
+                continue;
+            }
+            int minorAxisWeight = minorAxis.applyAsInt(mTempRect2)
+                    - minorAxis.applyAsInt(mTempRect);
+
+            float weight = majorAxisWeight * majorAxisWeight
+                    + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT;
+            if (weight < minWeight) {
+                minWeight = weight;
+                match = node;
+            }
+        }
+        return match;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) {
+            mCurrentSelection.delegate.onPerformActionForVirtualView(
+                    mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    /**
+     * Shows the keyboard drag popup for the provided view
+     */
+    public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) {
+        mIsOpen = true;
+        mLauncher.getDragLayer().addView(this);
+        mLauncher.getStateManager().addStateListener(this);
+
+        // Find current selection
+        CellLayout currentParent = (CellLayout) icon.getParent().getParent();
+        float[] iconPos = new float[] {currentParent.getCellWidth() / 2,
+                currentParent.getCellHeight() / 2};
+        Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false);
+
+        ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions);
+
+        DragAndDropAccessibilityDelegate dndDelegate =
+                currentParent.getDragAndDropAccessibilityDelegate();
+        setCurrentSelection(new VirtualNodeInfo(
+                dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1])));
+
+        mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE);
+        requestFocus();
+    }
+
+    private static class VirtualNodeInfo {
+        public final DragAndDropAccessibilityDelegate delegate;
+        public final int id;
+
+        VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) {
+            this.id = id;
+            this.delegate = delegate;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof VirtualNodeInfo)) {
+                return false;
+            }
+            VirtualNodeInfo that = (VirtualNodeInfo) o;
+            return id == that.id && delegate.equals(that.delegate);
+        }
+
+        public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) {
+            delegate.onPopulateNodeForVirtualView(id, nodeInfo);
+            return nodeInfo;
+        }
+
+        public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) {
+            delegate.onPopulateNodeForVirtualView(id, nodeInfo);
+            nodeInfo.getBoundsInScreen(out);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id, delegate);
+        }
+    }
+
+    private static class RectFocusIndicator extends ItemFocusIndicatorHelper<Rect> {
+
+        RectFocusIndicator(View container) {
+            super(container, Themes.getColorAccent(container.getContext()));
+            mPaint.setStrokeWidth(container.getResources()
+                    .getDimension(R.dimen.keyboard_drag_stroke_width));
+            mPaint.setStyle(Style.STROKE);
+        }
+
+        @Override
+        public void viewToRect(Rect item, Rect outRect) {
+            outRect.set(item);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 5464dd8..15f7730 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -455,12 +455,6 @@
     }
 
     @Override
-    public boolean dispatchUnhandledMove(View focused, int direction) {
-        // Consume the unhandled move if a container is open, to avoid switching pages underneath.
-        return AbstractFloatingView.getTopOpenView(mActivity) != null;
-    }
-
-    @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
         View topView = AbstractFloatingView.getTopOpenView(mActivity);
         if (topView != null) {