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