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