1 package com.android.launcher3.accessibility; 2 3 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; 4 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS; 5 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; 6 7 import static com.android.launcher3.LauncherState.NORMAL; 8 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback; 9 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE; 10 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; 11 12 import android.appwidget.AppWidgetProviderInfo; 13 import android.graphics.Point; 14 import android.graphics.Rect; 15 import android.graphics.RectF; 16 import android.os.Handler; 17 import android.util.Log; 18 import android.view.KeyEvent; 19 import android.view.View; 20 21 import com.android.launcher3.BubbleTextView; 22 import com.android.launcher3.ButtonDropTarget; 23 import com.android.launcher3.CellLayout; 24 import com.android.launcher3.Launcher; 25 import com.android.launcher3.LauncherSettings; 26 import com.android.launcher3.PendingAddItemInfo; 27 import com.android.launcher3.R; 28 import com.android.launcher3.Workspace; 29 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 30 import com.android.launcher3.dragndrop.DragOptions; 31 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition; 32 import com.android.launcher3.dragndrop.DragView; 33 import com.android.launcher3.folder.Folder; 34 import com.android.launcher3.keyboard.KeyboardDragAndDropView; 35 import com.android.launcher3.model.data.FolderInfo; 36 import com.android.launcher3.model.data.ItemInfo; 37 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 38 import com.android.launcher3.model.data.WorkspaceItemFactory; 39 import com.android.launcher3.model.data.WorkspaceItemInfo; 40 import com.android.launcher3.notification.NotificationListener; 41 import com.android.launcher3.popup.ArrowPopup; 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.IntSet; 46 import com.android.launcher3.util.ShortcutUtil; 47 import com.android.launcher3.util.Thunk; 48 import com.android.launcher3.views.BubbleTextHolder; 49 import com.android.launcher3.views.OptionsPopupView; 50 import com.android.launcher3.views.OptionsPopupView.OptionItem; 51 import com.android.launcher3.widget.LauncherAppWidgetHostView; 52 import com.android.launcher3.widget.util.WidgetSizes; 53 54 import java.util.ArrayList; 55 import java.util.Collections; 56 import java.util.List; 57 58 public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Launcher> { 59 60 private static final String TAG = "LauncherAccessibilityDelegate"; 61 62 public static final int REMOVE = R.id.action_remove; 63 public static final int UNINSTALL = R.id.action_uninstall; 64 public static final int DISMISS_PREDICTION = R.id.action_dismiss_prediction; 65 public static final int PIN_PREDICTION = R.id.action_pin_prediction; 66 public static final int RECONFIGURE = R.id.action_reconfigure; 67 public static final int INVALID = -1; 68 protected static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace; 69 protected static final int MOVE = R.id.action_move; 70 protected static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace; 71 protected static final int RESIZE = R.id.action_resize; 72 public static final int DEEP_SHORTCUTS = R.id.action_deep_shortcuts; 73 public static final int SHORTCUTS_AND_NOTIFICATIONS = R.id.action_shortcuts_and_notifications; 74 LauncherAccessibilityDelegate(Launcher launcher)75 public LauncherAccessibilityDelegate(Launcher launcher) { 76 super(launcher); 77 78 mActions.put(REMOVE, new LauncherAction( 79 REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X)); 80 mActions.put(UNINSTALL, new LauncherAction( 81 UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U)); 82 mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION, 83 R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X)); 84 mActions.put(RECONFIGURE, new LauncherAction( 85 RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E)); 86 mActions.put(ADD_TO_WORKSPACE, new LauncherAction( 87 ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P)); 88 mActions.put(MOVE, new LauncherAction( 89 MOVE, R.string.action_move, KeyEvent.KEYCODE_M)); 90 mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE, 91 R.string.action_move_to_workspace, KeyEvent.KEYCODE_P)); 92 mActions.put(RESIZE, new LauncherAction( 93 RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R)); 94 mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS, 95 R.string.action_deep_shortcut, KeyEvent.KEYCODE_S)); 96 mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS, 97 R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S)); 98 } 99 100 @Override getSupportedActions(View host, ItemInfo item, List<LauncherAction> out)101 protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) { 102 // If the request came from keyboard, do not add custom shortcuts as that is already 103 // exposed as a direct shortcut 104 if (ShortcutUtil.supportsShortcuts(item)) { 105 out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null 106 ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS)); 107 } 108 109 for (ButtonDropTarget target : mContext.getDropTargetBar().getDropTargets()) { 110 if (target.supportsAccessibilityDrop(item, host)) { 111 out.add(mActions.get(target.getAccessibilityAction())); 112 } 113 } 114 115 // Do not add move actions for keyboard request as this uses virtual nodes. 116 if (itemSupportsAccessibleDrag(item)) { 117 out.add(mActions.get(MOVE)); 118 119 if (item.container >= 0) { 120 out.add(mActions.get(MOVE_TO_WORKSPACE)); 121 } else if (item instanceof LauncherAppWidgetInfo) { 122 if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) { 123 out.add(mActions.get(RESIZE)); 124 } 125 } 126 } 127 128 if (supportAddToWorkSpace(item)) { 129 out.add(mActions.get(ADD_TO_WORKSPACE)); 130 } 131 } 132 supportAddToWorkSpace(ItemInfo item)133 private boolean supportAddToWorkSpace(ItemInfo item) { 134 return (item instanceof WorkspaceItemFactory) 135 || ((item instanceof WorkspaceItemInfo) 136 && (((WorkspaceItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0) 137 || ((item instanceof PendingAddItemInfo) 138 && (((PendingAddItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0); 139 } 140 141 /** 142 * Returns all the accessibility actions that can be handled by the host. 143 */ getSupportedActions(Launcher launcher, View host)144 public static List<LauncherAction> getSupportedActions(Launcher launcher, View host) { 145 if (host == null || !(host.getTag() instanceof ItemInfo)) { 146 return Collections.emptyList(); 147 } 148 PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher); 149 LauncherAccessibilityDelegate delegate = container != null 150 ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate(); 151 List<LauncherAction> result = new ArrayList<>(); 152 delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result); 153 return result; 154 } 155 156 @Override performAction(final View host, final ItemInfo item, int action, boolean fromKeyboard)157 protected boolean performAction(final View host, final ItemInfo item, int action, 158 boolean fromKeyboard) { 159 if (action == ACTION_LONG_CLICK) { 160 PreDragCondition dragCondition = null; 161 // Long press should be consumed for workspace items, and it should invoke the 162 // Shortcuts / Notifications / Actions pop-up menu, and not start a drag as the 163 // standard long press path does. 164 if (host instanceof BubbleTextView) { 165 dragCondition = ((BubbleTextView) host).startLongPressAction(); 166 } else if (host instanceof BubbleTextHolder) { 167 BubbleTextHolder holder = (BubbleTextHolder) host; 168 dragCondition = holder.getBubbleText() == null ? null 169 : holder.getBubbleText().startLongPressAction(); 170 } 171 return dragCondition != null; 172 } else if (action == MOVE) { 173 return beginAccessibleDrag(host, item, fromKeyboard); 174 } else if (action == ADD_TO_WORKSPACE) { 175 return addToWorkspace(item, true); 176 } else if (action == MOVE_TO_WORKSPACE) { 177 return moveToWorkspace(item); 178 } else if (action == RESIZE) { 179 final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; 180 List<OptionItem> actions = getSupportedResizeActions(host, info); 181 Rect pos = new Rect(); 182 mContext.getDragLayer().getDescendantRectRelativeToSelf(host, pos); 183 ArrowPopup popup = OptionsPopupView.show(mContext, new RectF(pos), actions, false); 184 popup.requestFocus(); 185 popup.addOnCloseCallback(() -> { 186 host.requestFocus(); 187 host.sendAccessibilityEvent(TYPE_VIEW_FOCUSED); 188 host.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null); 189 }); 190 return true; 191 } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) { 192 BubbleTextView btv = host instanceof BubbleTextView ? (BubbleTextView) host 193 : (host instanceof BubbleTextHolder 194 ? ((BubbleTextHolder) host).getBubbleText() : null); 195 return btv != null && PopupContainerWithArrow.showForIcon(btv) != null; 196 } else { 197 for (ButtonDropTarget dropTarget : mContext.getDropTargetBar().getDropTargets()) { 198 if (dropTarget.supportsAccessibilityDrop(item, host) 199 && action == dropTarget.getAccessibilityAction()) { 200 dropTarget.onAccessibilityDrop(host, item); 201 return true; 202 } 203 } 204 } 205 return false; 206 } 207 getSupportedResizeActions(View host, LauncherAppWidgetInfo info)208 private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { 209 List<OptionItem> actions = new ArrayList<>(); 210 AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo(); 211 if (providerInfo == null) { 212 return actions; 213 } 214 215 CellLayout layout; 216 if (host.getParent() instanceof DragView) { 217 layout = (CellLayout) ((DragView) host.getParent()).getContentViewParent().getParent(); 218 } else { 219 layout = (CellLayout) host.getParent().getParent(); 220 } 221 if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) { 222 if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) || 223 layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) { 224 actions.add(new OptionItem(mContext, 225 R.string.action_increase_width, 226 R.drawable.ic_widget_width_increase, 227 IGNORE, 228 v -> performResizeAction(R.string.action_increase_width, host, info))); 229 } 230 231 if (info.spanX > info.minSpanX && info.spanX > 1) { 232 actions.add(new OptionItem(mContext, 233 R.string.action_decrease_width, 234 R.drawable.ic_widget_width_decrease, 235 IGNORE, 236 v -> performResizeAction(R.string.action_decrease_width, host, info))); 237 } 238 } 239 240 if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { 241 if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) || 242 layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) { 243 actions.add(new OptionItem(mContext, 244 R.string.action_increase_height, 245 R.drawable.ic_widget_height_increase, 246 IGNORE, 247 v -> performResizeAction(R.string.action_increase_height, host, info))); 248 } 249 250 if (info.spanY > info.minSpanY && info.spanY > 1) { 251 actions.add(new OptionItem(mContext, 252 R.string.action_decrease_height, 253 R.drawable.ic_widget_height_decrease, 254 IGNORE, 255 v -> performResizeAction(R.string.action_decrease_height, host, info))); 256 } 257 } 258 return actions; 259 } 260 performResizeAction(int action, View host, LauncherAppWidgetInfo info)261 private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) { 262 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) host.getLayoutParams(); 263 CellLayout layout = (CellLayout) host.getParent().getParent(); 264 layout.markCellsAsUnoccupiedForView(host); 265 266 if (action == R.string.action_increase_width) { 267 if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) 268 && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) 269 || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) { 270 lp.setCellX(lp.getCellX() - 1); 271 info.cellX --; 272 } 273 lp.cellHSpan ++; 274 info.spanX ++; 275 } else if (action == R.string.action_decrease_width) { 276 lp.cellHSpan --; 277 info.spanX --; 278 } else if (action == R.string.action_increase_height) { 279 if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) { 280 lp.setCellY(lp.getCellY() - 1); 281 info.cellY --; 282 } 283 lp.cellVSpan ++; 284 info.spanY ++; 285 } else if (action == R.string.action_decrease_height) { 286 lp.cellVSpan --; 287 info.spanY --; 288 } 289 290 layout.markCellsAsOccupiedForView(host); 291 WidgetSizes.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mContext, 292 info.spanX, info.spanY); 293 host.requestLayout(); 294 mContext.getModelWriter().updateItemInDatabase(info); 295 announceConfirmation(mContext.getString(R.string.widget_resized, info.spanX, info.spanY)); 296 return true; 297 } 298 announceConfirmation(int resId)299 @Thunk void announceConfirmation(int resId) { 300 announceConfirmation(mContext.getResources().getString(resId)); 301 } 302 303 @Override beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard)304 protected boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) { 305 if (!itemSupportsAccessibleDrag(info)) { 306 return false; 307 } 308 309 mDragInfo = new DragInfo(); 310 mDragInfo.info = info; 311 mDragInfo.item = item; 312 mDragInfo.dragType = DragType.ICON; 313 if (info instanceof FolderInfo) { 314 mDragInfo.dragType = DragType.FOLDER; 315 } else if (info instanceof LauncherAppWidgetInfo) { 316 mDragInfo.dragType = DragType.WIDGET; 317 } 318 319 Rect pos = new Rect(); 320 mContext.getDragLayer().getDescendantRectRelativeToSelf(item, pos); 321 mContext.getDragController().addDragListener(this); 322 323 DragOptions options = new DragOptions(); 324 options.isAccessibleDrag = true; 325 options.isKeyboardDrag = fromKeyboard; 326 options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY()); 327 328 if (fromKeyboard) { 329 KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mContext.getLayoutInflater() 330 .inflate(R.layout.keyboard_drag_and_drop, mContext.getDragLayer(), false); 331 popup.showForIcon(item, info, options); 332 } else { 333 ItemLongClickListener.beginDrag(item, mContext, info, options); 334 } 335 return true; 336 } 337 338 /** 339 * Find empty space on the workspace and returns the screenId. 340 */ findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates)341 protected int findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) { 342 Workspace<?> workspace = mContext.getWorkspace(); 343 IntArray workspaceScreens = workspace.getScreenOrder(); 344 int screenId; 345 346 // First check if there is space on the current screen. 347 int screenIndex = workspace.getCurrentPage(); 348 screenId = workspaceScreens.get(screenIndex); 349 CellLayout layout = (CellLayout) workspace.getPageAt(screenIndex); 350 351 boolean found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); 352 screenIndex = 0; 353 while (!found && screenIndex < workspaceScreens.size()) { 354 screenId = workspaceScreens.get(screenIndex); 355 layout = (CellLayout) workspace.getPageAt(screenIndex); 356 found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); 357 screenIndex++; 358 } 359 360 if (found) { 361 return screenId; 362 } 363 364 workspace.addExtraEmptyScreens(); 365 IntSet emptyScreenIds = workspace.commitExtraEmptyScreens(); 366 if (emptyScreenIds.isEmpty()) { 367 // Couldn't create extra empty screens for some reason (e.g. Workspace is loading) 368 return -1; 369 } 370 371 screenId = emptyScreenIds.getArray().get(0); 372 layout = workspace.getScreenWithId(screenId); 373 found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY); 374 375 if (!found) { 376 Log.wtf(TAG, "Not enough space on an empty screen"); 377 } 378 return screenId; 379 } 380 381 /** 382 * Functionality to add the item {@link ItemInfo} to the workspace 383 * @param item item to be added 384 * @param accessibility true if the first item to be added to the workspace 385 * should be focused for accessibility. 386 * 387 * @return true if the item could be successfully added 388 */ addToWorkspace(ItemInfo item, boolean accessibility)389 public boolean addToWorkspace(ItemInfo item, boolean accessibility) { 390 final int[] coordinates = new int[2]; 391 final int screenId = findSpaceOnWorkspace(item, coordinates); 392 if (screenId == -1) { 393 return false; 394 } 395 mContext.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> { 396 if (item instanceof WorkspaceItemFactory) { 397 WorkspaceItemInfo info = ((WorkspaceItemFactory) item).makeWorkspaceItem(mContext); 398 mContext.getModelWriter().addItemToDatabase(info, 399 LauncherSettings.Favorites.CONTAINER_DESKTOP, 400 screenId, coordinates[0], coordinates[1]); 401 402 mContext.bindItems( 403 Collections.singletonList(info), 404 /* forceAnimateIcons= */ true, 405 /* focusFirstItemForAccessibility= */ accessibility); 406 announceConfirmation(R.string.item_added_to_workspace); 407 } else if (item instanceof PendingAddItemInfo) { 408 PendingAddItemInfo info = (PendingAddItemInfo) item; 409 Workspace<?> workspace = mContext.getWorkspace(); 410 workspace.snapToPage(workspace.getPageIndexForScreenId(screenId)); 411 mContext.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, 412 screenId, coordinates, info.spanX, info.spanY); 413 } else if (item instanceof WorkspaceItemInfo) { 414 WorkspaceItemInfo info = ((WorkspaceItemInfo) item).clone(); 415 mContext.getModelWriter().addItemToDatabase(info, 416 LauncherSettings.Favorites.CONTAINER_DESKTOP, 417 screenId, coordinates[0], coordinates[1]); 418 mContext.bindItems(Collections.singletonList(info), true, accessibility); 419 } else if (item instanceof FolderInfo fi) { 420 mContext.getModelWriter().addItemToDatabase(fi, 421 LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], 422 coordinates[1]); 423 fi.contents.forEach(member -> { 424 mContext.getModelWriter().addItemToDatabase(member, fi.id, -1, -1, -1); 425 }); 426 mContext.bindItems(Collections.singletonList(fi), true, accessibility); 427 } 428 })); 429 return true; 430 } 431 /** 432 * Functionality to move the item {@link ItemInfo} to the workspace 433 * @param item item to be moved 434 * 435 * @return true if the item could be successfully added 436 */ moveToWorkspace(ItemInfo item)437 public boolean moveToWorkspace(ItemInfo item) { 438 Folder folder = Folder.getOpen(mContext); 439 folder.close(true); 440 WorkspaceItemInfo info = (WorkspaceItemInfo) item; 441 folder.getInfo().remove(info, false); 442 443 final int[] coordinates = new int[2]; 444 final int screenId = findSpaceOnWorkspace(item, coordinates); 445 if (screenId == -1) { 446 return false; 447 } 448 mContext.getModelWriter().moveItemInDatabase(info, 449 LauncherSettings.Favorites.CONTAINER_DESKTOP, 450 screenId, coordinates[0], coordinates[1]); 451 452 // Bind the item in next frame so that if a new workspace page was created, 453 // it will get laid out. 454 new Handler().post(() -> { 455 mContext.bindItems(Collections.singletonList(item), true); 456 announceConfirmation(R.string.item_moved); 457 }); 458 return true; 459 } 460 } 461