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