1package com.android.launcher3.accessibility;
2
3import android.app.AlertDialog;
4import android.appwidget.AppWidgetProviderInfo;
5import android.content.DialogInterface;
6import android.graphics.Rect;
7import android.os.Bundle;
8import android.os.Handler;
9import android.text.TextUtils;
10import android.util.Log;
11import android.util.SparseArray;
12import android.view.View;
13import android.view.View.AccessibilityDelegate;
14import android.view.accessibility.AccessibilityNodeInfo;
15import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
16
17import com.android.launcher3.AppInfo;
18import com.android.launcher3.AppWidgetResizeFrame;
19import com.android.launcher3.BubbleTextView;
20import com.android.launcher3.CellLayout;
21import com.android.launcher3.DeleteDropTarget;
22import com.android.launcher3.DropTarget.DragObject;
23import com.android.launcher3.FolderInfo;
24import com.android.launcher3.InfoDropTarget;
25import com.android.launcher3.ItemInfo;
26import com.android.launcher3.Launcher;
27import com.android.launcher3.LauncherAppWidgetHostView;
28import com.android.launcher3.LauncherAppWidgetInfo;
29import com.android.launcher3.LauncherSettings;
30import com.android.launcher3.PendingAddItemInfo;
31import com.android.launcher3.R;
32import com.android.launcher3.ShortcutInfo;
33import com.android.launcher3.UninstallDropTarget;
34import com.android.launcher3.Workspace;
35import com.android.launcher3.dragndrop.DragController.DragListener;
36import com.android.launcher3.dragndrop.DragOptions;
37import com.android.launcher3.folder.Folder;
38import com.android.launcher3.popup.PopupContainerWithArrow;
39import com.android.launcher3.shortcuts.DeepShortcutManager;
40import com.android.launcher3.util.Thunk;
41
42import java.util.ArrayList;
43
44public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener {
45
46    private static final String TAG = "LauncherAccessibilityDelegate";
47
48    protected static final int REMOVE = R.id.action_remove;
49    protected static final int INFO = R.id.action_info;
50    protected static final int UNINSTALL = R.id.action_uninstall;
51    protected static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace;
52    protected static final int MOVE = R.id.action_move;
53    protected static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace;
54    protected static final int RESIZE = R.id.action_resize;
55    public static final int DEEP_SHORTCUTS = R.id.action_deep_shortcuts;
56
57    public enum DragType {
58        ICON,
59        FOLDER,
60        WIDGET
61    }
62
63    public static class DragInfo {
64        public DragType dragType;
65        public ItemInfo info;
66        public View item;
67    }
68
69    protected final SparseArray<AccessibilityAction> mActions = new SparseArray<>();
70    @Thunk final Launcher mLauncher;
71
72    private DragInfo mDragInfo = null;
73
74    public LauncherAccessibilityDelegate(Launcher launcher) {
75        mLauncher = launcher;
76
77        mActions.put(REMOVE, new AccessibilityAction(REMOVE,
78                launcher.getText(R.string.remove_drop_target_label)));
79        mActions.put(INFO, new AccessibilityAction(INFO,
80                launcher.getText(R.string.app_info_drop_target_label)));
81        mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL,
82                launcher.getText(R.string.uninstall_drop_target_label)));
83        mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE,
84                launcher.getText(R.string.action_add_to_workspace)));
85        mActions.put(MOVE, new AccessibilityAction(MOVE,
86                launcher.getText(R.string.action_move)));
87        mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE,
88                launcher.getText(R.string.action_move_to_workspace)));
89        mActions.put(RESIZE, new AccessibilityAction(RESIZE,
90                        launcher.getText(R.string.action_resize)));
91        mActions.put(DEEP_SHORTCUTS, new AccessibilityAction(DEEP_SHORTCUTS,
92                launcher.getText(R.string.action_deep_shortcut)));
93    }
94
95    @Override
96    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
97        super.onInitializeAccessibilityNodeInfo(host, info);
98        addSupportedActions(host, info, false);
99    }
100
101    public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
102        if (!(host.getTag() instanceof ItemInfo)) return;
103        ItemInfo item = (ItemInfo) host.getTag();
104
105        // If the request came from keyboard, do not add custom shortcuts as that is already
106        // exposed as a direct shortcut
107        if (!fromKeyboard && DeepShortcutManager.supportsShortcuts(item)) {
108            info.addAction(mActions.get(DEEP_SHORTCUTS));
109        }
110
111        if (DeleteDropTarget.supportsAccessibleDrop(item)) {
112            info.addAction(mActions.get(REMOVE));
113        }
114        if (UninstallDropTarget.supportsDrop(host.getContext(), item)) {
115            info.addAction(mActions.get(UNINSTALL));
116        }
117        if (InfoDropTarget.supportsDrop(host.getContext(), item)) {
118            info.addAction(mActions.get(INFO));
119        }
120
121        // Do not add move actions for keyboard request as this uses virtual nodes.
122        if (!fromKeyboard && ((item instanceof ShortcutInfo)
123                || (item instanceof LauncherAppWidgetInfo)
124                || (item instanceof FolderInfo))) {
125            info.addAction(mActions.get(MOVE));
126
127            if (item.container >= 0) {
128                info.addAction(mActions.get(MOVE_TO_WORKSPACE));
129            } else if (item instanceof LauncherAppWidgetInfo) {
130                if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) {
131                    info.addAction(mActions.get(RESIZE));
132                }
133            }
134        }
135
136        if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) {
137            info.addAction(mActions.get(ADD_TO_WORKSPACE));
138        }
139    }
140
141    @Override
142    public boolean performAccessibilityAction(View host, int action, Bundle args) {
143        if ((host.getTag() instanceof ItemInfo)
144                && performAction(host, (ItemInfo) host.getTag(), action)) {
145            return true;
146        }
147        return super.performAccessibilityAction(host, action, args);
148    }
149
150    public boolean performAction(final View host, final ItemInfo item, int action) {
151        if (action == REMOVE) {
152            DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host);
153            return true;
154        } else if (action == INFO) {
155            InfoDropTarget.startDetailsActivityForInfo(item, mLauncher, null);
156            return true;
157        } else if (action == UNINSTALL) {
158            return UninstallDropTarget.startUninstallActivity(mLauncher, item);
159        } else if (action == MOVE) {
160            beginAccessibleDrag(host, item);
161        } else if (action == ADD_TO_WORKSPACE) {
162            final int[] coordinates = new int[2];
163            final long screenId = findSpaceOnWorkspace(item, coordinates);
164            mLauncher.showWorkspace(true, new Runnable() {
165
166                @Override
167                public void run() {
168                    if (item instanceof AppInfo) {
169                        ShortcutInfo info = ((AppInfo) item).makeShortcut();
170                        mLauncher.getModelWriter().addItemToDatabase(info,
171                                LauncherSettings.Favorites.CONTAINER_DESKTOP,
172                                screenId, coordinates[0], coordinates[1]);
173
174                        ArrayList<ItemInfo> itemList = new ArrayList<>();
175                        itemList.add(info);
176                        mLauncher.bindItems(itemList, true);
177                    } else if (item instanceof PendingAddItemInfo) {
178                        PendingAddItemInfo info = (PendingAddItemInfo) item;
179                        Workspace workspace = mLauncher.getWorkspace();
180                        workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
181                        mLauncher.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP,
182                                screenId, coordinates, info.spanX, info.spanY);
183                    }
184                    announceConfirmation(R.string.item_added_to_workspace);
185                }
186            });
187            return true;
188        } else if (action == MOVE_TO_WORKSPACE) {
189            Folder folder = Folder.getOpen(mLauncher);
190            folder.close(true);
191            ShortcutInfo info = (ShortcutInfo) item;
192            folder.getInfo().remove(info, false);
193
194            final int[] coordinates = new int[2];
195            final long screenId = findSpaceOnWorkspace(item, coordinates);
196            mLauncher.getModelWriter().moveItemInDatabase(info,
197                    LauncherSettings.Favorites.CONTAINER_DESKTOP,
198                    screenId, coordinates[0], coordinates[1]);
199
200            // Bind the item in next frame so that if a new workspace page was created,
201            // it will get laid out.
202            new Handler().post(new Runnable() {
203
204                @Override
205                public void run() {
206                    ArrayList<ItemInfo> itemList = new ArrayList<>();
207                    itemList.add(item);
208                    mLauncher.bindItems(itemList, true);
209                    announceConfirmation(R.string.item_moved);
210                }
211            });
212        } else if (action == RESIZE) {
213            final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
214            final ArrayList<Integer> actions = getSupportedResizeActions(host, info);
215            CharSequence[] labels = new CharSequence[actions.size()];
216            for (int i = 0; i < actions.size(); i++) {
217                labels[i] = mLauncher.getText(actions.get(i));
218            }
219
220            new AlertDialog.Builder(mLauncher)
221                .setTitle(R.string.action_resize)
222                .setItems(labels, new DialogInterface.OnClickListener() {
223
224                    @Override
225                    public void onClick(DialogInterface dialog, int which) {
226                        performResizeAction(actions.get(which), host, info);
227                        dialog.dismiss();
228                    }
229                })
230                .show();
231            return true;
232        } else if (action == DEEP_SHORTCUTS) {
233            return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null;
234        }
235        return false;
236    }
237
238    private ArrayList<Integer> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
239        ArrayList<Integer> actions = new ArrayList<>();
240
241        AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
242        if (providerInfo == null) {
243            return actions;
244        }
245
246        CellLayout layout = (CellLayout) host.getParent().getParent();
247        if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) {
248            if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) ||
249                    layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) {
250                actions.add(R.string.action_increase_width);
251            }
252
253            if (info.spanX > info.minSpanX && info.spanX > 1) {
254                actions.add(R.string.action_decrease_width);
255            }
256        }
257
258        if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) {
259            if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) ||
260                    layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) {
261                actions.add(R.string.action_increase_height);
262            }
263
264            if (info.spanY > info.minSpanY && info.spanY > 1) {
265                actions.add(R.string.action_decrease_height);
266            }
267        }
268        return actions;
269    }
270
271    @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
272        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams();
273        CellLayout layout = (CellLayout) host.getParent().getParent();
274        layout.markCellsAsUnoccupiedForView(host);
275
276        if (action == R.string.action_increase_width) {
277            if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL)
278                    && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY))
279                    || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) {
280                lp.cellX --;
281                info.cellX --;
282            }
283            lp.cellHSpan ++;
284            info.spanX ++;
285        } else if (action == R.string.action_decrease_width) {
286            lp.cellHSpan --;
287            info.spanX --;
288        } else if (action == R.string.action_increase_height) {
289            if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) {
290                lp.cellY --;
291                info.cellY --;
292            }
293            lp.cellVSpan ++;
294            info.spanY ++;
295        } else if (action == R.string.action_decrease_height) {
296            lp.cellVSpan --;
297            info.spanY --;
298        }
299
300        layout.markCellsAsOccupiedForView(host);
301        Rect sizeRange = new Rect();
302        AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, sizeRange);
303        ((LauncherAppWidgetHostView) host).updateAppWidgetSize(null,
304                sizeRange.left, sizeRange.top, sizeRange.right, sizeRange.bottom);
305        host.requestLayout();
306        mLauncher.getModelWriter().updateItemInDatabase(info);
307        announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY));
308    }
309
310    @Thunk void announceConfirmation(int resId) {
311        announceConfirmation(mLauncher.getResources().getString(resId));
312    }
313
314    @Thunk void announceConfirmation(String confirmation) {
315        mLauncher.getDragLayer().announceForAccessibility(confirmation);
316
317    }
318
319    public boolean isInAccessibleDrag() {
320        return mDragInfo != null;
321    }
322
323    public DragInfo getDragInfo() {
324        return mDragInfo;
325    }
326
327    /**
328     * @param clickedTarget the actual view that was clicked
329     * @param dropLocation relative to {@param clickedTarget}. If provided, its center is used
330     * as the actual drop location otherwise the views center is used.
331     */
332    public void handleAccessibleDrop(View clickedTarget, Rect dropLocation,
333            String confirmation) {
334        if (!isInAccessibleDrag()) return;
335
336        int[] loc = new int[2];
337        if (dropLocation == null) {
338            loc[0] = clickedTarget.getWidth() / 2;
339            loc[1] = clickedTarget.getHeight() / 2;
340        } else {
341            loc[0] = dropLocation.centerX();
342            loc[1] = dropLocation.centerY();
343        }
344
345        mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc);
346        mLauncher.getDragController().completeAccessibleDrag(loc);
347
348        if (!TextUtils.isEmpty(confirmation)) {
349            announceConfirmation(confirmation);
350        }
351    }
352
353    public void beginAccessibleDrag(View item, ItemInfo info) {
354        mDragInfo = new DragInfo();
355        mDragInfo.info = info;
356        mDragInfo.item = item;
357        mDragInfo.dragType = DragType.ICON;
358        if (info instanceof FolderInfo) {
359            mDragInfo.dragType = DragType.FOLDER;
360        } else if (info instanceof LauncherAppWidgetInfo) {
361            mDragInfo.dragType = DragType.WIDGET;
362        }
363
364        CellLayout.CellInfo cellInfo = new CellLayout.CellInfo(item, info);
365
366        Rect pos = new Rect();
367        mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
368        mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY());
369
370        Folder folder = Folder.getOpen(mLauncher);
371        if (folder != null) {
372            if (!folder.getItemsInReadingOrder().contains(item)) {
373                folder.close(true);
374                folder = null;
375            }
376        }
377
378        mLauncher.getDragController().addDragListener(this);
379
380        DragOptions options = new DragOptions();
381        options.isAccessibleDrag = true;
382        if (folder != null) {
383            folder.startDrag(cellInfo.cell, options);
384        } else {
385            mLauncher.getWorkspace().startDrag(cellInfo, options);
386        }
387    }
388
389    @Override
390    public void onDragStart(DragObject dragObject, DragOptions options) {
391        // No-op
392    }
393
394    @Override
395    public void onDragEnd() {
396        mLauncher.getDragController().removeDragListener(this);
397        mDragInfo = null;
398    }
399
400    /**
401     * Find empty space on the workspace and returns the screenId.
402     */
403    protected long findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) {
404        Workspace workspace = mLauncher.getWorkspace();
405        ArrayList<Long> workspaceScreens = workspace.getScreenOrder();
406        long screenId;
407
408        // First check if there is space on the current screen.
409        int screenIndex = workspace.getCurrentPage();
410        screenId = workspaceScreens.get(screenIndex);
411        CellLayout layout = (CellLayout) workspace.getPageAt(screenIndex);
412
413        boolean found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
414        screenIndex = workspace.hasCustomContent() ? 1 : 0;
415        while (!found && screenIndex < workspaceScreens.size()) {
416            screenId = workspaceScreens.get(screenIndex);
417            layout = (CellLayout) workspace.getPageAt(screenIndex);
418            found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
419            screenIndex++;
420        }
421
422        if (found) {
423            return screenId;
424        }
425
426        workspace.addExtraEmptyScreen();
427        screenId = workspace.commitExtraEmptyScreen();
428        layout = workspace.getScreenWithId(screenId);
429        found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
430
431        if (!found) {
432            Log.wtf(TAG, "Not enough space on an empty screen");
433        }
434        return screenId;
435    }
436}
437