Home | History | Annotate | Download | only in accessibility
      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