• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3.accessibility;
2 
3 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
4 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
5 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
6 
7 import static com.android.launcher3.LauncherState.NORMAL;
8 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
9 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
10 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
11 
12 import android.appwidget.AppWidgetProviderInfo;
13 import android.graphics.Point;
14 import android.graphics.Rect;
15 import android.graphics.RectF;
16 import android.os.Handler;
17 import android.util.Log;
18 import android.view.KeyEvent;
19 import android.view.View;
20 
21 import com.android.launcher3.BubbleTextView;
22 import com.android.launcher3.ButtonDropTarget;
23 import com.android.launcher3.CellLayout;
24 import com.android.launcher3.Launcher;
25 import com.android.launcher3.LauncherSettings;
26 import com.android.launcher3.PendingAddItemInfo;
27 import com.android.launcher3.R;
28 import com.android.launcher3.Workspace;
29 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
30 import com.android.launcher3.dragndrop.DragOptions;
31 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
32 import com.android.launcher3.dragndrop.DragView;
33 import com.android.launcher3.folder.Folder;
34 import com.android.launcher3.keyboard.KeyboardDragAndDropView;
35 import com.android.launcher3.model.data.FolderInfo;
36 import com.android.launcher3.model.data.ItemInfo;
37 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
38 import com.android.launcher3.model.data.WorkspaceItemFactory;
39 import com.android.launcher3.model.data.WorkspaceItemInfo;
40 import com.android.launcher3.notification.NotificationListener;
41 import com.android.launcher3.popup.ArrowPopup;
42 import com.android.launcher3.popup.PopupContainerWithArrow;
43 import com.android.launcher3.touch.ItemLongClickListener;
44 import com.android.launcher3.util.IntArray;
45 import com.android.launcher3.util.IntSet;
46 import com.android.launcher3.util.ShortcutUtil;
47 import com.android.launcher3.util.Thunk;
48 import com.android.launcher3.views.BubbleTextHolder;
49 import com.android.launcher3.views.OptionsPopupView;
50 import com.android.launcher3.views.OptionsPopupView.OptionItem;
51 import com.android.launcher3.widget.LauncherAppWidgetHostView;
52 import com.android.launcher3.widget.util.WidgetSizes;
53 
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.List;
57 
58 public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Launcher> {
59 
60     private static final String TAG = "LauncherAccessibilityDelegate";
61 
62     public static final int REMOVE = R.id.action_remove;
63     public static final int UNINSTALL = R.id.action_uninstall;
64     public static final int DISMISS_PREDICTION = R.id.action_dismiss_prediction;
65     public static final int PIN_PREDICTION = R.id.action_pin_prediction;
66     public static final int RECONFIGURE = R.id.action_reconfigure;
67     public static final int INVALID = -1;
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 
LauncherAccessibilityDelegate(Launcher launcher)75     public LauncherAccessibilityDelegate(Launcher launcher) {
76         super(launcher);
77 
78         mActions.put(REMOVE, new LauncherAction(
79                 REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X));
80         mActions.put(UNINSTALL, new LauncherAction(
81                 UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U));
82         mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION,
83                 R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X));
84         mActions.put(RECONFIGURE, new LauncherAction(
85                 RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E));
86         mActions.put(ADD_TO_WORKSPACE, new LauncherAction(
87                 ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P));
88         mActions.put(MOVE, new LauncherAction(
89                 MOVE, R.string.action_move, KeyEvent.KEYCODE_M));
90         mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE,
91                 R.string.action_move_to_workspace, KeyEvent.KEYCODE_P));
92         mActions.put(RESIZE, new LauncherAction(
93                 RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R));
94         mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS,
95                 R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
96         mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS,
97                 R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S));
98     }
99 
100     @Override
getSupportedActions(View host, ItemInfo item, List<LauncherAction> out)101     protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
102         // If the request came from keyboard, do not add custom shortcuts as that is already
103         // exposed as a direct shortcut
104         if (ShortcutUtil.supportsShortcuts(item)) {
105             out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null
106                     ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS));
107         }
108 
109         for (ButtonDropTarget target : mContext.getDropTargetBar().getDropTargets()) {
110             if (target.supportsAccessibilityDrop(item, host)) {
111                 out.add(mActions.get(target.getAccessibilityAction()));
112             }
113         }
114 
115         // Do not add move actions for keyboard request as this uses virtual nodes.
116         if (itemSupportsAccessibleDrag(item)) {
117             out.add(mActions.get(MOVE));
118 
119             if (item.container >= 0) {
120                 out.add(mActions.get(MOVE_TO_WORKSPACE));
121             } else if (item instanceof LauncherAppWidgetInfo) {
122                 if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) {
123                     out.add(mActions.get(RESIZE));
124                 }
125             }
126         }
127 
128         if (supportAddToWorkSpace(item)) {
129             out.add(mActions.get(ADD_TO_WORKSPACE));
130         }
131     }
132 
supportAddToWorkSpace(ItemInfo item)133     private boolean supportAddToWorkSpace(ItemInfo item) {
134         return (item instanceof WorkspaceItemFactory)
135                 || ((item instanceof WorkspaceItemInfo)
136                     && (((WorkspaceItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0)
137                 || ((item instanceof PendingAddItemInfo)
138                     && (((PendingAddItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0);
139     }
140 
141     /**
142      * Returns all the accessibility actions that can be handled by the host.
143      */
getSupportedActions(Launcher launcher, View host)144     public static List<LauncherAction> getSupportedActions(Launcher launcher, View host) {
145         if (host == null || !(host.getTag() instanceof  ItemInfo)) {
146             return Collections.emptyList();
147         }
148         PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
149         LauncherAccessibilityDelegate delegate = container != null
150                 ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate();
151         List<LauncherAction> result = new ArrayList<>();
152         delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result);
153         return result;
154     }
155 
156     @Override
performAction(final View host, final ItemInfo item, int action, boolean fromKeyboard)157     protected boolean performAction(final View host, final ItemInfo item, int action,
158             boolean fromKeyboard) {
159         if (action == ACTION_LONG_CLICK) {
160             PreDragCondition dragCondition = null;
161             // Long press should be consumed for workspace items, and it should invoke the
162             // Shortcuts / Notifications / Actions pop-up menu, and not start a drag as the
163             // standard long press path does.
164             if (host instanceof BubbleTextView) {
165                 dragCondition = ((BubbleTextView) host).startLongPressAction();
166             } else if (host instanceof BubbleTextHolder) {
167                 BubbleTextHolder holder = (BubbleTextHolder) host;
168                 dragCondition = holder.getBubbleText() == null ? null
169                         : holder.getBubbleText().startLongPressAction();
170             }
171             return dragCondition != null;
172         } else if (action == MOVE) {
173             return beginAccessibleDrag(host, item, fromKeyboard);
174         } else if (action == ADD_TO_WORKSPACE) {
175             return addToWorkspace(item, true);
176         } else if (action == MOVE_TO_WORKSPACE) {
177             return moveToWorkspace(item);
178         } else if (action == RESIZE) {
179             final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
180             List<OptionItem> actions = getSupportedResizeActions(host, info);
181             Rect pos = new Rect();
182             mContext.getDragLayer().getDescendantRectRelativeToSelf(host, pos);
183             ArrowPopup popup = OptionsPopupView.show(mContext, new RectF(pos), actions, false);
184             popup.requestFocus();
185             popup.addOnCloseCallback(() -> {
186                 host.requestFocus();
187                 host.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
188                 host.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
189             });
190             return true;
191         } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) {
192             BubbleTextView btv = host instanceof BubbleTextView ? (BubbleTextView) host
193                     : (host instanceof BubbleTextHolder
194                             ? ((BubbleTextHolder) host).getBubbleText() : null);
195             return btv != null && PopupContainerWithArrow.showForIcon(btv) != null;
196         } else {
197             for (ButtonDropTarget dropTarget : mContext.getDropTargetBar().getDropTargets()) {
198                 if (dropTarget.supportsAccessibilityDrop(item, host)
199                         && action == dropTarget.getAccessibilityAction()) {
200                     dropTarget.onAccessibilityDrop(host, item);
201                     return true;
202                 }
203             }
204         }
205         return false;
206     }
207 
getSupportedResizeActions(View host, LauncherAppWidgetInfo info)208     private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
209         List<OptionItem> actions = new ArrayList<>();
210         AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
211         if (providerInfo == null) {
212             return actions;
213         }
214 
215         CellLayout layout;
216         if (host.getParent() instanceof DragView) {
217             layout = (CellLayout) ((DragView) host.getParent()).getContentViewParent().getParent();
218         } else {
219             layout = (CellLayout) host.getParent().getParent();
220         }
221         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) {
222             if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) ||
223                     layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) {
224                 actions.add(new OptionItem(mContext,
225                         R.string.action_increase_width,
226                         R.drawable.ic_widget_width_increase,
227                         IGNORE,
228                         v -> performResizeAction(R.string.action_increase_width, host, info)));
229             }
230 
231             if (info.spanX > info.minSpanX && info.spanX > 1) {
232                 actions.add(new OptionItem(mContext,
233                         R.string.action_decrease_width,
234                         R.drawable.ic_widget_width_decrease,
235                         IGNORE,
236                         v -> performResizeAction(R.string.action_decrease_width, host, info)));
237             }
238         }
239 
240         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) {
241             if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) ||
242                     layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) {
243                 actions.add(new OptionItem(mContext,
244                         R.string.action_increase_height,
245                         R.drawable.ic_widget_height_increase,
246                         IGNORE,
247                         v -> performResizeAction(R.string.action_increase_height, host, info)));
248             }
249 
250             if (info.spanY > info.minSpanY && info.spanY > 1) {
251                 actions.add(new OptionItem(mContext,
252                         R.string.action_decrease_height,
253                         R.drawable.ic_widget_height_decrease,
254                         IGNORE,
255                         v -> performResizeAction(R.string.action_decrease_height, host, info)));
256             }
257         }
258         return actions;
259     }
260 
performResizeAction(int action, View host, LauncherAppWidgetInfo info)261     private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
262         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) host.getLayoutParams();
263         CellLayout layout = (CellLayout) host.getParent().getParent();
264         layout.markCellsAsUnoccupiedForView(host);
265 
266         if (action == R.string.action_increase_width) {
267             if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL)
268                     && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY))
269                     || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) {
270                 lp.setCellX(lp.getCellX() - 1);
271                 info.cellX --;
272             }
273             lp.cellHSpan ++;
274             info.spanX ++;
275         } else if (action == R.string.action_decrease_width) {
276             lp.cellHSpan --;
277             info.spanX --;
278         } else if (action == R.string.action_increase_height) {
279             if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) {
280                 lp.setCellY(lp.getCellY() - 1);
281                 info.cellY --;
282             }
283             lp.cellVSpan ++;
284             info.spanY ++;
285         } else if (action == R.string.action_decrease_height) {
286             lp.cellVSpan --;
287             info.spanY --;
288         }
289 
290         layout.markCellsAsOccupiedForView(host);
291         WidgetSizes.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mContext,
292                 info.spanX, info.spanY);
293         host.requestLayout();
294         mContext.getModelWriter().updateItemInDatabase(info);
295         announceConfirmation(mContext.getString(R.string.widget_resized, info.spanX, info.spanY));
296         return true;
297     }
298 
announceConfirmation(int resId)299     @Thunk void announceConfirmation(int resId) {
300         announceConfirmation(mContext.getResources().getString(resId));
301     }
302 
303     @Override
beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard)304     protected boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) {
305         if (!itemSupportsAccessibleDrag(info)) {
306             return false;
307         }
308 
309         mDragInfo = new DragInfo();
310         mDragInfo.info = info;
311         mDragInfo.item = item;
312         mDragInfo.dragType = DragType.ICON;
313         if (info instanceof FolderInfo) {
314             mDragInfo.dragType = DragType.FOLDER;
315         } else if (info instanceof LauncherAppWidgetInfo) {
316             mDragInfo.dragType = DragType.WIDGET;
317         }
318 
319         Rect pos = new Rect();
320         mContext.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
321         mContext.getDragController().addDragListener(this);
322 
323         DragOptions options = new DragOptions();
324         options.isAccessibleDrag = true;
325         options.isKeyboardDrag = fromKeyboard;
326         options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
327 
328         if (fromKeyboard) {
329             KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mContext.getLayoutInflater()
330                     .inflate(R.layout.keyboard_drag_and_drop, mContext.getDragLayer(), false);
331             popup.showForIcon(item, info, options);
332         } else {
333             ItemLongClickListener.beginDrag(item, mContext, info, options);
334         }
335         return true;
336     }
337 
338     /**
339      * Find empty space on the workspace and returns the screenId.
340      */
findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates)341     protected int findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) {
342         Workspace<?> workspace = mContext.getWorkspace();
343         IntArray workspaceScreens = workspace.getScreenOrder();
344         int screenId;
345 
346         // First check if there is space on the current screen.
347         int screenIndex = workspace.getCurrentPage();
348         screenId = workspaceScreens.get(screenIndex);
349         CellLayout layout = (CellLayout) workspace.getPageAt(screenIndex);
350 
351         boolean found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
352         screenIndex = 0;
353         while (!found && screenIndex < workspaceScreens.size()) {
354             screenId = workspaceScreens.get(screenIndex);
355             layout = (CellLayout) workspace.getPageAt(screenIndex);
356             found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
357             screenIndex++;
358         }
359 
360         if (found) {
361             return screenId;
362         }
363 
364         workspace.addExtraEmptyScreens();
365         IntSet emptyScreenIds = workspace.commitExtraEmptyScreens();
366         if (emptyScreenIds.isEmpty()) {
367             // Couldn't create extra empty screens for some reason (e.g. Workspace is loading)
368             return -1;
369         }
370 
371         screenId = emptyScreenIds.getArray().get(0);
372         layout = workspace.getScreenWithId(screenId);
373         found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
374 
375         if (!found) {
376             Log.wtf(TAG, "Not enough space on an empty screen");
377         }
378         return screenId;
379     }
380 
381     /**
382      * Functionality to add the item {@link ItemInfo} to the workspace
383      * @param item item to be added
384      * @param accessibility true if the first item to be added to the workspace
385      *     should be focused for accessibility.
386      *
387      * @return true if the item could be successfully added
388      */
addToWorkspace(ItemInfo item, boolean accessibility)389     public boolean addToWorkspace(ItemInfo item, boolean accessibility) {
390         final int[] coordinates = new int[2];
391         final int screenId = findSpaceOnWorkspace(item, coordinates);
392         if (screenId == -1) {
393             return false;
394         }
395         mContext.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
396             if (item instanceof WorkspaceItemFactory) {
397                 WorkspaceItemInfo info = ((WorkspaceItemFactory) item).makeWorkspaceItem(mContext);
398                 mContext.getModelWriter().addItemToDatabase(info,
399                         LauncherSettings.Favorites.CONTAINER_DESKTOP,
400                         screenId, coordinates[0], coordinates[1]);
401 
402                 mContext.bindItems(
403                         Collections.singletonList(info),
404                         /* forceAnimateIcons= */ true,
405                         /* focusFirstItemForAccessibility= */ accessibility);
406                 announceConfirmation(R.string.item_added_to_workspace);
407             } else if (item instanceof PendingAddItemInfo) {
408                 PendingAddItemInfo info = (PendingAddItemInfo) item;
409                 Workspace<?> workspace = mContext.getWorkspace();
410                 workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
411                 mContext.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP,
412                         screenId, coordinates, info.spanX, info.spanY);
413             } else if (item instanceof WorkspaceItemInfo) {
414                 WorkspaceItemInfo info = ((WorkspaceItemInfo) item).clone();
415                 mContext.getModelWriter().addItemToDatabase(info,
416                         LauncherSettings.Favorites.CONTAINER_DESKTOP,
417                         screenId, coordinates[0], coordinates[1]);
418                 mContext.bindItems(Collections.singletonList(info), true, accessibility);
419             } else if (item instanceof FolderInfo fi) {
420                 mContext.getModelWriter().addItemToDatabase(fi,
421                         LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0],
422                         coordinates[1]);
423                 fi.contents.forEach(member -> {
424                     mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1);
425                 });
426                 mContext.bindItems(Collections.singletonList(fi), true, accessibility);
427             }
428         }));
429         return true;
430     }
431     /**
432      * Functionality to move the item {@link ItemInfo} to the workspace
433      * @param item item to be moved
434      *
435      * @return true if the item could be successfully added
436      */
moveToWorkspace(ItemInfo item)437     public boolean moveToWorkspace(ItemInfo item) {
438         Folder folder = Folder.getOpen(mContext);
439         folder.close(true);
440         WorkspaceItemInfo info = (WorkspaceItemInfo) item;
441         folder.getInfo().remove(info, false);
442 
443         final int[] coordinates = new int[2];
444         final int screenId = findSpaceOnWorkspace(item, coordinates);
445         if (screenId == -1) {
446             return false;
447         }
448         mContext.getModelWriter().moveItemInDatabase(info,
449                 LauncherSettings.Favorites.CONTAINER_DESKTOP,
450                 screenId, coordinates[0], coordinates[1]);
451 
452         // Bind the item in next frame so that if a new workspace page was created,
453         // it will get laid out.
454         new Handler().post(() -> {
455             mContext.bindItems(Collections.singletonList(item), true);
456             announceConfirmation(R.string.item_moved);
457         });
458         return true;
459     }
460 }
461