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