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