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