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