OSDN Git Service

Enabling accessible drag and drop
authorAdam Cohen <adamcohen@google.com>
Sat, 24 Jan 2015 00:11:55 +0000 (16:11 -0800)
committerAdam Cohen <adamcohen@google.com>
Mon, 9 Mar 2015 18:29:28 +0000 (11:29 -0700)
-> Using the context menu, and a new two stage system, this allows
   users to curate icons and widgets on the workspace
-> Move icons / widgets to any empty cell on any existing screen, or
   create a new screen (appended to the right, as with regular drag
   and drop)
-> Move icons into existing folders
-> Create folders by moving an icon onto another icon
-> Also added confirmations for these and some existing accessibility actions

Limitations:
-> Currently, no support for drag and drop in folders
-> Considering moving the drag view so it doesn't occlude any
   content (in particular, when user changes pages)
-> In this mode, accessibility framework seems to have
   problems with the next / prev operations

Bug: 18482913

Change-Id: I19b0be9dc8bfa766d430408c8ad9303c716b89b2

Android.mk
res/values/config.xml
res/values/strings.xml
src/com/android/launcher3/CellLayout.java
src/com/android/launcher3/DragController.java
src/com/android/launcher3/DropTarget.java
src/com/android/launcher3/Launcher.java
src/com/android/launcher3/LauncherAccessibilityDelegate.java
src/com/android/launcher3/LauncherAppState.java
src/com/android/launcher3/SearchDropTargetBar.java
src/com/android/launcher3/Workspace.java

index 632dd09..5267469 100644 (file)
@@ -23,6 +23,8 @@ include $(CLEAR_VARS)
 
 LOCAL_MODULE_TAGS := optional
 
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+
 LOCAL_SRC_FILES := $(call all-java-files-under, src) \
     $(call all-java-files-under, WallpaperPicker/src) \
     $(call all-renderscript-files-under, src) \
index cbec512..1acace6 100644 (file)
     <item type="id" name="action_uninstall" />
     <item type="id" name="action_info" />
     <item type="id" name="action_add_to_workspace" />
+    <item type="id" name="action_move" />
 </resources>
index 8b7e6c1..74b8814 100644 (file)
@@ -305,4 +305,32 @@ s -->
 <!-- Strings for accessibility actions -->
     <!-- Accessibility action to add an app to workspace. [CHAR_LIMIT=30] -->
     <string name="action_add_to_workspace">Add To Workspace</string>
+
+    <!-- Accessibility confirmation for item added to workspace -->
+    <string name="item_added_to_workspace">Item added to workspace</string>
+
+    <!-- Accessibility confirmation for item removed-->
+    <string name="item_removed_from_workspace">Item removed from workspace</string>
+
+    <!-- Accessibility action to move an item on the workspace. [CHAR_LIMIT=30] -->
+    <string name="action_move">Move Item</string>
+
+    <!-- Accessibility description to move item to empty cell. -->
+    <string name="move_to_empty_cell">Move to empty cell <xliff:g id="number" example="1">%1$s</xliff:g>, <xliff:g id="number" example="1">%2$s</xliff:g></string>
+
+    <!-- Accessibility confirmation for item move -->
+    <string name="item_moved">Item moved</string>
+
+    <!-- Accessibility description to move item into an existing folder. -->
+    <string name="add_to_folder">Add to folder: <xliff:g id="name" example="Games">%1$s</xliff:g></string>
+
+    <!-- Accessibility confirmation for item added to folder-->
+    <string name="added_to_folder">Item added to folder</string>
+
+    <!-- Accessibility description to create folder with another item. -->
+    <string name="create_folder_with">Create folder with: <xliff:g id="name" example="Game">%1$s</xliff:g></string>
+
+    <!-- Accessibility confirmation for folder created -->
+    <string name="folder_created">Folder created</string>
+
 </resources>
index a3500aa..c57090d 100644 (file)
@@ -22,6 +22,7 @@ import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -33,25 +34,34 @@ import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
 import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.widget.ExploreByTouchHelper;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Animation;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.LayoutAnimationController;
 
 import com.android.launcher3.FolderIcon.FolderRingAnimator;
+import com.android.launcher3.LauncherAccessibilityDelegate.DragType;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Stack;
 
 public class CellLayout extends ViewGroup {
@@ -169,6 +179,14 @@ public class CellLayout extends ViewGroup {
 
     private final static Paint sPaint = new Paint();
 
+    // Related to accessible drag and drop
+    DragAndDropAccessibilityDelegate mTouchHelper = new DragAndDropAccessibilityDelegate(this);
+    private boolean mUseTouchHelper = false;
+    OnClickListener mOldClickListener = null;
+    OnClickListener mOldWorkspaceListener = null;
+    private int mDownX = 0;
+    private int mDownY = 0;
+
     public CellLayout(Context context) {
         this(context, null);
     }
@@ -294,6 +312,282 @@ public class CellLayout extends ViewGroup {
         addView(mShortcutsAndWidgets);
     }
 
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public void enableAccessibleDrag(boolean enable) {
+        mUseTouchHelper = enable;
+        if (!enable) {
+            ViewCompat.setAccessibilityDelegate(this, null);
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+            setOnClickListener(mLauncher);
+        } else {
+            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+            setOnClickListener(mTouchHelper);
+        }
+
+        // Invalidate the accessibility hierarchy
+        if (getParent() != null) {
+            getParent().notifySubtreeAccessibilityStateChanged(
+                    this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+        }
+    }
+
+    @Override
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // Always attempt to dispatch hover events to accessibility first.
+        if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) {
+            return true;
+        }
+        return super.dispatchHoverEvent(event);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mDownX = (int) event.getX();
+            mDownY = (int) event.getY();
+        }
+        return super.dispatchTouchEvent(event);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mUseTouchHelper ||
+                (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) {
+            return true;
+        }
+        return false;
+    }
+
+    class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper implements OnClickListener {
+        private final Rect mTempRect = new Rect();
+
+        public DragAndDropAccessibilityDelegate(View forView) {
+            super(forView);
+        }
+
+        private int getViewIdAt(float x, float y) {
+            if (x < 0 || y < 0 || x > getMeasuredWidth() || y > getMeasuredHeight()) {
+                return ExploreByTouchHelper.INVALID_ID;
+            }
+
+            // Map coords to cell
+            int cellX = (int) Math.floor(x / (mCellWidth + mWidthGap));
+            int cellY = (int) Math.floor(y / (mCellHeight + mHeightGap));
+
+            // Map cell to id
+            int id = cellX * mCountY + cellY;
+            return id;
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            return nearestDropLocation(getViewIdAt(x, y));
+        }
+
+        protected int nearestDropLocation(int id) {
+            int count = mCountX * mCountY;
+            for (int delta = 0; delta < count; delta++) {
+                if (id + delta <= (count - 1)) {
+                    int target = intersectsValidDropTarget(id + delta);
+                    if (target >= 0) {
+                        return target;
+                    }
+                } else if (id - delta >= 0) {
+                    int target = intersectsValidDropTarget(id - delta);
+                    if (target >= 0) {
+                        return target;
+                    }
+                }
+            }
+            return ExploreByTouchHelper.INVALID_ID;
+        }
+
+        /**
+         * Find the virtual view id corresponding to the top left corner of any drop region by which
+         * the passed id is contained. For an icon, this is simply
+         *
+         * @param id the id we're interested examining (ie. does it fit there?)
+         * @return the view id of the top left corner of a valid drop region or -1 if there is no
+         *         such valid region. For the icon, this can just be -1 or id.
+         */
+        protected int intersectsValidDropTarget(int id) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo();
+
+            int y = id % mCountY;
+            int x = id / mCountY;
+
+            if (dragInfo.dragType == DragType.WIDGET) {
+                // For a widget, every cell must be vacant. In addition, we will return any valid
+                // drop target by which the passed id is contained.
+                boolean fits = false;
+
+                // These represent the amount that we can back off if we hit a problem. They
+                // get consumed as we move up and to the right, trying new regions.
+                int spanX = dragInfo.info.spanX;
+                int spanY = dragInfo.info.spanY;
+
+                for (int m = 0; m < spanX; m++) {
+                    for (int n = 0; n < spanY; n++) {
+
+                        fits = true;
+                        int x0 = x - m;
+                        int y0 = y - n;
+
+                        if (x0 < 0 || y0 < 0) continue;
+
+                        for (int i = x0; i < x0 + spanX; i++) {
+                            if (!fits) break;
+                            for (int j = y0; j < y0 + spanY; j++) {
+                                if (i >= mCountX || j >= mCountY || mOccupied[i][j]) {
+                                    fits = false;
+                                    break;
+                                }
+                            }
+                        }
+                        if (fits) {
+                            return x0 * mCountY + y0;
+                        }
+                    }
+                }
+                return -1;
+            } else {
+                // For an icon, we simply check the view directly below
+                View child = getChildAt(x, y);
+                if (child == null || child == dragInfo.item) {
+                    // Empty cell. Good for an icon or folder.
+                    return id;
+                } else if (dragInfo.dragType != DragType.FOLDER) {
+                    // For icons, we can consider cells that have another icon or a folder.
+                    ItemInfo info = (ItemInfo) child.getTag();
+                    if (info instanceof AppInfo || info instanceof FolderInfo ||
+                            info instanceof ShortcutInfo) {
+                        return id;
+                    }
+                }
+                return -1;
+            }
+        }
+
+        @Override
+        protected 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 = mCountX * mCountY;
+
+            for (int i = 0; i < nCells; i++) {
+                if (intersectsValidDropTarget(i) >= 0) {
+                    virtualViews.add(i);
+                }
+            }
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
+            if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
+                String confirmation = getConfirmationForIconDrop(viewId);
+                LauncherAppState.getInstance().getAccessibilityDelegate()
+                        .handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onClick(View arg0) {
+            int viewId = getViewIdAt(mDownX, mDownY);
+
+            String confirmation = getConfirmationForIconDrop(viewId);
+            LauncherAppState.getInstance().getAccessibilityDelegate()
+                    .handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation);
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) {
+            if (id == ExploreByTouchHelper.INVALID_ID) {
+                throw new IllegalArgumentException("Invalid virtual view id");
+            }
+            // We're required to set something here.
+            event.setContentDescription("");
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
+            if (id == ExploreByTouchHelper.INVALID_ID) {
+                throw new IllegalArgumentException("Invalid virtual view id");
+            }
+
+            node.setContentDescription(getLocationDescriptionForIconDrop(id));
+            node.setBoundsInParent(getItemBounds(id));
+
+            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+            node.setClickable(true);
+            node.setFocusable(true);
+        }
+
+        private String getLocationDescriptionForIconDrop(int id) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo();
+
+            int y = id % mCountY;
+            int x = id / mCountY;
+
+            Resources res = getContext().getResources();
+            View child = getChildAt(x, y);
+            if (child == null || child == dragInfo.item) {
+                return res.getString(R.string.move_to_empty_cell, x, y);
+            } else {
+                ItemInfo info = (ItemInfo) child.getTag();
+                if (info instanceof AppInfo || info instanceof ShortcutInfo) {
+                    return res.getString(R.string.create_folder_with, info.title);
+                } else if (info instanceof FolderInfo) {
+                    return res.getString(R.string.add_to_folder, info.title);
+                }
+            }
+            return "";
+        }
+
+        private String getConfirmationForIconDrop(int id) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo();
+
+            int y = id % mCountY;
+            int x = id / mCountY;
+
+            Resources res = getContext().getResources();
+            View child = getChildAt(x, y);
+            if (child == null || child == dragInfo.item) {
+                return res.getString(R.string.item_moved);
+            } else {
+                ItemInfo info = (ItemInfo) child.getTag();
+                if (info instanceof AppInfo || info instanceof ShortcutInfo) {
+                    return res.getString(R.string.folder_created);
+
+                } else if (info instanceof FolderInfo) {
+                    return res.getString(R.string.added_to_folder);
+                }
+            }
+            return "";
+        }
+
+        private Rect getItemBounds(int id) {
+            int cellY = id % mCountY;
+            int cellX = id / mCountY;
+            int x = getPaddingLeft() + (int) (cellX * (mCellWidth + mWidthGap));
+            int y = getPaddingTop() + (int) (cellY * (mCellHeight + mHeightGap));
+
+            Rect bounds = mTempRect;
+            bounds.set(x, y, x + mCellWidth, y + mCellHeight);
+            return bounds;
+        }
+    }
+
     public void enableHardwareLayer(boolean hasLayer) {
         mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint);
     }
@@ -679,18 +973,6 @@ public class CellLayout extends ViewGroup {
         mShortcutsAndWidgets.removeViewsInLayout(start, count);
     }
 
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        // First we clear the tag to ensure that on every touch down we start with a fresh slate,
-        // even in the case where we return early. Not clearing here was causing bugs whereby on
-        // long-press we'd end up picking up an item from a previous drag operation.
-        if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) {
-            return true;
-        }
-
-        return false;
-    }
-
     /**
      * Given a point, return the cell that strictly encloses that point
      * @param x X coordinate of the point
index 480dce9..09c59a0 100644 (file)
@@ -73,6 +73,9 @@ public class DragController {
     /** Whether or not we're dragging. */
     private boolean mDragging;
 
+    /** Whether or not this is an accessible drag operation */
+    private boolean mIsAccessibleDrag;
+
     /** X coordinate of the down event. */
     private int mMotionDownX;
 
@@ -182,7 +185,7 @@ public class DragController {
                 (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2);
 
         startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null,
-                null, initialDragViewScale);
+                null, initialDragViewScale, false);
 
         if (dragAction == DRAG_ACTION_MOVE) {
             v.setVisibility(View.GONE);
@@ -202,10 +205,11 @@ public class DragController {
      *        {@link #DRAG_ACTION_COPY}
      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
      *          Makes dragging feel more precise, e.g. you can clip out a transparent border
+     * @param accessible whether this drag should occur in accessibility mode
      */
     public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
             DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
-            float initialDragViewScale) {
+            float initialDragViewScale, boolean accessible) {
         if (PROFILE_DRAWING_DURING_DRAG) {
             android.os.Debug.startMethodTracing("Launcher");
         }
@@ -228,12 +232,21 @@ public class DragController {
         final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
 
         mDragging = true;
+        mIsAccessibleDrag = accessible;
 
         mDragObject = new DropTarget.DragObject();
 
         mDragObject.dragComplete = false;
-        mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
-        mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
+        if (mIsAccessibleDrag) {
+            // For an accessible drag, we assume the view is being dragged from the center.
+            mDragObject.xOffset = b.getWidth() / 2;
+            mDragObject.yOffset = b.getHeight() / 2;
+            mDragObject.accessibleDrag = true;
+        } else {
+            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
+            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
+        }
+
         mDragObject.dragSource = source;
         mDragObject.dragInfo = dragInfo;
 
@@ -349,6 +362,7 @@ public class DragController {
     private void endDrag() {
         if (mDragging) {
             mDragging = false;
+            mIsAccessibleDrag = false;
             clearScrollRunnable();
             boolean isDeferred = false;
             if (mDragObject.dragView != null) {
@@ -421,6 +435,10 @@ public class DragController {
                     + mDragging);
         }
 
+        if (mIsAccessibleDrag) {
+            return false;
+        }
+
         // Update the velocity tracker
         acquireVelocityTrackerAndAddMovement(ev);
 
@@ -560,7 +578,7 @@ public class DragController {
      * Call this from a drag source view.
      */
     public boolean onTouchEvent(MotionEvent ev) {
-        if (!mDragging) {
+        if (!mDragging || mIsAccessibleDrag) {
             return false;
         }
 
@@ -617,6 +635,34 @@ public class DragController {
     }
 
     /**
+     * Since accessible drag and drop won't cause the same sequence of touch events, we manually
+     * inject the appropriate state.
+     */
+    public void prepareAccessibleDrag(int x, int y) {
+        mMotionDownX = x;
+        mMotionDownY = y;
+        mLastDropTarget = null;
+    }
+
+    /**
+     * As above, since accessible drag and drop won't cause the same sequence of touch events,
+     * we manually ensure appropriate drag and drop events get emulated for accessible drag.
+     */
+    public void completeAccessibleDrag(int[] location) {
+        final int[] coordinates = mCoordinatesTemp;
+
+        // We make sure that we prime the target for drop.
+        DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates);
+        mDragObject.x = coordinates[0];
+        mDragObject.y = coordinates[1];
+        checkTouchMove(dropTarget);
+
+        // Perform the drop
+        drop(location[0], location[1]);
+        endDrag();
+    }
+
+    /**
      * Determines whether the user flung the current item to delete it.
      *
      * @return the vector at which the item was flung, or null if no fling was detected.
index 7ede427..94ae82b 100644 (file)
@@ -54,6 +54,9 @@ public interface DropTarget {
         /** Where the drag originated */
         public DragSource dragSource = null;
 
+        /** The object is part of an accessible drag operation */
+        public boolean accessibleDrag;
+
         /** Post drag animation runnable */
         public Runnable postAnimationRunnable = null;
 
index 1ad8b27..e58d0da 100644 (file)
@@ -2447,6 +2447,10 @@ public class Launcher extends Activity
             return;
         }
 
+        if (LauncherAppState.getInstance().getAccessibilityDelegate().onBackPressed()) {
+            return;
+        }
+
         if (isAllAppsVisible()) {
             if (mAppsCustomizeContent.getContentType() ==
                     AppsCustomizePagedView.ContentType.Applications) {
@@ -3165,7 +3169,7 @@ public class Launcher extends Activity
         View itemUnderLongClick = null;
         if (v.getTag() instanceof ItemInfo) {
             ItemInfo info = (ItemInfo) v.getTag();
-            longClickCellInfo = new CellLayout.CellInfo(v, info);;
+            longClickCellInfo = new CellLayout.CellInfo(v, info);
             itemUnderLongClick = longClickCellInfo.cell;
             resetAddInfo();
         }
index c9e277e..0ae1c0e 100644 (file)
@@ -1,11 +1,16 @@
 package com.android.launcher3;
 
 import android.annotation.TargetApi;
+import android.content.res.Resources;
+import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.util.SparseArray;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
@@ -20,6 +25,21 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate {
     public static final int INFO = R.id.action_info;
     public static final int UNINSTALL = R.id.action_uninstall;
     public static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace;
+    public static final int MOVE = R.id.action_move;
+
+    enum DragType {
+        ICON,
+        FOLDER,
+        WIDGET
+    }
+
+    public static class DragInfo {
+        DragType dragType;
+        ItemInfo info;
+        View item;
+    }
+
+    private DragInfo mDragInfo = null;
 
     private final SparseArray<AccessibilityAction> mActions =
             new SparseArray<AccessibilityAction>();
@@ -36,6 +56,9 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate {
                 launcher.getText(R.string.delete_target_uninstall_label)));
         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)));
+
     }
 
     @Override
@@ -49,6 +72,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate {
                 || (item instanceof FolderInfo)) {
             // Workspace shortcut / widget
             info.addAction(mActions.get(REMOVE));
+            info.addAction(mActions.get(MOVE));
         } else if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) {
             // App or Widget from customization tray
             if (item instanceof AppInfo) {
@@ -69,14 +93,21 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate {
     }
 
     public boolean performAction(View host, ItemInfo item, int action) {
+        Resources res = mLauncher.getResources();
         if (action == REMOVE) {
-            return DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host);
+            if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) {
+                announceConfirmation(R.string.item_removed_from_workspace);
+                return true;
+            }
+            return false;
         } else if (action == INFO) {
             InfoDropTarget.startDetailsActivityForInfo(item, mLauncher);
             return true;
         } else if (action == UNINSTALL) {
             DeleteDropTarget.uninstallApp(mLauncher, (AppInfo) item);
             return true;
+        } else if (action == MOVE) {
+            beginAccessibleDrag(host, item);
         } else if (action == ADD_TO_WORKSPACE) {
             final int preferredPage = mLauncher.getWorkspace().getCurrentPage();
             final ScreenPosProvider screenProvider = new ScreenPosProvider() {
@@ -90,20 +121,92 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate {
                 final ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>();
                 addShortcuts.add(((AppInfo) item).makeShortcut());
                 mLauncher.showWorkspace(true, new Runnable() {
-
                     @Override
                     public void run() {
                         mLauncher.getModel().addAndBindAddedWorkspaceApps(
                                 mLauncher, addShortcuts, screenProvider, 0, true);
+                        announceConfirmation(R.string.item_added_to_workspace);
                     }
                 });
                 return true;
             } else if (item instanceof PendingAddItemInfo) {
                 mLauncher.getModel().addAndBindPendingItem(
                         mLauncher, (PendingAddItemInfo) item, screenProvider, 0);
+                announceConfirmation(R.string.item_added_to_workspace);
                 return true;
             }
         }
         return false;
     }
+
+    private void announceConfirmation(int resId) {
+        announceConfirmation(mLauncher.getResources().getString(resId));
+    }
+
+    private void announceConfirmation(String confirmation) {
+        mLauncher.getDragLayer().announceForAccessibility(confirmation);
+
+    }
+
+    public boolean isInAccessibleDrag() {
+        return mDragInfo != null;
+    }
+
+    public DragInfo getDragInfo() {
+        return mDragInfo;
+    }
+
+    public void handleAccessibleDrop(CellLayout targetContainer, Rect dropLocation,
+            String confirmation) {
+        if (!isInAccessibleDrag()) return;
+
+        int[] loc = new int[2];
+        loc[0] = dropLocation.centerX();
+        loc[1] = dropLocation.centerY();
+
+        mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(targetContainer, loc);
+        mLauncher.getDragController().completeAccessibleDrag(loc);
+
+        endAccessibleDrag();
+        announceConfirmation(confirmation);
+    }
+
+    public void beginAccessibleDrag(View item, ItemInfo info) {
+        mDragInfo = new DragInfo();
+        mDragInfo.info = info;
+        mDragInfo.item = item;
+        mDragInfo.dragType = DragType.ICON;
+        if (info instanceof FolderInfo) {
+            mDragInfo.dragType = DragType.FOLDER;
+        } else if (info instanceof LauncherAppWidgetInfo) {
+            mDragInfo.dragType = DragType.WIDGET;
+        }
+
+        CellLayout.CellInfo cellInfo = new CellLayout.CellInfo(item, info);
+
+        Rect pos = new Rect();
+        mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
+
+        mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY());
+        mLauncher.getWorkspace().enableAccessibleDrag(true);
+        mLauncher.getWorkspace().startDrag(cellInfo, true);
+    }
+
+    public boolean onBackPressed() {
+        if (isInAccessibleDrag()) {
+            cancelAccessibleDrag();
+            return true;
+        }
+        return false;
+    }
+
+    private void cancelAccessibleDrag() {
+        mLauncher.getDragController().cancelDrag();
+        endAccessibleDrag();
+    }
+
+    private void endAccessibleDrag() {
+        mDragInfo = null;
+        mLauncher.getWorkspace().enableAccessibleDrag(false);
+    }
 }
index 8e6557f..d8896cc 100644 (file)
@@ -62,7 +62,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks {
     private static LauncherAppState INSTANCE;
 
     private DynamicGrid mDynamicGrid;
-    private AccessibilityDelegate mAccessibilityDelegate;
+    private LauncherAccessibilityDelegate mAccessibilityDelegate;
 
     public static LauncherAppState getInstance() {
         if (INSTANCE == null) {
@@ -168,7 +168,7 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks {
         return mModel;
     }
 
-    AccessibilityDelegate getAccessibilityDelegate() {
+    LauncherAccessibilityDelegate getAccessibilityDelegate() {
         return mAccessibilityDelegate;
     }
 
index 99c2e08..cc17820 100644 (file)
@@ -197,6 +197,10 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D
      */
     @Override
     public void onDragStart(DragSource source, Object info, int dragAction) {
+        showDeleteTarget();
+    }
+
+    public void showDeleteTarget() {
         // Animate out the QSB search bar, and animate in the drop target bar
         prepareStartAnimation(mDropTargetBar);
         mDropTargetBarAnim.start();
@@ -206,6 +210,16 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D
         }
     }
 
+    public void hideDeleteTarget() {
+        // Restore the QSB search bar, and animate out the drop target bar
+        prepareStartAnimation(mDropTargetBar);
+        mDropTargetBarAnim.reverse();
+        if (!mIsSearchBarHidden) {
+            prepareStartAnimation(mQSBSearchBar);
+            mQSBSearchBarAnim.reverse();
+        }
+    }
+
     public void deferOnDragEnd() {
         mDeferOnDragEnd = true;
     }
@@ -213,13 +227,7 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D
     @Override
     public void onDragEnd() {
         if (!mDeferOnDragEnd) {
-            // Restore the QSB search bar, and animate out the drop target bar
-            prepareStartAnimation(mDropTargetBar);
-            mDropTargetBarAnim.reverse();
-            if (!mIsSearchBarHidden) {
-                prepareStartAnimation(mQSBSearchBar);
-                mQSBSearchBarAnim.reverse();
-            }
+            hideDeleteTarget();
         } else {
             mDeferOnDragEnd = false;
         }
index b9c1f4d..125a2ed 100644 (file)
@@ -26,6 +26,7 @@ import android.animation.PropertyValuesHolder;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.TargetApi;
 import android.app.WallpaperManager;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
@@ -44,6 +45,7 @@ import android.graphics.Rect;
 import android.graphics.Region.Op;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Parcelable;
@@ -572,6 +574,10 @@ public class Workspace extends SmoothPagedView
         mWorkspaceScreens.put(screenId, newScreen);
         mScreenOrder.add(insertIndex, screenId);
         addView(newScreen, insertIndex);
+
+        if (LauncherAppState.getInstance().getAccessibilityDelegate().isInAccessibleDrag()) {
+            newScreen.enableAccessibleDrag(true);
+        }
         return screenId;
     }
 
@@ -1621,7 +1627,6 @@ public class Workspace extends SmoothPagedView
                     float scrollProgress = getScrollProgress(screenCenter, child, i);
                     float alpha = 1 - Math.abs(scrollProgress);
                     child.getShortcutsAndWidgets().setAlpha(alpha);
-                    //child.setBackgroundAlphaMultiplier(1 - alpha);
                 }
             }
         }
@@ -1634,6 +1639,23 @@ public class Workspace extends SmoothPagedView
         }
     }
 
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public void enableAccessibleDrag(boolean enable) {
+        for (int i = 0; i < getChildCount(); i++) {
+            CellLayout child = (CellLayout) getChildAt(i);
+            child.enableAccessibleDrag(enable);
+        }
+
+        if (enable) {
+            // We need to allow our individual children to become click handlers in this case
+            setOnClickListener(null);
+        } else {
+            // Reset our click listener
+            setOnClickListener(mLauncher);
+        }
+        mLauncher.getHotseat().getLayout().enableAccessibleDrag(enable);
+    }
+
     public boolean hasCustomContent() {
         return (mScreenOrder.size() > 0 && mScreenOrder.get(0) == CUSTOM_CONTENT_SCREEN_ID);
     }
@@ -2184,7 +2206,7 @@ public class Workspace extends SmoothPagedView
 
     private void updateAccessibilityFlags() {
         int accessible = mState == State.NORMAL ?
-                IMPORTANT_FOR_ACCESSIBILITY_YES :
+                IMPORTANT_FOR_ACCESSIBILITY_NO :
                 IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
         setImportantForAccessibility(accessible);
     }
@@ -2674,7 +2696,11 @@ public class Workspace extends SmoothPagedView
         return b;
     }
 
-    void startDrag(CellLayout.CellInfo cellInfo) {
+    public void startDrag(CellLayout.CellInfo cellInfo) {
+        startDrag(cellInfo, false);
+    }
+
+    public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) {
         View child = cellInfo.cell;
 
         // Make sure the drag was started by a long press as opposed to a long click.
@@ -2687,10 +2713,14 @@ public class Workspace extends SmoothPagedView
         CellLayout layout = (CellLayout) child.getParent().getParent();
         layout.prepareChildForDrag(child);
 
-        beginDragShared(child, this);
+        beginDragShared(child, this, accessible);
     }
 
     public void beginDragShared(View child, DragSource source) {
+        beginDragShared(child, source, false);
+    }
+
+    public void beginDragShared(View child, DragSource source, boolean accessible) {
         child.clearFocus();
         child.setPressed(false);
 
@@ -2744,7 +2774,7 @@ public class Workspace extends SmoothPagedView
         }
 
         DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
-                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
+                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible);
         dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
 
         if (child.getParent() instanceof ShortcutAndWidgetContainer) {
@@ -2794,7 +2824,7 @@ public class Workspace extends SmoothPagedView
 
         // Start the drag
         DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
-                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
+                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, false);
         dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
 
         // Recycle temporary bitmaps
@@ -3149,7 +3179,8 @@ public class Workspace extends SmoothPagedView
 
                         final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
-                        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
+                        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
+                                && !d.accessibleDrag) {
                             final Runnable addResizeFrame = new Runnable() {
                                 public void run() {
                                     DragLayer dragLayer = mLauncher.getDragLayer();
@@ -3638,7 +3669,7 @@ public class Workspace extends SmoothPagedView
                     mTargetCell[1]);
 
             manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
-                    targetCellDistance, dragOverView);
+                    targetCellDistance, dragOverView, d.accessibleDrag);
 
             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                     mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
@@ -3676,15 +3707,21 @@ public class Workspace extends SmoothPagedView
     }
 
     private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout,
-            int[] targetCell, float distance, View dragOverView) {
+            int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) {
         boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance,
                 false);
-
         if (mDragMode == DRAG_MODE_NONE && userFolderPending &&
                 !mFolderCreationAlarm.alarmPending()) {
-            mFolderCreationAlarm.setOnAlarmListener(new
-                    FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]));
-            mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
+
+            FolderCreationAlarmListener listener = new
+                    FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]);
+
+            if (!accessibleDrag) {
+                mFolderCreationAlarm.setOnAlarmListener(listener);
+                mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
+            } else {
+                listener.onAlarm(mFolderCreationAlarm);
+            }
             return;
         }