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