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