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