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