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