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