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