1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3;
18 
19 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
20 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
21 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
23 import static com.android.launcher3.LauncherState.ALL_APPS;
24 import static com.android.launcher3.LauncherState.EDIT_MODE;
25 import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
26 import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED;
27 import static com.android.launcher3.LauncherState.FLAG_WORKSPACE_INACCESSIBLE;
28 import static com.android.launcher3.LauncherState.HINT_STATE;
29 import static com.android.launcher3.LauncherState.NORMAL;
30 import static com.android.launcher3.LauncherState.SPRING_LOADED;
31 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
32 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
33 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
34 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
35 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
36 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
37 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
38 
39 import android.animation.Animator;
40 import android.animation.AnimatorListenerAdapter;
41 import android.animation.LayoutTransition;
42 import android.animation.ValueAnimator;
43 import android.animation.ValueAnimator.AnimatorUpdateListener;
44 import android.annotation.SuppressLint;
45 import android.app.WallpaperManager;
46 import android.appwidget.AppWidgetHostView;
47 import android.appwidget.AppWidgetProviderInfo;
48 import android.content.Context;
49 import android.content.res.Resources;
50 import android.graphics.Bitmap;
51 import android.graphics.Point;
52 import android.graphics.PointF;
53 import android.graphics.Rect;
54 import android.graphics.drawable.Drawable;
55 import android.os.Handler;
56 import android.os.Message;
57 import android.os.Parcelable;
58 import android.util.AttributeSet;
59 import android.util.Log;
60 import android.util.SparseArray;
61 import android.view.Gravity;
62 import android.view.LayoutInflater;
63 import android.view.MotionEvent;
64 import android.view.View;
65 import android.view.ViewGroup;
66 import android.view.accessibility.AccessibilityNodeInfo;
67 import android.widget.FrameLayout;
68 import android.widget.Toast;
69 
70 import androidx.annotation.Nullable;
71 import androidx.annotation.VisibleForTesting;
72 
73 import com.android.app.animation.Interpolators;
74 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
75 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
76 import com.android.launcher3.anim.PendingAnimation;
77 import com.android.launcher3.apppairs.AppPairIcon;
78 import com.android.launcher3.celllayout.CellInfo;
79 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
80 import com.android.launcher3.celllayout.CellPosMapper;
81 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
82 import com.android.launcher3.config.FeatureFlags;
83 import com.android.launcher3.dot.FolderDotInfo;
84 import com.android.launcher3.dragndrop.DragController;
85 import com.android.launcher3.dragndrop.DragLayer;
86 import com.android.launcher3.dragndrop.DragOptions;
87 import com.android.launcher3.dragndrop.DragView;
88 import com.android.launcher3.dragndrop.DraggableView;
89 import com.android.launcher3.dragndrop.SpringLoadedDragController;
90 import com.android.launcher3.folder.Folder;
91 import com.android.launcher3.folder.FolderIcon;
92 import com.android.launcher3.folder.PreviewBackground;
93 import com.android.launcher3.graphics.DragPreviewProvider;
94 import com.android.launcher3.icons.BitmapRenderer;
95 import com.android.launcher3.icons.FastBitmapDrawable;
96 import com.android.launcher3.logger.LauncherAtom;
97 import com.android.launcher3.logging.InstanceId;
98 import com.android.launcher3.logging.StatsLogManager;
99 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
100 import com.android.launcher3.model.data.AppPairInfo;
101 import com.android.launcher3.model.data.FolderInfo;
102 import com.android.launcher3.model.data.ItemInfo;
103 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
104 import com.android.launcher3.model.data.WorkspaceItemInfo;
105 import com.android.launcher3.pageindicators.PageIndicator;
106 import com.android.launcher3.statemanager.StateManager;
107 import com.android.launcher3.statemanager.StateManager.StateHandler;
108 import com.android.launcher3.states.StateAnimationConfig;
109 import com.android.launcher3.touch.WorkspaceTouchListener;
110 import com.android.launcher3.util.EdgeEffectCompat;
111 import com.android.launcher3.util.Executors;
112 import com.android.launcher3.util.IntArray;
113 import com.android.launcher3.util.IntSet;
114 import com.android.launcher3.util.IntSparseArrayMap;
115 import com.android.launcher3.util.LauncherBindableItemsContainer;
116 import com.android.launcher3.util.OverlayEdgeEffect;
117 import com.android.launcher3.util.PackageUserKey;
118 import com.android.launcher3.util.RunnableList;
119 import com.android.launcher3.util.Thunk;
120 import com.android.launcher3.util.WallpaperOffsetInterpolator;
121 import com.android.launcher3.widget.LauncherAppWidgetHostView;
122 import com.android.launcher3.widget.LauncherWidgetHolder;
123 import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
124 import com.android.launcher3.widget.NavigableAppWidgetHostView;
125 import com.android.launcher3.widget.PendingAddShortcutInfo;
126 import com.android.launcher3.widget.PendingAddWidgetInfo;
127 import com.android.launcher3.widget.PendingAppWidgetHostView;
128 import com.android.launcher3.widget.WidgetManagerHelper;
129 import com.android.launcher3.widget.util.WidgetSizes;
130 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
131 import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayTouchProxy;
132 
133 import java.util.ArrayList;
134 import java.util.Iterator;
135 import java.util.List;
136 import java.util.function.Consumer;
137 import java.util.function.Predicate;
138 import java.util.stream.Collectors;
139 
140 /**
141  * The workspace is a wide area with a wallpaper and a finite number of pages.
142  * Each page contains a number of icons, folders or widgets the user can
143  * interact with. A workspace is meant to be used with a fixed width only.
144  *
145  * @param <T> Class that extends View and PageIndicator
146  */
147 public class Workspace<T extends View & PageIndicator> extends PagedView<T>
148         implements DropTarget, DragSource, View.OnTouchListener, CellLayoutContainer,
149         DragController.DragListener, Insettable, StateHandler<LauncherState>,
150         WorkspaceLayoutManager, LauncherBindableItemsContainer, LauncherOverlayCallbacks {
151 
152     /**
153      * The value that {@link #mTransitionProgress} must be greater than for
154      * {@link #transitionStateShouldAllowDrop()} to return true.
155      */
156     private static final float ALLOW_DROP_TRANSITION_PROGRESS = 0.25f;
157 
158     /**
159      * The value that {@link #mTransitionProgress} must be greater than for
160      * {@link #isFinishedSwitchingState()} ()} to return true.
161      */
162     private static final float FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS = 0.5f;
163 
164     private static final float SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE = 0.15f;
165 
166     private static final boolean ENFORCE_DRAG_EVENT_ORDER = false;
167 
168     private static final int ADJACENT_SCREEN_DROP_DURATION = 300;
169 
170     public static final int DEFAULT_PAGE = 0;
171 
172     private final int mAllAppsIconSize;
173 
174     private LayoutTransition mLayoutTransition;
175     @Thunk
176     final WallpaperManager mWallpaperManager;
177 
178     protected ShortcutAndWidgetContainer mDragSourceInternal;
179 
180     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
181     @Thunk
182     public final IntSparseArrayMap<CellLayout> mWorkspaceScreens = new IntSparseArrayMap<>();
183 
184     @Thunk
185     final IntArray mScreenOrder = new IntArray();
186 
187     @Thunk
188     boolean mDeferRemoveExtraEmptyScreen = false;
189 
190     /**
191      * CellInfo for the cell that is currently being dragged
192      */
193     protected CellInfo mDragInfo;
194 
195     /**
196      * Target drop area calculated during last acceptDrop call.
197      */
198     @Thunk
199     int[] mTargetCell = new int[2];
200     private int mDragOverX = -1;
201     private int mDragOverY = -1;
202 
203     /**
204      * The CellLayout that is currently being dragged over
205      */
206     @Thunk
207     CellLayout mDragTargetLayout = null;
208     /**
209      * The CellLayout that we will show as highlighted
210      */
211     private CellLayout mDragOverlappingLayout = null;
212 
213     /**
214      * The CellLayout which will be dropped to
215      */
216     private CellLayout mDropToLayout = null;
217 
218     @Thunk
219     final Launcher mLauncher;
220     @Thunk
221     DragController mDragController;
222 
223     protected final int[] mTempXY = new int[2];
224     private final float[] mTempFXY = new float[2];
225     private final Rect mTempRect = new Rect();
226     @Thunk
227     float[] mDragViewVisualCenter = new float[2];
228 
229     private SpringLoadedDragController mSpringLoadedDragController;
230 
231     private boolean mIsSwitchingState = false;
232 
233     boolean mChildrenLayersEnabled = true;
234 
235     private boolean mStripScreensOnPageStopMoving = false;
236 
237     private boolean mWorkspaceFadeInAdjacentScreens;
238 
239     final WallpaperOffsetInterpolator mWallpaperOffset;
240     private boolean mUnlockWallpaperFromDefaultPageOnLayout;
241 
242     public static final int REORDER_TIMEOUT = 650;
243     protected final Alarm mReorderAlarm = new Alarm();
244     private PreviewBackground mFolderCreateBg;
245     /** The underlying view that we are dragging something over. */
246     private View mDragOverView = null;
247     private FolderIcon mDragOverFolderIcon = null;
248     private boolean mCreateUserFolderOnDrop = false;
249     private boolean mAddToExistingFolderOnDrop = false;
250 
251     // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget)
252     private float mXDown;
253     private float mYDown;
254     private View mFirstPagePinnedItem;
255     private boolean mIsEventOverFirstPagePinnedItem;
256 
257     final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
258     final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
259     final static float TOUCH_SLOP_DAMPING_FACTOR = 4;
260 
261     // Relating to the animation of items being dropped externally
262     public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0;
263     public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1;
264     public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2;
265     public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3;
266     public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4;
267 
268     // Related to dragging, folder creation and reordering
269     private static final int DRAG_MODE_NONE = 0;
270     private static final int DRAG_MODE_CREATE_FOLDER = 1;
271     private static final int DRAG_MODE_ADD_TO_FOLDER = 2;
272     private static final int DRAG_MODE_REORDER = 3;
273     protected int mDragMode = DRAG_MODE_NONE;
274     @Thunk
275     int mLastReorderX = -1;
276     @Thunk
277     int mLastReorderY = -1;
278 
279     private SparseArray<Parcelable> mSavedStates;
280     private final IntArray mRestoredPages = new IntArray();
281 
282     private float mCurrentScale;
283     private float mTransitionProgress;
284 
285     // State related to Launcher Overlay
286     private OverlayEdgeEffect mOverlayEdgeEffect;
287     private boolean mOverlayShown = false;
288     private float mOverlayProgress; // 1 -> overlay completely visible, 0 -> home visible
289     private final List<LauncherOverlayCallbacks> mOverlayCallbacks = new ArrayList<>();
290 
291     private boolean mForceDrawAdjacentPages = false;
292 
293     // Handles workspace state transitions
294     private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
295 
296     private final StatsLogManager mStatsLogManager;
297 
298     /**
299      * Used to inflate the Workspace from XML.
300      *
301      * @param context The application's context.
302      * @param attrs   The attributes set containing the Workspace's customization values.
303      */
Workspace(Context context, AttributeSet attrs)304     public Workspace(Context context, AttributeSet attrs) {
305         this(context, attrs, 0);
306     }
307 
308     /**
309      * Used to inflate the Workspace from XML.
310      *
311      * @param context  The application's context.
312      * @param attrs    The attributes set containing the Workspace's customization values.
313      * @param defStyle Unused.
314      */
Workspace(Context context, AttributeSet attrs, int defStyle)315     public Workspace(Context context, AttributeSet attrs, int defStyle) {
316         super(context, attrs, defStyle);
317 
318         mLauncher = Launcher.getLauncher(context);
319         mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
320         mWallpaperManager = WallpaperManager.getInstance(context);
321         mAllAppsIconSize = mLauncher.getDeviceProfile().allAppsIconSizePx;
322         mWallpaperOffset = new WallpaperOffsetInterpolator(this);
323 
324         setHapticFeedbackEnabled(false);
325         initWorkspace();
326 
327         // Disable multitouch across the workspace/all apps/customize tray
328         setMotionEventSplittingEnabled(true);
329         setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
330         mStatsLogManager = StatsLogManager.newInstance(context);
331     }
332 
333     @Override
setInsets(Rect insets)334     public void setInsets(Rect insets) {
335         DeviceProfile grid = mLauncher.getDeviceProfile();
336 
337         mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
338 
339         Rect padding = grid.workspacePadding;
340         setPadding(padding.left, padding.top, padding.right, padding.bottom);
341         mInsets.set(insets);
342 
343         if (mWorkspaceFadeInAdjacentScreens) {
344             // In landscape mode the page spacing is set to the default.
345             setPageSpacing(grid.edgeMarginPx);
346         } else {
347             // In portrait, we want the pages spaced such that there is no
348             // overhang of the previous / next page into the current page viewport.
349             // We assume symmetrical padding in portrait mode.
350             int maxInsets = Math.max(insets.left, insets.right);
351             int maxPadding = Math.max(grid.edgeMarginPx, padding.left + 1);
352             setPageSpacing(Math.max(maxInsets, maxPadding));
353         }
354 
355         updateCellLayoutMeasures();
356         updateWorkspaceWidgetsSizes();
357         setPageIndicatorInset();
358     }
359 
setPageIndicatorInset()360     private void setPageIndicatorInset() {
361         DeviceProfile grid = mLauncher.getDeviceProfile();
362 
363         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPageIndicator.getLayoutParams();
364 
365         // Set insets for page indicator
366         Rect padding = grid.workspacePadding;
367         if (grid.isVerticalBarLayout()) {
368             lp.leftMargin = padding.left + grid.workspaceCellPaddingXPx;
369             lp.rightMargin = padding.right + grid.workspaceCellPaddingXPx;
370             lp.bottomMargin = padding.bottom;
371         } else {
372             lp.leftMargin = lp.rightMargin = 0;
373             lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
374             lp.bottomMargin = grid.hotseatBarSizePx;
375         }
376         mPageIndicator.setLayoutParams(lp);
377     }
378 
updateCellLayoutMeasures()379     private void updateCellLayoutMeasures() {
380         Rect padding = mLauncher.getDeviceProfile().cellLayoutPaddingPx;
381         mWorkspaceScreens.forEach(cellLayout -> {
382             cellLayout.setPadding(padding.left, padding.top, padding.right, padding.bottom);
383             cellLayout.setSpaceBetweenCellLayoutsPx(getPageSpacing() / 4);
384         });
385     }
386 
updateWorkspaceWidgetsSizes()387     private void updateWorkspaceWidgetsSizes() {
388         int numberOfScreens = mScreenOrder.size();
389         for (int i = 0; i < numberOfScreens; i++) {
390             ShortcutAndWidgetContainer shortcutAndWidgetContainer =
391                     mWorkspaceScreens.get(mScreenOrder.get(i)).getShortcutsAndWidgets();
392             int shortcutsAndWidgetCount = shortcutAndWidgetContainer.getChildCount();
393             for (int j = 0; j < shortcutsAndWidgetCount; j++) {
394                 View view = shortcutAndWidgetContainer.getChildAt(j);
395                 if (view instanceof LauncherAppWidgetHostView
396                         && view.getTag() instanceof LauncherAppWidgetInfo) {
397                     LauncherAppWidgetInfo launcherAppWidgetInfo =
398                             (LauncherAppWidgetInfo) view.getTag();
399                     WidgetSizes.updateWidgetSizeRanges((LauncherAppWidgetHostView) view,
400                             mLauncher, launcherAppWidgetInfo.spanX, launcherAppWidgetInfo.spanY);
401                 }
402             }
403         }
404     }
405 
406     /**
407      * Estimates the size of an item using spans: hSpan, vSpan.
408      *
409      * @return MAX_VALUE for each dimension if unsuccessful.
410      */
estimateItemSize(ItemInfo itemInfo)411     public int[] estimateItemSize(ItemInfo itemInfo) {
412         int[] size = new int[2];
413         if (getChildCount() > 0) {
414             // Use the first page to estimate the child position
415             CellLayout cl = (CellLayout) getChildAt(0);
416             boolean isWidget = itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
417 
418             Rect r = estimateItemPosition(cl, 0, 0, itemInfo.spanX, itemInfo.spanY);
419 
420             float scale = 1;
421             if (isWidget) {
422                 DeviceProfile profile = mLauncher.getDeviceProfile();
423                 final PointF appWidgetScale = profile.getAppWidgetScale(null);
424                 scale = Utilities.shrinkRect(r, appWidgetScale.x, appWidgetScale.y);
425             }
426             size[0] = r.width();
427             size[1] = r.height();
428 
429             if (isWidget) {
430                 size[0] /= scale;
431                 size[1] /= scale;
432             }
433             return size;
434         } else {
435             size[0] = Integer.MAX_VALUE;
436             size[1] = Integer.MAX_VALUE;
437             return size;
438         }
439     }
440 
getWallpaperOffsetForCenterPage()441     public float getWallpaperOffsetForCenterPage() {
442         return getWallpaperOffsetForPage(getPageNearestToCenterOfScreen());
443     }
444 
getWallpaperOffsetForPage(int page)445     private float getWallpaperOffsetForPage(int page) {
446         int pageScroll = getScrollForPage(page);
447         return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
448     }
449 
450     /**
451      * Returns the number of pages used for the wallpaper parallax.
452      */
getNumPagesForWallpaperParallax()453     public int getNumPagesForWallpaperParallax() {
454         return mWallpaperOffset.getNumPagesForWallpaperParallax();
455     }
456 
estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan)457     public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) {
458         Rect r = new Rect();
459         cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
460         return r;
461     }
462 
463     @Override
onDragStart(DragObject dragObject, DragOptions options)464     public void onDragStart(DragObject dragObject, DragOptions options) {
465         if (ENFORCE_DRAG_EVENT_ORDER) {
466             enforceDragParity("onDragStart", 0, 0);
467         }
468 
469         if (mDragInfo != null && mDragInfo.cell != null) {
470             CellLayout layout = (CellLayout) (mDragInfo.cell instanceof LauncherAppWidgetHostView
471                     ? dragObject.dragView.getContentViewParent().getParent()
472                     : mDragInfo.cell.getParent().getParent());
473             layout.markCellsAsUnoccupiedForView(mDragInfo.cell);
474         }
475 
476         updateChildrenLayersEnabled();
477 
478         // Do not add a new page if it is a accessible drag which was not started by the workspace.
479         // We do not support accessibility drag from other sources and instead provide a direct
480         // action for move/add to homescreen.
481         // When a accessible drag is started by the folder, we only allow rearranging withing the
482         // folder.
483         boolean addNewPage = !(options.isAccessibleDrag && dragObject.dragSource != this);
484         if (addNewPage) {
485             mDeferRemoveExtraEmptyScreen = false;
486             addExtraEmptyScreenOnDrag(dragObject);
487 
488             if (dragObject.dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
489                     && dragObject.dragSource != this) {
490                 // When dragging a widget from different source, move to a page which has
491                 // enough space to place this widget (after rearranging/resizing). We special case
492                 // widgets as they cannot be placed inside a folder.
493                 // Start at the current page and search right (on LTR) until finding a page with
494                 // enough space. Since an empty screen is the furthest right, a page must be found.
495                 int currentPage = getDestinationPage();
496                 for (int pageIndex = currentPage; pageIndex < getPageCount(); pageIndex++) {
497                     CellLayout page = (CellLayout) getPageAt(pageIndex);
498                     if (page.hasReorderSolution(dragObject.dragInfo)) {
499                         setCurrentPage(pageIndex);
500                         break;
501                     }
502                 }
503             }
504         }
505 
506         if (!mLauncher.isInState(EDIT_MODE)) {
507             mLauncher.getStateManager().goToState(SPRING_LOADED);
508         }
509         mStatsLogManager.logger().withItemInfo(dragObject.dragInfo)
510                 .withInstanceId(dragObject.logInstanceId)
511                 .log(LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
512     }
513 
isTwoPanelEnabled()514     private boolean isTwoPanelEnabled() {
515         return !FOLDABLE_SINGLE_PAGE.get() && mLauncher.mDeviceProfile.isTwoPanels;
516     }
517 
deferRemoveExtraEmptyScreen()518     public void deferRemoveExtraEmptyScreen() {
519         mDeferRemoveExtraEmptyScreen = true;
520     }
521 
522     @Override
onDragEnd()523     public void onDragEnd() {
524         if (ENFORCE_DRAG_EVENT_ORDER) {
525             enforceDragParity("onDragEnd", 0, 0);
526         }
527 
528         updateChildrenLayersEnabled();
529         StateManager<LauncherState, Launcher> stateManager = mLauncher.getStateManager();
530         stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
531             @Override
532             public void onStateTransitionComplete(LauncherState finalState) {
533                 if (finalState == NORMAL) {
534                     if (!mDeferRemoveExtraEmptyScreen) {
535                         removeExtraEmptyScreen(true /* stripEmptyScreens */);
536                     }
537                     stateManager.removeStateListener(this);
538                 }
539             }
540         });
541 
542         mDragInfo = null;
543         mDragSourceInternal = null;
544     }
545 
546     /**
547      * Initializes various states for this workspace.
548      */
initWorkspace()549     protected void initWorkspace() {
550         mCurrentPage = DEFAULT_PAGE;
551         setClipToPadding(false);
552 
553         setupLayoutTransition();
554 
555         // Set the wallpaper dimensions when Launcher starts up
556         setWallpaperDimension();
557     }
558 
setupLayoutTransition()559     private void setupLayoutTransition() {
560         // We want to show layout transitions when pages are deleted, to close the gap.
561         mLayoutTransition = new LayoutTransition();
562 
563         mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
564         mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
565         // Change the interpolators such that the fade animation plays before the move animation.
566         // This prevents empty adjacent pages to overlay during animation
567         mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING,
568                 Interpolators.clampToProgress(Interpolators.ACCELERATE_DECELERATE, 0, 0.5f));
569         mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING,
570                 Interpolators.clampToProgress(Interpolators.ACCELERATE_DECELERATE, 0.5f, 1));
571 
572         mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
573         mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
574         setLayoutTransition(mLayoutTransition);
575     }
576 
enableLayoutTransitions()577     void enableLayoutTransitions() {
578         setLayoutTransition(mLayoutTransition);
579     }
580 
disableLayoutTransitions()581     void disableLayoutTransitions() {
582         setLayoutTransition(null);
583     }
584 
585     @Override
onViewAdded(View child)586     public void onViewAdded(View child) {
587         if (!(child instanceof CellLayout)) {
588             throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
589         }
590         CellLayout cl = ((CellLayout) child);
591         cl.setOnInterceptTouchListener(this);
592         cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
593         super.onViewAdded(child);
594     }
595 
596     /**
597      * Initializes and binds the first page
598      */
bindAndInitFirstWorkspaceScreen()599     public void bindAndInitFirstWorkspaceScreen() {
600         if ((!FeatureFlags.QSB_ON_FIRST_SCREEN
601                 || !mLauncher.getIsFirstPagePinnedItemEnabled())
602                 || SHOULD_SHOW_FIRST_PAGE_WIDGET) {
603             mFirstPagePinnedItem = null;
604             return;
605         }
606 
607         // Add the first page
608         CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, getChildCount());
609         if (mFirstPagePinnedItem == null) {
610             // In transposed layout, we add the first page pinned widget in the Grid.
611             // As workspace does not touch the edges, we do not need a full
612             // width first page pinned item.
613             mFirstPagePinnedItem = LayoutInflater.from(getContext())
614                     .inflate(R.layout.search_container_workspace, firstPage, false);
615         }
616 
617         int cellHSpan = mLauncher.getDeviceProfile().inv.numSearchContainerColumns;
618         CellLayoutLayoutParams lp = new CellLayoutLayoutParams(0, 0, cellHSpan, 1);
619         lp.canReorder = false;
620         if (!firstPage.addViewToCellLayout(
621                 mFirstPagePinnedItem, 0, R.id.search_container_workspace, lp, true)) {
622             Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
623             mFirstPagePinnedItem = null;
624         }
625     }
626 
removeAllWorkspaceScreens()627     public void removeAllWorkspaceScreens() {
628         // Disable all layout transitions before removing all pages to ensure that we don't get the
629         // transition animations competing with us changing the scroll when we add pages
630         disableLayoutTransitions();
631 
632         // Recycle the first page pinned item
633         if (mFirstPagePinnedItem != null) {
634             ((ViewGroup) mFirstPagePinnedItem.getParent()).removeView(mFirstPagePinnedItem);
635         }
636 
637         // Remove the pages and clear the screen models
638         removeFolderListeners();
639         removeAllViews();
640         mScreenOrder.clear();
641         mWorkspaceScreens.clear();
642 
643         // Ensure that the first page is always present
644         if (!enableSmartspaceRemovalToggle()) {
645             bindAndInitFirstWorkspaceScreen();
646         }
647 
648         // Remove any deferred refresh callbacks
649         mLauncher.mHandler.removeCallbacksAndMessages(DeferredWidgetRefresh.class);
650 
651         // Re-enable the layout transitions
652         enableLayoutTransitions();
653     }
654 
insertNewWorkspaceScreenBeforeEmptyScreen(int screenId)655     public void insertNewWorkspaceScreenBeforeEmptyScreen(int screenId) {
656         // Find the index to insert this view into.  If the empty screen exists, then
657         // insert it before that.
658         int insertIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
659         if (insertIndex < 0) {
660             insertIndex = mScreenOrder.size();
661         }
662         insertNewWorkspaceScreen(screenId, insertIndex);
663     }
664 
insertNewWorkspaceScreen(int screenId)665     public void insertNewWorkspaceScreen(int screenId) {
666         insertNewWorkspaceScreen(screenId, getChildCount());
667     }
668 
insertNewWorkspaceScreen(int screenId, int insertIndex)669     public CellLayout insertNewWorkspaceScreen(int screenId, int insertIndex) {
670         if (mWorkspaceScreens.containsKey(screenId)) {
671             throw new RuntimeException("Screen id " + screenId + " already exists!");
672         }
673 
674         // Inflate the cell layout, but do not add it automatically so that we can get the newly
675         // created CellLayout.
676         DeviceProfile dp = mLauncher.getDeviceProfile();
677         CellLayout newScreen;
678         if (FOLDABLE_SINGLE_PAGE.get() && dp.isTwoPanels) {
679             newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
680                     R.layout.workspace_screen_foldable, this, false /* attachToRoot */);
681         } else {
682             newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
683                     R.layout.workspace_screen, this, false /* attachToRoot */);
684         }
685         newScreen.setCellLayoutContainer(this);
686 
687         mWorkspaceScreens.put(screenId, newScreen);
688         mScreenOrder.add(insertIndex, screenId);
689         addView(newScreen, insertIndex);
690         mStateTransitionAnimation.applyChildState(
691                 mLauncher.getStateManager().getState(), newScreen, insertIndex);
692 
693         updatePageScrollValues();
694         updateCellLayoutMeasures();
695         return newScreen;
696     }
697 
addExtraEmptyScreenOnDrag(DragObject dragObject)698     private void addExtraEmptyScreenOnDrag(DragObject dragObject) {
699         boolean lastChildOnScreen = false;
700         boolean childOnFinalScreen = false;
701 
702         if (mDragSourceInternal != null) {
703             int dragSourceChildCount = mDragSourceInternal.getChildCount();
704 
705             // If the icon was dragged from Hotseat, there is no page pair
706             if (isTwoPanelEnabled() && !(mDragSourceInternal.getParent() instanceof Hotseat)) {
707                 int pagePairScreenId = getScreenPair(getCellPosMapper().mapModelToPresenter(
708                         dragObject.dragInfo).screenId);
709                 CellLayout pagePair = mWorkspaceScreens.get(pagePairScreenId);
710                 dragSourceChildCount += pagePair.getShortcutsAndWidgets().getChildCount();
711             }
712 
713             // When the drag view content is a LauncherAppWidgetHostView, we should increment the
714             // drag source child count by 1 because the widget in drag has been detached from its
715             // original parent, ShortcutAndWidgetContainer, and reattached to the DragView.
716             if (dragObject.dragView.getContentView() instanceof LauncherAppWidgetHostView) {
717                 dragSourceChildCount++;
718             }
719 
720             if (dragSourceChildCount == 1) {
721                 lastChildOnScreen = true;
722             }
723             CellLayout cl = (CellLayout) mDragSourceInternal.getParent();
724             if (!FOLDABLE_SINGLE_PAGE.get() && getLeftmostVisiblePageForIndex(indexOfChild(cl))
725                     == getLeftmostVisiblePageForIndex(getPageCount() - 1)) {
726                 childOnFinalScreen = true;
727             }
728         }
729 
730         // If this is the last item on the final screen
731         if (lastChildOnScreen && childOnFinalScreen) {
732             return;
733         }
734 
735         forEachExtraEmptyPageId(extraEmptyPageId -> {
736             if (!mWorkspaceScreens.containsKey(extraEmptyPageId)) {
737                 insertNewWorkspaceScreen(extraEmptyPageId);
738             }
739         });
740     }
741 
742 
743     /**
744      * Returns if the given screenId is already in the Workspace
745      */
containsScreenId(int screenId)746     public boolean containsScreenId(int screenId) {
747         return this.mWorkspaceScreens.containsKey(screenId);
748     }
749 
750     /**
751      * Inserts extra empty pages to the end of the existing workspaces.
752      * Usually we add one extra empty screen, but when two panel home is enabled we add
753      * two extra screens.
754      **/
addExtraEmptyScreens()755     public void addExtraEmptyScreens() {
756         forEachExtraEmptyPageId(extraEmptyPageId -> {
757             if (!mWorkspaceScreens.containsKey(extraEmptyPageId)) {
758                 insertNewWorkspaceScreen(extraEmptyPageId);
759             }
760         });
761     }
762 
763     /**
764      * Calls the consumer with all the necessary extra empty page IDs.
765      * On a normal one panel Workspace that means only EXTRA_EMPTY_SCREEN_ID,
766      * but in a two panel scenario this also includes EXTRA_EMPTY_SCREEN_SECOND_ID.
767      */
forEachExtraEmptyPageId(Consumer<Integer> callback)768     private void forEachExtraEmptyPageId(Consumer<Integer> callback) {
769         callback.accept(EXTRA_EMPTY_SCREEN_ID);
770         if (isTwoPanelEnabled()) {
771             callback.accept(EXTRA_EMPTY_SCREEN_SECOND_ID);
772         }
773     }
774 
775     /**
776      * If two panel home is enabled we convert the last two screens that are visible at the same
777      * time. In other cases we only convert the last page.
778      */
convertFinalScreenToEmptyScreenIfNecessary()779     private void convertFinalScreenToEmptyScreenIfNecessary() {
780         if (mLauncher.isWorkspaceLoading()) {
781             // Invalid and dangerous operation if workspace is loading
782             return;
783         }
784 
785         int panelCount = getPanelCount();
786         if (hasExtraEmptyScreens() || mScreenOrder.size() < panelCount) {
787             return;
788         }
789 
790         SparseArray<CellLayout> finalScreens = new SparseArray<>();
791 
792         int pageCount = mScreenOrder.size();
793         // First we add the last page(s) to the finalScreens collection. The number of final pages
794         // depends on the panel count.
795         for (int pageIndex = pageCount - panelCount; pageIndex < pageCount; pageIndex++) {
796             int screenId = mScreenOrder.get(pageIndex);
797             CellLayout screen = mWorkspaceScreens.get(screenId);
798             if (screen == null || screen.getShortcutsAndWidgets().getChildCount() != 0
799                     || screen.isDropPending()) {
800                 // Final screen doesn't exist or it isn't empty or there's a pending drop
801                 return;
802             }
803             finalScreens.append(screenId, screen);
804         }
805 
806         // Then we remove the final screens from the collections (but not from the view hierarchy)
807         // and we store them as extra empty screens.
808         for (int i = 0; i < finalScreens.size(); i++) {
809             int screenId = finalScreens.keyAt(i);
810 
811             // We don't want to remove the first screen even if it's empty because that's where
812             // first page pinned item would go if it gets turned back on.
813             if (enableSmartspaceRemovalToggle() && screenId == FIRST_SCREEN_ID) {
814                 continue;
815             }
816 
817             CellLayout screen = finalScreens.get(screenId);
818 
819             mWorkspaceScreens.remove(screenId);
820             mScreenOrder.removeValue(screenId);
821 
822             int newScreenId = mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)
823                     ? EXTRA_EMPTY_SCREEN_SECOND_ID : EXTRA_EMPTY_SCREEN_ID;
824             mWorkspaceScreens.put(newScreenId, screen);
825             mScreenOrder.add(newScreenId);
826         }
827     }
828 
removeExtraEmptyScreen(boolean stripEmptyScreens)829     public void removeExtraEmptyScreen(boolean stripEmptyScreens) {
830         removeExtraEmptyScreenDelayed(0, stripEmptyScreens, null);
831     }
832 
833     /**
834      * The purpose of this method is to remove empty pages from Workspace.
835      * Empty page(s) from the end of mWorkspaceScreens will always be removed. The pages with
836      * ID = Workspace.EXTRA_EMPTY_SCREEN_IDS will be removed if there are other non-empty pages.
837      * If there are no more non-empty pages left, extra empty page(s) will either stay or get added.
838      * <p>
839      * If stripEmptyScreens is true, all empty pages (not just the ones on the end) will be removed
840      * from the Workspace, and if there are no more pages left then extra empty page(s) will be
841      * added.
842      * <p>
843      * The number of extra empty pages is equal to what getPanelCount() returns.
844      * <p>
845      * After the method returns the possible pages are:
846      * stripEmptyScreens = true : [non-empty pages, extra empty page(s) alone]
847      * stripEmptyScreens = false : [non-empty pages, empty pages (not in the end),
848      * extra empty page(s) alone]
849      */
removeExtraEmptyScreenDelayed( int delay, boolean stripEmptyScreens, Runnable onComplete)850     public void removeExtraEmptyScreenDelayed(
851             int delay, boolean stripEmptyScreens, Runnable onComplete) {
852         if (mLauncher.isWorkspaceLoading()) {
853             // Don't strip empty screens if the workspace is still loading
854             return;
855         }
856 
857         if (delay > 0) {
858             postDelayed(
859                     () -> removeExtraEmptyScreenDelayed(0, stripEmptyScreens, onComplete), delay);
860             return;
861         }
862 
863         // First we convert the last page to an extra page if the last page is empty
864         // and we don't already have an extra page.
865         convertFinalScreenToEmptyScreenIfNecessary();
866         // Then we remove the extra page(s) if they are not the only pages left in Workspace.
867         if (hasExtraEmptyScreens()) {
868             forEachExtraEmptyPageId(extraEmptyPageId -> {
869                 removeView(mWorkspaceScreens.get(extraEmptyPageId));
870                 mWorkspaceScreens.remove(extraEmptyPageId);
871                 mScreenOrder.removeValue(extraEmptyPageId);
872             });
873 
874             setCurrentPage(getNextPage());
875 
876             // Update the page indicator to reflect the removed page.
877             showPageIndicatorAtCurrentScroll();
878         }
879 
880         if (stripEmptyScreens) {
881             // This will remove all empty pages from the Workspace. If there are no more pages left,
882             // it will add extra page(s) so that users can put items on at least one page.
883             stripEmptyScreens();
884         }
885 
886         if (onComplete != null) {
887             onComplete.run();
888         }
889     }
890 
hasExtraEmptyScreens()891     public boolean hasExtraEmptyScreens() {
892         return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)
893                 && getChildCount() > getPanelCount()
894                 && (!isTwoPanelEnabled()
895                 || mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_SECOND_ID));
896     }
897 
898     /**
899      * Commits the extra empty pages then returns the screen ids of those new screens.
900      * Usually there's only one extra empty screen, but when two panel home is enabled we commit
901      * two extra screens.
902      * <p>
903      * Returns an empty IntSet in case we cannot commit any new screens.
904      */
commitExtraEmptyScreens()905     public IntSet commitExtraEmptyScreens() {
906         if (mLauncher.isWorkspaceLoading()) {
907             // Invalid and dangerous operation if workspace is loading
908             return new IntSet();
909         }
910 
911         IntSet extraEmptyPageIds = new IntSet();
912         forEachExtraEmptyPageId(extraEmptyPageId ->
913                 extraEmptyPageIds.add(commitExtraEmptyScreen(extraEmptyPageId)));
914 
915         return extraEmptyPageIds;
916     }
917 
commitExtraEmptyScreen(int emptyScreenId)918     private int commitExtraEmptyScreen(int emptyScreenId) {
919         CellLayout cl = mWorkspaceScreens.get(emptyScreenId);
920         mWorkspaceScreens.remove(emptyScreenId);
921         mScreenOrder.removeValue(emptyScreenId);
922 
923         int newScreenId = LauncherAppState.getInstance(getContext())
924                 .getModel().getModelDbController().getNewScreenId();
925         // Launcher database isn't aware of empty pages that are already bound, so we need to
926         // skip those IDs manually.
927         while (mWorkspaceScreens.containsKey(newScreenId)) {
928             newScreenId++;
929         }
930 
931         mWorkspaceScreens.put(newScreenId, cl);
932         mScreenOrder.add(newScreenId);
933 
934         return newScreenId;
935     }
936 
937     @Override
getHotseat()938     public Hotseat getHotseat() {
939         return mLauncher.getHotseat();
940     }
941 
942     @Override
onAddDropTarget(DropTarget target)943     public void onAddDropTarget(DropTarget target) {
944         mDragController.addDropTarget(target);
945     }
946 
947     @Override
getScreenWithId(int screenId)948     public CellLayout getScreenWithId(int screenId) {
949         return mWorkspaceScreens.get(screenId);
950     }
951 
952     @Override
getCellLayoutId(CellLayout layout)953     public int getCellLayoutId(CellLayout layout) {
954         int index = mWorkspaceScreens.indexOfValue(layout);
955         if (index != -1) {
956             return mWorkspaceScreens.keyAt(index);
957         }
958         return -1;
959     }
960 
getPageIndexForScreenId(int screenId)961     public int getPageIndexForScreenId(int screenId) {
962         return indexOfChild(mWorkspaceScreens.get(screenId));
963     }
964 
965     @Override
getCellLayoutIndex(CellLayout cellLayout)966     public int getCellLayoutIndex(CellLayout cellLayout) {
967         return indexOfChild(mWorkspaceScreens.get(getCellLayoutId(cellLayout)));
968     }
969 
970     @Override
getPanelCount()971     public int getPanelCount() {
972         return isTwoPanelEnabled() ? 2 : super.getPanelCount();
973     }
974 
getCurrentPageScreenIds()975     public IntSet getCurrentPageScreenIds() {
976         return IntSet.wrap(getScreenIdForPageIndex(getCurrentPage()));
977     }
978 
getScreenIdForPageIndex(int index)979     public int getScreenIdForPageIndex(int index) {
980         if (0 <= index && index < mScreenOrder.size()) {
981             return mScreenOrder.get(index);
982         }
983         return -1;
984     }
985 
getScreenOrder()986     public IntArray getScreenOrder() {
987         return mScreenOrder;
988     }
989 
990     /**
991      * Returns the screen ID of a page that is shown together with the given page screen ID when the
992      * two panel UI is enabled.
993      */
getScreenPair(int screenId)994     public int getScreenPair(int screenId) {
995         if (screenId == EXTRA_EMPTY_SCREEN_ID) {
996             return EXTRA_EMPTY_SCREEN_SECOND_ID;
997         } else if (screenId == EXTRA_EMPTY_SCREEN_SECOND_ID) {
998             return EXTRA_EMPTY_SCREEN_ID;
999         } else if (screenId % 2 == 0) {
1000             return screenId + 1;
1001         } else {
1002             return screenId - 1;
1003         }
1004     }
1005 
1006     /**
1007      * Returns {@link CellLayout} that is shown together with the given {@link CellLayout} when the
1008      * two panel UI is enabled.
1009      */
1010     @Nullable
getScreenPair(CellLayout cellLayout)1011     public CellLayout getScreenPair(CellLayout cellLayout) {
1012         if (!isTwoPanelEnabled()) {
1013             return null;
1014         }
1015         int screenId = getCellLayoutId(cellLayout);
1016         if (screenId == -1) {
1017             return null;
1018         }
1019         return getScreenWithId(getScreenPair(screenId));
1020     }
1021 
stripEmptyScreens()1022     public void stripEmptyScreens() {
1023         if (mLauncher.isWorkspaceLoading()) {
1024             // Don't strip empty screens if the workspace is still loading.
1025             // This is dangerous and can result in data loss.
1026             return;
1027         }
1028 
1029         if (isPageInTransition()) {
1030             mStripScreensOnPageStopMoving = true;
1031             return;
1032         }
1033 
1034         int currentPage = getNextPage();
1035         IntArray removeScreens = new IntArray();
1036         int total = mWorkspaceScreens.size();
1037         for (int i = 0; i < total; i++) {
1038             int id = mWorkspaceScreens.keyAt(i);
1039             CellLayout cl = mWorkspaceScreens.valueAt(i);
1040             // FIRST_SCREEN_ID can never be removed.
1041             if (((!FeatureFlags.QSB_ON_FIRST_SCREEN
1042                     || SHOULD_SHOW_FIRST_PAGE_WIDGET)
1043                     || id > FIRST_SCREEN_ID)
1044                     && cl.getShortcutsAndWidgets().getChildCount() == 0) {
1045                 removeScreens.add(id);
1046             }
1047         }
1048 
1049         // When two panel home is enabled we only remove an empty page if both visible pages are
1050         // empty.
1051         if (isTwoPanelEnabled()) {
1052             // We go through all the pages that were marked as removable and check their page pair
1053             Iterator<Integer> removeScreensIterator = removeScreens.iterator();
1054             while (removeScreensIterator.hasNext()) {
1055                 int pageToRemove = removeScreensIterator.next();
1056                 int pagePair = getScreenPair(pageToRemove);
1057                 if (!removeScreens.contains(pagePair)) {
1058                     // The page pair isn't empty so we want to remove the current page from the
1059                     // removable pages' collection
1060                     removeScreensIterator.remove();
1061                 }
1062             }
1063         }
1064 
1065         // We enforce at least one page (two pages on two panel home) to add new items to.
1066         // In the case that we remove the last such screen(s), we convert the last screen(s)
1067         // to the empty screen(s)
1068         int minScreens = getPanelCount();
1069 
1070         int pageShift = 0;
1071         for (int i = 0; i < removeScreens.size(); i++) {
1072             int id = removeScreens.get(i);
1073             CellLayout cl = mWorkspaceScreens.get(id);
1074             mWorkspaceScreens.remove(id);
1075             mScreenOrder.removeValue(id);
1076 
1077             if (getChildCount() > minScreens) {
1078                 // If this isn't the last page, just remove it
1079                 if (indexOfChild(cl) < currentPage) {
1080                     pageShift++;
1081                 }
1082                 removeView(cl);
1083             } else {
1084                 // The last page(s) should be converted into extra empty page(s)
1085                 int extraScreenId = isTwoPanelEnabled() && id % 2 == 1
1086                         // This is the right panel in a two panel scenario
1087                         ? EXTRA_EMPTY_SCREEN_SECOND_ID
1088                         // This is either the last screen in a one panel scenario, or the left panel
1089                         // in a two panel scenario when there are only two empty pages left
1090                         : EXTRA_EMPTY_SCREEN_ID;
1091                 mWorkspaceScreens.put(extraScreenId, cl);
1092                 mScreenOrder.add(extraScreenId);
1093             }
1094         }
1095 
1096         if (pageShift >= 0) {
1097             setCurrentPage(currentPage - pageShift);
1098         }
1099     }
1100 
1101     /**
1102      * Needed here because launcher has a fullscreen exclusion rect and doesn't pilfer the pointers.
1103      */
1104     @Override
onInterceptTouchEvent(MotionEvent ev)1105     public boolean onInterceptTouchEvent(MotionEvent ev) {
1106         if (isTrackpadMultiFingerSwipe(ev)) {
1107             return false;
1108         }
1109         return super.onInterceptTouchEvent(ev);
1110     }
1111 
1112     /**
1113      * Needed here because launcher has a fullscreen exclusion rect and doesn't pilfer the pointers.
1114      */
1115     @SuppressLint("ClickableViewAccessibility")
1116     @Override
onTouchEvent(MotionEvent ev)1117     public boolean onTouchEvent(MotionEvent ev) {
1118         if (isTrackpadMultiFingerSwipe(ev)) {
1119             return false;
1120         }
1121         return super.onTouchEvent(ev);
1122     }
1123 
1124     /**
1125      * Called directly from a CellLayout (not by the framework), after we've been added as a
1126      * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout
1127      * that it should intercept touch events, which is not something that is normally supported.
1128      */
1129     @SuppressLint("ClickableViewAccessibility")
1130     @Override
onTouch(View v, MotionEvent event)1131     public boolean onTouch(View v, MotionEvent event) {
1132         return shouldConsumeTouch(v);
1133     }
1134 
shouldConsumeTouch(View v)1135     private boolean shouldConsumeTouch(View v) {
1136         return !workspaceIconsCanBeDragged()
1137                 || (!workspaceInModalState() && !isVisible(v));
1138     }
1139 
isSwitchingState()1140     public boolean isSwitchingState() {
1141         return mIsSwitchingState;
1142     }
1143 
1144     /**
1145      * This differs from isSwitchingState in that we take into account how far the transition
1146      * has completed.
1147      */
isFinishedSwitchingState()1148     public boolean isFinishedSwitchingState() {
1149         return !mIsSwitchingState
1150                 || (mTransitionProgress > FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS);
1151     }
1152 
1153     @Override
dispatchUnhandledMove(View focused, int direction)1154     public boolean dispatchUnhandledMove(View focused, int direction) {
1155         if (workspaceInModalState() || !isFinishedSwitchingState()) {
1156             // when the home screens are shrunken, shouldn't allow side-scrolling
1157             return false;
1158         }
1159         return super.dispatchUnhandledMove(focused, direction);
1160     }
1161 
1162     @Override
updateIsBeingDraggedOnTouchDown(MotionEvent ev)1163     protected void updateIsBeingDraggedOnTouchDown(MotionEvent ev) {
1164         super.updateIsBeingDraggedOnTouchDown(ev);
1165 
1166         mXDown = ev.getX();
1167         mYDown = ev.getY();
1168         if (mFirstPagePinnedItem != null) {
1169             final float[] tempFXY = new float[2];
1170             tempFXY[0] = mXDown;
1171             tempFXY[1] = mYDown;
1172             Utilities.mapCoordInSelfToDescendant(mFirstPagePinnedItem, this, tempFXY);
1173             mIsEventOverFirstPagePinnedItem = mFirstPagePinnedItem.getLeft() <= tempFXY[0]
1174                     && mFirstPagePinnedItem.getRight() >= tempFXY[0]
1175                     && mFirstPagePinnedItem.getTop() <= tempFXY[1]
1176                     && mFirstPagePinnedItem.getBottom() >= tempFXY[1];
1177         } else {
1178             mIsEventOverFirstPagePinnedItem = false;
1179         }
1180     }
1181 
1182     @Override
determineScrollingStart(MotionEvent ev)1183     protected void determineScrollingStart(MotionEvent ev) {
1184         if (!isFinishedSwitchingState() || mIsEventOverFirstPagePinnedItem) return;
1185 
1186         float deltaX = ev.getX() - mXDown;
1187         float absDeltaX = Math.abs(deltaX);
1188         float absDeltaY = Math.abs(ev.getY() - mYDown);
1189 
1190         if (Float.compare(absDeltaX, 0f) == 0) return;
1191 
1192         float slope = absDeltaY / absDeltaX;
1193         float theta = (float) Math.atan(slope);
1194 
1195         if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
1196             cancelCurrentPageLongPress();
1197         }
1198 
1199         if (theta > MAX_SWIPE_ANGLE) {
1200             // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace
1201             return;
1202         } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
1203             // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to
1204             // increase the touch slop to make it harder to begin scrolling the workspace. This
1205             // results in vertically scrolling widgets to more easily. The higher the angle, the
1206             // more we increase touch slop.
1207             theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
1208             float extraRatio = (float)
1209                     Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
1210             super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
1211         } else {
1212             // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special
1213             super.determineScrollingStart(ev);
1214         }
1215     }
1216 
onPageBeginTransition()1217     protected void onPageBeginTransition() {
1218         super.onPageBeginTransition();
1219         updateChildrenLayersEnabled();
1220     }
1221 
onPageEndTransition()1222     protected void onPageEndTransition() {
1223         super.onPageEndTransition();
1224         updateChildrenLayersEnabled();
1225 
1226         if (mDragController.isDragging()) {
1227             if (workspaceInModalState()) {
1228                 // If we are in springloaded mode, then force an event to check if the current touch
1229                 // is under a new page (to scroll to)
1230                 mDragController.forceTouchMove();
1231             }
1232         }
1233 
1234         if (mStripScreensOnPageStopMoving) {
1235             stripEmptyScreens();
1236             mStripScreensOnPageStopMoving = false;
1237         }
1238 
1239         // Inform the Launcher activity that the page transition ended so that it can react to the
1240         // newly visible page if it wants to.
1241         mLauncher.onPageEndTransition();
1242     }
1243 
setLauncherOverlay(LauncherOverlayTouchProxy overlay)1244     public void setLauncherOverlay(LauncherOverlayTouchProxy overlay) {
1245         final EdgeEffectCompat newEffect;
1246         if (overlay == null) {
1247             newEffect = new EdgeEffectCompat(getContext());
1248             mOverlayEdgeEffect = null;
1249         } else {
1250             newEffect = mOverlayEdgeEffect = new OverlayEdgeEffect(getContext(), overlay);
1251             overlay.setOverlayCallbacks(this);
1252         }
1253 
1254         if (mIsRtl) {
1255             mEdgeGlowRight = newEffect;
1256         } else {
1257             mEdgeGlowLeft = newEffect;
1258         }
1259         onOverlayScrollChanged(0);
1260     }
1261 
hasOverlay()1262     public boolean hasOverlay() {
1263         return mOverlayEdgeEffect != null;
1264     }
1265 
1266     @Override
snapToDestination()1267     protected void snapToDestination() {
1268         if (mOverlayEdgeEffect != null && !mOverlayEdgeEffect.isFinished()) {
1269             snapToPageImmediately(0);
1270         } else {
1271             super.snapToDestination();
1272         }
1273     }
1274 
1275     @Override
onScrollChanged(int l, int t, int oldl, int oldt)1276     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
1277         super.onScrollChanged(l, t, oldl, oldt);
1278 
1279         // Update the page indicator progress.
1280         // Unlike from other states, we show the page indicator when transitioning from HINT_STATE.
1281         boolean isSwitchingState = mIsSwitchingState
1282                 && mLauncher.getStateManager().getCurrentStableState() != HINT_STATE;
1283         boolean isTransitioning = isSwitchingState
1284                 || (getLayoutTransition() != null && getLayoutTransition().isRunning());
1285         if (!isTransitioning) {
1286             showPageIndicatorAtCurrentScroll();
1287         }
1288 
1289         updatePageAlphaValues();
1290         updatePageScrollValues();
1291         enableHwLayersOnVisiblePages();
1292     }
1293 
showPageIndicatorAtCurrentScroll()1294     public void showPageIndicatorAtCurrentScroll() {
1295         if (mPageIndicator != null) {
1296             mPageIndicator.setScroll(getScrollX(), computeMaxScroll());
1297         }
1298     }
1299 
1300     @Override
shouldFlingForVelocity(int velocityX)1301     protected boolean shouldFlingForVelocity(int velocityX) {
1302         // When the overlay is moving, the fling or settle transition is controlled by the overlay.
1303         return Float.compare(Math.abs(mOverlayProgress), 0) == 0
1304                 && super.shouldFlingForVelocity(velocityX);
1305     }
1306 
1307     /**
1308      * The overlay scroll is being controlled locally, just update our overlay effect
1309      */
1310     @Override
onOverlayScrollChanged(float scroll)1311     public void onOverlayScrollChanged(float scroll) {
1312         mOverlayProgress = Utilities.boundToRange(scroll, 0, 1);
1313         if (Float.compare(mOverlayProgress, 1f) == 0) {
1314             if (!mOverlayShown) {
1315                 mOverlayShown = true;
1316                 mLauncher.onOverlayVisibilityChanged(true);
1317             }
1318         } else if (Float.compare(mOverlayProgress, 0f) == 0) {
1319             if (mOverlayShown) {
1320                 mOverlayShown = false;
1321                 mLauncher.onOverlayVisibilityChanged(false);
1322             }
1323         }
1324         int count = mOverlayCallbacks.size();
1325         for (int i = 0; i < count; i++) {
1326             mOverlayCallbacks.get(i).onOverlayScrollChanged(mOverlayProgress);
1327         }
1328     }
1329 
1330     /**
1331      * Adds a callback for receiving overlay progress
1332      */
addOverlayCallback(LauncherOverlayCallbacks callback)1333     public void addOverlayCallback(LauncherOverlayCallbacks callback) {
1334         mOverlayCallbacks.add(callback);
1335         callback.onOverlayScrollChanged(mOverlayProgress);
1336     }
1337 
1338     /**
1339      * Removes a previously added overlay progress callback
1340      */
removeOverlayCallback(LauncherOverlayCallbacks callback)1341     public void removeOverlayCallback(LauncherOverlayCallbacks callback) {
1342         mOverlayCallbacks.remove(callback);
1343     }
1344 
1345     @Override
notifyPageSwitchListener(int prevPage)1346     protected void notifyPageSwitchListener(int prevPage) {
1347         super.notifyPageSwitchListener(prevPage);
1348         if (prevPage != mCurrentPage) {
1349             StatsLogManager.EventEnum event = (prevPage < mCurrentPage)
1350                     ? LAUNCHER_SWIPERIGHT : LAUNCHER_SWIPELEFT;
1351             mLauncher.getStatsLogManager().logger()
1352                     .withSrcState(LAUNCHER_STATE_HOME)
1353                     .withDstState(LAUNCHER_STATE_HOME)
1354                     .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
1355                             .setWorkspace(
1356                                     LauncherAtom.WorkspaceContainer.newBuilder()
1357                                             .setPageIndex(prevPage)).build())
1358                     .log(event);
1359         }
1360     }
1361 
setWallpaperDimension()1362     protected void setWallpaperDimension() {
1363         Executors.THREAD_POOL_EXECUTOR.execute(new Runnable() {
1364             @Override
1365             public void run() {
1366                 final Point size = LauncherAppState.getIDP(getContext()).defaultWallpaperSize;
1367                 if (size.x != mWallpaperManager.getDesiredMinimumWidth()
1368                         || size.y != mWallpaperManager.getDesiredMinimumHeight()) {
1369                     mWallpaperManager.suggestDesiredDimensions(size.x, size.y);
1370                 }
1371             }
1372         });
1373     }
1374 
lockWallpaperToDefaultPage()1375     public void lockWallpaperToDefaultPage() {
1376         mWallpaperOffset.setLockToDefaultPage(true);
1377     }
1378 
unlockWallpaperFromDefaultPageOnNextLayout()1379     public void unlockWallpaperFromDefaultPageOnNextLayout() {
1380         if (mWallpaperOffset.isLockedToDefaultPage()) {
1381             mUnlockWallpaperFromDefaultPageOnLayout = true;
1382             requestLayout();
1383         }
1384     }
1385 
1386     @Override
computeScroll()1387     public void computeScroll() {
1388         super.computeScroll();
1389         mWallpaperOffset.syncWithScroll();
1390     }
1391 
1392     @Override
announceForAccessibility(CharSequence text)1393     public void announceForAccessibility(CharSequence text) {
1394         // Don't announce if apps is on top of us.
1395         if (!mLauncher.isInState(ALL_APPS)) {
1396             super.announceForAccessibility(text);
1397         }
1398     }
1399 
updatePageAlphaValues()1400     private void updatePageAlphaValues() {
1401         // We need to check the isDragging case because updatePageAlphaValues is called between
1402         // goToState(SPRING_LOADED) and onStartStateTransition.
1403         if (!workspaceInModalState() && !mIsSwitchingState && !mDragController.isDragging()) {
1404             int screenCenter = getScrollX() + getMeasuredWidth() / 2;
1405             for (int i = 0; i < getChildCount(); i++) {
1406                 CellLayout child = (CellLayout) getChildAt(i);
1407                 if (child != null) {
1408                     float scrollProgress = getScrollProgress(screenCenter, child, i);
1409                     float alpha = 1 - Math.abs(scrollProgress);
1410                     if (mWorkspaceFadeInAdjacentScreens) {
1411                         child.getShortcutsAndWidgets().setAlpha(alpha);
1412                     } else {
1413                         // Pages that are off-screen aren't important for accessibility.
1414                         child.getShortcutsAndWidgets().setImportantForAccessibility(
1415                                 alpha > 0 ? IMPORTANT_FOR_ACCESSIBILITY_AUTO
1416                                         : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1417                     }
1418                 }
1419             }
1420         }
1421     }
1422 
updatePageScrollValues()1423     private void updatePageScrollValues() {
1424         int screenCenter = getScrollX() + getMeasuredWidth() / 2;
1425         for (int i = 0; i < getChildCount(); i++) {
1426             CellLayout child = (CellLayout) getChildAt(i);
1427             if (child != null) {
1428                 float scrollProgress = getScrollProgress(screenCenter, child, i);
1429                 child.setScrollProgress(scrollProgress);
1430             }
1431         }
1432     }
1433 
onAttachedToWindow()1434     protected void onAttachedToWindow() {
1435         super.onAttachedToWindow();
1436         mWallpaperOffset.setWindowToken(getWindowToken());
1437         computeScroll();
1438     }
1439 
onDetachedFromWindow()1440     protected void onDetachedFromWindow() {
1441         super.onDetachedFromWindow();
1442         mWallpaperOffset.setWindowToken(null);
1443     }
1444 
1445     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)1446     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1447         if (mUnlockWallpaperFromDefaultPageOnLayout) {
1448             mWallpaperOffset.setLockToDefaultPage(false);
1449             mUnlockWallpaperFromDefaultPageOnLayout = false;
1450         }
1451         if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
1452             mWallpaperOffset.syncWithScroll();
1453             mWallpaperOffset.jumpToFinal();
1454         }
1455         super.onLayout(changed, left, top, right, bottom);
1456         updatePageAlphaValues();
1457     }
1458 
1459     @Override
getDescendantFocusability()1460     public int getDescendantFocusability() {
1461         if (workspaceInModalState()) {
1462             return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
1463         }
1464         return super.getDescendantFocusability();
1465     }
1466 
workspaceInModalState()1467     private boolean workspaceInModalState() {
1468         return !mLauncher.isInState(NORMAL);
1469     }
1470 
workspaceInScrollableState()1471     private boolean workspaceInScrollableState() {
1472         return mLauncher.isInState(SPRING_LOADED) || mLauncher.isInState(EDIT_MODE)
1473                 || !workspaceInModalState();
1474     }
1475 
1476     /**
1477      * Returns whether a drag should be allowed to be started from the current workspace state.
1478      */
workspaceIconsCanBeDragged()1479     public boolean workspaceIconsCanBeDragged() {
1480         return mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED);
1481     }
1482 
updateChildrenLayersEnabled()1483     private void updateChildrenLayersEnabled() {
1484         boolean enableChildrenLayers = mIsSwitchingState || isPageInTransition();
1485 
1486         if (enableChildrenLayers != mChildrenLayersEnabled) {
1487             mChildrenLayersEnabled = enableChildrenLayers;
1488             if (mChildrenLayersEnabled) {
1489                 enableHwLayersOnVisiblePages();
1490             } else {
1491                 for (int i = 0; i < getPageCount(); i++) {
1492                     final CellLayout cl = (CellLayout) getChildAt(i);
1493                     cl.enableHardwareLayer(false);
1494                 }
1495             }
1496         }
1497     }
1498 
enableHwLayersOnVisiblePages()1499     private void enableHwLayersOnVisiblePages() {
1500         if (mChildrenLayersEnabled) {
1501             final int screenCount = getChildCount();
1502 
1503             final int[] visibleScreens = getVisibleChildrenRange();
1504             int leftScreen = visibleScreens[0];
1505             int rightScreen = visibleScreens[1];
1506             if (mForceDrawAdjacentPages) {
1507                 // In overview mode, make sure that the two side pages are visible.
1508                 leftScreen = Utilities.boundToRange(getCurrentPage() - 1, 0, rightScreen);
1509                 rightScreen = Utilities.boundToRange(getCurrentPage() + 1,
1510                         leftScreen, getPageCount() - 1);
1511             }
1512 
1513             if (leftScreen == rightScreen) {
1514                 // make sure we're caching at least two pages always
1515                 if (rightScreen < screenCount - 1) {
1516                     rightScreen++;
1517                 } else if (leftScreen > 0) {
1518                     leftScreen--;
1519                 }
1520             }
1521 
1522             for (int i = 0; i < screenCount; i++) {
1523                 final CellLayout layout = (CellLayout) getPageAt(i);
1524                 // enable layers between left and right screen inclusive.
1525                 boolean enableLayer = leftScreen <= i && i <= rightScreen;
1526                 layout.enableHardwareLayer(enableLayer);
1527             }
1528         }
1529     }
1530 
onWallpaperTap(MotionEvent ev)1531     public void onWallpaperTap(MotionEvent ev) {
1532         final int[] position = mTempXY;
1533         getLocationOnScreen(position);
1534 
1535         int pointerIndex = ev.getActionIndex();
1536         position[0] += (int) ev.getX(pointerIndex);
1537         position[1] += (int) ev.getY(pointerIndex);
1538 
1539         mWallpaperManager.sendWallpaperCommand(getWindowToken(),
1540                 ev.getAction() == MotionEvent.ACTION_UP
1541                         ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP,
1542                 position[0], position[1], 0, null);
1543     }
1544 
onStartStateTransition()1545     private void onStartStateTransition() {
1546         mIsSwitchingState = true;
1547         mTransitionProgress = 0;
1548 
1549         updateChildrenLayersEnabled();
1550     }
1551 
onEndStateTransition()1552     private void onEndStateTransition() {
1553         mIsSwitchingState = false;
1554         mForceDrawAdjacentPages = false;
1555         mTransitionProgress = 1;
1556 
1557         updateChildrenLayersEnabled();
1558         updateAccessibilityFlags();
1559     }
1560 
1561     /**
1562      * Sets the current workspace {@link LauncherState} and updates the UI without any animations
1563      */
1564     @Override
setState(LauncherState toState)1565     public void setState(LauncherState toState) {
1566         onStartStateTransition();
1567         mLauncher.getStateManager().getState().onLeavingState(mLauncher, toState);
1568         mStateTransitionAnimation.setState(toState);
1569         onEndStateTransition();
1570     }
1571 
1572     /**
1573      * Sets the current workspace {@link LauncherState}, then animates the UI
1574      */
1575     @Override
setStateWithAnimation( LauncherState toState, StateAnimationConfig config, PendingAnimation animation)1576     public void setStateWithAnimation(
1577             LauncherState toState, StateAnimationConfig config, PendingAnimation animation) {
1578         StateTransitionListener listener = new StateTransitionListener();
1579         mLauncher.getStateManager().getState().onLeavingState(mLauncher, toState);
1580         mStateTransitionAnimation.setStateWithAnimation(toState, config, animation);
1581 
1582         // Invalidate the pages now, so that we have the visible pages before the
1583         // animation is started
1584         if (toState.hasFlag(FLAG_MULTI_PAGE)) {
1585             mForceDrawAdjacentPages = true;
1586         }
1587         invalidate(); // This will call dispatchDraw(), which calls getVisiblePages().
1588 
1589         ValueAnimator stepAnimator = ValueAnimator.ofFloat(0, 1);
1590         stepAnimator.addUpdateListener(listener);
1591         stepAnimator.addListener(listener);
1592         animation.add(stepAnimator);
1593     }
1594 
getStateTransitionAnimation()1595     public WorkspaceStateTransitionAnimation getStateTransitionAnimation() {
1596         return mStateTransitionAnimation;
1597     }
1598 
updateAccessibilityFlags()1599     public void updateAccessibilityFlags() {
1600         // TODO: Update the accessibility flags appropriately when dragging.
1601         int accessibilityFlag =
1602                 mLauncher.getStateManager().getState().hasFlag(FLAG_WORKSPACE_INACCESSIBLE)
1603                         ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
1604                         : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
1605         if (!mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
1606             int total = getPageCount();
1607             for (int i = 0; i < total; i++) {
1608                 updateAccessibilityFlags(accessibilityFlag, (CellLayout) getPageAt(i));
1609             }
1610             setImportantForAccessibility(accessibilityFlag);
1611         }
1612     }
1613 
1614     @Override
createAccessibilityNodeInfo()1615     public AccessibilityNodeInfo createAccessibilityNodeInfo() {
1616         if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
1617             // TAPL tests verify that workspace is not present in Overview and AllApps states.
1618             // TAPL can work only if UIDevice is set up as setCompressedLayoutHeirarchy(false).
1619             // Hiding workspace from the tests when it's
1620             // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS.
1621             return AccessibilityNodeInfo.obtain();
1622         }
1623         return super.createAccessibilityNodeInfo();
1624     }
1625 
updateAccessibilityFlags(int accessibilityFlag, CellLayout page)1626     private void updateAccessibilityFlags(int accessibilityFlag, CellLayout page) {
1627         page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
1628         page.getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
1629         page.setContentDescription(null);
1630         page.setAccessibilityDelegate(null);
1631     }
1632 
startDrag(CellInfo cellInfo, DragOptions options)1633     public void startDrag(CellInfo cellInfo, DragOptions options) {
1634         View child = cellInfo.cell;
1635 
1636         mDragInfo = cellInfo;
1637         child.setVisibility(INVISIBLE);
1638 
1639         if (options.isAccessibleDrag) {
1640             mDragController.addDragListener(
1641                     new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
1642                         @Override
1643                         protected void enableAccessibleDrag(boolean enable,
1644                                 @Nullable DragObject dragObject) {
1645                             super.enableAccessibleDrag(enable, dragObject);
1646                             setEnableForLayout(mLauncher.getHotseat(), enable);
1647                             if (enable && dragObject != null
1648                                     && dragObject.dragInfo instanceof LauncherAppWidgetInfo) {
1649                                 mLauncher.getHotseat().setImportantForAccessibility(
1650                                         IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
1651                             }
1652                         }
1653                     });
1654         }
1655 
1656         beginDragShared(child, this, options);
1657     }
1658 
beginDragShared(View child, DragSource source, DragOptions options)1659     public void beginDragShared(View child, DragSource source, DragOptions options) {
1660         Object dragObject = child.getTag();
1661         if (!(dragObject instanceof ItemInfo)) {
1662             String msg = "Drag started with a view that has no tag set. This "
1663                     + "will cause a crash (issue 11627249) down the line. "
1664                     + "View: " + child + "  tag: " + child.getTag();
1665             throw new IllegalStateException(msg);
1666         }
1667         beginDragShared(child, null, source, (ItemInfo) dragObject,
1668                 new DragPreviewProvider(child), options);
1669     }
1670 
1671     /**
1672      * Core functionality for beginning a drag operation for an item that will be dropped within
1673      * the workspace
1674      */
beginDragShared(View child, DraggableView draggableView, DragSource source, ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions)1675     public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
1676             ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
1677 
1678         float iconScale = 1f;
1679         if (child instanceof BubbleTextView) {
1680             Drawable icon = ((BubbleTextView) child).getIcon();
1681             if (icon instanceof FastBitmapDrawable) {
1682                 iconScale = ((FastBitmapDrawable) icon).getAnimatedScale();
1683             }
1684         }
1685 
1686         // Clear the pressed state if necessary
1687         child.clearFocus();
1688         child.setPressed(false);
1689         if (child instanceof BubbleTextView) {
1690             BubbleTextView icon = (BubbleTextView) child;
1691             icon.clearPressedBackground();
1692         }
1693 
1694         if (draggableView == null && child instanceof DraggableView) {
1695             draggableView = (DraggableView) child;
1696         }
1697 
1698         final View contentView = previewProvider.getContentView();
1699         final float scale;
1700         // The draggable drawable follows the touch point around on the screen
1701         final Drawable drawable;
1702         if (contentView == null) {
1703             drawable = previewProvider.createDrawable();
1704             scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
1705         } else {
1706             drawable = null;
1707             scale = previewProvider.getScaleAndPosition(contentView, mTempXY);
1708         }
1709 
1710         int dragLayerX = mTempXY[0];
1711         int dragLayerY = mTempXY[1];
1712 
1713         Rect dragRect = new Rect();
1714 
1715         if (draggableView != null) {
1716             draggableView.getSourceVisualDragBounds(dragRect);
1717             dragLayerY += dragRect.top;
1718         }
1719 
1720 
1721         if (child.getParent() instanceof ShortcutAndWidgetContainer) {
1722             mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
1723         }
1724 
1725         if (child instanceof BubbleTextView) {
1726             BubbleTextView btv = (BubbleTextView) child;
1727             if (!dragOptions.isAccessibleDrag) {
1728                 dragOptions.preDragCondition = btv.startLongPressAction();
1729             }
1730             if (btv.isDisplaySearchResult()) {
1731                 dragOptions.preDragEndScale = (float) mAllAppsIconSize / btv.getIconSize();
1732             }
1733         }
1734 
1735         if (dragOptions.preDragCondition != null) {
1736             int xDragOffSet = dragOptions.preDragCondition.getDragOffset().x;
1737             int yDragOffSet = dragOptions.preDragCondition.getDragOffset().y;
1738             if (xDragOffSet != 0 || yDragOffSet != 0) {
1739                 dragLayerX += xDragOffSet;
1740                 dragLayerY += yDragOffSet;
1741             }
1742         }
1743 
1744         final DragView dv;
1745         if (contentView instanceof View) {
1746             dv = mDragController.startDrag(
1747                     contentView,
1748                     draggableView,
1749                     dragLayerX,
1750                     dragLayerY,
1751                     source,
1752                     dragObject,
1753                     dragRect,
1754                     scale * iconScale,
1755                     scale,
1756                     dragOptions);
1757         } else {
1758             dv = mDragController.startDrag(
1759                     drawable,
1760                     draggableView,
1761                     dragLayerX,
1762                     dragLayerY,
1763                     source,
1764                     dragObject,
1765                     dragRect,
1766                     scale * iconScale,
1767                     scale,
1768                     dragOptions);
1769         }
1770         return dv;
1771     }
1772 
transitionStateShouldAllowDrop()1773     private boolean transitionStateShouldAllowDrop() {
1774         return (!isSwitchingState() || mTransitionProgress > ALLOW_DROP_TRANSITION_PROGRESS) &&
1775                 workspaceIconsCanBeDragged();
1776     }
1777 
1778     /**
1779      * {@inheritDoc}
1780      */
1781     @Override
acceptDrop(DragObject d)1782     public boolean acceptDrop(DragObject d) {
1783         // If it's an external drop (e.g. from All Apps), check if it should be accepted
1784         CellLayout dropTargetLayout = mDropToLayout;
1785         if (d.dragSource != this) {
1786             // Don't accept the drop if we're not over a valid drop target at time of drop
1787             if (dropTargetLayout == null) {
1788                 return false;
1789             }
1790             if (!transitionStateShouldAllowDrop()) return false;
1791 
1792             mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
1793 
1794             // We want the point to be mapped to the dragTarget.
1795             mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
1796 
1797             int spanX;
1798             int spanY;
1799             if (mDragInfo != null) {
1800                 final CellInfo dragCellInfo = mDragInfo;
1801                 spanX = dragCellInfo.spanX;
1802                 spanY = dragCellInfo.spanY;
1803             } else {
1804                 spanX = d.dragInfo.spanX;
1805                 spanY = d.dragInfo.spanY;
1806             }
1807 
1808             int minSpanX = spanX;
1809             int minSpanY = spanY;
1810             if (d.dragInfo instanceof PendingAddWidgetInfo) {
1811                 minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX;
1812                 minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY;
1813             }
1814 
1815             mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
1816                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout,
1817                     mTargetCell);
1818             float distance = dropTargetLayout.getDistanceFromWorkspaceCellVisualCenter(
1819                     mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
1820             if (mCreateUserFolderOnDrop && willCreateUserFolder(d.dragInfo,
1821                     dropTargetLayout, mTargetCell, distance, true)) {
1822                 return true;
1823             }
1824 
1825             if (mAddToExistingFolderOnDrop && willAddToExistingUserFolder(d.dragInfo,
1826                     dropTargetLayout, mTargetCell, distance)) {
1827                 return true;
1828             }
1829 
1830             int[] resultSpan = new int[2];
1831             mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
1832                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
1833                     null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP);
1834             boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
1835 
1836             // Don't accept the drop if there's no room for the item
1837             if (!foundCell) {
1838                 onNoCellFound(dropTargetLayout, d.dragInfo, d.logInstanceId);
1839                 return false;
1840             }
1841         }
1842 
1843         int screenId = getCellLayoutId(dropTargetLayout);
1844         if (Workspace.EXTRA_EMPTY_SCREEN_IDS.contains(screenId)) {
1845             commitExtraEmptyScreens();
1846         }
1847 
1848         return true;
1849     }
1850 
willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float distance, boolean considerTimeout)1851     boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell,
1852                                  float distance, boolean considerTimeout) {
1853         if (distance > target.getFolderCreationRadius(targetCell)) return false;
1854         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
1855         return willCreateUserFolder(info, dropOverView, considerTimeout);
1856     }
1857 
willCreateUserFolder(ItemInfo info, View dropOverView, boolean considerTimeout)1858     boolean willCreateUserFolder(ItemInfo info, View dropOverView, boolean considerTimeout) {
1859         if (dropOverView != null) {
1860             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) dropOverView.getLayoutParams();
1861             if (lp.useTmpCoords && (lp.getTmpCellX() != lp.getCellX()
1862                     || lp.getTmpCellY() != lp.getCellY())) {
1863                 return false;
1864             }
1865         }
1866 
1867         boolean hasntMoved = false;
1868         if (mDragInfo != null) {
1869             hasntMoved = dropOverView == mDragInfo.cell;
1870         }
1871 
1872         if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) {
1873             return false;
1874         }
1875 
1876         boolean aboveShortcut = Folder.willAccept(dropOverView.getTag())
1877                 && ((ItemInfo) dropOverView.getTag()).container != CONTAINER_HOTSEAT_PREDICTION;
1878         boolean willBecomeShortcut = Folder.willAcceptItemType(info.itemType);
1879 
1880         return (aboveShortcut && willBecomeShortcut);
1881     }
1882 
willAddToExistingUserFolder(ItemInfo dragInfo, CellLayout target, int[] targetCell, float distance)1883     boolean willAddToExistingUserFolder(ItemInfo dragInfo, CellLayout target, int[] targetCell,
1884                                         float distance) {
1885         if (distance > target.getFolderCreationRadius(targetCell)) return false;
1886         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
1887         return willAddToExistingUserFolder(dragInfo, dropOverView);
1888 
1889     }
1890 
willAddToExistingUserFolder(ItemInfo dragInfo, View dropOverView)1891     boolean willAddToExistingUserFolder(ItemInfo dragInfo, View dropOverView) {
1892         if (dropOverView != null) {
1893             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) dropOverView.getLayoutParams();
1894             if (lp.useTmpCoords && (lp.getTmpCellX() != lp.getCellX()
1895                     || lp.getTmpCellY() != lp.getCellY())) {
1896                 return false;
1897             }
1898         }
1899 
1900         if (dropOverView instanceof FolderIcon) {
1901             FolderIcon fi = (FolderIcon) dropOverView;
1902             if (fi.acceptDrop(dragInfo)) {
1903                 return true;
1904             }
1905         }
1906         return false;
1907     }
1908 
createUserFolderIfNecessary(View newView, int container, CellLayout target, int[] targetCell, float distance, boolean external, DragObject d)1909     boolean createUserFolderIfNecessary(View newView, int container, CellLayout target,
1910             int[] targetCell, float distance, boolean external, DragObject d) {
1911         if (distance > target.getFolderCreationRadius(targetCell)) return false;
1912         View v = target.getChildAt(targetCell[0], targetCell[1]);
1913 
1914         boolean hasntMoved = false;
1915         if (mDragInfo != null) {
1916             CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell);
1917             hasntMoved = (mDragInfo.cellX == targetCell[0] &&
1918                     mDragInfo.cellY == targetCell[1]) && (cellParent == target);
1919         }
1920 
1921         if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false;
1922         mCreateUserFolderOnDrop = false;
1923         final int screenId = getCellLayoutId(target);
1924 
1925         boolean aboveShortcut = Folder.willAccept(v.getTag());
1926         boolean willBecomeShortcut = Folder.willAccept(newView.getTag());
1927 
1928         if (aboveShortcut && willBecomeShortcut) {
1929             ItemInfo sourceInfo = (ItemInfo) newView.getTag();
1930             ItemInfo destInfo = (ItemInfo) v.getTag();
1931             // if the drag started here, we need to remove it from the workspace
1932             if (!external) {
1933                 getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
1934             }
1935 
1936             Rect folderLocation = new Rect();
1937             float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation);
1938             target.removeView(v);
1939             mStatsLogManager.logger().withItemInfo(destInfo).withInstanceId(d.logInstanceId)
1940                     .log(LauncherEvent.LAUNCHER_ITEM_DROP_FOLDER_CREATED);
1941             FolderIcon fi = mLauncher.addFolder(target, container, screenId, targetCell[0],
1942                     targetCell[1]);
1943             destInfo.cellX = -1;
1944             destInfo.cellY = -1;
1945             sourceInfo.cellX = -1;
1946             sourceInfo.cellY = -1;
1947 
1948             // If the dragView is null, we can't animate
1949             boolean animate = d != null;
1950             if (animate) {
1951                 // In order to keep everything continuous, we hand off the currently rendered
1952                 // folder background to the newly created icon. This preserves animation state.
1953                 fi.setFolderBackground(mFolderCreateBg);
1954                 mFolderCreateBg = new PreviewBackground(getContext());
1955                 fi.performCreateAnimation(destInfo, v, sourceInfo, d, folderLocation, scale);
1956             } else {
1957                 fi.prepareCreateAnimation(v);
1958                 fi.addItem(destInfo);
1959                 fi.addItem(sourceInfo);
1960             }
1961             return true;
1962         }
1963         return false;
1964     }
1965 
addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell, float distance, DragObject d, boolean external)1966     boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell,
1967             float distance, DragObject d, boolean external) {
1968         if (distance > target.getFolderCreationRadius(targetCell)) return false;
1969 
1970         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
1971         if (!mAddToExistingFolderOnDrop) return false;
1972         mAddToExistingFolderOnDrop = false;
1973 
1974         if (dropOverView instanceof FolderIcon) {
1975             FolderIcon fi = (FolderIcon) dropOverView;
1976             if (fi.acceptDrop(d.dragInfo)) {
1977                 mStatsLogManager.logger().withItemInfo(fi.mInfo).withInstanceId(d.logInstanceId)
1978                         .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON);
1979                 fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
1980                 // if the drag started here, we need to remove it from the workspace
1981                 if (!external) {
1982                     getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
1983                 }
1984                 return true;
1985             }
1986         }
1987         return false;
1988     }
1989 
1990     @Override
prepareAccessibilityDrop()1991     public void prepareAccessibilityDrop() {}
1992 
1993     @Override
onDrop(final DragObject d, DragOptions options)1994     public void onDrop(final DragObject d, DragOptions options) {
1995         mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
1996         CellLayout dropTargetLayout = mDropToLayout;
1997 
1998         // We want the point to be mapped to the dragTarget.
1999         if (dropTargetLayout != null) {
2000             mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
2001         }
2002 
2003         boolean droppedOnOriginalCell = false;
2004 
2005         boolean snappedToNewPage = false;
2006         boolean resizeOnDrop = false;
2007         Runnable onCompleteRunnable = null;
2008         if (d.dragSource != this || mDragInfo == null) {
2009             final int[] touchXY = new int[]{(int) mDragViewVisualCenter[0],
2010                     (int) mDragViewVisualCenter[1]};
2011             onDropExternal(touchXY, dropTargetLayout, d);
2012         } else {
2013             final View cell = mDragInfo.cell;
2014             boolean droppedOnOriginalCellDuringTransition = false;
2015 
2016             if (dropTargetLayout != null && !d.cancelled) {
2017                 // Move internally
2018                 boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
2019                 boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
2020                 int container = hasMovedIntoHotseat ?
2021                         LauncherSettings.Favorites.CONTAINER_HOTSEAT :
2022                         LauncherSettings.Favorites.CONTAINER_DESKTOP;
2023                 int screenId = (mTargetCell[0] < 0) ?
2024                         mDragInfo.screenId : getCellLayoutId(dropTargetLayout);
2025                 int spanX = mDragInfo != null ? mDragInfo.spanX : 1;
2026                 int spanY = mDragInfo != null ? mDragInfo.spanY : 1;
2027                 // First we find the cell nearest to point at which the item is
2028                 // dropped, without any consideration to whether there is an item there.
2029 
2030                 mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
2031                         mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
2032                 float distance = dropTargetLayout.getDistanceFromWorkspaceCellVisualCenter(
2033                         mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
2034 
2035                 // If the item being dropped is a shortcut and the nearest drop
2036                 // cell also contains a shortcut, then create a folder with the two shortcuts.
2037                 if (createUserFolderIfNecessary(cell, container, dropTargetLayout, mTargetCell,
2038                         distance, false, d)
2039                         || addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
2040                         distance, d, false)) {
2041                     if (!mLauncher.isInState(EDIT_MODE)) {
2042                         mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
2043                     }
2044                     return;
2045                 }
2046 
2047                 // Aside from the special case where we're dropping a shortcut onto a shortcut,
2048                 // we need to find the nearest cell location that is vacant
2049                 ItemInfo item = d.dragInfo;
2050                 int minSpanX = item.spanX;
2051                 int minSpanY = item.spanY;
2052                 if (item.minSpanX > 0 && item.minSpanY > 0) {
2053                     minSpanX = item.minSpanX;
2054                     minSpanY = item.minSpanY;
2055                 }
2056 
2057                 CellPos originalPresenterPos = getCellPosMapper().mapModelToPresenter(item);
2058                 droppedOnOriginalCell = originalPresenterPos.screenId == screenId
2059                         && item.container == container
2060                         && originalPresenterPos.cellX == mTargetCell[0]
2061                         && originalPresenterPos.cellY == mTargetCell[1];
2062                 droppedOnOriginalCellDuringTransition = droppedOnOriginalCell && mIsSwitchingState;
2063 
2064                 // When quickly moving an item, a user may accidentally rearrange their
2065                 // workspace. So instead we move the icon back safely to its original position.
2066                 boolean returnToOriginalCellToPreventShuffling = !isFinishedSwitchingState()
2067                         && !droppedOnOriginalCellDuringTransition && !dropTargetLayout
2068                         .isRegionVacant(mTargetCell[0], mTargetCell[1], spanX, spanY);
2069                 int[] resultSpan = new int[2];
2070                 if (returnToOriginalCellToPreventShuffling) {
2071                     mTargetCell[0] = mTargetCell[1] = -1;
2072                 } else {
2073                     mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
2074                             (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
2075                             cell, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
2076                 }
2077 
2078                 boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
2079 
2080                 // if the widget resizes on drop
2081                 if (foundCell && (cell instanceof AppWidgetHostView) &&
2082                         (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
2083                     resizeOnDrop = true;
2084                     item.spanX = resultSpan[0];
2085                     item.spanY = resultSpan[1];
2086                     AppWidgetHostView awhv = (AppWidgetHostView) cell;
2087                     WidgetSizes.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
2088                             resultSpan[1]);
2089                 }
2090 
2091                 if (foundCell) {
2092                     int targetScreenIndex = getPageIndexForScreenId(screenId);
2093                     int snapScreen = getLeftmostVisiblePageForIndex(targetScreenIndex);
2094                     // On large screen devices two pages can be shown at the same time, and snap
2095                     // isn't needed if the source and target screens appear at the same time
2096                     if (snapScreen != mCurrentPage && !hasMovedIntoHotseat) {
2097                         snapToPage(snapScreen);
2098                         snappedToNewPage = true;
2099                     }
2100                     final ItemInfo info = (ItemInfo) cell.getTag();
2101                     if (hasMovedLayouts) {
2102                         // Reparent the view
2103                         CellLayout parentCell = getParentCellLayoutForView(cell);
2104                         if (parentCell != null) {
2105                             parentCell.removeView(cell);
2106                         } else if (mDragInfo.cell instanceof LauncherAppWidgetHostView) {
2107                             d.dragView.detachContentView(/* reattachToPreviousParent= */ false);
2108                         } else if (FeatureFlags.IS_STUDIO_BUILD) {
2109                             throw new NullPointerException("mDragInfo.cell has null parent");
2110                         }
2111                         addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
2112                                 info.spanX, info.spanY);
2113                     }
2114 
2115                     // update the item's position after drop
2116                     CellLayoutLayoutParams lp = (CellLayoutLayoutParams) cell.getLayoutParams();
2117                     lp.setTmpCellX(mTargetCell[0]);
2118                     lp.setCellX(mTargetCell[0]);
2119                     lp.setTmpCellY(mTargetCell[1]);
2120                     lp.setCellY(mTargetCell[1]);
2121                     lp.cellHSpan = item.spanX;
2122                     lp.cellVSpan = item.spanY;
2123                     lp.isLockedToGrid = true;
2124 
2125                     if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
2126                             cell instanceof LauncherAppWidgetHostView) {
2127 
2128                         // We post this call so that the widget has a chance to be placed
2129                         // in its final location
2130                         onCompleteRunnable = getWidgetResizeFrameRunnable(options,
2131                                 (LauncherAppWidgetHostView) cell, dropTargetLayout);
2132                     }
2133                     mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
2134                             lp.getCellX(), lp.getCellY(), item.spanX, item.spanY);
2135                 } else {
2136                     if (!returnToOriginalCellToPreventShuffling) {
2137                         onNoCellFound(dropTargetLayout, d.dragInfo, d.logInstanceId);
2138                     }
2139                     if (mDragInfo.cell instanceof LauncherAppWidgetHostView) {
2140                         d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
2141                     }
2142 
2143                     // If we can't find a drop location, we return the item to its original position
2144                     CellLayoutLayoutParams lp = (CellLayoutLayoutParams) cell.getLayoutParams();
2145                     mTargetCell[0] = lp.getCellX();
2146                     mTargetCell[1] = lp.getCellY();
2147                     CellLayout layout = (CellLayout) cell.getParent().getParent();
2148                     layout.markCellsAsOccupiedForView(cell);
2149                 }
2150             } else {
2151                 // When drag is cancelled, reattach content view back to its original parent.
2152                 if (cell instanceof LauncherAppWidgetHostView) {
2153                     d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
2154 
2155                     final CellLayout cellLayout = getParentCellLayoutForView(cell);
2156                     boolean pageIsVisible = isVisible(cellLayout);
2157 
2158                     if (pageIsVisible) {
2159                         onCompleteRunnable = getWidgetResizeFrameRunnable(options,
2160                                 (LauncherAppWidgetHostView) cell, cellLayout);
2161                     }
2162                 }
2163             }
2164 
2165             final CellLayout parent = (CellLayout) cell.getParent().getParent();
2166             if (d.dragView.hasDrawn()) {
2167                 if (droppedOnOriginalCellDuringTransition) {
2168                     // Animate the item to its original position, while simultaneously exiting
2169                     // spring-loaded mode so the page meets the icon where it was picked up.
2170                     final RunnableList callbackList = new RunnableList();
2171                     final Runnable onCompleteCallback = onCompleteRunnable;
2172                     LauncherState currentState = mLauncher.getStateManager().getState();
2173                     mLauncher.getDragController().animateDragViewToOriginalPosition(
2174                             /* onComplete= */ callbackList::executeAllAndDestroy, cell,
2175                             currentState.getTransitionDuration(mLauncher, true /* isToState */));
2176                     if (!mLauncher.isInState(EDIT_MODE)) {
2177                         mLauncher.getStateManager().goToState(NORMAL, /* delay= */ 0,
2178                                 onCompleteCallback == null
2179                                         ? null
2180                                         : forSuccessCallback(
2181                                                 () -> callbackList.add(onCompleteCallback)));
2182                     } else if (onCompleteCallback != null) {
2183                         forSuccessCallback(() -> callbackList.add(onCompleteCallback));
2184                     }
2185                     mLauncher.getDropTargetBar().onDragEnd();
2186                     parent.onDropChild(cell);
2187                     return;
2188                 }
2189                 final ItemInfo info = (ItemInfo) cell.getTag();
2190                 boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
2191                         || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
2192                 if (isWidget && dropTargetLayout != null) {
2193                     // animate widget to a valid place
2194                     int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
2195                             ANIMATE_INTO_POSITION_AND_DISAPPEAR;
2196                     animateWidgetDrop(info, parent, d.dragView, null, animationType, cell, false);
2197                 } else {
2198                     int duration = snappedToNewPage ? ADJACENT_SCREEN_DROP_DURATION : -1;
2199                     mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
2200                             this);
2201                 }
2202             } else {
2203                 d.deferDragViewCleanupPostAnimation = false;
2204                 cell.setVisibility(VISIBLE);
2205             }
2206             parent.onDropChild(cell);
2207 
2208             if (!mLauncher.isInState(EDIT_MODE)) {
2209                 mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY,
2210                         onCompleteRunnable == null ? null : forSuccessCallback(onCompleteRunnable));
2211             } else if (onCompleteRunnable != null) {
2212                 forSuccessCallback(onCompleteRunnable);
2213             }
2214             mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
2215                     .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
2216         }
2217 
2218         if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
2219             d.stateAnnouncer.completeAction(R.string.item_moved);
2220         }
2221     }
2222 
2223     @Nullable
getWidgetResizeFrameRunnable(DragOptions options, LauncherAppWidgetHostView hostView, CellLayout cellLayout)2224     private Runnable getWidgetResizeFrameRunnable(DragOptions options,
2225             LauncherAppWidgetHostView hostView, CellLayout cellLayout) {
2226         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
2227         if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
2228             return () -> {
2229                 if (!isPageInTransition()) {
2230                     AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
2231                 }
2232             };
2233         }
2234         return null;
2235     }
2236 
2237     public void onNoCellFound(
2238             View dropTargetLayout, ItemInfo itemInfo, @Nullable InstanceId logInstanceId) {
2239         int strId = mLauncher.isHotseatLayout(dropTargetLayout)
2240                 ? R.string.hotseat_out_of_space : R.string.out_of_space;
2241         Toast.makeText(mLauncher, mLauncher.getString(strId), Toast.LENGTH_SHORT).show();
2242         StatsLogManager.StatsLogger logger = mStatsLogManager.logger().withItemInfo(itemInfo);
2243         if (logInstanceId != null) {
2244             logger = logger.withInstanceId(logInstanceId);
2245         }
2246         logger.log(LauncherEvent.LAUNCHER_ITEM_DROP_FAILED_INSUFFICIENT_SPACE);
2247     }
2248 
2249     /**
2250      * Computes and returns the area relative to dragLayer which is used to display a page.
2251      * In case we have multiple pages displayed at the same time, we return the union of the areas.
2252      */
2253     public Rect getPageAreaRelativeToDragLayer() {
2254         Rect area = new Rect();
2255         int nextPage = getNextPage();
2256         int panelCount = getPanelCount();
2257         for (int page = nextPage; page < nextPage + panelCount; page++) {
2258             CellLayout child = (CellLayout) getChildAt(page);
2259             if (child == null) {
2260                 break;
2261             }
2262 
2263             ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets();
2264             Rect tmpRect = new Rect();
2265             mLauncher.getDragLayer().getDescendantRectRelativeToSelf(boundingLayout, tmpRect);
2266             area.union(tmpRect);
2267         }
2268 
2269         return area;
2270     }
2271 
2272     @Override
2273     public void onDragEnter(DragObject d) {
2274         if (ENFORCE_DRAG_EVENT_ORDER) {
2275             enforceDragParity("onDragEnter", 1, 1);
2276         }
2277 
2278         mCreateUserFolderOnDrop = false;
2279         mAddToExistingFolderOnDrop = false;
2280 
2281         mDropToLayout = null;
2282         mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
2283         setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1]);
2284     }
2285 
2286     @Override
2287     public void onDragExit(DragObject d) {
2288         if (ENFORCE_DRAG_EVENT_ORDER) {
2289             enforceDragParity("onDragExit", -1, 0);
2290         }
2291 
2292         // Here we store the final page that will be dropped to, if the workspace in fact
2293         // receives the drop
2294         mDropToLayout = mDragTargetLayout;
2295         if (mDragMode == DRAG_MODE_CREATE_FOLDER) {
2296             mCreateUserFolderOnDrop = true;
2297         } else if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) {
2298             mAddToExistingFolderOnDrop = true;
2299         }
2300 
2301         // Reset the previous drag target
2302         setCurrentDropLayout(null);
2303         setCurrentDragOverlappingLayout(null);
2304 
2305         mSpringLoadedDragController.cancel();
2306     }
2307 
2308     private void enforceDragParity(String event, int update, int expectedValue) {
2309         enforceDragParity(this, event, update, expectedValue);
2310         for (int i = 0; i < getChildCount(); i++) {
2311             enforceDragParity(getChildAt(i), event, update, expectedValue);
2312         }
2313     }
2314 
2315     private void enforceDragParity(View v, String event, int update, int expectedValue) {
2316         Object tag = v.getTag(R.id.drag_event_parity);
2317         int value = tag == null ? 0 : (Integer) tag;
2318         value += update;
2319         v.setTag(R.id.drag_event_parity, value);
2320 
2321         if (value != expectedValue) {
2322             Log.e(TAG, event + ": Drag contract violated: " + value);
2323         }
2324     }
2325 
2326     void setCurrentDropLayout(CellLayout layout) {
2327         if (mDragTargetLayout != null) {
2328             mDragTargetLayout.revertTempState();
2329             mDragTargetLayout.onDragExit();
2330         }
2331         mDragTargetLayout = layout;
2332         if (mDragTargetLayout != null) {
2333             mDragTargetLayout.onDragEnter();
2334         }
2335         cleanupReorder(true);
2336         cleanupFolderCreation();
2337         setCurrentDropOverCell(-1, -1);
2338     }
2339 
2340     void setCurrentDragOverlappingLayout(CellLayout layout) {
2341         if (mDragOverlappingLayout != null) {
2342             mDragOverlappingLayout.setIsDragOverlapping(false);
2343         }
2344         mDragOverlappingLayout = layout;
2345         if (mDragOverlappingLayout != null) {
2346             mDragOverlappingLayout.setIsDragOverlapping(true);
2347         }
2348     }
2349 
2350     void setCurrentDropOverCell(int x, int y) {
2351         if (x != mDragOverX || y != mDragOverY) {
2352             mDragOverX = x;
2353             mDragOverY = y;
2354             setDragMode(DRAG_MODE_NONE);
2355         }
2356     }
2357 
2358     void setDragMode(int dragMode) {
2359         if (dragMode != mDragMode) {
2360             if (dragMode == DRAG_MODE_NONE) {
2361                 cleanupAddToFolder();
2362                 // We don't want to cancel the re-order alarm every time the target cell changes
2363                 // as this feels to slow / unresponsive.
2364                 cleanupReorder(false);
2365                 cleanupFolderCreation();
2366             } else if (dragMode == DRAG_MODE_ADD_TO_FOLDER) {
2367                 cleanupReorder(true);
2368                 cleanupFolderCreation();
2369             } else if (dragMode == DRAG_MODE_CREATE_FOLDER) {
2370                 cleanupAddToFolder();
2371                 cleanupReorder(true);
2372             } else if (dragMode == DRAG_MODE_REORDER) {
2373                 cleanupAddToFolder();
2374                 cleanupFolderCreation();
2375             }
2376             mDragMode = dragMode;
2377         }
2378     }
2379 
2380     protected void cleanupFolderCreation() {
2381         if (mFolderCreateBg != null) {
2382             mFolderCreateBg.animateToRest();
2383         }
2384 
2385         if (mDragOverView instanceof AppPairIcon api) {
2386             api.getIconDrawableArea().onTemporaryContainerChange(null);
2387             mDragOverView = null;
2388         }
2389     }
2390 
2391     private void cleanupAddToFolder() {
2392         if (mDragOverFolderIcon != null) {
2393             mDragOverFolderIcon.onDragExit();
2394             mDragOverFolderIcon = null;
2395         }
2396     }
2397 
2398     protected void cleanupReorder(boolean cancelAlarm) {
2399         // Any pending reorders are canceled
2400         if (cancelAlarm) {
2401             mReorderAlarm.cancelAlarm();
2402         }
2403         mLastReorderX = -1;
2404         mLastReorderY = -1;
2405     }
2406 
2407     /*
2408      *
2409      * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
2410      * coordinate space. The argument xy is modified with the return result.
2411      */
2412     private void mapPointFromSelfToChild(View v, float[] xy) {
2413         xy[0] = xy[0] - v.getLeft();
2414         xy[1] = xy[1] - v.getTop();
2415     }
2416 
2417     /**
2418      * Updates the point in {@param xy} to point to the co-ordinate space of {@param layout}
2419      *
2420      * @param layout either hotseat of a page in workspace
2421      * @param xy     the point location in workspace co-ordinate space
2422      */
2423     private void mapPointFromDropLayout(CellLayout layout, float[] xy) {
2424         if (mLauncher.isHotseatLayout(layout)) {
2425             mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true);
2426             mLauncher.getDragLayer().mapCoordInSelfToDescendant(layout, xy);
2427         } else {
2428             mapPointFromSelfToChild(layout, xy);
2429         }
2430     }
2431 
2432     private boolean isDragWidget(DragObject d) {
2433         return (d.dragInfo instanceof LauncherAppWidgetInfo ||
2434                 d.dragInfo instanceof PendingAddWidgetInfo);
2435     }
2436 
2437     public void onDragOver(DragObject d) {
2438         // Skip drag over events while we are dragging over side pages
2439         if (!transitionStateShouldAllowDrop()) return;
2440 
2441         ItemInfo item = d.dragInfo;
2442         if (item == null) {
2443             if (FeatureFlags.IS_STUDIO_BUILD) {
2444                 throw new NullPointerException("DragObject has null info");
2445             }
2446             return;
2447         }
2448 
2449         // Ensure that we have proper spans for the item that we are dropping
2450         if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found");
2451         mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
2452 
2453         final View child = (mDragInfo == null) ? null : mDragInfo.cell;
2454         if (setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1])) {
2455             if (mDragTargetLayout == null || mLauncher.isHotseatLayout(mDragTargetLayout)) {
2456                 mSpringLoadedDragController.cancel();
2457             } else {
2458                 mSpringLoadedDragController.setAlarm(mDragTargetLayout);
2459             }
2460         }
2461 
2462         // Handle the drag over
2463         if (mDragTargetLayout != null) {
2464             // We want the point to be mapped to the dragTarget.
2465             mapPointFromDropLayout(mDragTargetLayout, mDragViewVisualCenter);
2466 
2467             int minSpanX = item.spanX;
2468             int minSpanY = item.spanY;
2469             if (item.minSpanX > 0 && item.minSpanY > 0) {
2470                 minSpanX = item.minSpanX;
2471                 minSpanY = item.minSpanY;
2472             }
2473 
2474             mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
2475                     (int) mDragViewVisualCenter[1], item.spanX, item.spanY,
2476                     mDragTargetLayout, mTargetCell);
2477             int reorderX = mTargetCell[0];
2478             int reorderY = mTargetCell[1];
2479 
2480             setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);
2481 
2482             float targetCellDistance = mDragTargetLayout.getDistanceFromWorkspaceCellVisualCenter(
2483                     mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
2484 
2485             manageFolderFeedback(targetCellDistance, d);
2486 
2487             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
2488                             mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
2489                     item.spanY, child, mTargetCell);
2490 
2491             manageReorderOnDragOver(d, targetCellDistance, nearestDropOccupied, minSpanX, minSpanY,
2492                     reorderX, reorderY);
2493 
2494             if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
2495                     !nearestDropOccupied) {
2496                 if (mDragTargetLayout != null) {
2497                     mDragTargetLayout.revertTempState();
2498                 }
2499             }
2500         }
2501     }
2502 
2503     protected void manageReorderOnDragOver(DragObject d, float targetCellDistance,
2504             boolean nearestDropOccupied, int minSpanX, int minSpanY, int reorderX, int reorderY) {
2505 
2506         ItemInfo item = d.dragInfo;
2507         final View child = (mDragInfo == null) ? null : mDragInfo.cell;
2508         if (!nearestDropOccupied) {
2509             int[] span = new int[2];
2510             mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
2511                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
2512                     child, mTargetCell, span, CellLayout.MODE_SHOW_REORDER_HINT);
2513             mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1], span[0],
2514                     span[1], d);
2515             nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
2516                             mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
2517                     item.spanY, child, mTargetCell);
2518         } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
2519                 && (mLastReorderX != reorderX || mLastReorderY != reorderY)
2520                 && targetCellDistance < mDragTargetLayout.getReorderRadius(mTargetCell, item.spanX,
2521                 item.spanY)) {
2522             mReorderAlarm.cancelAlarm();
2523             mLastReorderX = reorderX;
2524             mLastReorderY = reorderY;
2525             mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
2526                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
2527                     child, mTargetCell, new int[2], CellLayout.MODE_SHOW_REORDER_HINT);
2528             // Otherwise, if we aren't adding to or creating a folder and there's no pending
2529             // reorder, then we schedule a reorder
2530             ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
2531                     minSpanX, minSpanY, item.spanX, item.spanY, d, child);
2532             mReorderAlarm.setOnAlarmListener(listener);
2533             mReorderAlarm.setAlarm(REORDER_TIMEOUT);
2534         }
2535     }
2536 
2537     /**
2538      * Updates {@link #mDragTargetLayout} and {@link #mDragOverlappingLayout}
2539      * based on the DragObject's position.
2540      *
2541      * The layout will be:
2542      * - The Hotseat if the drag object is over it
2543      * - A side page if we are in spring-loaded mode and the drag object is over it
2544      * - The current page otherwise
2545      *
2546      * @return whether the layout is different from the current {@link #mDragTargetLayout}.
2547      */
2548     private boolean setDropLayoutForDragObject(DragObject d, float centerX, float centerY) {
2549         CellLayout layout = null;
2550         if (shouldUseHotseatAsDropLayout(d)) {
2551             layout = mLauncher.getHotseat();
2552         } else if (!isDragObjectOverSmartSpace(d)) {
2553             // If the object is over qsb/smartspace, we don't want to highlight anything.
2554 
2555             // Check neighbour pages
2556             layout = checkDragObjectIsOverNeighbourPages(d, centerX);
2557 
2558             if (layout == null) {
2559                 // Check visible pages
2560                 IntSet visiblePageIndices = getVisiblePageIndices();
2561                 for (int visiblePageIndex : visiblePageIndices) {
2562                     layout = verifyInsidePage(visiblePageIndex, d.x, d.y);
2563                     if (layout != null) break;
2564                 }
2565             }
2566         }
2567 
2568         // Update the current drop layout if the target changed
2569         if (layout != mDragTargetLayout) {
2570             setCurrentDropLayout(layout);
2571             setCurrentDragOverlappingLayout(layout);
2572             return true;
2573         }
2574         return false;
2575     }
2576 
2577     private boolean shouldUseHotseatAsDropLayout(DragObject dragObject) {
2578         if (mLauncher.getHotseat() == null
2579                 || mLauncher.getHotseat().getShortcutsAndWidgets() == null
2580                 || isDragWidget(dragObject)) {
2581             return false;
2582         }
2583         View hotseatShortcuts = mLauncher.getHotseat().getShortcutsAndWidgets();
2584         getViewBoundsRelativeToWorkspace(hotseatShortcuts, mTempRect);
2585         return mTempRect.contains(dragObject.x, dragObject.y);
2586     }
2587 
2588     private boolean isDragObjectOverSmartSpace(DragObject dragObject) {
2589         if (mFirstPagePinnedItem == null) {
2590             return false;
2591         }
2592         getViewBoundsRelativeToWorkspace(mFirstPagePinnedItem, mTempRect);
2593         return mTempRect.contains(dragObject.x, dragObject.y);
2594     }
2595 
2596     private CellLayout checkDragObjectIsOverNeighbourPages(DragObject d, float centerX) {
2597         if (isPageInTransition()) {
2598             return null;
2599         }
2600 
2601         // Check the workspace pages whether the object is over any of them
2602 
2603         // Note, centerX represents the center of the object that is being dragged, visually.
2604         // d.x represents the location of the finger within the dragged item.
2605         float touchX;
2606         float touchY = d.y;
2607 
2608         // Go through the pages and check if the dragged item is inside one of them. This block
2609         // is responsible for determining whether we need to snap to a different screen.
2610         int nextPage = getNextPage();
2611         IntSet pageIndexesToVerify = IntSet.wrap(nextPage - 1,
2612                 nextPage + (isTwoPanelEnabled() ? 2 : 1));
2613 
2614         for (int pageIndex : pageIndexesToVerify) {
2615             // When deciding whether to perform a page switch, we need to consider the most
2616             // extreme X coordinate between the finger location and the center of the object
2617             // being dragged. This is either the max or the min of the two depending on whether
2618             // dragging to the left / right, respectively.
2619             touchX = (((pageIndex < nextPage) && !mIsRtl) || (pageIndex > nextPage && mIsRtl))
2620                     ? Math.min(d.x, centerX) : Math.max(d.x, centerX);
2621             CellLayout layout = verifyInsidePage(pageIndex, touchX, touchY);
2622             if (layout != null) {
2623                 return layout;
2624             }
2625         }
2626         return null;
2627     }
2628 
2629     /**
2630      * Gets the given view's bounds relative to Workspace
2631      */
2632     private void getViewBoundsRelativeToWorkspace(View view, Rect outRect) {
2633         mLauncher.getDragLayer()
2634                 .getDescendantRectRelativeToSelf(view, mTempRect);
2635         // map draglayer relative bounds to workspace
2636         mLauncher.getDragLayer().mapRectInSelfToDescendant(this, mTempRect);
2637         outRect.set(mTempRect);
2638     }
2639 
2640     /**
2641      * Returns the child CellLayout if the point is inside the page coordinates, null otherwise.
2642      */
2643     private CellLayout verifyInsidePage(int pageNo, float x, float y) {
2644         if (pageNo >= 0 && pageNo < getPageCount()) {
2645             CellLayout cl = (CellLayout) getChildAt(pageNo);
2646             if (x >= cl.getLeft() && x <= cl.getRight()
2647                     && y >= cl.getTop() && y <= cl.getBottom()) {
2648                 // This point is inside the cell layout
2649                 return cl;
2650             }
2651         }
2652         return null;
2653     }
2654 
2655     private void manageFolderFeedback(float distance, DragObject dragObject) {
2656         if (distance > mDragTargetLayout.getFolderCreationRadius(mTargetCell)) {
2657             if ((mDragMode == DRAG_MODE_ADD_TO_FOLDER
2658                     || mDragMode == DRAG_MODE_CREATE_FOLDER)) {
2659                 setDragMode(DRAG_MODE_NONE);
2660             }
2661             return;
2662         }
2663 
2664         mDragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]);
2665         ItemInfo info = dragObject.dragInfo;
2666         boolean userFolderPending = willCreateUserFolder(info, mDragOverView, false);
2667         if (mDragMode == DRAG_MODE_NONE && userFolderPending) {
2668 
2669             mFolderCreateBg = new PreviewBackground(getContext());
2670             mFolderCreateBg.setup(mLauncher, mLauncher, null,
2671                     mDragOverView.getMeasuredWidth(), mDragOverView.getPaddingTop());
2672 
2673             // The full preview background should appear behind the icon
2674             mFolderCreateBg.isClipping = false;
2675 
2676             if (mDragOverView instanceof AppPairIcon api) {
2677                 api.getIconDrawableArea().onTemporaryContainerChange(DISPLAY_FOLDER);
2678             }
2679 
2680             mFolderCreateBg.animateToAccept(mDragTargetLayout, mTargetCell[0], mTargetCell[1]);
2681             mDragTargetLayout.clearDragOutlines();
2682             setDragMode(DRAG_MODE_CREATE_FOLDER);
2683 
2684             if (dragObject.stateAnnouncer != null) {
2685                 dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
2686                         .getDescriptionForDropOver(mDragOverView, getContext()));
2687             }
2688             return;
2689         }
2690 
2691         boolean willAddToFolder = willAddToExistingUserFolder(info, mDragOverView);
2692         if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
2693             mDragOverFolderIcon = ((FolderIcon) mDragOverView);
2694             mDragOverFolderIcon.onDragEnter(info);
2695             if (mDragTargetLayout != null) {
2696                 mDragTargetLayout.clearDragOutlines();
2697             }
2698             setDragMode(DRAG_MODE_ADD_TO_FOLDER);
2699 
2700             if (dragObject.stateAnnouncer != null) {
2701                 dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
2702                         .getDescriptionForDropOver(mDragOverView, getContext()));
2703             }
2704             return;
2705         }
2706 
2707         if (mDragMode == DRAG_MODE_ADD_TO_FOLDER && !willAddToFolder) {
2708             setDragMode(DRAG_MODE_NONE);
2709         }
2710         if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) {
2711             setDragMode(DRAG_MODE_NONE);
2712         }
2713     }
2714 
2715     class ReorderAlarmListener implements OnAlarmListener {
2716         final float[] dragViewCenter;
2717         final int minSpanX, minSpanY, spanX, spanY;
2718         final DragObject dragObject;
2719         final View child;
2720 
2721         public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX,
2722                                     int spanY, DragObject dragObject, View child) {
2723             this.dragViewCenter = dragViewCenter;
2724             this.minSpanX = minSpanX;
2725             this.minSpanY = minSpanY;
2726             this.spanX = spanX;
2727             this.spanY = spanY;
2728             this.child = child;
2729             this.dragObject = dragObject;
2730         }
2731 
2732         public void onAlarm(Alarm alarm) {
2733             int[] resultSpan = new int[2];
2734             mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
2735                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, mDragTargetLayout,
2736                     mTargetCell);
2737 
2738             mTargetCell = mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
2739                     (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
2740                     child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
2741 
2742             if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
2743                 mDragTargetLayout.revertTempState();
2744             } else {
2745                 setDragMode(DRAG_MODE_REORDER);
2746             }
2747 
2748             mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
2749                     resultSpan[0], resultSpan[1], dragObject);
2750         }
2751     }
2752 
2753     @Override
2754     public void getHitRectRelativeToDragLayer(Rect outRect) {
2755         // We want the workspace to have the whole area of the display (it will find the correct
2756         // cell layout to drop to in the existing drag/drop logic.
2757         mLauncher.getDragLayer().getDescendantRectRelativeToSelf(this, outRect);
2758     }
2759 
2760     /**
2761      * Drop an item that didn't originate on one of the workspace screens.
2762      * It may have come from Launcher (e.g. from all apps or customize), or it may have
2763      * come from another app altogether.
2764      * <p>
2765      * NOTE: This can also be called when we are outside of a drag event, when we want
2766      * to add an item to one of the workspace screens.
2767      */
2768     private void onDropExternal(final int[] touchXY, final CellLayout cellLayout, DragObject d) {
2769         if (d.dragInfo instanceof PendingAddShortcutInfo) {
2770             WorkspaceItemInfo si = ((PendingAddShortcutInfo) d.dragInfo)
2771                     .getActivityInfo(mLauncher).createWorkspaceItemInfo();
2772             if (si != null) {
2773                 d.dragInfo = si;
2774             }
2775         }
2776 
2777         ItemInfo info = d.dragInfo;
2778         int spanX = info.spanX;
2779         int spanY = info.spanY;
2780         if (mDragInfo != null) {
2781             spanX = mDragInfo.spanX;
2782             spanY = mDragInfo.spanY;
2783         }
2784 
2785         final int container = mLauncher.isHotseatLayout(cellLayout)
2786                 ? LauncherSettings.Favorites.CONTAINER_HOTSEAT
2787                 : LauncherSettings.Favorites.CONTAINER_DESKTOP;
2788         final int screenId = getCellLayoutId(cellLayout);
2789         if (!mLauncher.isHotseatLayout(cellLayout)
2790                 && screenId != getScreenIdForPageIndex(mCurrentPage)
2791                 && !mLauncher.isInState(SPRING_LOADED)
2792                 && !mLauncher.isInState(EDIT_MODE)) {
2793             snapToPage(getPageIndexForScreenId(screenId));
2794         }
2795 
2796         if (info instanceof PendingAddItemInfo) {
2797             final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) info;
2798 
2799             boolean findNearestVacantCell = true;
2800             if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
2801                 mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
2802                         cellLayout, mTargetCell);
2803                 float distance = cellLayout.getDistanceFromWorkspaceCellVisualCenter(
2804                         mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
2805                 if (willCreateUserFolder(d.dragInfo, cellLayout, mTargetCell, distance, true)
2806                         || willAddToExistingUserFolder(
2807                         d.dragInfo, cellLayout, mTargetCell, distance)) {
2808                     findNearestVacantCell = false;
2809                 }
2810             }
2811 
2812             final ItemInfo item = d.dragInfo;
2813             boolean updateWidgetSize = false;
2814             if (findNearestVacantCell) {
2815                 int minSpanX = item.spanX;
2816                 int minSpanY = item.spanY;
2817                 if (item.minSpanX > 0 && item.minSpanY > 0) {
2818                     minSpanX = item.minSpanX;
2819                     minSpanY = item.minSpanY;
2820                 }
2821                 int[] resultSpan = new int[2];
2822                 mTargetCell = cellLayout.performReorder((int) mDragViewVisualCenter[0],
2823                         (int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY,
2824                         null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL);
2825 
2826                 if (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY) {
2827                     updateWidgetSize = true;
2828                 }
2829                 item.spanX = resultSpan[0];
2830                 item.spanY = resultSpan[1];
2831             }
2832 
2833             Runnable onAnimationCompleteRunnable = new Runnable() {
2834                 @Override
2835                 public void run() {
2836                     // Normally removeExtraEmptyScreen is called in Workspace#onDrop, but when
2837                     // adding an item that may not be dropped right away (due to a config activity)
2838                     // we defer the removal until the activity returns.
2839                     deferRemoveExtraEmptyScreen();
2840 
2841                     // When dragging and dropping from customization tray, we deal with creating
2842                     // widgets/shortcuts/folders in a slightly different way
2843                     mLauncher.addPendingItem(pendingInfo, container, screenId, mTargetCell,
2844                             item.spanX, item.spanY);
2845                     mStatsLogManager.logger().withItemInfo(d.dragInfo)
2846                             .withInstanceId(d.logInstanceId)
2847                             .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
2848                 }
2849             };
2850             boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
2851                     || pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
2852 
2853             AppWidgetHostView finalView = isWidget ?
2854                     ((PendingAddWidgetInfo) pendingInfo).boundWidget : null;
2855 
2856             if (finalView != null && updateWidgetSize) {
2857                 WidgetSizes.updateWidgetSizeRanges(finalView, mLauncher, item.spanX, item.spanY);
2858             }
2859 
2860             int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR;
2861             if (isWidget && ((PendingAddWidgetInfo) pendingInfo).info != null &&
2862                     ((PendingAddWidgetInfo) pendingInfo).getHandler().needsConfigure()) {
2863                 animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN;
2864             }
2865             animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable,
2866                     animationStyle, finalView, true);
2867         } else {
2868             // This is for other drag/drop cases, like dragging from All Apps
2869             mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
2870             View view = mLauncher.getItemInflater()
2871                     .inflateItem(info, mLauncher.getModelWriter(), cellLayout);
2872             d.dragInfo = info = (ItemInfo) view.getTag();
2873 
2874             // First we find the cell nearest to point at which the item is
2875             // dropped, without any consideration to whether there is an item there.
2876             if (touchXY != null) {
2877                 mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
2878                         cellLayout, mTargetCell);
2879                 float distance = cellLayout.getDistanceFromWorkspaceCellVisualCenter(
2880                         mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
2881                 if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance,
2882                         true, d)) {
2883                     return;
2884                 }
2885                 if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d,
2886                         true)) {
2887                     return;
2888                 }
2889             }
2890 
2891             if (touchXY != null) {
2892                 // when dragging and dropping, just find the closest free spot
2893                 mTargetCell = cellLayout.performReorder((int) mDragViewVisualCenter[0],
2894                         (int) mDragViewVisualCenter[1], 1, 1, 1, 1,
2895                         null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL);
2896             } else {
2897                 cellLayout.findCellForSpan(mTargetCell, 1, 1);
2898             }
2899             // Add the item to DB before adding to screen ensures that the container and other
2900             // values of the info is properly updated.
2901             mLauncher.getModelWriter().addOrMoveItemInDatabase(info, container, screenId,
2902                     mTargetCell[0], mTargetCell[1]);
2903 
2904             addInScreen(view, container, screenId, mTargetCell[0], mTargetCell[1],
2905                     info.spanX, info.spanY);
2906             cellLayout.onDropChild(view);
2907             cellLayout.getShortcutsAndWidgets().measureChild(view);
2908 
2909             if (d.dragView != null) {
2910                 // We wrap the animation call in the temporary set and reset of the current
2911                 // cellLayout to its final transform -- this means we animate the drag view to
2912                 // the correct final location.
2913                 setFinalTransitionTransform();
2914                 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view, this);
2915                 resetTransitionTransform();
2916             }
2917             mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
2918                     .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
2919         }
2920 
2921     }
2922 
2923     private Drawable createWidgetDrawable(ItemInfo widgetInfo, View layout) {
2924         int[] unScaledSize = estimateItemSize(widgetInfo);
2925         int visibility = layout.getVisibility();
2926         layout.setVisibility(VISIBLE);
2927 
2928         int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY);
2929         int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY);
2930         layout.measure(width, height);
2931         layout.layout(0, 0, unScaledSize[0], unScaledSize[1]);
2932         Bitmap b = BitmapRenderer.createHardwareBitmap(
2933                 unScaledSize[0], unScaledSize[1], layout::draw);
2934         layout.setVisibility(visibility);
2935         return new FastBitmapDrawable(b);
2936     }
2937 
2938     private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY,
2939             DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, boolean scale,
2940             final View finalView) {
2941         // Now we animate the dragView, (ie. the widget or shortcut preview) into its final
2942         // location and size on the home screen.
2943         int spanX = info.spanX;
2944         int spanY = info.spanY;
2945 
2946         Rect r = estimateItemPosition(layout, targetCell[0], targetCell[1], spanX, spanY);
2947         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
2948             DeviceProfile profile = mLauncher.getDeviceProfile();
2949             if (finalView instanceof NavigableAppWidgetHostView) {
2950                 Rect widgetPadding = profile.widgetPadding;
2951                 r.left -= widgetPadding.left;
2952                 r.right += widgetPadding.right;
2953                 r.top -= widgetPadding.top;
2954                 r.bottom += widgetPadding.bottom;
2955             }
2956             PointF appWidgetScale = profile.getAppWidgetScale(null);
2957             Utilities.shrinkRect(r, appWidgetScale.x, appWidgetScale.y);
2958         }
2959 
2960         mTempFXY[0] = r.left;
2961         mTempFXY[1] = r.top;
2962         setFinalTransitionTransform();
2963         float cellLayoutScale =
2964                 mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, mTempFXY, true);
2965         resetTransitionTransform();
2966         Utilities.roundArray(mTempFXY, loc);
2967 
2968         if (scale) {
2969             float dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth();
2970             float dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight();
2971 
2972             // The animation will scale the dragView about its center, so we need to center about
2973             // the final location.
2974             loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2
2975                     - Math.ceil(layout.getUnusedHorizontalSpace() / 2f);
2976             loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2;
2977             scaleXY[0] = dragViewScaleX * cellLayoutScale;
2978             scaleXY[1] = dragViewScaleY * cellLayoutScale;
2979         } else {
2980             // Since we are not cross-fading the dragView, align the drag view to the
2981             // final cell position.
2982             float dragScale = dragView.getInitialScale() * cellLayoutScale;
2983             loc[0] += (dragScale - 1) * dragView.getWidth() / 2;
2984             loc[1] += (dragScale - 1) * dragView.getHeight() / 2;
2985             scaleXY[0] = scaleXY[1] = dragScale;
2986 
2987             // If a dragRegion was provided, offset the final position accordingly.
2988             Rect dragRegion = dragView.getDragRegion();
2989             if (dragRegion != null) {
2990                 loc[0] += cellLayoutScale * dragRegion.left;
2991                 loc[1] += cellLayoutScale * dragRegion.top;
2992             }
2993         }
2994     }
2995 
2996     public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
2997             final Runnable onCompleteRunnable, int animationType, @Nullable final View finalView,
2998             boolean external) {
2999         int[] finalPos = new int[2];
3000         float scaleXY[] = new float[2];
3001         boolean scalePreview = !(info instanceof PendingAddShortcutInfo);
3002         getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell,
3003                 scalePreview, finalView);
3004 
3005         Resources res = mLauncher.getResources();
3006         final int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200;
3007 
3008         boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET ||
3009                 info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
3010         if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external)
3011                 && finalView != null
3012                 && dragView.getContentView() != finalView) {
3013             Drawable crossFadeDrawable = createWidgetDrawable(info, finalView);
3014             dragView.crossFadeContent(crossFadeDrawable, (int) (duration * 0.8f));
3015         } else if (isWidget && external) {
3016             scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0], scaleXY[1]);
3017         }
3018 
3019         DragLayer dragLayer = mLauncher.getDragLayer();
3020         if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) {
3021             mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f,
3022                     DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
3023         } else {
3024             int endStyle;
3025             if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) {
3026                 endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE;
3027             } else {
3028                 endStyle = DragLayer.ANIMATION_END_DISAPPEAR;
3029             }
3030 
3031             Runnable onComplete = new Runnable() {
3032                 @Override
3033                 public void run() {
3034                     if (finalView != null) {
3035                         finalView.setVisibility(VISIBLE);
3036                     }
3037                     if (onCompleteRunnable != null) {
3038                         onCompleteRunnable.run();
3039                     }
3040                 }
3041             };
3042             dragLayer.animateViewIntoPosition(dragView, finalPos[0],
3043                     finalPos[1], 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
3044                     duration, this);
3045         }
3046     }
3047 
3048     public void setFinalTransitionTransform() {
3049         if (isSwitchingState()) {
3050             mCurrentScale = getScaleX();
3051             setScaleX(mStateTransitionAnimation.getFinalScale());
3052             setScaleY(mStateTransitionAnimation.getFinalScale());
3053         }
3054     }
3055 
3056     public void resetTransitionTransform() {
3057         if (isSwitchingState()) {
3058             setScaleX(mCurrentScale);
3059             setScaleY(mCurrentScale);
3060         }
3061     }
3062 
3063     /**
3064      * Return the current CellInfo describing our current drag; this method exists
3065      * so that Launcher can sync this object with the correct info when the activity is created/
3066      * destroyed
3067      */
3068     public CellInfo getDragInfo() {
3069         return mDragInfo;
3070     }
3071 
3072     /**
3073      * Calculate the nearest cell where the given object would be dropped.
3074      * <p>
3075      * pixelX and pixelY should be in the coordinate system of layout
3076      */
3077     @Thunk
3078     int[] findNearestArea(int pixelX, int pixelY,
3079                           int spanX, int spanY, CellLayout layout, int[] recycle) {
3080         return layout.findNearestAreaIgnoreOccupied(
3081                 pixelX, pixelY, spanX, spanY, recycle);
3082     }
3083 
3084     void setup(DragController dragController) {
3085         mSpringLoadedDragController = new SpringLoadedDragController(mLauncher);
3086         mDragController = dragController;
3087 
3088         // hardware layers on children are enabled on startup, but should be disabled until
3089         // needed
3090         updateChildrenLayersEnabled();
3091     }
3092 
3093     /**
3094      * Called at the end of a drag which originated on the workspace.
3095      */
3096     public void onDropCompleted(final View target, final DragObject d,
3097                                 final boolean success) {
3098         if (success) {
3099             if (target != this && mDragInfo != null) {
3100                 removeWorkspaceItem(mDragInfo.cell);
3101             }
3102         } else if (mDragInfo != null) {
3103             // When drag is cancelled, reattach content view back to its original parent.
3104             if (mDragInfo.cell instanceof LauncherAppWidgetHostView && d.dragView != null) {
3105                 d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
3106             }
3107             final CellLayout cellLayout = mLauncher.getCellLayout(
3108                     mDragInfo.container, mDragInfo.screenId);
3109             if (cellLayout != null) {
3110                 cellLayout.onDropChild(mDragInfo.cell);
3111             } else if (FeatureFlags.IS_STUDIO_BUILD) {
3112                 throw new RuntimeException("Invalid state: cellLayout == null in "
3113                         + "Workspace#onDropCompleted. Please file a bug. ");
3114             }
3115         }
3116         View cell = getHomescreenIconByItemId(d.originalDragInfo.id);
3117         if (d.cancelled && cell != null) {
3118             cell.setVisibility(VISIBLE);
3119         }
3120         mDragInfo = null;
3121     }
3122 
3123     /**
3124      * For opposite operation. See {@link #addInScreen}.
3125      */
3126     public void removeWorkspaceItem(View v) {
3127         CellLayout parentCell = getParentCellLayoutForView(v);
3128         if (parentCell != null) {
3129             parentCell.removeView(v);
3130         } else if (FeatureFlags.IS_STUDIO_BUILD) {
3131             // When an app is uninstalled using the drop target, we wait until resume to remove
3132             // the icon. We also remove all the corresponding items from the workspace at
3133             // {@link Launcher#bindComponentsRemoved}. That call can come before or after
3134             // {@link Launcher#mOnResumeCallbacks} depending on how busy the worker thread is.
3135             Log.e(TAG, "mDragInfo.cell has null parent");
3136         }
3137         if (v instanceof DropTarget) {
3138             mDragController.removeDropTarget((DropTarget) v);
3139         }
3140     }
3141 
3142     /**
3143      * Removed widget from workspace by appWidgetId
3144      *
3145      * @param appWidgetId
3146      */
3147     public void removeWidget(int appWidgetId) {
3148         mapOverItems((info, view) -> {
3149             if (info instanceof LauncherAppWidgetInfo) {
3150                 LauncherAppWidgetInfo appWidgetInfo = (LauncherAppWidgetInfo) info;
3151                 if (appWidgetInfo.appWidgetId == appWidgetId) {
3152                     mLauncher.removeItem(view, appWidgetInfo, true,
3153                             "widget is removed in response to widget remove broadcast");
3154                     return true;
3155                 }
3156             }
3157             return false;
3158         });
3159     }
3160 
3161     /**
3162      * Removes all folder listeners
3163      */
3164     public void removeFolderListeners() {
3165         mapOverItems(new ItemOperator() {
3166             @Override
3167             public boolean evaluate(ItemInfo info, View view) {
3168                 if (view instanceof FolderIcon) {
3169                     ((FolderIcon) view).removeListeners();
3170                 }
3171                 return false;
3172             }
3173         });
3174     }
3175 
3176     public boolean isDropEnabled() {
3177         return true;
3178     }
3179 
3180     @Override
3181     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
3182         // We don't dispatch restoreInstanceState to our children using this code path.
3183         // Some pages will be restored immediately as their items are bound immediately, and
3184         // others we will need to wait until after their items are bound.
3185         mSavedStates = container;
3186     }
3187 
3188     public void restoreInstanceStateForChild(int child) {
3189         if (mSavedStates != null) {
3190             mRestoredPages.add(child);
3191             CellLayout cl = (CellLayout) getChildAt(child);
3192             if (cl != null) {
3193                 cl.restoreInstanceState(mSavedStates);
3194             }
3195         }
3196     }
3197 
3198     public void restoreInstanceStateForRemainingPages() {
3199         int count = getChildCount();
3200         for (int i = 0; i < count; i++) {
3201             if (!mRestoredPages.contains(i)) {
3202                 restoreInstanceStateForChild(i);
3203             }
3204         }
3205         mRestoredPages.clear();
3206         mSavedStates = null;
3207     }
3208 
3209     @Override
3210     public boolean scrollLeft() {
3211         boolean result = false;
3212         if (!mIsSwitchingState && workspaceInScrollableState()) {
3213             result = super.scrollLeft();
3214         }
3215         Folder openFolder = Folder.getOpen(mLauncher);
3216         if (openFolder != null) {
3217             openFolder.completeDragExit();
3218         }
3219         return result;
3220     }
3221 
3222     @Override
3223     public boolean scrollRight() {
3224         boolean result = false;
3225         if (!mIsSwitchingState && workspaceInScrollableState()) {
3226             result = super.scrollRight();
3227         }
3228         Folder openFolder = Folder.getOpen(mLauncher);
3229         if (openFolder != null) {
3230             openFolder.completeDragExit();
3231         }
3232         return result;
3233     }
3234 
3235     /**
3236      * Returns a specific CellLayout
3237      */
3238     CellLayout getParentCellLayoutForView(View v) {
3239         for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
3240             if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) {
3241                 return layout;
3242             }
3243         }
3244         return null;
3245     }
3246 
3247     /**
3248      * Returns a list of all the CellLayouts on the Homescreen.
3249      */
3250     private CellLayout[] getWorkspaceAndHotseatCellLayouts() {
3251         int screenCount = getChildCount();
3252         final CellLayout[] layouts;
3253         if (mLauncher.getHotseat() != null) {
3254             layouts = new CellLayout[screenCount + 1];
3255             layouts[screenCount] = mLauncher.getHotseat();
3256         } else {
3257             layouts = new CellLayout[screenCount];
3258         }
3259         for (int screen = 0; screen < screenCount; screen++) {
3260             layouts[screen] = (CellLayout) getChildAt(screen);
3261         }
3262         return layouts;
3263     }
3264 
3265     public View getHomescreenIconByItemId(final int id) {
3266         return getFirstMatch((info, v) -> info != null && info.id == id);
3267     }
3268 
3269     public LauncherAppWidgetHostView getWidgetForAppWidgetId(final int appWidgetId) {
3270         return (LauncherAppWidgetHostView) getFirstMatch((info, v) ->
3271                 (info instanceof LauncherAppWidgetInfo) &&
3272                         ((LauncherAppWidgetInfo) info).appWidgetId == appWidgetId);
3273     }
3274 
3275     public View getFirstMatch(final ItemOperator operator) {
3276         final View[] value = new View[1];
3277         mapOverItems(new ItemOperator() {
3278             @Override
3279             public boolean evaluate(ItemInfo info, View v) {
3280                 if (operator.evaluate(info, v)) {
3281                     value[0] = v;
3282                     return true;
3283                 }
3284                 return false;
3285             }
3286         });
3287         return value[0];
3288     }
3289 
3290     void clearDropTargets() {
3291         mapOverItems(new ItemOperator() {
3292             @Override
3293             public boolean evaluate(ItemInfo info, View v) {
3294                 if (v instanceof DropTarget) {
3295                     mDragController.removeDropTarget((DropTarget) v);
3296                 }
3297                 // not done, process all the shortcuts
3298                 return false;
3299             }
3300         });
3301     }
3302 
3303     /**
3304      * Removes items that match the {@param matcher}. When applications are removed
3305      * as a part of an update, this is called to ensure that other widgets and application
3306      * shortcuts are not removed.
3307      */
3308     public void removeItemsByMatcher(final Predicate<ItemInfo> matcher) {
3309         for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
3310             ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
3311             // Iterate in reverse order as we are removing items
3312             for (int i = container.getChildCount() - 1; i >= 0; i--) {
3313                 View child = container.getChildAt(i);
3314                 ItemInfo info = (ItemInfo) child.getTag();
3315 
3316                 if (matcher.test(info)) {
3317                     layout.removeViewInLayout(child);
3318                     if (child instanceof DropTarget) {
3319                         mDragController.removeDropTarget((DropTarget) child);
3320                     }
3321                 } else if (child instanceof FolderIcon) {
3322                     FolderInfo folderInfo = (FolderInfo) info;
3323                     List<ItemInfo> matches = folderInfo.getContents().stream()
3324                             .filter(matcher)
3325                             .collect(Collectors.toList());
3326                     if (!matches.isEmpty()) {
3327                         folderInfo.removeAll(matches, false);
3328                         if (((FolderIcon) child).getFolder().isOpen()) {
3329                             ((FolderIcon) child).getFolder().close(false /* animate */);
3330                         }
3331                     }
3332                 } else if (info instanceof AppPairInfo api) {
3333                     // If an app pair's member apps are being removed, delete the whole app pair.
3334                     if (api.anyMatch(matcher)) {
3335                         mLauncher.removeItem(child, info, true);
3336                     }
3337                 }
3338             }
3339         }
3340 
3341         // Strip all the empty screens
3342         stripEmptyScreens();
3343     }
3344 
3345     @Override
3346     public void mapOverItems(ItemOperator op) {
3347         for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
3348             if (mapOverCellLayout(layout, op) != null) {
3349                 return;
3350             }
3351         }
3352     }
3353 
3354     /**
3355      * Perform {param operator} over all the items in a given {param layout}.
3356      *
3357      * @return The first item that satisfies the operator or null.
3358      */
3359     public View mapOverCellLayout(CellLayout layout, ItemOperator operator) {
3360         // TODO(b/128460496) Potential race condition where layout is not yet loaded
3361         if (layout == null) {
3362             return null;
3363         }
3364         ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
3365         // map over all the shortcuts on the workspace
3366         final int itemCount = container.getChildCount();
3367         for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
3368             View item = container.getChildAt(itemIdx);
3369             if (operator.evaluate((ItemInfo) item.getTag(), item)) {
3370                 return item;
3371             }
3372         }
3373         return null;
3374     }
3375 
3376     public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
3377         final PackageUserKey packageUserKey = new PackageUserKey(null, null);
3378         Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
3379                 || updatedDots.test(packageUserKey);
3380 
3381         ItemOperator op = (info, v) -> {
3382             if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
3383                 if (matcher.test(info)) {
3384                     ((BubbleTextView) v).applyDotState(info, true /* animate */);
3385                 }
3386             } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
3387                 FolderInfo fi = (FolderInfo) info;
3388                 if (fi.anyMatch(matcher)) {
3389                     FolderDotInfo folderDotInfo = new FolderDotInfo();
3390                     for (ItemInfo si : fi.getContents()) {
3391                         folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
3392                     }
3393                     ((FolderIcon) v).setDotInfo(folderDotInfo);
3394                 }
3395             }
3396 
3397             // process all the shortcuts
3398             return false;
3399         };
3400 
3401         mapOverItems(op);
3402         Folder folder = Folder.getOpen(mLauncher);
3403         if (folder != null) {
3404             folder.iterateOverItems(op);
3405         }
3406     }
3407 
3408     /**
3409      * Remove workspace icons & widget information related to items in matcher.
3410      *
3411      * @param matcher the matcher generated by the caller.
3412      */
3413     public void persistRemoveItemsByMatcher(Predicate<ItemInfo> matcher,
3414                                             @Nullable final String reason) {
3415         mLauncher.getModelWriter().deleteItemsFromDatabase(matcher, reason);
3416         removeItemsByMatcher(matcher);
3417     }
3418 
3419     public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> changedInfo) {
3420         if (!changedInfo.isEmpty()) {
3421             DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo,
3422                     mLauncher.getAppWidgetHolder());
3423 
3424             LauncherAppWidgetInfo item = changedInfo.get(0);
3425             final AppWidgetProviderInfo widgetInfo;
3426             WidgetManagerHelper widgetHelper = new WidgetManagerHelper(getContext());
3427             if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
3428                 widgetInfo = widgetHelper.findProvider(item.providerName, item.user);
3429             } else {
3430                 widgetInfo = widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId,
3431                         item.getTargetComponent());
3432             }
3433 
3434             if (widgetInfo != null) {
3435                 // Re-inflate the widgets which have changed status
3436                 widgetRefresh.run();
3437             } else {
3438                 // widgetRefresh will automatically run when the packages are updated.
3439                 // For now just update the progress bars
3440                 mapOverItems(new ItemOperator() {
3441                     @Override
3442                     public boolean evaluate(ItemInfo info, View view) {
3443                         if (view instanceof PendingAppWidgetHostView
3444                                 && changedInfo.contains(info)) {
3445                             ((LauncherAppWidgetInfo) info).installProgress = 100;
3446                             ((PendingAppWidgetHostView) view).applyState();
3447                         }
3448                         // process all the shortcuts
3449                         return false;
3450                     }
3451                 });
3452             }
3453         }
3454     }
3455 
3456     public boolean isOverlayShown() {
3457         return mOverlayShown;
3458     }
3459 
3460     /**
3461      * Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it.
3462      */
3463     public void moveToDefaultScreen() {
3464         int page = DEFAULT_PAGE;
3465         if (!workspaceInModalState() && getNextPage() != page) {
3466             snapToPage(page);
3467         }
3468         View child = getChildAt(page);
3469         if (child != null) {
3470             child.requestFocus();
3471         }
3472     }
3473 
3474     /**
3475      * Set the given view's pivot point to match the workspace's, so that it scales together. Since
3476      * both this view and workspace can move, transform the point manually instead of using
3477      * dragLayer.getDescendantCoordRelativeToSelf and related methods.
3478      */
3479     public void setPivotToScaleWithSelf(View sibling) {
3480         sibling.setPivotY(getPivotY() + getTop()
3481                 - sibling.getTop() - sibling.getTranslationY());
3482         sibling.setPivotX(getPivotX() + getLeft()
3483                 - sibling.getLeft() - sibling.getTranslationX());
3484     }
3485 
3486     @Override
3487     public int getExpectedHeight() {
3488         return getMeasuredHeight() <= 0 || !mIsLayoutValid
3489                 ? mLauncher.getDeviceProfile().heightPx : getMeasuredHeight();
3490     }
3491 
3492     @Override
3493     public int getExpectedWidth() {
3494         return getMeasuredWidth() <= 0 || !mIsLayoutValid
3495                 ? mLauncher.getDeviceProfile().widthPx : getMeasuredWidth();
3496     }
3497 
3498     @Override
3499     protected boolean canAnnouncePageDescription() {
3500         // Disable announcements while overscrolling potentially to overlay screen because if we end
3501         // up on the overlay screen, it will take care of announcing itself.
3502         return Float.compare(mOverlayProgress, 0f) == 0;
3503     }
3504 
3505     @Override
3506     protected String getCurrentPageDescription() {
3507         int pageIndex = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
3508         return getPageDescription(pageIndex);
3509     }
3510 
3511     /**
3512      * @param page page index.
3513      * @return Description of the page at the given page index.
3514      */
3515     @Override
3516     public String getPageDescription(int page) {
3517         int nScreens = getChildCount();
3518         int extraScreenId = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
3519         if (extraScreenId >= 0 && nScreens > 1) {
3520             if (page == extraScreenId) {
3521                 return getContext().getString(R.string.workspace_new_page);
3522             }
3523             nScreens--;
3524         }
3525         if (nScreens == 0) {
3526             // When the workspace is not loaded, we do not know how many screen will be bound.
3527             return getContext().getString(R.string.home_screen);
3528         }
3529         int panelCount = getPanelCount();
3530         int currentPage = (page / panelCount) + 1;
3531         int totalPages = nScreens / panelCount + nScreens % panelCount;
3532         return getContext().getString(R.string.workspace_scroll_format, currentPage, totalPages);
3533     }
3534 
3535     @Override
3536     protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) {
3537         DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
3538         if (!deviceProfile.isTablet) {
3539             return super.isSignificantMove(absoluteDelta, pageOrientedSize);
3540         }
3541 
3542         return absoluteDelta
3543                 > deviceProfile.availableWidthPx * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE;
3544     }
3545 
3546     @Override
3547     public CellPosMapper getCellPosMapper() {
3548         return mLauncher.getCellPosMapper();
3549     }
3550 
3551     /**
3552      * Used as a workaround to ensure that the AppWidgetService receives the
3553      * PACKAGE_ADDED broadcast before updating widgets.
3554      */
3555     private class DeferredWidgetRefresh implements Runnable, ProviderChangedListener {
3556         private final ArrayList<LauncherAppWidgetInfo> mInfos;
3557         private final LauncherWidgetHolder mWidgetHolder;
3558         private final Handler mHandler;
3559 
3560         private boolean mRefreshPending;
3561 
3562         DeferredWidgetRefresh(ArrayList<LauncherAppWidgetInfo> infos,
3563                               LauncherWidgetHolder holder) {
3564             mInfos = infos;
3565             mWidgetHolder = holder;
3566             mHandler = mLauncher.mHandler;
3567             mRefreshPending = true;
3568 
3569             mWidgetHolder.addProviderChangeListener(this);
3570             // Force refresh after 10 seconds, if we don't get the provider changed event.
3571             // This could happen when the provider is no longer available in the app.
3572             Message msg = Message.obtain(mHandler, this);
3573             msg.obj = DeferredWidgetRefresh.class;
3574             mHandler.sendMessageDelayed(msg, 10000);
3575         }
3576 
3577         @Override
3578         public void run() {
3579             mWidgetHolder.removeProviderChangeListener(this);
3580             mHandler.removeCallbacks(this);
3581 
3582             if (!mRefreshPending) {
3583                 return;
3584             }
3585 
3586             mRefreshPending = false;
3587 
3588             ArrayList<PendingAppWidgetHostView> views = new ArrayList<>(mInfos.size());
3589             mapOverItems((info, view) -> {
3590                 if (view instanceof PendingAppWidgetHostView && mInfos.contains(info)) {
3591                     views.add((PendingAppWidgetHostView) view);
3592                 }
3593                 // process all children
3594                 return false;
3595             });
3596             for (PendingAppWidgetHostView view : views) {
3597                 view.reInflate();
3598             }
3599         }
3600 
3601         @Override
3602         public void notifyWidgetProvidersChanged() {
3603             run();
3604         }
3605     }
3606 
3607     private class StateTransitionListener extends AnimatorListenerAdapter
3608             implements AnimatorUpdateListener {
3609 
3610         @Override
3611         public void onAnimationUpdate(ValueAnimator anim) {
3612             mTransitionProgress = anim.getAnimatedFraction();
3613         }
3614 
3615         @Override
3616         public void onAnimationStart(Animator animation) {
3617             onStartStateTransition();
3618         }
3619 
3620         @Override
3621         public void onAnimationEnd(Animator animation) {
3622             onEndStateTransition();
3623         }
3624     }
3625 }
3626