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