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