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