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