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