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