1 package com.android.launcher3.accessibility;
2 
3 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
4 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
5 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
6 
7 import static com.android.launcher3.LauncherState.NORMAL;
8 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
9 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
10 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
11 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
12 
13 import android.animation.AnimatorSet;
14 import android.appwidget.AppWidgetProviderInfo;
15 import android.graphics.Point;
16 import android.graphics.Rect;
17 import android.graphics.RectF;
18 import android.os.Handler;
19 import android.util.Log;
20 import android.util.Pair;
21 import android.view.KeyEvent;
22 import android.view.View;
23 import android.view.accessibility.AccessibilityEvent;
24 
25 import androidx.annotation.Nullable;
26 
27 import com.android.launcher3.BubbleTextView;
28 import com.android.launcher3.ButtonDropTarget;
29 import com.android.launcher3.CellLayout;
30 import com.android.launcher3.Launcher;
31 import com.android.launcher3.LauncherSettings;
32 import com.android.launcher3.PendingAddItemInfo;
33 import com.android.launcher3.R;
34 import com.android.launcher3.Workspace;
35 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
36 import com.android.launcher3.dragndrop.DragOptions;
37 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
38 import com.android.launcher3.dragndrop.DragView;
39 import com.android.launcher3.folder.Folder;
40 import com.android.launcher3.keyboard.KeyboardDragAndDropView;
41 import com.android.launcher3.model.data.AppInfo;
42 import com.android.launcher3.model.data.AppPairInfo;
43 import com.android.launcher3.model.data.CollectionInfo;
44 import com.android.launcher3.model.data.FolderInfo;
45 import com.android.launcher3.model.data.ItemInfo;
46 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
47 import com.android.launcher3.model.data.WorkspaceItemFactory;
48 import com.android.launcher3.model.data.WorkspaceItemInfo;
49 import com.android.launcher3.popup.ArrowPopup;
50 import com.android.launcher3.popup.PopupContainerWithArrow;
51 import com.android.launcher3.touch.ItemLongClickListener;
52 import com.android.launcher3.util.IntArray;
53 import com.android.launcher3.util.IntSet;
54 import com.android.launcher3.util.ShortcutUtil;
55 import com.android.launcher3.util.Thunk;
56 import com.android.launcher3.views.BubbleTextHolder;
57 import com.android.launcher3.views.OptionsPopupView;
58 import com.android.launcher3.views.OptionsPopupView.OptionItem;
59 import com.android.launcher3.widget.LauncherAppWidgetHostView;
60 import com.android.launcher3.widget.PendingAddWidgetInfo;
61 import com.android.launcher3.widget.util.WidgetSizes;
62 
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.List;
66 import java.util.function.Consumer;
67 
68 public class LauncherAccessibilityDelegate extends BaseAccessibilityDelegate<Launcher> {
69 
70     private static final String TAG = "LauncherAccessibilityDelegate";
71 
72     public static final int REMOVE = R.id.action_remove;
73     public static final int UNINSTALL = R.id.action_uninstall;
74     public static final int DISMISS_PREDICTION = R.id.action_dismiss_prediction;
75     public static final int PIN_PREDICTION = R.id.action_pin_prediction;
76     public static final int RECONFIGURE = R.id.action_reconfigure;
77     public static final int INVALID = -1;
78     protected static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace;
79     protected static final int MOVE = R.id.action_move;
80     protected static final int MOVE_TO_WORKSPACE = R.id.action_move_to_workspace;
81     protected static final int RESIZE = R.id.action_resize;
82     public static final int DEEP_SHORTCUTS = R.id.action_deep_shortcuts;
83 
LauncherAccessibilityDelegate(Launcher launcher)84     public LauncherAccessibilityDelegate(Launcher launcher) {
85         super(launcher);
86 
87         mActions.put(REMOVE, new LauncherAction(
88                 REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X));
89         mActions.put(UNINSTALL, new LauncherAction(
90                 UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U));
91         mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION,
92                 R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X));
93         mActions.put(RECONFIGURE, new LauncherAction(
94                 RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E));
95         mActions.put(ADD_TO_WORKSPACE, new LauncherAction(
96                 ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P));
97         mActions.put(MOVE, new LauncherAction(
98                 MOVE, R.string.action_move, KeyEvent.KEYCODE_M));
99         mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE,
100                 R.string.action_move_to_workspace, KeyEvent.KEYCODE_P));
101         mActions.put(RESIZE, new LauncherAction(
102                 RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R));
103         mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS,
104                 R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
105     }
106 
107     @Override
getSupportedActions(View host, ItemInfo item, List<LauncherAction> out)108     protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
109         // If the request came from keyboard, do not add custom shortcuts as that is already
110         // exposed as a direct shortcut
111         if (ShortcutUtil.supportsShortcuts(item)) {
112             out.add(mActions.get(DEEP_SHORTCUTS));
113         }
114 
115         for (ButtonDropTarget target : mContext.getDropTargetBar().getDropTargets()) {
116             if (target.supportsAccessibilityDrop(item, host)) {
117                 out.add(mActions.get(target.getAccessibilityAction()));
118             }
119         }
120 
121         // Do not add move actions for keyboard request as this uses virtual nodes.
122         if (itemSupportsAccessibleDrag(item)) {
123             out.add(mActions.get(MOVE));
124 
125             if (item.container >= 0) {
126                 out.add(mActions.get(MOVE_TO_WORKSPACE));
127             } else if (item instanceof LauncherAppWidgetInfo) {
128                 if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) {
129                     out.add(mActions.get(RESIZE));
130                 }
131             }
132         }
133 
134         if (supportAddToWorkSpace(item)) {
135             out.add(mActions.get(ADD_TO_WORKSPACE));
136         }
137     }
138 
supportAddToWorkSpace(ItemInfo item)139     private boolean supportAddToWorkSpace(ItemInfo item) {
140         return ((item instanceof AppInfo)
141                     && (((AppInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0)
142                 || ((item instanceof WorkspaceItemInfo)
143                     && (((WorkspaceItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0)
144                 || ((item instanceof PendingAddItemInfo)
145                     && (((PendingAddItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0);
146     }
147 
148     /**
149      * Returns all the accessibility actions that can be handled by the host.
150      */
getSupportedActions(Launcher launcher, View host)151     public static List<LauncherAction> getSupportedActions(Launcher launcher, View host) {
152         if (host == null || !(host.getTag() instanceof  ItemInfo)) {
153             return Collections.emptyList();
154         }
155         PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
156         LauncherAccessibilityDelegate delegate = container != null
157                 ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate();
158         List<LauncherAction> result = new ArrayList<>();
159         delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result);
160         return result;
161     }
162 
163     @Override
performAction(final View host, final ItemInfo item, int action, boolean fromKeyboard)164     protected boolean performAction(final View host, final ItemInfo item, int action,
165             boolean fromKeyboard) {
166         if (action == ACTION_LONG_CLICK) {
167             PreDragCondition dragCondition = null;
168             // Long press should be consumed for workspace items, and it should invoke the
169             // Shortcuts / Notifications / Actions pop-up menu, and not start a drag as the
170             // standard long press path does.
171             if (host instanceof BubbleTextView) {
172                 dragCondition = ((BubbleTextView) host).startLongPressAction();
173             } else if (host instanceof BubbleTextHolder) {
174                 BubbleTextHolder holder = (BubbleTextHolder) host;
175                 dragCondition = holder.getBubbleText() == null ? null
176                         : holder.getBubbleText().startLongPressAction();
177             }
178             return dragCondition != null;
179         } else if (action == MOVE) {
180             return beginAccessibleDrag(host, item, fromKeyboard);
181         } else if (action == ADD_TO_WORKSPACE) {
182             return addToWorkspace(item, true /*accessibility*/, null /*finishCallback*/);
183         } else if (action == MOVE_TO_WORKSPACE) {
184             return moveToWorkspace(item);
185         } else if (action == RESIZE) {
186             final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
187             List<OptionItem> actions = getSupportedResizeActions(host, info);
188             Rect pos = new Rect();
189             mContext.getDragLayer().getDescendantRectRelativeToSelf(host, pos);
190             ArrowPopup popup = OptionsPopupView.show(mContext, new RectF(pos), actions, false);
191             popup.requestFocus();
192             popup.addOnCloseCallback(() -> {
193                 host.requestFocus();
194                 host.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
195                 host.performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
196             });
197             return true;
198         } else if (action == DEEP_SHORTCUTS) {
199             BubbleTextView btv = host instanceof BubbleTextView ? (BubbleTextView) host
200                     : (host instanceof BubbleTextHolder
201                             ? ((BubbleTextHolder) host).getBubbleText() : null);
202             return btv != null && PopupContainerWithArrow.showForIcon(btv) != null;
203         } else {
204             for (ButtonDropTarget dropTarget : mContext.getDropTargetBar().getDropTargets()) {
205                 if (dropTarget.supportsAccessibilityDrop(item, host)
206                         && action == dropTarget.getAccessibilityAction()) {
207                     dropTarget.onAccessibilityDrop(host, item);
208                     return true;
209                 }
210             }
211         }
212         return false;
213     }
214 
getSupportedResizeActions(View host, LauncherAppWidgetInfo info)215     private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
216         List<OptionItem> actions = new ArrayList<>();
217         AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
218         if (providerInfo == null) {
219             return actions;
220         }
221 
222         CellLayout layout;
223         if (host.getParent() instanceof DragView) {
224             layout = (CellLayout) ((DragView) host.getParent()).getContentViewParent().getParent();
225         } else {
226             layout = (CellLayout) host.getParent().getParent();
227         }
228         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) {
229             if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) ||
230                     layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) {
231                 actions.add(new OptionItem(mContext,
232                         R.string.action_increase_width,
233                         R.drawable.ic_widget_width_increase,
234                         IGNORE,
235                         v -> performResizeAction(R.string.action_increase_width, host, info)));
236             }
237 
238             if (info.spanX > info.minSpanX && info.spanX > 1) {
239                 actions.add(new OptionItem(mContext,
240                         R.string.action_decrease_width,
241                         R.drawable.ic_widget_width_decrease,
242                         IGNORE,
243                         v -> performResizeAction(R.string.action_decrease_width, host, info)));
244             }
245         }
246 
247         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) {
248             if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) ||
249                     layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) {
250                 actions.add(new OptionItem(mContext,
251                         R.string.action_increase_height,
252                         R.drawable.ic_widget_height_increase,
253                         IGNORE,
254                         v -> performResizeAction(R.string.action_increase_height, host, info)));
255             }
256 
257             if (info.spanY > info.minSpanY && info.spanY > 1) {
258                 actions.add(new OptionItem(mContext,
259                         R.string.action_decrease_height,
260                         R.drawable.ic_widget_height_decrease,
261                         IGNORE,
262                         v -> performResizeAction(R.string.action_decrease_height, host, info)));
263             }
264         }
265         return actions;
266     }
267 
performResizeAction(int action, View host, LauncherAppWidgetInfo info)268     private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
269         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) host.getLayoutParams();
270         CellLayout layout = (CellLayout) host.getParent().getParent();
271         layout.markCellsAsUnoccupiedForView(host);
272 
273         if (action == R.string.action_increase_width) {
274             if (((host.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL)
275                     && layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY))
276                     || !layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY)) {
277                 lp.setCellX(lp.getCellX() - 1);
278                 info.cellX --;
279             }
280             lp.cellHSpan ++;
281             info.spanX ++;
282         } else if (action == R.string.action_decrease_width) {
283             lp.cellHSpan --;
284             info.spanX --;
285         } else if (action == R.string.action_increase_height) {
286             if (!layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1)) {
287                 lp.setCellY(lp.getCellY() - 1);
288                 info.cellY --;
289             }
290             lp.cellVSpan ++;
291             info.spanY ++;
292         } else if (action == R.string.action_decrease_height) {
293             lp.cellVSpan --;
294             info.spanY --;
295         }
296 
297         layout.markCellsAsOccupiedForView(host);
298         WidgetSizes.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mContext,
299                 info.spanX, info.spanY);
300         host.requestLayout();
301         mContext.getModelWriter().updateItemInDatabase(info);
302         announceConfirmation(mContext.getString(R.string.widget_resized, info.spanX, info.spanY));
303         return true;
304     }
305 
announceConfirmation(int resId)306     @Thunk void announceConfirmation(int resId) {
307         announceConfirmation(mContext.getResources().getString(resId));
308     }
309 
310     @Override
beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard)311     protected boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) {
312         if (!itemSupportsAccessibleDrag(info)) {
313             return false;
314         }
315 
316         mDragInfo = new DragInfo();
317         mDragInfo.info = info;
318         mDragInfo.item = item;
319         mDragInfo.dragType = DragType.ICON;
320         if (info instanceof FolderInfo) {
321             mDragInfo.dragType = DragType.FOLDER;
322         } else if (info instanceof AppPairInfo) {
323             mDragInfo.dragType = DragType.APP_PAIR;
324         } else if (info instanceof LauncherAppWidgetInfo) {
325             mDragInfo.dragType = DragType.WIDGET;
326         }
327 
328         Rect pos = new Rect();
329         mContext.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
330         mContext.getDragController().addDragListener(this);
331 
332         DragOptions options = new DragOptions();
333         options.isAccessibleDrag = true;
334         options.isKeyboardDrag = fromKeyboard;
335         options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
336 
337         if (fromKeyboard) {
338             KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mContext.getLayoutInflater()
339                     .inflate(R.layout.keyboard_drag_and_drop, mContext.getDragLayer(), false);
340             popup.showForIcon(item, info, options);
341         } else {
342             ItemLongClickListener.beginDrag(item, mContext, info, options);
343         }
344         return true;
345     }
346 
347     /**
348      * Find empty space on the workspace and returns the screenId.
349      */
findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates)350     protected int findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) {
351         Workspace<?> workspace = mContext.getWorkspace();
352         IntArray workspaceScreens = workspace.getScreenOrder();
353         int screenId;
354 
355         // First check if there is space on the current screen.
356         int screenIndex = workspace.getCurrentPage();
357         screenId = workspaceScreens.get(screenIndex);
358         CellLayout layout = (CellLayout) workspace.getPageAt(screenIndex);
359 
360         boolean found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
361         screenIndex = 0;
362         while (!found && screenIndex < workspaceScreens.size()) {
363             screenId = workspaceScreens.get(screenIndex);
364             layout = (CellLayout) workspace.getPageAt(screenIndex);
365             found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
366             screenIndex++;
367         }
368 
369         if (found) {
370             return screenId;
371         }
372 
373         workspace.addExtraEmptyScreens();
374         IntSet emptyScreenIds = workspace.commitExtraEmptyScreens();
375         if (emptyScreenIds.isEmpty()) {
376             // Couldn't create extra empty screens for some reason (e.g. Workspace is loading)
377             return -1;
378         }
379 
380         screenId = emptyScreenIds.getArray().get(0);
381         layout = workspace.getScreenWithId(screenId);
382         found = layout.findCellForSpan(outCoordinates, info.spanX, info.spanY);
383 
384         if (!found) {
385             Log.wtf(TAG, "Not enough space on an empty screen");
386         }
387         return screenId;
388     }
389 
390     /**
391      * Functionality to add the item {@link ItemInfo} to the workspace
392      * @param item item to be added
393      * @param accessibility true if the first item to be added to the workspace
394      *     should be focused for accessibility.
395      * @param finishCallback Callback which will be run after this item has been added
396      *                       and the view has been transitioned to the workspace, or on failure.
397      *
398      * @return true if the item could be successfully added
399      */
addToWorkspace(ItemInfo item, boolean accessibility, @Nullable Consumer<Boolean> finishCallback)400     public boolean addToWorkspace(ItemInfo item, boolean accessibility,
401             @Nullable Consumer<Boolean> finishCallback) {
402         final int[] coordinates = new int[2];
403         final int screenId = findSpaceOnWorkspace(item, coordinates);
404         if (screenId == -1) {
405             if (finishCallback != null) {
406                 finishCallback.accept(false /*success*/);
407             }
408             return false;
409         }
410         mContext.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
411             if (item instanceof WorkspaceItemFactory) {
412                 WorkspaceItemInfo info = ((WorkspaceItemFactory) item).makeWorkspaceItem(mContext);
413                 mContext.getModelWriter().addItemToDatabase(info,
414                         LauncherSettings.Favorites.CONTAINER_DESKTOP,
415                         screenId, coordinates[0], coordinates[1]);
416 
417                 bindItem(info, accessibility, finishCallback);
418                 announceConfirmation(R.string.item_added_to_workspace);
419             } else if (item instanceof PendingAddItemInfo) {
420                 PendingAddItemInfo info = (PendingAddItemInfo) item;
421                 if (info instanceof PendingAddWidgetInfo widgetInfo
422                         && widgetInfo.bindOptions == null) {
423                     widgetInfo.bindOptions = widgetInfo.getDefaultSizeOptions(mContext);
424                 }
425                 Workspace<?> workspace = mContext.getWorkspace();
426                 workspace.post(() -> {
427                     workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
428                     workspace.setOnPageTransitionEndCallback(() -> {
429                         mContext.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP,
430                                 screenId, coordinates, info.spanX, info.spanY);
431                         if (finishCallback != null) {
432                             finishCallback.accept(/* success= */ true);
433                         }
434                     });
435                 });
436             } else if (item instanceof WorkspaceItemInfo) {
437                 WorkspaceItemInfo info = ((WorkspaceItemInfo) item).clone();
438                 mContext.getModelWriter().addItemToDatabase(info,
439                         LauncherSettings.Favorites.CONTAINER_DESKTOP,
440                         screenId, coordinates[0], coordinates[1]);
441                 bindItem(info, accessibility, finishCallback);
442             } else if (item instanceof CollectionInfo ci) {
443                 Workspace<?> workspace = mContext.getWorkspace();
444                 workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
445                 mContext.getModelWriter().addItemToDatabase(ci,
446                         LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0],
447                         coordinates[1]);
448                 ci.getContents().forEach(member ->
449                         mContext.getModelWriter()
450                                 .addItemToDatabase(member, ci.id, -1, -1, -1));
451                 bindItem(ci, accessibility, finishCallback);
452             }
453         }));
454         return true;
455     }
456 
bindItem(ItemInfo item, boolean focusForAccessibility, @Nullable Consumer<Boolean> finishCallback)457     private void bindItem(ItemInfo item, boolean focusForAccessibility,
458             @Nullable Consumer<Boolean> finishCallback) {
459         View view = mContext.getItemInflater().inflateItem(item, mContext.getModelWriter());
460         if (view == null) {
461             if (finishCallback != null) {
462                 finishCallback.accept(false /*success*/);
463             }
464             return;
465         }
466         AnimatorSet anim = new AnimatorSet();
467         anim.addListener(forEndCallback((success) -> {
468             if (focusForAccessibility) {
469                 view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
470             }
471             if (finishCallback != null) {
472                 finishCallback.accept(success);
473             }
474         }));
475         mContext.bindInflatedItems(Collections.singletonList(Pair.create(item, view)), anim);
476     }
477 
478     /**
479      * Functionality to move the item {@link ItemInfo} to the workspace
480      * @param item item to be moved
481      *
482      * @return true if the item could be successfully added
483      */
moveToWorkspace(ItemInfo item)484     public boolean moveToWorkspace(ItemInfo item) {
485         Folder folder = Folder.getOpen(mContext);
486         folder.close(true);
487         WorkspaceItemInfo info = (WorkspaceItemInfo) item;
488         folder.getInfo().remove(info, false);
489 
490         final int[] coordinates = new int[2];
491         final int screenId = findSpaceOnWorkspace(item, coordinates);
492         if (screenId == -1) {
493             return false;
494         }
495         mContext.getModelWriter().moveItemInDatabase(info,
496                 LauncherSettings.Favorites.CONTAINER_DESKTOP,
497                 screenId, coordinates[0], coordinates[1]);
498 
499         // Bind the item in next frame so that if a new workspace page was created,
500         // it will get laid out.
501         new Handler().post(() -> {
502             mContext.bindItems(Collections.singletonList(item), true);
503             announceConfirmation(R.string.item_moved);
504         });
505         return true;
506     }
507 }
508