content: /*
   * Copyright (C) 2008 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.launcher3;
  
  import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
  import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
  import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_TRANSITION_MS;
  import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
  import static com.android.launcher3.LauncherState.ALL_APPS;
  import static com.android.launcher3.LauncherState.NORMAL;
  import static com.android.launcher3.LauncherState.SPRING_LOADED;
  import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
  import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.LayoutTransition;
  import android.animation.ObjectAnimator;
  import android.animation.ValueAnimator;
  import android.animation.ValueAnimator.AnimatorUpdateListener;
  import android.annotation.SuppressLint;
  import android.app.WallpaperManager;
  import android.appwidget.AppWidgetHostView;
  import android.appwidget.AppWidgetProviderInfo;
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.graphics.drawable.Drawable;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Message;
  import android.os.Parcelable;
  import android.os.UserHandle;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.SparseArray;
  import android.view.LayoutInflater;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.ViewTreeObserver;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.widget.Toast;
  
  import com.android.launcher3.Launcher.LauncherOverlay;
  import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
  import com.android.launcher3.LauncherStateManager.AnimationConfig;
  import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
  import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
  import com.android.launcher3.anim.AnimatorSetBuilder;
  import com.android.launcher3.anim.Interpolators;
  import com.android.launcher3.compat.AppWidgetManagerCompat;
  import com.android.launcher3.config.FeatureFlags;
  import com.android.launcher3.dot.FolderDotInfo;
  import com.android.launcher3.dragndrop.DragController;
  import com.android.launcher3.dragndrop.DragLayer;
  import com.android.launcher3.dragndrop.DragOptions;
  import com.android.launcher3.dragndrop.DragView;
  import com.android.launcher3.dragndrop.SpringLoadedDragController;
  import com.android.launcher3.folder.Folder;
  import com.android.launcher3.folder.FolderIcon;
  import com.android.launcher3.folder.PreviewBackground;
  import com.android.launcher3.graphics.DragPreviewProvider;
  import com.android.launcher3.graphics.PreloadIconDrawable;
  import com.android.launcher3.graphics.RotationMode;
  import com.android.launcher3.logging.UserEventDispatcher;
  import com.android.launcher3.pageindicators.WorkspacePageIndicator;
  import com.android.launcher3.popup.PopupContainerWithArrow;
  import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
  import com.android.launcher3.testing.TestProtocol;
  import com.android.launcher3.touch.WorkspaceTouchListener;
  import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
  import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
  import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
  import com.android.launcher3.util.Executors;
  import com.android.launcher3.util.IntArray;
  import com.android.launcher3.util.IntSparseArrayMap;
  import com.android.launcher3.util.ItemInfoMatcher;
  import com.android.launcher3.util.PackageUserKey;
  import com.android.launcher3.util.Thunk;
  import com.android.launcher3.util.WallpaperOffsetInterpolator;
  import com.android.launcher3.widget.LauncherAppWidgetHostView;
  import com.android.launcher3.widget.PendingAddShortcutInfo;
  import com.android.launcher3.widget.PendingAddWidgetInfo;
  import com.android.launcher3.widget.PendingAppWidgetHostView;
  
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.function.Predicate;
  
  /**
   * The workspace is a wide area with a wallpaper and a finite number of pages.
   * Each page contains a number of icons, folders or widgets the user can
   * interact with. A workspace is meant to be used with a fixed width only.
   */
  public class Workspace extends PagedView<WorkspacePageIndicator>
          implements DropTarget, DragSource, View.OnTouchListener,
          DragController.DragListener, Insettable, LauncherStateManager.StateHandler,
          WorkspaceLayoutManager {
  
      /** The value that {@link #mTransitionProgress} must be greater than for
       * {@link #transitionStateShouldAllowDrop()} to return true. */
      private static final float ALLOW_DROP_TRANSITION_PROGRESS = 0.25f;
  
      /** The value that {@link #mTransitionProgress} must be greater than for
       * {@link #isFinishedSwitchingState()} ()} to return true. */
      private static final float FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS = 0.5f;
  
      private static final boolean ENFORCE_DRAG_EVENT_ORDER = false;
  
      private static final int SNAP_OFF_EMPTY_SCREEN_DURATION = 400;
      private static final int FADE_EMPTY_SCREEN_DURATION = 150;
  
      private static final int ADJACENT_SCREEN_DROP_DURATION = 300;
  
      private static final int DEFAULT_PAGE = 0;
  
      private LayoutTransition mLayoutTransition;
      @Thunk final WallpaperManager mWallpaperManager;
  
      private ShortcutAndWidgetContainer mDragSourceInternal;
  
      @Thunk final IntSparseArrayMap<CellLayout> mWorkspaceScreens = new IntSparseArrayMap<>();
      @Thunk final IntArray mScreenOrder = new IntArray();
  
      @Thunk Runnable mRemoveEmptyScreenRunnable;
      @Thunk boolean mDeferRemoveExtraEmptyScreen = false;
  
      /**
       * CellInfo for the cell that is currently being dragged
       */
      private CellLayout.CellInfo mDragInfo;
  
      /**
       * Target drop area calculated during last acceptDrop call.
       */
      @Thunk int[] mTargetCell = new int[2];
      private int mDragOverX = -1;
      private int mDragOverY = -1;
  
      /**
       * The CellLayout that is currently being dragged over
       */
      @Thunk CellLayout mDragTargetLayout = null;
      /**
       * The CellLayout that we will show as highlighted
       */
      private CellLayout mDragOverlappingLayout = null;
  
      /**
       * The CellLayout which will be dropped to
       */
      private CellLayout mDropToLayout = null;
  
      @Thunk final Launcher mLauncher;
      @Thunk DragController mDragController;
  
      private final Rect mTempRect = new Rect();
      private final int[] mTempXY = new int[2];
      private final float[] mTempFXY = new float[2];
      @Thunk float[] mDragViewVisualCenter = new float[2];
      private final float[] mTempTouchCoordinates = new float[2];
  
      private SpringLoadedDragController mSpringLoadedDragController;
  
      private boolean mIsSwitchingState = false;
  
      boolean mChildrenLayersEnabled = true;
  
      private boolean mStripScreensOnPageStopMoving = false;
  
      private DragPreviewProvider mOutlineProvider = null;
      private boolean mWorkspaceFadeInAdjacentScreens;
  
      final WallpaperOffsetInterpolator mWallpaperOffset;
      private boolean mUnlockWallpaperFromDefaultPageOnLayout;
  
      // Variables relating to the creation of user folders by hovering shortcuts over shortcuts
      private static final int FOLDER_CREATION_TIMEOUT = 0;
      public static final int REORDER_TIMEOUT = 650;
      private final Alarm mFolderCreationAlarm = new Alarm();
      private final Alarm mReorderAlarm = new Alarm();
      private PreviewBackground mFolderCreateBg;
      private FolderIcon mDragOverFolderIcon = null;
      private boolean mCreateUserFolderOnDrop = false;
      private boolean mAddToExistingFolderOnDrop = false;
      private float mMaxDistanceForFolderCreation;
  
      // Variables relating to touch disambiguation (scrolling workspace vs. scrolling a widget)
      private float mXDown;
      private float mYDown;
      final static float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
      final static float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
      final static float TOUCH_SLOP_DAMPING_FACTOR = 4;
  
      // Relating to the animation of items being dropped externally
      public static final int ANIMATE_INTO_POSITION_AND_DISAPPEAR = 0;
      public static final int ANIMATE_INTO_POSITION_AND_REMAIN = 1;
      public static final int ANIMATE_INTO_POSITION_AND_RESIZE = 2;
      public static final int COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION = 3;
      public static final int CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION = 4;
  
      // Related to dragging, folder creation and reordering
      private static final int DRAG_MODE_NONE = 0;
      private static final int DRAG_MODE_CREATE_FOLDER = 1;
      private static final int DRAG_MODE_ADD_TO_FOLDER = 2;
      private static final int DRAG_MODE_REORDER = 3;
      private int mDragMode = DRAG_MODE_NONE;
      @Thunk int mLastReorderX = -1;
      @Thunk int mLastReorderY = -1;
  
      private SparseArray<Parcelable> mSavedStates;
      private final IntArray mRestoredPages = new IntArray();
  
      private float mCurrentScale;
      private float mTransitionProgress;
  
      // State related to Launcher Overlay
      LauncherOverlay mLauncherOverlay;
      boolean mScrollInteractionBegan;
      boolean mStartedSendingScrollEvents;
      float mLastOverlayScroll = 0;
      boolean mOverlayShown = false;
      private Runnable mOnOverlayHiddenCallback;
  
      private boolean mForceDrawAdjacentPages = false;
  
      // Total over scrollX in the overlay direction.
      private float mOverlayTranslation;
  
      // Handles workspace state transitions
      private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
  
      /**
       * Used to inflate the Workspace from XML.
       *
       * @param context The application's context.
       * @param attrs The attributes set containing the Workspace's customization values.
       */
      public Workspace(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      /**
       * Used to inflate the Workspace from XML.
       *
       * @param context The application's context.
       * @param attrs The attributes set containing the Workspace's customization values.
       * @param defStyle Unused.
       */
      public Workspace(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
  
          mLauncher = Launcher.getLauncher(context);
          mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
          mWallpaperManager = WallpaperManager.getInstance(context);
  
          mWallpaperOffset = new WallpaperOffsetInterpolator(this);
  
          setHapticFeedbackEnabled(false);
          initWorkspace();
  
          // Disable multitouch across the workspace/all apps/customize tray
          setMotionEventSplittingEnabled(true);
          setOnTouchListener(new WorkspaceTouchListener(mLauncher, this));
      }
  
      @Override
      public void setInsets(Rect insets) {
          DeviceProfile grid = mLauncher.getDeviceProfile();
          DeviceProfile stableGrid = mLauncher.getWallpaperDeviceProfile();
  
          mMaxDistanceForFolderCreation = stableGrid.isTablet
                  ? 0.75f * stableGrid.iconSizePx
                  : 0.55f * stableGrid.iconSizePx;
          mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
  
          Rect padding = stableGrid.workspacePadding;
  
          RotationMode rotationMode = mLauncher.getRotationMode();
  
          rotationMode.mapRect(padding, mTempRect);
          setPadding(mTempRect.left, mTempRect.top, mTempRect.right, mTempRect.bottom);
          rotationMode.mapRect(stableGrid.getInsets(), mInsets);
  
          if (mWorkspaceFadeInAdjacentScreens) {
              // In landscape mode the page spacing is set to the default.
              setPageSpacing(grid.edgeMarginPx);
          } else {
              // In portrait, we want the pages spaced such that there is no
              // overhang of the previous / next page into the current page viewport.
              // We assume symmetrical padding in portrait mode.
              setPageSpacing(Math.max(grid.edgeMarginPx, padding.left + 1));
          }
  
  
          int paddingLeftRight = stableGrid.cellLayoutPaddingLeftRightPx;
          int paddingBottom = stableGrid.cellLayoutBottomPaddingPx;
          for (int i = mWorkspaceScreens.size() - 1; i >= 0; i--) {
              CellLayout page = mWorkspaceScreens.valueAt(i);
              page.setRotationMode(rotationMode);
              page.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
          }
      }
  
      /**
       * Estimates the size of an item using spans: hSpan, vSpan.
       *
       * @return MAX_VALUE for each dimension if unsuccessful.
       */
      public int[] estimateItemSize(ItemInfo itemInfo) {
          int[] size = new int[2];
          if (getChildCount() > 0) {
              // Use the first page to estimate the child position
              CellLayout cl = (CellLayout) getChildAt(0);
              boolean isWidget = itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
  
              Rect r = estimateItemPosition(cl, 0, 0, itemInfo.spanX, itemInfo.spanY);
  
              float scale = 1;
              if (isWidget) {
                  DeviceProfile profile = mLauncher.getWallpaperDeviceProfile();
                  scale = Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
              }
              size[0] = r.width();
              size[1] = r.height();
  
              if (isWidget) {
                  size[0] /= scale;
                  size[1] /= scale;
              }
              return size;
          } else {
              size[0] = Integer.MAX_VALUE;
              size[1] = Integer.MAX_VALUE;
              return size;
          }
      }
  
      public float getWallpaperOffsetForCenterPage() {
          int pageScroll = getScrollForPage(getPageNearestToCenterOfScreen());
          return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
      }
  
      public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) {
          Rect r = new Rect();
          cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
          return r;
      }
  
      @Override
      public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
          if (ENFORCE_DRAG_EVENT_ORDER) {
              enforceDragParity("onDragStart", 0, 0);
          }
  
          if (mDragInfo != null && mDragInfo.cell != null) {
              CellLayout layout = (CellLayout) mDragInfo.cell.getParent().getParent();
              layout.markCellsAsUnoccupiedForView(mDragInfo.cell);
          }
  
          if (mOutlineProvider != null) {
              if (dragObject.dragView != null) {
                  Bitmap preview = dragObject.dragView.getPreviewBitmap();
  
                  // The outline is used to visualize where the item will land if dropped
                  mOutlineProvider.generateDragOutline(preview);
              }
          }
  
          updateChildrenLayersEnabled();
  
          // Do not add a new page if it is a accessible drag which was not started by the workspace.
          // We do not support accessibility drag from other sources and instead provide a direct
          // action for move/add to homescreen.
          // When a accessible drag is started by the folder, we only allow rearranging withing the
          // folder.
          boolean addNewPage = !(options.isAccessibleDrag && dragObject.dragSource != this);
  
          if (addNewPage) {
              mDeferRemoveExtraEmptyScreen = false;
              addExtraEmptyScreenOnDrag();
  
              if (dragObject.dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
                      && dragObject.dragSource != this) {
                  // When dragging a widget from different source, move to a page which has
                  // enough space to place this widget (after rearranging/resizing). We special case
                  // widgets as they cannot be placed inside a folder.
                  // Start at the current page and search right (on LTR) until finding a page with
                  // enough space. Since an empty screen is the furthest right, a page must be found.
                  int currentPage = getPageNearestToCenterOfScreen();
                  for (int pageIndex = currentPage; pageIndex < getPageCount(); pageIndex++) {
                      CellLayout page = (CellLayout) getPageAt(pageIndex);
                      if (page.hasReorderSolution(dragObject.dragInfo)) {
                          setCurrentPage(pageIndex);
                          break;
                      }
                  }
              }
          }
  
          // Always enter the spring loaded mode
          if (TestProtocol.sDebugTracing) {
              Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Switching to SPRING_LOADED");
          }
          mLauncher.getStateManager().goToState(SPRING_LOADED);
      }
  
      public void deferRemoveExtraEmptyScreen() {
          mDeferRemoveExtraEmptyScreen = true;
      }
  
      @Override
      public void onDragEnd() {
          if (ENFORCE_DRAG_EVENT_ORDER) {
              enforceDragParity("onDragEnd", 0, 0);
          }
  
          if (!mDeferRemoveExtraEmptyScreen) {
              removeExtraEmptyScreen(true, mDragSourceInternal != null);
          }
  
          updateChildrenLayersEnabled();
          mDragInfo = null;
          mOutlineProvider = null;
          mDragSourceInternal = null;
      }
  
      /**
       * Initializes various states for this workspace.
       */
      protected void initWorkspace() {
          mCurrentPage = DEFAULT_PAGE;
          setClipToPadding(false);
  
          setupLayoutTransition();
  
          // Set the wallpaper dimensions when Launcher starts up
          setWallpaperDimension();
      }
  
      private void setupLayoutTransition() {
          // We want to show layout transitions when pages are deleted, to close the gap.
          mLayoutTransition = new LayoutTransition();
          mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
          mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
          mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
          mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
          setLayoutTransition(mLayoutTransition);
      }
  
      void enableLayoutTransitions() {
          setLayoutTransition(mLayoutTransition);
      }
      void disableLayoutTransitions() {
          setLayoutTransition(null);
      }
  
      @Override
      public void onViewAdded(View child) {
          if (!(child instanceof CellLayout)) {
              throw new IllegalArgumentException("A Workspace can only have CellLayout children.");
          }
          CellLayout cl = ((CellLayout) child);
          cl.setOnInterceptTouchListener(this);
          cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
          super.onViewAdded(child);
      }
  
      /**
       * Initializes and binds the first page
       * @param qsb an existing qsb to recycle or null.
       */
      public void bindAndInitFirstWorkspaceScreen(View qsb) {
          if (!FeatureFlags.QSB_ON_FIRST_SCREEN) {
              return;
          }
          // Add the first page
          CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, 0);
          // Always add a QSB on the first screen.
          if (qsb == null) {
              // In transposed layout, we add the QSB in the Grid. As workspace does not touch the
              // edges, we do not need a full width QSB.
              qsb = LayoutInflater.from(getContext())
                      .inflate(R.layout.search_container_workspace,firstPage, false);
          }
  
          CellLayout.LayoutParams lp = new CellLayout.LayoutParams(0, 0, firstPage.getCountX(), 1);
          lp.canReorder = false;
          if (!firstPage.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true)) {
              Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
          }
      }
  
      public void removeAllWorkspaceScreens() {
          // Disable all layout transitions before removing all pages to ensure that we don't get the
          // transition animations competing with us changing the scroll when we add pages
          disableLayoutTransitions();
  
          // Recycle the QSB widget
          View qsb = findViewById(R.id.search_container_workspace);
          if (qsb != null) {
              ((ViewGroup) qsb.getParent()).removeView(qsb);
          }
  
          // Remove the pages and clear the screen models
          removeFolderListeners();
          removeAllViews();
          mScreenOrder.clear();
          mWorkspaceScreens.clear();
  
          // Remove any deferred refresh callbacks
          mLauncher.mHandler.removeCallbacksAndMessages(DeferredWidgetRefresh.class);
  
          // Ensure that the first page is always present
          bindAndInitFirstWorkspaceScreen(qsb);
  
          // Re-enable the layout transitions
          enableLayoutTransitions();
      }
  
      public void insertNewWorkspaceScreenBeforeEmptyScreen(int screenId) {
          // Find the index to insert this view into.  If the empty screen exists, then
          // insert it before that.
          int insertIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
          if (insertIndex < 0) {
              insertIndex = mScreenOrder.size();
          }
          insertNewWorkspaceScreen(screenId, insertIndex);
      }
  
      public void insertNewWorkspaceScreen(int screenId) {
          insertNewWorkspaceScreen(screenId, getChildCount());
      }
  
      public CellLayout insertNewWorkspaceScreen(int screenId, int insertIndex) {
          if (mWorkspaceScreens.containsKey(screenId)) {
              throw new RuntimeException("Screen id " + screenId + " already exists!");
          }
  
          // Inflate the cell layout, but do not add it automatically so that we can get the newly
          // created CellLayout.
          CellLayout newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
                          R.layout.workspace_screen, this, false /* attachToRoot */);
          DeviceProfile grid = mLauncher.getWallpaperDeviceProfile();
          int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
          int paddingBottom = grid.cellLayoutBottomPaddingPx;
          newScreen.setRotationMode(mLauncher.getRotationMode());
          newScreen.setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
  
          mWorkspaceScreens.put(screenId, newScreen);
          mScreenOrder.add(insertIndex, screenId);
          addView(newScreen, insertIndex);
          mStateTransitionAnimation.applyChildState(
                  mLauncher.getStateManager().getState(), newScreen, insertIndex);
  
          if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
              newScreen.enableAccessibleDrag(true, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
          }
  
          return newScreen;
      }
  
      public void addExtraEmptyScreenOnDrag() {
          boolean lastChildOnScreen = false;
          boolean childOnFinalScreen = false;
  
          // Cancel any pending removal of empty screen
          mRemoveEmptyScreenRunnable = null;
  
          if (mDragSourceInternal != null) {
              if (mDragSourceInternal.getChildCount() == 1) {
                  lastChildOnScreen = true;
              }
              CellLayout cl = (CellLayout) mDragSourceInternal.getParent();
              if (indexOfChild(cl) == getChildCount() - 1) {
                  childOnFinalScreen = true;
              }
          }
  
          // If this is the last item on the final screen
          if (lastChildOnScreen && childOnFinalScreen) {
              return;
          }
          if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) {
              insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID);
          }
      }
  
      public boolean addExtraEmptyScreen() {
          if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) {
              insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID);
              return true;
          }
          return false;
      }
  
      private void convertFinalScreenToEmptyScreenIfNecessary() {
          if (mLauncher.isWorkspaceLoading()) {
              // Invalid and dangerous operation if workspace is loading
              return;
          }
  
          if (hasExtraEmptyScreen() || mScreenOrder.size() == 0) return;
          int finalScreenId = mScreenOrder.get(mScreenOrder.size() - 1);
  
          CellLayout finalScreen = mWorkspaceScreens.get(finalScreenId);
  
          // If the final screen is empty, convert it to the extra empty screen
          if (finalScreen.getShortcutsAndWidgets().getChildCount() == 0 &&
                  !finalScreen.isDropPending()) {
              mWorkspaceScreens.remove(finalScreenId);
              mScreenOrder.removeValue(finalScreenId);
  
              // if this is the last screen, convert it to the empty screen
              mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, finalScreen);
              mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID);
          }
      }
  
      public void removeExtraEmptyScreen(final boolean animate, boolean stripEmptyScreens) {
          removeExtraEmptyScreenDelayed(animate, null, 0, stripEmptyScreens);
      }
  
      public void removeExtraEmptyScreenDelayed(final boolean animate, final Runnable onComplete,
              final int delay, final boolean stripEmptyScreens) {
          if (mLauncher.isWorkspaceLoading()) {
              // Don't strip empty screens if the workspace is still loading
              return;
          }
  
          if (delay > 0) {
              postDelayed(new Runnable() {
                  @Override
                  public void run() {
                      removeExtraEmptyScreenDelayed(animate, onComplete, 0, stripEmptyScreens);
                  }
              }, delay);
              return;
          }
  
          convertFinalScreenToEmptyScreenIfNecessary();
          if (hasExtraEmptyScreen()) {
              int emptyIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
              if (getNextPage() == emptyIndex) {
                  snapToPage(getNextPage() - 1, SNAP_OFF_EMPTY_SCREEN_DURATION);
                  fadeAndRemoveEmptyScreen(SNAP_OFF_EMPTY_SCREEN_DURATION, FADE_EMPTY_SCREEN_DURATION,
                          onComplete, stripEmptyScreens);
              } else {
                  snapToPage(getNextPage(), 0);
                  fadeAndRemoveEmptyScreen(0, FADE_EMPTY_SCREEN_DURATION,
                          onComplete, stripEmptyScreens);
              }
              return;
          } else if (stripEmptyScreens) {
              // If we're not going to strip the empty screens after removing
              // the extra empty screen, do it right away.
              stripEmptyScreens();
          }
  
          if (onComplete != null) {
              onComplete.run();
          }
      }
  
      private void fadeAndRemoveEmptyScreen(int delay, int duration, final Runnable onComplete,
              final boolean stripEmptyScreens) {
          // XXX: Do we need to update LM workspace screens below?
          final CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID);
  
          mRemoveEmptyScreenRunnable = new Runnable() {
              @Override
              public void run() {
                  if (hasExtraEmptyScreen()) {
                      mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
                      mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
                      removeView(cl);
                      if (stripEmptyScreens) {
                          stripEmptyScreens();
                      }
                      // Update the page indicator to reflect the removed page.
                      showPageIndicatorAtCurrentScroll();
                  }
              }
          };
  
          ObjectAnimator oa = ObjectAnimator.ofFloat(cl, ALPHA, 0f);
          oa.setDuration(duration);
          oa.setStartDelay(delay);
          oa.addListener(new AnimatorListenerAdapter() {
              @Override
              public void onAnimationEnd(Animator animation) {
                  if (mRemoveEmptyScreenRunnable != null) {
                      mRemoveEmptyScreenRunnable.run();
                  }
                  if (onComplete != null) {
                      onComplete.run();
                  }
              }
          });
          oa.start();
      }
  
      public boolean hasExtraEmptyScreen() {
          return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && getChildCount() > 1;
      }
  
      public int commitExtraEmptyScreen() {
          if (mLauncher.isWorkspaceLoading()) {
              // Invalid and dangerous operation if workspace is loading
              return -1;
          }
  
          CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID);
          mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
          mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID);
  
          int newId = LauncherSettings.Settings.call(getContext().getContentResolver(),
                  LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                  .getInt(LauncherSettings.Settings.EXTRA_VALUE);
          mWorkspaceScreens.put(newId, cl);
          mScreenOrder.add(newId);
  
          return newId;
      }
  
      @Override
      public Hotseat getHotseat() {
          return mLauncher.getHotseat();
      }
  
      @Override
      public void onAddDropTarget(DropTarget target) {
          mDragController.addDropTarget(target);
      }
  
      @Override
      public CellLayout getScreenWithId(int screenId) {
          return mWorkspaceScreens.get(screenId);
      }
  
      public int getIdForScreen(CellLayout layout) {
          int index = mWorkspaceScreens.indexOfValue(layout);
          if (index != -1) {
              return mWorkspaceScreens.keyAt(index);
          }
          return -1;
      }
  
      public int getPageIndexForScreenId(int screenId) {
          return indexOfChild(mWorkspaceScreens.get(screenId));
      }
  
      public int getScreenIdForPageIndex(int index) {
          if (0 <= index && index < mScreenOrder.size()) {
              return mScreenOrder.get(index);
          }
          return -1;
      }
  
      public IntArray getScreenOrder() {
          return mScreenOrder;
      }
  
      public void stripEmptyScreens() {
          if (mLauncher.isWorkspaceLoading()) {
              // Don't strip empty screens if the workspace is still loading.
              // This is dangerous and can result in data loss.
              return;
          }
  
          if (isPageInTransition()) {
              mStripScreensOnPageStopMoving = true;
              return;
          }
  
          int currentPage = getNextPage();
          IntArray removeScreens = new IntArray();
          int total = mWorkspaceScreens.size();
          for (int i = 0; i < total; i++) {
              int id = mWorkspaceScreens.keyAt(i);
              CellLayout cl = mWorkspaceScreens.valueAt(i);
              // FIRST_SCREEN_ID can never be removed.
              if ((!FeatureFlags.QSB_ON_FIRST_SCREEN || id > FIRST_SCREEN_ID)
                      && cl.getShortcutsAndWidgets().getChildCount() == 0) {
                  removeScreens.add(id);
              }
          }
  
          boolean isInAccessibleDrag = mLauncher.getAccessibilityDelegate().isInAccessibleDrag();
  
          // We enforce at least one page to add new items to. In the case that we remove the last
          // such screen, we convert the last screen to the empty screen
          int minScreens = 1;
  
          int pageShift = 0;
          for (int i = 0; i < removeScreens.size(); i++) {
              int id = removeScreens.get(i);
              CellLayout cl = mWorkspaceScreens.get(id);
              mWorkspaceScreens.remove(id);
              mScreenOrder.removeValue(id);
  
              if (getChildCount() > minScreens) {
                  if (indexOfChild(cl) < currentPage) {
                      pageShift++;
                  }
  
                  if (isInAccessibleDrag) {
                      cl.enableAccessibleDrag(false, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
                  }
  
                  removeView(cl);
              } else {
                  // if this is the last screen, convert it to the empty screen
                  mRemoveEmptyScreenRunnable = null;
                  mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, cl);
                  mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID);
              }
          }
  
          if (pageShift >= 0) {
              setCurrentPage(currentPage - pageShift);
          }
      }
  
      /**
       * Called directly from a CellLayout (not by the framework), after we've been added as a
       * listener via setOnInterceptTouchEventListener(). This allows us to tell the CellLayout
       * that it should intercept touch events, which is not something that is normally supported.
       */
      @SuppressLint("ClickableViewAccessibility")
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          return shouldConsumeTouch(v);
      }
  
      private boolean shouldConsumeTouch(View v) {
          return !workspaceIconsCanBeDragged()
                  || (!workspaceInModalState() && indexOfChild(v) != mCurrentPage);
      }
  
      public boolean isSwitchingState() {
          return mIsSwitchingState;
      }
  
      /** This differs from isSwitchingState in that we take into account how far the transition
       *  has completed. */
      public boolean isFinishedSwitchingState() {
          return !mIsSwitchingState
                  || (mTransitionProgress > FINISHED_SWITCHING_STATE_TRANSITION_PROGRESS);
      }
  
      @Override
      public boolean dispatchUnhandledMove(View focused, int direction) {
          if (workspaceInModalState() || !isFinishedSwitchingState()) {
              // when the home screens are shrunken, shouldn't allow side-scrolling
              return false;
          }
          return super.dispatchUnhandledMove(focused, direction);
      }
  
      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
              mXDown = ev.getX();
              mYDown = ev.getY();
          }
          return super.onInterceptTouchEvent(ev);
      }
  
      @Override
      protected void determineScrollingStart(MotionEvent ev) {
          if (!isFinishedSwitchingState()) return;
  
          float deltaX = ev.getX() - mXDown;
          float absDeltaX = Math.abs(deltaX);
          float absDeltaY = Math.abs(ev.getY() - mYDown);
  
          if (Float.compare(absDeltaX, 0f) == 0) return;
  
          float slope = absDeltaY / absDeltaX;
          float theta = (float) Math.atan(slope);
  
          if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
              cancelCurrentPageLongPress();
          }
  
          if (theta > MAX_SWIPE_ANGLE) {
              // Above MAX_SWIPE_ANGLE, we don't want to ever start scrolling the workspace
              return;
          } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
              // Above START_DAMPING_TOUCH_SLOP_ANGLE and below MAX_SWIPE_ANGLE, we want to
              // increase the touch slop to make it harder to begin scrolling the workspace. This
              // results in vertically scrolling widgets to more easily. The higher the angle, the
              // more we increase touch slop.
              theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
              float extraRatio = (float)
                      Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
              super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
          } else {
              // Below START_DAMPING_TOUCH_SLOP_ANGLE, we don't do anything special
              super.determineScrollingStart(ev);
          }
      }
  
      protected void onPageBeginTransition() {
          super.onPageBeginTransition();
          updateChildrenLayersEnabled();
      }
  
      protected void onPageEndTransition() {
          super.onPageEndTransition();
          updateChildrenLayersEnabled();
  
          if (mDragController.isDragging()) {
              if (workspaceInModalState()) {
                  // If we are in springloaded mode, then force an event to check if the current touch
                  // is under a new page (to scroll to)
                  mDragController.forceTouchMove();
              }
          }
  
          if (mStripScreensOnPageStopMoving) {
              stripEmptyScreens();
              mStripScreensOnPageStopMoving = false;
          }
      }
  
      protected void onScrollInteractionBegin() {
          super.onScrollInteractionBegin();
          mScrollInteractionBegan = true;
      }
  
      protected void onScrollInteractionEnd() {
          super.onScrollInteractionEnd();
          mScrollInteractionBegan = false;
          if (mStartedSendingScrollEvents) {
              mStartedSendingScrollEvents = false;
              mLauncherOverlay.onScrollInteractionEnd();
          }
      }
  
      public void setLauncherOverlay(LauncherOverlay overlay) {
          mLauncherOverlay = overlay;
          // A new overlay has been set. Reset event tracking
          mStartedSendingScrollEvents = false;
          onOverlayScrollChanged(0);
      }
  
  
      private boolean isScrollingOverlay() {
          return mLauncherOverlay != null &&
                  ((mIsRtl && getUnboundedScrollX() > mMaxScrollX)
                          || (!mIsRtl && getUnboundedScrollX() < mMinScrollX));
      }
  
      @Override
      protected void snapToDestination() {
          // If we're overscrolling the overlay, we make sure to immediately reset the PagedView
          // to it's baseline position instead of letting the overscroll settle. The overlay handles
          // it's own settling, and every gesture to the overlay should be self-contained and start
          // from 0, so we zero it out here.
          if (isScrollingOverlay()) {
              // We reset mWasInOverscroll so that PagedView doesn't zero out the overscroll
              // interaction when we call snapToPageImmediately.
              mWasInOverscroll = false;
              snapToPageImmediately(0);
          } else {
              super.snapToDestination();
          }
      }
  
      @Override
      protected void onScrollChanged(int l, int t, int oldl, int oldt) {
          super.onScrollChanged(l, t, oldl, oldt);
  
          // Update the page indicator progress.
          boolean isTransitioning = mIsSwitchingState
                  || (getLayoutTransition() != null && getLayoutTransition().isRunning());
          if (!isTransitioning) {
              showPageIndicatorAtCurrentScroll();
          }
  
          updatePageAlphaValues();
          enableHwLayersOnVisiblePages();
      }
  
      public void showPageIndicatorAtCurrentScroll() {
          if (mPageIndicator != null) {
              mPageIndicator.setScroll(getScrollX(), computeMaxScrollX());
          }
      }
  
      @Override
      protected void overScroll(int amount) {
          boolean shouldScrollOverlay = mLauncherOverlay != null && !mScroller.isSpringing() &&
                  ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl));
  
          boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlayScroll != 0 &&
                  ((amount >= 0 && !mIsRtl) || (amount <= 0 && mIsRtl));
  
          if (shouldScrollOverlay) {
              if (!mStartedSendingScrollEvents && mScrollInteractionBegan) {
                  mStartedSendingScrollEvents = true;
                  mLauncherOverlay.onScrollInteractionBegin();
              }
  
              mLastOverlayScroll = Math.abs(((float) amount) / getMeasuredWidth());
              mLauncherOverlay.onScrollChange(mLastOverlayScroll, mIsRtl);
          } else {
              dampedOverScroll(amount);
          }
  
          if (shouldZeroOverlay) {
              mLauncherOverlay.onScrollChange(0, mIsRtl);
          }
      }
  
      @Override
      protected boolean onOverscroll(int amount) {
          // Enforce overscroll on -1 direction
          if ((amount > 0 && !mIsRtl) || (amount < 0 && mIsRtl)) return false;
          return super.onOverscroll(amount);
      }
  
      @Override
      protected boolean shouldFlingForVelocity(int velocityX) {
          // When the overlay is moving, the fling or settle transition is controlled by the overlay.
          return Float.compare(Math.abs(mOverlayTranslation), 0) == 0 &&
                  super.shouldFlingForVelocity(velocityX);
      }
  
      /**
       * The overlay scroll is being controlled locally, just update our overlay effect
       */
      public void onOverlayScrollChanged(float scroll) {
          if (Float.compare(scroll, 1f) == 0) {
              if (!mOverlayShown) {
                  mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                          Action.Direction.LEFT, ContainerType.WORKSPACE, 0);
                  mLauncher.getStatsLogManager().logSwipeOnContainer(true, 0);
              }
              mOverlayShown = true;
              // Not announcing the overlay page for accessibility since it announces itself.
          } else if (Float.compare(scroll, 0f) == 0) {
              if (mOverlayShown) {
                  UserEventDispatcher ued = mLauncher.getUserEventDispatcher();
                  if (!ued.isPreviousHomeGesture()) {
                      mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                          Action.Direction.RIGHT, ContainerType.WORKSPACE, -1);
                      mLauncher.getStatsLogManager().logSwipeOnContainer(false, -1);
                  }
              } else if (Float.compare(mOverlayTranslation, 0f) != 0) {
                  // When arriving to 0 overscroll from non-zero overscroll, announce page for
                  // accessibility since default announcements were disabled while in overscroll
                  // state.
                  // Not doing this if mOverlayShown because in that case the accessibility service
                  // will announce the launcher window description upon regaining focus after
                  // switching from the overlay screen.
                  announcePageForAccessibility();
              }
              mOverlayShown = false;
              tryRunOverlayCallback();
          }
  
          float offset = 0f;
  
          scroll = Math.max(scroll - offset, 0);
          scroll = Math.min(1, scroll / (1 - offset));
  
          float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(scroll);
          float transX = mLauncher.getDragLayer().getMeasuredWidth() * scroll;
  
          if (mIsRtl) {
              transX = -transX;
          }
          mOverlayTranslation = transX;
  
          // TODO(adamcohen): figure out a final effect here. We may need to recommend
          // different effects based on device performance. On at least one relatively high-end
          // device I've tried, translating the launcher causes things to get quite laggy.
          mLauncher.getDragLayer().setTranslationX(transX);
          mLauncher.getDragLayer().getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha);
      }
  
      /**
       * @return false if the callback is still pending
       */
      private boolean tryRunOverlayCallback() {
          if (mOnOverlayHiddenCallback == null) {
              // Return true as no callback is pending. This is used by OnWindowFocusChangeListener
              // to remove itself if multiple focus handles were added.
              return true;
          }
          if (mOverlayShown || !hasWindowFocus()) {
              return false;
          }
  
          mOnOverlayHiddenCallback.run();
          mOnOverlayHiddenCallback = null;
          return true;
      }
  
      /**
       * Runs the given callback when the minus one overlay is hidden. Specifically, it is run
       * when launcher's window has focus and the overlay is no longer being shown. If a callback
       * is already present, the new callback will chain off it so both are run.
       *
       * @return Whether the callback was deferred.
       */
      public boolean runOnOverlayHidden(Runnable callback) {
          if (mOnOverlayHiddenCallback == null) {
              mOnOverlayHiddenCallback = callback;
          } else {
              // Chain the new callback onto the previous callback(s).
              Runnable oldCallback = mOnOverlayHiddenCallback;
              mOnOverlayHiddenCallback = () -> {
                  oldCallback.run();
                  callback.run();
              };
          }
          if (!tryRunOverlayCallback()) {
              ViewTreeObserver observer = getViewTreeObserver();
              if (observer != null && observer.isAlive()) {
                  observer.addOnWindowFocusChangeListener(
                          new ViewTreeObserver.OnWindowFocusChangeListener() {
                              @Override
                              public void onWindowFocusChanged(boolean hasFocus) {
                                  if (tryRunOverlayCallback() && observer.isAlive()) {
                                      observer.removeOnWindowFocusChangeListener(this);
                                  }
                              }});
              }
              return true;
          }
          return false;
      }
  
      @Override
      protected void notifyPageSwitchListener(int prevPage) {
          super.notifyPageSwitchListener(prevPage);
          if (prevPage != mCurrentPage) {
              int swipeDirection = (prevPage < mCurrentPage) ? Action.Direction.RIGHT : Action.Direction.LEFT;
              mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                      swipeDirection, ContainerType.WORKSPACE, prevPage);
          }
      }
  
      protected void setWallpaperDimension() {
          Executors.THREAD_POOL_EXECUTOR.execute(new Runnable() {
              @Override
              public void run() {
                  final Point size = LauncherAppState.getIDP(getContext()).defaultWallpaperSize;
                  if (size.x != mWallpaperManager.getDesiredMinimumWidth()
                          || size.y != mWallpaperManager.getDesiredMinimumHeight()) {
                      mWallpaperManager.suggestDesiredDimensions(size.x, size.y);
                  }
              }
          });
      }
  
      public void lockWallpaperToDefaultPage() {
          mWallpaperOffset.setLockToDefaultPage(true);
      }
  
      public void unlockWallpaperFromDefaultPageOnNextLayout() {
          if (mWallpaperOffset.isLockedToDefaultPage()) {
              mUnlockWallpaperFromDefaultPageOnLayout = true;
              requestLayout();
          }
      }
  
      @Override
      public void computeScroll() {
          super.computeScroll();
          mWallpaperOffset.syncWithScroll();
      }
  
      public void computeScrollWithoutInvalidation() {
          computeScrollHelper(false);
      }
  
      @Override
      protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
          if (!isSwitchingState()) {
              super.determineScrollingStart(ev, touchSlopScale);
          }
      }
  
      @Override
      public void announceForAccessibility(CharSequence text) {
          // Don't announce if apps is on top of us.
          if (!mLauncher.isInState(ALL_APPS)) {
              super.announceForAccessibility(text);
          }
      }
  
      private void updatePageAlphaValues() {
          // We need to check the isDragging case because updatePageAlphaValues is called between
          // goToState(SPRING_LOADED) and onStartStateTransition.
          if (!workspaceInModalState() && !mIsSwitchingState && !mDragController.isDragging()) {
              int screenCenter = getScrollX() + getMeasuredWidth() / 2;
              for (int i = 0; i < getChildCount(); i++) {
                  CellLayout child = (CellLayout) getChildAt(i);
                  if (child != null) {
                      float scrollProgress = getScrollProgress(screenCenter, child, i);
                      float alpha = 1 - Math.abs(scrollProgress);
                      if (mWorkspaceFadeInAdjacentScreens) {
                          child.getShortcutsAndWidgets().setAlpha(alpha);
                      } else {
                          // Pages that are off-screen aren't important for accessibility.
                          child.getShortcutsAndWidgets().setImportantForAccessibility(
                                  alpha > 0 ? IMPORTANT_FOR_ACCESSIBILITY_AUTO
                                          : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
                      }
                  }
              }
          }
      }
  
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
          IBinder windowToken = getWindowToken();
          mWallpaperOffset.setWindowToken(windowToken);
          computeScroll();
          mDragController.setWindowToken(windowToken);
      }
  
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          mWallpaperOffset.setWindowToken(null);
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          if (mUnlockWallpaperFromDefaultPageOnLayout) {
              mWallpaperOffset.setLockToDefaultPage(false);
              mUnlockWallpaperFromDefaultPageOnLayout = false;
          }
          if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
              mWallpaperOffset.syncWithScroll();
              mWallpaperOffset.jumpToFinal();
          }
          super.onLayout(changed, left, top, right, bottom);
          updatePageAlphaValues();
      }
  
      @Override
      public int getDescendantFocusability() {
          if (workspaceInModalState()) {
              return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
          }
          return super.getDescendantFocusability();
      }
  
      private boolean workspaceInModalState() {
          return !mLauncher.isInState(NORMAL);
      }
  
      private boolean workspaceInScrollableState() {
          return mLauncher.isInState(SPRING_LOADED) || !workspaceInModalState();
      }
  
      /** Returns whether a drag should be allowed to be started from the current workspace state. */
      public boolean workspaceIconsCanBeDragged() {
          return mLauncher.getStateManager().getState().workspaceIconsCanBeDragged;
      }
  
      private void updateChildrenLayersEnabled() {
          boolean enableChildrenLayers = mIsSwitchingState || isPageInTransition();
  
          if (enableChildrenLayers != mChildrenLayersEnabled) {
              mChildrenLayersEnabled = enableChildrenLayers;
              if (mChildrenLayersEnabled) {
                  enableHwLayersOnVisiblePages();
              } else {
                  for (int i = 0; i < getPageCount(); i++) {
                      final CellLayout cl = (CellLayout) getChildAt(i);
                      cl.enableHardwareLayer(false);
                  }
              }
          }
      }
  
      private void enableHwLayersOnVisiblePages() {
          if (mChildrenLayersEnabled) {
              final int screenCount = getChildCount();
  
              final int[] visibleScreens = getVisibleChildrenRange();
              int leftScreen = visibleScreens[0];
              int rightScreen = visibleScreens[1];
              if (mForceDrawAdjacentPages) {
                  // In overview mode, make sure that the two side pages are visible.
                  leftScreen = Utilities.boundToRange(getCurrentPage() - 1, 0, rightScreen);
                  rightScreen = Utilities.boundToRange(getCurrentPage() + 1,
                      leftScreen, getPageCount() - 1);
              }
  
              if (leftScreen == rightScreen) {
                  // make sure we're caching at least two pages always
                  if (rightScreen < screenCount - 1) {
                      rightScreen++;
                  } else if (leftScreen > 0) {
                      leftScreen--;
                  }
              }
  
              for (int i = 0; i < screenCount; i++) {
                  final CellLayout layout = (CellLayout) getPageAt(i);
                  // enable layers between left and right screen inclusive.
                  boolean enableLayer = leftScreen <= i && i <= rightScreen;
                  layout.enableHardwareLayer(enableLayer);
              }
          }
      }
  
      public void onWallpaperTap(MotionEvent ev) {
          final int[] position = mTempXY;
          getLocationOnScreen(position);
  
          int pointerIndex = ev.getActionIndex();
          position[0] += (int) ev.getX(pointerIndex);
          position[1] += (int) ev.getY(pointerIndex);
  
          mWallpaperManager.sendWallpaperCommand(getWindowToken(),
                  ev.getAction() == MotionEvent.ACTION_UP
                          ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP,
                  position[0], position[1], 0, null);
      }
  
      public void prepareDragWithProvider(DragPreviewProvider outlineProvider) {
          mOutlineProvider = outlineProvider;
      }
  
      public void snapToPageFromOverView(int whichPage) {
          snapToPage(whichPage, OVERVIEW_TRANSITION_MS, Interpolators.ZOOM_IN);
      }
  
      private void onStartStateTransition(LauncherState state) {
          mIsSwitchingState = true;
          mTransitionProgress = 0;
  
          updateChildrenLayersEnabled();
      }
  
      private void onEndStateTransition() {
          mIsSwitchingState = false;
          mForceDrawAdjacentPages = false;
          mTransitionProgress = 1;
  
          updateChildrenLayersEnabled();
          updateAccessibilityFlags();
      }
  
      /**
       * Sets the current workspace {@link LauncherState} and updates the UI without any animations
       */
      @Override
      public void setState(LauncherState toState) {
          onStartStateTransition(toState);
          mStateTransitionAnimation.setState(toState);
          onEndStateTransition();
      }
  
      /**
       * Sets the current workspace {@link LauncherState}, then animates the UI
       */
      @Override
      public void setStateWithAnimation(LauncherState toState,
              AnimatorSetBuilder builder, AnimationConfig config) {
          StateTransitionListener listener = new StateTransitionListener(toState);
          mStateTransitionAnimation.setStateWithAnimation(toState, builder, config);
  
          // Invalidate the pages now, so that we have the visible pages before the
          // animation is started
          if (toState.hasMultipleVisiblePages) {
              mForceDrawAdjacentPages = true;
          }
          invalidate(); // This will call dispatchDraw(), which calls getVisiblePages().
  
          ValueAnimator stepAnimator = ValueAnimator.ofFloat(0, 1);
          stepAnimator.addUpdateListener(listener);
          stepAnimator.setDuration(config.duration);
          stepAnimator.addListener(listener);
          builder.play(stepAnimator);
      }
  
      public WorkspaceStateTransitionAnimation getStateTransitionAnimation() {
          return mStateTransitionAnimation;
      }
  
      public void updateAccessibilityFlags() {
          // TODO: Update the accessibility flags appropriately when dragging.
          int accessibilityFlag = mLauncher.getStateManager().getState().workspaceAccessibilityFlag;
          if (!mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
              int total = getPageCount();
              for (int i = 0; i < total; i++) {
                  updateAccessibilityFlags(accessibilityFlag, (CellLayout) getPageAt(i));
              }
              setImportantForAccessibility(accessibilityFlag);
          }
      }
  
      @Override
      public AccessibilityNodeInfo createAccessibilityNodeInfo() {
          if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
              // TAPL tests verify that workspace is not present in Overview and AllApps states.
              // TAPL can work only if UIDevice is set up as setCompressedLayoutHeirarchy(false).
              // Hiding workspace from the tests when it's
              // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS.
              return null;
          }
          return super.createAccessibilityNodeInfo();
      }
  
      private void updateAccessibilityFlags(int accessibilityFlag, CellLayout page) {
          page.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
          page.getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
          page.setContentDescription(null);
          page.setAccessibilityDelegate(null);
      }
  
      public void startDrag(CellLayout.CellInfo cellInfo, DragOptions options) {
          View child = cellInfo.cell;
  
          mDragInfo = cellInfo;
          child.setVisibility(INVISIBLE);
  
          if (options.isAccessibleDrag) {
              mDragController.addDragListener(new AccessibleDragListenerAdapter(
                      this, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG) {
                  @Override
                  protected void enableAccessibleDrag(boolean enable) {
                      super.enableAccessibleDrag(enable);
                      setEnableForLayout(mLauncher.getHotseat(),enable);
                  }
              });
          }
  
          beginDragShared(child, this, options);
      }
  
      public void beginDragShared(View child, DragSource source, DragOptions options) {
          Object dragObject = child.getTag();
          if (!(dragObject instanceof ItemInfo)) {
              String msg = "Drag started with a view that has no tag set. This "
                      + "will cause a crash (issue 11627249) down the line. "
                      + "View: " + child + "  tag: " + child.getTag();
              throw new IllegalStateException(msg);
          }
          beginDragShared(child, source, (ItemInfo) dragObject,
                  new DragPreviewProvider(child), options);
      }
  
      public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
              DragPreviewProvider previewProvider, DragOptions dragOptions) {
          if (TestProtocol.sDebugTracing) {
              Log.d(TestProtocol.NO_CONTEXT_MENU, "beginDragShared");
          }
          float iconScale = 1f;
          if (child instanceof BubbleTextView) {
              Drawable icon = ((BubbleTextView) child).getIcon();
              if (icon instanceof FastBitmapDrawable) {
                  iconScale = ((FastBitmapDrawable) icon).getAnimatedScale();
              }
          }
  
          child.clearFocus();
          child.setPressed(false);
          mOutlineProvider = previewProvider;
  
          // The drag bitmap follows the touch point around on the screen
          final Bitmap b = previewProvider.createDragBitmap();
          int halfPadding = previewProvider.previewPadding / 2;
  
          float scale = previewProvider.getScaleAndPosition(b, mTempXY);
          int dragLayerX = mTempXY[0];
          int dragLayerY = mTempXY[1];
  
          DeviceProfile grid = mLauncher.getDeviceProfile();
          Point dragVisualizeOffset = null;
          Rect dragRect = null;
          if (child instanceof BubbleTextView) {
              dragRect = new Rect();
              BubbleTextView.getIconBounds(child, dragRect, grid.iconSizePx);
              dragLayerY += dragRect.top;
              // Note: The dragRect is used to calculate drag layer offsets, but the
              // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
              dragVisualizeOffset = new Point(- halfPadding, halfPadding);
          } else if (child instanceof FolderIcon) {
              int previewSize = grid.folderIconSizePx;
              dragVisualizeOffset = new Point(- halfPadding, halfPadding - child.getPaddingTop());
              dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize);
          } else if (previewProvider instanceof ShortcutDragPreviewProvider) {
              dragVisualizeOffset = new Point(- halfPadding, halfPadding);
          }
  
          // Clear the pressed state if necessary
          if (child instanceof BubbleTextView) {
              BubbleTextView icon = (BubbleTextView) child;
              icon.clearPressedBackground();
          }
  
          if (child.getParent() instanceof ShortcutAndWidgetContainer) {
              mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
          }
  
          if (child instanceof BubbleTextView && !dragOptions.isAccessibleDrag) {
              PopupContainerWithArrow popupContainer = PopupContainerWithArrow
                      .showForIcon((BubbleTextView) child);
              if (popupContainer != null) {
                  dragOptions.preDragCondition = popupContainer.createPreDragCondition();
  
                  mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
              }
          }
  
          DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
                  dragObject, dragVisualizeOffset, dragRect, scale * iconScale, scale, dragOptions);
          dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
          return dv;
      }
  
      private boolean transitionStateShouldAllowDrop() {
          return (!isSwitchingState() || mTransitionProgress > ALLOW_DROP_TRANSITION_PROGRESS) &&
                  workspaceIconsCanBeDragged();
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public boolean acceptDrop(DragObject d) {
          // If it's an external drop (e.g. from All Apps), check if it should be accepted
          CellLayout dropTargetLayout = mDropToLayout;
          if (d.dragSource != this) {
              // Don't accept the drop if we're not over a screen at time of drop
              if (dropTargetLayout == null) {
                  return false;
              }
              if (!transitionStateShouldAllowDrop()) return false;
  
              mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
  
              // We want the point to be mapped to the dragTarget.
              mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
  
              int spanX;
              int spanY;
              if (mDragInfo != null) {
                  final CellLayout.CellInfo dragCellInfo = mDragInfo;
                  spanX = dragCellInfo.spanX;
                  spanY = dragCellInfo.spanY;
              } else {
                  spanX = d.dragInfo.spanX;
                  spanY = d.dragInfo.spanY;
              }
  
              int minSpanX = spanX;
              int minSpanY = spanY;
              if (d.dragInfo instanceof PendingAddWidgetInfo) {
                  minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX;
                  minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY;
              }
  
              mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                      (int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout,
                      mTargetCell);
              float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                      mDragViewVisualCenter[1], mTargetCell);
              if (mCreateUserFolderOnDrop && willCreateUserFolder(d.dragInfo,
                      dropTargetLayout, mTargetCell, distance, true)) {
                  return true;
              }
  
              if (mAddToExistingFolderOnDrop && willAddToExistingUserFolder(d.dragInfo,
                      dropTargetLayout, mTargetCell, distance)) {
                  return true;
              }
  
              int[] resultSpan = new int[2];
              mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                      (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
                      null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP);
              boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
  
              // Don't accept the drop if there's no room for the item
              if (!foundCell) {
                  onNoCellFound(dropTargetLayout);
                  return false;
              }
          }
  
          int screenId = getIdForScreen(dropTargetLayout);
          if (screenId == EXTRA_EMPTY_SCREEN_ID) {
              commitExtraEmptyScreen();
          }
  
          return true;
      }
  
      boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell,
              float distance, boolean considerTimeout) {
          if (distance > mMaxDistanceForFolderCreation) return false;
          View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
          return willCreateUserFolder(info, dropOverView, considerTimeout);
      }
  
      boolean willCreateUserFolder(ItemInfo info, View dropOverView, boolean considerTimeout) {
          if (dropOverView != null) {
              CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
              if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY)) {
                  return false;
              }
          }
  
          boolean hasntMoved = false;
          if (mDragInfo != null) {
              hasntMoved = dropOverView == mDragInfo.cell;
          }
  
          if (dropOverView == null || hasntMoved || (considerTimeout && !mCreateUserFolderOnDrop)) {
              return false;
          }
  
          boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo);
          boolean willBecomeShortcut =
                  (info.itemType == ITEM_TYPE_APPLICATION ||
                          info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
                          info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT);
  
          return (aboveShortcut && willBecomeShortcut);
      }
  
      boolean willAddToExistingUserFolder(ItemInfo dragInfo, CellLayout target, int[] targetCell,
              float distance) {
          if (distance > mMaxDistanceForFolderCreation) return false;
          View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
          return willAddToExistingUserFolder(dragInfo, dropOverView);
  
      }
      boolean willAddToExistingUserFolder(ItemInfo dragInfo, View dropOverView) {
          if (dropOverView != null) {
              CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
              if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY)) {
                  return false;
              }
          }
  
          if (dropOverView instanceof FolderIcon) {
              FolderIcon fi = (FolderIcon) dropOverView;
              if (fi.acceptDrop(dragInfo)) {
                  return true;
              }
          }
          return false;
      }
  
      boolean createUserFolderIfNecessary(View newView, int container, CellLayout target,
              int[] targetCell, float distance, boolean external, DragView dragView) {
          if (distance > mMaxDistanceForFolderCreation) return false;
          View v = target.getChildAt(targetCell[0], targetCell[1]);
  
          boolean hasntMoved = false;
          if (mDragInfo != null) {
              CellLayout cellParent = getParentCellLayoutForView(mDragInfo.cell);
              hasntMoved = (mDragInfo.cellX == targetCell[0] &&
                      mDragInfo.cellY == targetCell[1]) && (cellParent == target);
          }
  
          if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false;
          mCreateUserFolderOnDrop = false;
          final int screenId = getIdForScreen(target);
  
          boolean aboveShortcut = (v.getTag() instanceof WorkspaceItemInfo);
          boolean willBecomeShortcut = (newView.getTag() instanceof WorkspaceItemInfo);
  
          if (aboveShortcut && willBecomeShortcut) {
              WorkspaceItemInfo sourceInfo = (WorkspaceItemInfo) newView.getTag();
              WorkspaceItemInfo destInfo = (WorkspaceItemInfo) v.getTag();
              // if the drag started here, we need to remove it from the workspace
              if (!external) {
                  getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
              }
  
              Rect folderLocation = new Rect();
              float scale = mLauncher.getDragLayer().getDescendantRectRelativeToSelf(v, folderLocation);
              target.removeView(v);
  
              FolderIcon fi =
                  mLauncher.addFolder(target, container, screenId, targetCell[0], targetCell[1]);
              destInfo.cellX = -1;
              destInfo.cellY = -1;
              sourceInfo.cellX = -1;
              sourceInfo.cellY = -1;
  
              // If the dragView is null, we can't animate
              boolean animate = dragView != null;
              if (animate) {
                  // In order to keep everything continuous, we hand off the currently rendered
                  // folder background to the newly created icon. This preserves animation state.
                  fi.setFolderBackground(mFolderCreateBg);
                  mFolderCreateBg = new PreviewBackground();
                  fi.performCreateAnimation(destInfo, v, sourceInfo, dragView, folderLocation, scale
                  );
              } else {
                  fi.prepareCreateAnimation(v);
                  fi.addItem(destInfo);
                  fi.addItem(sourceInfo);
              }
              return true;
          }
          return false;
      }
  
      boolean addToExistingFolderIfNecessary(View newView, CellLayout target, int[] targetCell,
              float distance, DragObject d, boolean external) {
          if (distance > mMaxDistanceForFolderCreation) return false;
  
          View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
          if (!mAddToExistingFolderOnDrop) return false;
          mAddToExistingFolderOnDrop = false;
  
          if (dropOverView instanceof FolderIcon) {
              FolderIcon fi = (FolderIcon) dropOverView;
              if (fi.acceptDrop(d.dragInfo)) {
                  fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
  
                  // if the drag started here, we need to remove it from the workspace
                  if (!external) {
                      getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
                  }
                  return true;
              }
          }
          return false;
      }
  
      @Override
      public void prepareAccessibilityDrop() { }
  
      public void onDrop(final DragObject d, DragOptions options) {
          if (TestProtocol.sDebugTracing) {
              Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDrop");
          }
          mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
          CellLayout dropTargetLayout = mDropToLayout;
  
          // We want the point to be mapped to the dragTarget.
          if (dropTargetLayout != null) {
              mapPointFromDropLayout(dropTargetLayout, mDragViewVisualCenter);
          }
  
          boolean droppedOnOriginalCell = false;
  
          int snapScreen = -1;
          boolean resizeOnDrop = false;
          if (d.dragSource != this || mDragInfo == null) {
              final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                      (int) mDragViewVisualCenter[1] };
              onDropExternal(touchXY, dropTargetLayout, d);
          } else {
              final View cell = mDragInfo.cell;
              boolean droppedOnOriginalCellDuringTransition = false;
              Runnable onCompleteRunnable = null;
  
              if (dropTargetLayout != null && !d.cancelled) {
                  // Move internally
                  boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
                  boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
                  int container = hasMovedIntoHotseat ?
                          LauncherSettings.Favorites.CONTAINER_HOTSEAT :
                          LauncherSettings.Favorites.CONTAINER_DESKTOP;
                  int screenId = (mTargetCell[0] < 0) ?
                          mDragInfo.screenId : getIdForScreen(dropTargetLayout);
                  int spanX = mDragInfo != null ? mDragInfo.spanX : 1;
                  int spanY = mDragInfo != null ? mDragInfo.spanY : 1;
                  // First we find the cell nearest to point at which the item is
                  // dropped, without any consideration to whether there is an item there.
  
                  mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
                          mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
                  float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                          mDragViewVisualCenter[1], mTargetCell);
  
                  // If the item being dropped is a shortcut and the nearest drop
                  // cell also contains a shortcut, then create a folder with the two shortcuts.
                  if (createUserFolderIfNecessary(cell, container,
                          dropTargetLayout, mTargetCell, distance, false, d.dragView) ||
                          addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                                  distance, d, false)) {
                      mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                      return;
                  }
  
                  // Aside from the special case where we're dropping a shortcut onto a shortcut,
                  // we need to find the nearest cell location that is vacant
                  ItemInfo item = d.dragInfo;
                  int minSpanX = item.spanX;
                  int minSpanY = item.spanY;
                  if (item.minSpanX > 0 && item.minSpanY > 0) {
                      minSpanX = item.minSpanX;
                      minSpanY = item.minSpanY;
                  }
  
                  droppedOnOriginalCell = item.screenId == screenId && item.container == container
                          && item.cellX == mTargetCell[0] && item.cellY == mTargetCell[1];
                  droppedOnOriginalCellDuringTransition = droppedOnOriginalCell && mIsSwitchingState;
  
                  // When quickly moving an item, a user may accidentally rearrange their
                  // workspace. So instead we move the icon back safely to its original position.
                  boolean returnToOriginalCellToPreventShuffling = !isFinishedSwitchingState()
                          && !droppedOnOriginalCellDuringTransition && !dropTargetLayout
                          .isRegionVacant(mTargetCell[0], mTargetCell[1], spanX, spanY);
                  int[] resultSpan = new int[2];
                  if (returnToOriginalCellToPreventShuffling) {
                      mTargetCell[0] = mTargetCell[1] = -1;
                  } else {
                      mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                              (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
                              mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
                  }
  
                  boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
  
                  // if the widget resizes on drop
                  if (foundCell && (cell instanceof AppWidgetHostView) &&
                          (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
                      resizeOnDrop = true;
                      item.spanX = resultSpan[0];
                      item.spanY = resultSpan[1];
                      AppWidgetHostView awhv = (AppWidgetHostView) cell;
                      AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
                              resultSpan[1]);
                  }
  
                  if (foundCell) {
                      if (getScreenIdForPageIndex(mCurrentPage) != screenId && !hasMovedIntoHotseat) {
                          snapScreen = getPageIndexForScreenId(screenId);
                          snapToPage(snapScreen);
                      }
  
                      final ItemInfo info = (ItemInfo) cell.getTag();
                      if (hasMovedLayouts) {
                          // Reparent the view
                          CellLayout parentCell = getParentCellLayoutForView(cell);
                          if (parentCell != null) {
                              parentCell.removeView(cell);
                          } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
                              throw new NullPointerException("mDragInfo.cell has null parent");
                          }
                          addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
                                  info.spanX, info.spanY);
                      }
  
                      // update the item's position after drop
                      CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                      lp.cellX = lp.tmpCellX = mTargetCell[0];
                      lp.cellY = lp.tmpCellY = mTargetCell[1];
                      lp.cellHSpan = item.spanX;
                      lp.cellVSpan = item.spanY;
                      lp.isLockedToGrid = true;
  
                      if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
                              cell instanceof LauncherAppWidgetHostView) {
                          final CellLayout cellLayout = dropTargetLayout;
                          // We post this call so that the widget has a chance to be placed
                          // in its final location
  
                          final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                          LauncherAppWidgetProviderInfo pInfo = (LauncherAppWidgetProviderInfo) hostView.getAppWidgetInfo();
                          if (pInfo != null && (pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE || pInfo.minSpanX > 1 || pInfo.minSpanY > 1)
                                  && !d.accessibleDrag) {
                              onCompleteRunnable = new Runnable() {
                                  public void run() {
                                      if (!isPageInTransition()) {
                                          AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
                                      }
                                  }
                              };
                          }
                      }
  
                      mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
                              lp.cellX, lp.cellY, item.spanX, item.spanY);
                  } else {
                      if (!returnToOriginalCellToPreventShuffling) {
                          onNoCellFound(dropTargetLayout);
                      }
  
                      // If we can't find a drop location, we return the item to its original position
                      CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                      mTargetCell[0] = lp.cellX;
                      mTargetCell[1] = lp.cellY;
                      CellLayout layout = (CellLayout) cell.getParent().getParent();
                      layout.markCellsAsOccupiedForView(cell);
                  }
              }
  
              final CellLayout parent = (CellLayout) cell.getParent().getParent();
              if (d.dragView.hasDrawn()) {
                  if (droppedOnOriginalCellDuringTransition) {
                      // Animate the item to its original position, while simultaneously exiting
                      // spring-loaded mode so the page meets the icon where it was picked up.
                      mLauncher.getDragController().animateDragViewToOriginalPosition(
                              onCompleteRunnable, cell, SPRING_LOADED_TRANSITION_MS);
                      mLauncher.getStateManager().goToState(NORMAL);
                      mLauncher.getDropTargetBar().onDragEnd();
                      parent.onDropChild(cell);
                      return;
                  }
                  final ItemInfo info = (ItemInfo) cell.getTag();
                  boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
                          || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
                  if (isWidget) {
                      int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
                              ANIMATE_INTO_POSITION_AND_DISAPPEAR;
                      animateWidgetDrop(info, parent, d.dragView, null, animationType, cell, false);
                  } else {
                      int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
                      mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
                              this);
                  }
              } else {
                  d.deferDragViewCleanupPostAnimation = false;
                  cell.setVisibility(VISIBLE);
              }
              parent.onDropChild(cell);
  
              mLauncher.getStateManager().goToState(
                      NORMAL, SPRING_LOADED_EXIT_DELAY, onCompleteRunnable);
          }
  
          if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
              d.stateAnnouncer.completeAction(R.string.item_moved);
          }
      }
  
      public void onNoCellFound(View dropTargetLayout) {
          int strId = mLauncher.isHotseatLayout(dropTargetLayout)
                  ? R.string.hotseat_out_of_space : R.string.out_of_space;
          Toast.makeText(mLauncher, mLauncher.getString(strId), Toast.LENGTH_SHORT).show();
      }
  
      /**
       * Computes the area relative to dragLayer which is used to display a page.
       */
      public void getPageAreaRelativeToDragLayer(Rect outArea) {
          CellLayout child = (CellLayout) getChildAt(getNextPage());
          if (child == null) {
              return;
          }
  
          ShortcutAndWidgetContainer boundingLayout = child.getShortcutsAndWidgets();
          mLauncher.getDragLayer().getDescendantRectRelativeToSelf(boundingLayout, outArea);
      }
  
      @Override
      public void onDragEnter(DragObject d) {
          if (ENFORCE_DRAG_EVENT_ORDER) {
              enforceDragParity("onDragEnter", 1, 1);
          }
  
          mCreateUserFolderOnDrop = false;
          mAddToExistingFolderOnDrop = false;
  
          mDropToLayout = null;
          mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
          setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1]);
      }
  
      @Override
      public void onDragExit(DragObject d) {
          if (ENFORCE_DRAG_EVENT_ORDER) {
              enforceDragParity("onDragExit", -1, 0);
          }
  
          // Here we store the final page that will be dropped to, if the workspace in fact
          // receives the drop
          mDropToLayout = mDragTargetLayout;
          if (mDragMode == DRAG_MODE_CREATE_FOLDER) {
              mCreateUserFolderOnDrop = true;
          } else if (mDragMode == DRAG_MODE_ADD_TO_FOLDER) {
              mAddToExistingFolderOnDrop = true;
          }
  
          // Reset the previous drag target
          setCurrentDropLayout(null);
          setCurrentDragOverlappingLayout(null);
  
          mSpringLoadedDragController.cancel();
      }
  
      private void enforceDragParity(String event, int update, int expectedValue) {
          enforceDragParity(this, event, update, expectedValue);
          for (int i = 0; i < getChildCount(); i++) {
              enforceDragParity(getChildAt(i), event, update, expectedValue);
          }
      }
  
      private void enforceDragParity(View v, String event, int update, int expectedValue) {
          Object tag = v.getTag(R.id.drag_event_parity);
          int value = tag == null ? 0 : (Integer) tag;
          value += update;
          v.setTag(R.id.drag_event_parity, value);
  
          if (value != expectedValue) {
              Log.e(TAG, event + ": Drag contract violated: " + value);
          }
      }
  
      void setCurrentDropLayout(CellLayout layout) {
          if (mDragTargetLayout != null) {
              mDragTargetLayout.revertTempState();
              mDragTargetLayout.onDragExit();
          }
          mDragTargetLayout = layout;
          if (mDragTargetLayout != null) {
              mDragTargetLayout.onDragEnter();
          }
          cleanupReorder(true);
          cleanupFolderCreation();
          setCurrentDropOverCell(-1, -1);
      }
  
      void setCurrentDragOverlappingLayout(CellLayout layout) {
          if (mDragOverlappingLayout != null) {
              mDragOverlappingLayout.setIsDragOverlapping(false);
          }
          mDragOverlappingLayout = layout;
          if (mDragOverlappingLayout != null) {
              mDragOverlappingLayout.setIsDragOverlapping(true);
          }
          // Invalidating the scrim will also force this CellLayout
          // to be invalidated so that it is highlighted if necessary.
          mLauncher.getDragLayer().getScrim().invalidate();
      }
  
      public CellLayout getCurrentDragOverlappingLayout() {
          return mDragOverlappingLayout;
      }
  
      void setCurrentDropOverCell(int x, int y) {
          if (x != mDragOverX || y != mDragOverY) {
              mDragOverX = x;
              mDragOverY = y;
              setDragMode(DRAG_MODE_NONE);
          }
      }
  
      void setDragMode(int dragMode) {
          if (dragMode != mDragMode) {
              if (dragMode == DRAG_MODE_NONE) {
                  cleanupAddToFolder();
                  // We don't want to cancel the re-order alarm every time the target cell changes
                  // as this feels to slow / unresponsive.
                  cleanupReorder(false);
                  cleanupFolderCreation();
              } else if (dragMode == DRAG_MODE_ADD_TO_FOLDER) {
                  cleanupReorder(true);
                  cleanupFolderCreation();
              } else if (dragMode == DRAG_MODE_CREATE_FOLDER) {
                  cleanupAddToFolder();
                  cleanupReorder(true);
              } else if (dragMode == DRAG_MODE_REORDER) {
                  cleanupAddToFolder();
                  cleanupFolderCreation();
              }
              mDragMode = dragMode;
          }
      }
  
      private void cleanupFolderCreation() {
          if (mFolderCreateBg != null) {
              mFolderCreateBg.animateToRest();
          }
          mFolderCreationAlarm.setOnAlarmListener(null);
          mFolderCreationAlarm.cancelAlarm();
      }
  
      private void cleanupAddToFolder() {
          if (mDragOverFolderIcon != null) {
              mDragOverFolderIcon.onDragExit();
              mDragOverFolderIcon = null;
          }
      }
  
      private void cleanupReorder(boolean cancelAlarm) {
          // Any pending reorders are canceled
          if (cancelAlarm) {
              mReorderAlarm.cancelAlarm();
          }
          mLastReorderX = -1;
          mLastReorderY = -1;
      }
  
     /*
      *
      * Convert the 2D coordinate xy from the parent View's coordinate space to this CellLayout's
      * coordinate space. The argument xy is modified with the return result.
      */
     private void mapPointFromSelfToChild(View v, float[] xy) {
         xy[0] = xy[0] - v.getLeft();
         xy[1] = xy[1] - v.getTop();
     }
  
     boolean isPointInSelfOverHotseat(int x, int y) {
         mTempFXY[0] = x;
         mTempFXY[1] = y;
         mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, mTempFXY, true);
         View hotseat = mLauncher.getHotseat();
         return mTempFXY[0] >= hotseat.getLeft() &&
                 mTempFXY[0] <= hotseat.getRight() &&
                 mTempFXY[1] >= hotseat.getTop() &&
                 mTempFXY[1] <= hotseat.getBottom();
     }
  
      /**
       * Updates the point in {@param xy} to point to the co-ordinate space of {@param layout}
       * @param layout either hotseat of a page in workspace
       * @param xy the point location in workspace co-ordinate space
       */
     private void mapPointFromDropLayout(CellLayout layout, float[] xy) {
         if (mLauncher.isHotseatLayout(layout)) {
             mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, xy, true);
             mLauncher.getDragLayer().mapCoordInSelfToDescendant(layout, xy);
         } else {
             mapPointFromSelfToChild(layout, xy);
         }
     }
  
      private boolean isDragWidget(DragObject d) {
          return (d.dragInfo instanceof LauncherAppWidgetInfo ||
                  d.dragInfo instanceof PendingAddWidgetInfo);
      }
  
      public void onDragOver(DragObject d) {
          // Skip drag over events while we are dragging over side pages
          if (!transitionStateShouldAllowDrop()) return;
  
          ItemInfo item = d.dragInfo;
          if (item == null) {
              if (FeatureFlags.IS_DOGFOOD_BUILD) {
                  throw new NullPointerException("DragObject has null info");
              }
              return;
          }
  
          // Ensure that we have proper spans for the item that we are dropping
          if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found");
          mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
  
          final View child = (mDragInfo == null) ? null : mDragInfo.cell;
          if (setDropLayoutForDragObject(d, mDragViewVisualCenter[0], mDragViewVisualCenter[1])) {
              if (mLauncher.isHotseatLayout(mDragTargetLayout)) {
                  mSpringLoadedDragController.cancel();
              } else {
                  mSpringLoadedDragController.setAlarm(mDragTargetLayout);
              }
          }
  
          // Handle the drag over
          if (mDragTargetLayout != null) {
              // We want the point to be mapped to the dragTarget.
              mapPointFromDropLayout(mDragTargetLayout, mDragViewVisualCenter);
  
              int minSpanX = item.spanX;
              int minSpanY = item.spanY;
              if (item.minSpanX > 0 && item.minSpanY > 0) {
                  minSpanX = item.minSpanX;
                  minSpanY = item.minSpanY;
              }
  
              mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                      (int) mDragViewVisualCenter[1], minSpanX, minSpanY,
                      mDragTargetLayout, mTargetCell);
              int reorderX = mTargetCell[0];
              int reorderY = mTargetCell[1];
  
              setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);
  
              float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
                      mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
  
              manageFolderFeedback(mDragTargetLayout, mTargetCell, targetCellDistance, d);
  
              boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                      mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
                      item.spanY, child, mTargetCell);
  
              if (!nearestDropOccupied) {
                  mDragTargetLayout.visualizeDropLocation(child, mOutlineProvider,
                          mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d);
              } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                      && !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
                      mLastReorderY != reorderY)) {
  
                  int[] resultSpan = new int[2];
                  mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                          (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
                          child, mTargetCell, resultSpan, CellLayout.MODE_SHOW_REORDER_HINT);
  
                  // Otherwise, if we aren't adding to or creating a folder and there's no pending
                  // reorder, then we schedule a reorder
                  ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
                          minSpanX, minSpanY, item.spanX, item.spanY, d, child);
                  mReorderAlarm.setOnAlarmListener(listener);
                  mReorderAlarm.setAlarm(REORDER_TIMEOUT);
              }
  
              if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
                      !nearestDropOccupied) {
                  if (mDragTargetLayout != null) {
                      mDragTargetLayout.revertTempState();
                  }
              }
          }
      }
  
      /**
       * Updates {@link #mDragTargetLayout} and {@link #mDragOverlappingLayout}
       * based on the DragObject's position.
       *
       * The layout will be:
       * - The Hotseat if the drag object is over it
       * - A side page if we are in spring-loaded mode and the drag object is over it
       * - The current page otherwise
       *
       * @return whether the layout is different from the current {@link #mDragTargetLayout}.
       */
      private boolean setDropLayoutForDragObject(DragObject d, float centerX, float centerY) {
          CellLayout layout = null;
          // Test to see if we are over the hotseat first
          if (mLauncher.getHotseat() != null && !isDragWidget(d)) {
              if (isPointInSelfOverHotseat(d.x, d.y)) {
                  layout = mLauncher.getHotseat();
              }
          }
  
          int nextPage = getNextPage();
          if (layout == null && !isPageInTransition()) {
              // Check if the item is dragged over left page
              mTempTouchCoordinates[0] = Math.min(centerX, d.x);
              mTempTouchCoordinates[1] = d.y;
              layout = verifyInsidePage(nextPage + (mIsRtl ? 1 : -1), mTempTouchCoordinates);
          }
  
          if (layout == null && !isPageInTransition()) {
              // Check if the item is dragged over right page
              mTempTouchCoordinates[0] = Math.max(centerX, d.x);
              mTempTouchCoordinates[1] = d.y;
              layout = verifyInsidePage(nextPage + (mIsRtl ? -1 : 1), mTempTouchCoordinates);
          }
  
          // Always pick the current page.
          if (layout == null && nextPage >= 0 && nextPage < getPageCount()) {
              layout = (CellLayout) getChildAt(nextPage);
          }
          if (layout != mDragTargetLayout) {
              setCurrentDropLayout(layout);
              setCurrentDragOverlappingLayout(layout);
              return true;
          }
          return false;
      }
  
      /**
       * Returns the child CellLayout if the point is inside the page coordinates, null otherwise.
       */
      private CellLayout verifyInsidePage(int pageNo, float[] touchXy)  {
          if (pageNo >= 0 && pageNo < getPageCount()) {
              CellLayout cl = (CellLayout) getChildAt(pageNo);
              mapPointFromSelfToChild(cl, touchXy);
              if (touchXy[0] >= 0 && touchXy[0] <= cl.getWidth() &&
                      touchXy[1] >= 0 && touchXy[1] <= cl.getHeight()) {
                  // This point is inside the cell layout
                  return cl;
              }
          }
          return null;
      }
  
      private void manageFolderFeedback(CellLayout targetLayout,
              int[] targetCell, float distance, DragObject dragObject) {
          if (distance > mMaxDistanceForFolderCreation) {
              if (mDragMode != DRAG_MODE_NONE) {
                  setDragMode(DRAG_MODE_NONE);
              }
              return;
          }
  
          final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]);
          ItemInfo info = dragObject.dragInfo;
          boolean userFolderPending = willCreateUserFolder(info, dragOverView, false);
          if (mDragMode == DRAG_MODE_NONE && userFolderPending &&
                  !mFolderCreationAlarm.alarmPending()) {
  
              FolderCreationAlarmListener listener = new
                      FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]);
  
              if (!dragObject.accessibleDrag) {
                  mFolderCreationAlarm.setOnAlarmListener(listener);
                  mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
              } else {
                  listener.onAlarm(mFolderCreationAlarm);
              }
  
              if (dragObject.stateAnnouncer != null) {
                  dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
                          .getDescriptionForDropOver(dragOverView, getContext()));
              }
              return;
          }
  
          boolean willAddToFolder = willAddToExistingUserFolder(info, dragOverView);
          if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
              mDragOverFolderIcon = ((FolderIcon) dragOverView);
              mDragOverFolderIcon.onDragEnter(info);
              if (targetLayout != null) {
                  targetLayout.clearDragOutlines();
              }
              setDragMode(DRAG_MODE_ADD_TO_FOLDER);
  
              if (dragObject.stateAnnouncer != null) {
                  dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
                          .getDescriptionForDropOver(dragOverView, getContext()));
              }
              return;
          }
  
          if (mDragMode == DRAG_MODE_ADD_TO_FOLDER && !willAddToFolder) {
              setDragMode(DRAG_MODE_NONE);
          }
          if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) {
              setDragMode(DRAG_MODE_NONE);
          }
      }
  
      class FolderCreationAlarmListener implements OnAlarmListener {
          final CellLayout layout;
          final int cellX;
          final int cellY;
  
          final PreviewBackground bg = new PreviewBackground();
  
          public FolderCreationAlarmListener(CellLayout layout, int cellX, int cellY) {
              this.layout = layout;
              this.cellX = cellX;
              this.cellY = cellY;
  
              BubbleTextView cell = (BubbleTextView) layout.getChildAt(cellX, cellY);
              bg.setup(mLauncher, mLauncher, null, cell.getMeasuredWidth(), cell.getPaddingTop());
  
              // The full preview background should appear behind the icon
              bg.isClipping = false;
          }
  
          public void onAlarm(Alarm alarm) {
              mFolderCreateBg = bg;
              mFolderCreateBg.animateToAccept(layout, cellX, cellY);
              layout.clearDragOutlines();
              setDragMode(DRAG_MODE_CREATE_FOLDER);
          }
      }
  
      class ReorderAlarmListener implements OnAlarmListener {
          final float[] dragViewCenter;
          final int minSpanX, minSpanY, spanX, spanY;
          final DragObject dragObject;
          final View child;
  
          public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX,
                  int spanY, DragObject dragObject, View child) {
              this.dragViewCenter = dragViewCenter;
              this.minSpanX = minSpanX;
              this.minSpanY = minSpanY;
              this.spanX = spanX;
              this.spanY = spanY;
              this.child = child;
              this.dragObject = dragObject;
          }
  
          public void onAlarm(Alarm alarm) {
              int[] resultSpan = new int[2];
              mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                      (int) mDragViewVisualCenter[1], minSpanX, minSpanY, mDragTargetLayout,
                      mTargetCell);
              mLastReorderX = mTargetCell[0];
              mLastReorderY = mTargetCell[1];
  
              mTargetCell = mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                  (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
                  child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
  
              if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
                  mDragTargetLayout.revertTempState();
              } else {
                  setDragMode(DRAG_MODE_REORDER);
              }
  
              boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
              mDragTargetLayout.visualizeDropLocation(child, mOutlineProvider,
                  mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject);
          }
      }
  
      @Override
      public void getHitRectRelativeToDragLayer(Rect outRect) {
          // We want the workspace to have the whole area of the display (it will find the correct
          // cell layout to drop to in the existing drag/drop logic.
          mLauncher.getDragLayer().getDescendantRectRelativeToSelf(this, outRect);
      }
  
      /**
       * Drop an item that didn't originate on one of the workspace screens.
       * It may have come from Launcher (e.g. from all apps or customize), or it may have
       * come from another app altogether.
       *
       * NOTE: This can also be called when we are outside of a drag event, when we want
       * to add an item to one of the workspace screens.
       */
      private void onDropExternal(final int[] touchXY, final CellLayout cellLayout, DragObject d) {
          if (TestProtocol.sDebugTracing) {
              Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDropExternal");
          }
          if (d.dragInfo instanceof PendingAddShortcutInfo) {
              WorkspaceItemInfo si = ((PendingAddShortcutInfo) d.dragInfo)
                      .activityInfo.createWorkspaceItemInfo();
              if (si != null) {
                  d.dragInfo = si;
              }
          }
  
          ItemInfo info = d.dragInfo;
          int spanX = info.spanX;
          int spanY = info.spanY;
          if (mDragInfo != null) {
              spanX = mDragInfo.spanX;
              spanY = mDragInfo.spanY;
          }
  
          final int container = mLauncher.isHotseatLayout(cellLayout) ?
                  LauncherSettings.Favorites.CONTAINER_HOTSEAT :
                      LauncherSettings.Favorites.CONTAINER_DESKTOP;
          final int screenId = getIdForScreen(cellLayout);
          if (!mLauncher.isHotseatLayout(cellLayout)
                  && screenId != getScreenIdForPageIndex(mCurrentPage)
                  && !mLauncher.isInState(SPRING_LOADED)) {
              snapToPage(getPageIndexForScreenId(screenId));
          }
  
          if (info instanceof PendingAddItemInfo) {
              final PendingAddItemInfo pendingInfo = (PendingAddItemInfo) info;
  
              boolean findNearestVacantCell = true;
              if (pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
                  mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
                          cellLayout, mTargetCell);
                  float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                          mDragViewVisualCenter[1], mTargetCell);
                  if (willCreateUserFolder(d.dragInfo, cellLayout, mTargetCell, distance, true)
                          || willAddToExistingUserFolder(
                                  d.dragInfo, cellLayout, mTargetCell, distance)) {
                      findNearestVacantCell = false;
                  }
              }
  
              final ItemInfo item = d.dragInfo;
              boolean updateWidgetSize = false;
              if (findNearestVacantCell) {
                  int minSpanX = item.spanX;
                  int minSpanY = item.spanY;
                  if (item.minSpanX > 0 && item.minSpanY > 0) {
                      minSpanX = item.minSpanX;
                      minSpanY = item.minSpanY;
                  }
                  int[] resultSpan = new int[2];
                  mTargetCell = cellLayout.performReorder((int) mDragViewVisualCenter[0],
                          (int) mDragViewVisualCenter[1], minSpanX, minSpanY, info.spanX, info.spanY,
                          null, mTargetCell, resultSpan, CellLayout.MODE_ON_DROP_EXTERNAL);
  
                  if (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY) {
                      updateWidgetSize = true;
                  }
                  item.spanX = resultSpan[0];
                  item.spanY = resultSpan[1];
              }
  
              Runnable onAnimationCompleteRunnable = new Runnable() {
                  @Override
                  public void run() {
                      // Normally removeExtraEmptyScreen is called in Workspace#onDragEnd, but when
                      // adding an item that may not be dropped right away (due to a config activity)
                      // we defer the removal until the activity returns.
                      deferRemoveExtraEmptyScreen();
  
                      // When dragging and dropping from customization tray, we deal with creating
                      // widgets/shortcuts/folders in a slightly different way
                      mLauncher.addPendingItem(pendingInfo, container, screenId, mTargetCell,
                              item.spanX, item.spanY);
                  }
              };
              boolean isWidget = pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
                      || pendingInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
  
              AppWidgetHostView finalView = isWidget ?
                      ((PendingAddWidgetInfo) pendingInfo).boundWidget : null;
  
              if (finalView != null && updateWidgetSize) {
                  AppWidgetResizeFrame.updateWidgetSizeRanges(finalView, mLauncher, item.spanX,
                          item.spanY);
              }
  
              int animationStyle = ANIMATE_INTO_POSITION_AND_DISAPPEAR;
              if (isWidget && ((PendingAddWidgetInfo) pendingInfo).info != null &&
                      ((PendingAddWidgetInfo) pendingInfo).getHandler().needsConfigure()) {
                  animationStyle = ANIMATE_INTO_POSITION_AND_REMAIN;
              }
              animateWidgetDrop(info, cellLayout, d.dragView, onAnimationCompleteRunnable,
                      animationStyle, finalView, true);
          } else {
              // This is for other drag/drop cases, like dragging from All Apps
              mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
  
              View view;
  
              switch (info.itemType) {
                  case ITEM_TYPE_APPLICATION:
                  case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                  case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                      if (info instanceof AppInfo) {
                          // Came from all apps -- make a copy
                          info = ((AppInfo) info).makeWorkspaceItem();
                          d.dragInfo = info;
                      }
                      view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
                      break;
                  case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                      view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
                              (FolderInfo) info);
                      break;
                  default:
                      throw new IllegalStateException("Unknown item type: " + info.itemType);
              }
  
              // First we find the cell nearest to point at which the item is
              // dropped, without any consideration to whether there is an item there.
              if (touchXY != null) {
                  mTargetCell = findNearestArea(touchXY[0], touchXY[1], spanX, spanY,
                          cellLayout, mTargetCell);
                  float distance = cellLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                          mDragViewVisualCenter[1], mTargetCell);
                  if (createUserFolderIfNecessary(view, container, cellLayout, mTargetCell, distance,
                          true, d.dragView)) {
                      return;
                  }
                  if (addToExistingFolderIfNecessary(view, cellLayout, mTargetCell, distance, d,
                          true)) {
                      return;
                  }
              }
  
              if (touchXY != null) {
                  // when dragging and dropping, just find the closest free spot
                  mTargetCell = cellLayout.performReorder((int) mDragViewVisualCenter[0],
                          (int) mDragViewVisualCenter[1], 1, 1, 1, 1,
                          null, mTargetCell, null, CellLayout.MODE_ON_DROP_EXTERNAL);
              } else {
                  cellLayout.findCellForSpan(mTargetCell, 1, 1);
              }
              // Add the item to DB before adding to screen ensures that the container and other
              // values of the info is properly updated.
              mLauncher.getModelWriter().addOrMoveItemInDatabase(info, container, screenId,
                      mTargetCell[0], mTargetCell[1]);
  
              addInScreen(view, container, screenId, mTargetCell[0], mTargetCell[1],
                      info.spanX, info.spanY);
              cellLayout.onDropChild(view);
              cellLayout.getShortcutsAndWidgets().measureChild(view);
  
              if (d.dragView != null) {
                  // We wrap the animation call in the temporary set and reset of the current
                  // cellLayout to its final transform -- this means we animate the drag view to
                  // the correct final location.
                  setFinalTransitionTransform();
                  mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, view, this);
                  resetTransitionTransform();
              }
          }
      }
  
      public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
          int[] unScaledSize = estimateItemSize(widgetInfo);
          int visibility = layout.getVisibility();
          layout.setVisibility(VISIBLE);
  
          int width = MeasureSpec.makeMeasureSpec(unScaledSize[0], MeasureSpec.EXACTLY);
          int height = MeasureSpec.makeMeasureSpec(unScaledSize[1], MeasureSpec.EXACTLY);
          Bitmap b = Bitmap.createBitmap(unScaledSize[0], unScaledSize[1],
                  Bitmap.Config.ARGB_8888);
          layout.measure(width, height);
          layout.layout(0, 0, unScaledSize[0], unScaledSize[1]);
          layout.draw(new Canvas(b));
          layout.setVisibility(visibility);
          return b;
      }
  
      private void getFinalPositionForDropAnimation(int[] loc, float[] scaleXY,
              DragView dragView, CellLayout layout, ItemInfo info, int[] targetCell, boolean scale) {
          // Now we animate the dragView, (ie. the widget or shortcut preview) into its final
          // location and size on the home screen.
          int spanX = info.spanX;
          int spanY = info.spanY;
  
          Rect r = estimateItemPosition(layout, targetCell[0], targetCell[1], spanX, spanY);
          if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
              DeviceProfile profile = mLauncher.getDeviceProfile();
              Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
          }
  
          mTempFXY[0] = r.left;
          mTempFXY[1] = r.top;
          setFinalTransitionTransform();
          float cellLayoutScale =
                  mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(layout, mTempFXY, true);
          resetTransitionTransform();
          Utilities.roundArray(mTempFXY, loc);
  
          if (scale) {
              float dragViewScaleX = (1.0f * r.width()) / dragView.getMeasuredWidth();
              float dragViewScaleY = (1.0f * r.height()) / dragView.getMeasuredHeight();
  
              // The animation will scale the dragView about its center, so we need to center about
              // the final location.
              loc[0] -= (dragView.getMeasuredWidth() - cellLayoutScale * r.width()) / 2
                      - Math.ceil(layout.getUnusedHorizontalSpace() / 2f);
              loc[1] -= (dragView.getMeasuredHeight() - cellLayoutScale * r.height()) / 2;
              scaleXY[0] = dragViewScaleX * cellLayoutScale;
              scaleXY[1] = dragViewScaleY * cellLayoutScale;
          } else {
              // Since we are not cross-fading the dragView, align the drag view to the
              // final cell position.
              float dragScale = dragView.getInitialScale() * cellLayoutScale;
              loc[0] += (dragScale - 1) * dragView.getWidth() / 2;
              loc[1] += (dragScale - 1) * dragView.getHeight() / 2;
              scaleXY[0] = scaleXY[1] = dragScale;
  
              // If a dragRegion was provided, offset the final position accordingly.
              Rect dragRegion = dragView.getDragRegion();
              if (dragRegion != null) {
                  loc[0] += cellLayoutScale * dragRegion.left;
                  loc[1] += cellLayoutScale * dragRegion.top;
              }
          }
      }
  
      public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
              final Runnable onCompleteRunnable, int animationType, final View finalView,
              boolean external) {
          Rect from = new Rect();
          mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from);
  
          int[] finalPos = new int[2];
          float scaleXY[] = new float[2];
          boolean scalePreview = !(info instanceof PendingAddShortcutInfo);
          getFinalPositionForDropAnimation(finalPos, scaleXY, dragView, cellLayout, info, mTargetCell,
                  scalePreview);
  
          Resources res = mLauncher.getResources();
          final int duration = res.getInteger(R.integer.config_dropAnimMaxDuration) - 200;
  
          boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET ||
                  info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
          if ((animationType == ANIMATE_INTO_POSITION_AND_RESIZE || external) && finalView != null) {
              Bitmap crossFadeBitmap = createWidgetBitmap(info, finalView);
              dragView.setCrossFadeBitmap(crossFadeBitmap);
              dragView.crossFade((int) (duration * 0.8f));
          } else if (isWidget && external) {
              scaleXY[0] = scaleXY[1] = Math.min(scaleXY[0],  scaleXY[1]);
          }
  
          DragLayer dragLayer = mLauncher.getDragLayer();
          if (animationType == CANCEL_TWO_STAGE_WIDGET_DROP_ANIMATION) {
              mLauncher.getDragLayer().animateViewIntoPosition(dragView, finalPos, 0f, 0.1f, 0.1f,
                      DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
          } else {
              int endStyle;
              if (animationType == ANIMATE_INTO_POSITION_AND_REMAIN) {
                  endStyle = DragLayer.ANIMATION_END_REMAIN_VISIBLE;
              } else {
                  endStyle = DragLayer.ANIMATION_END_DISAPPEAR;
              }
  
              Runnable onComplete = new Runnable() {
                  @Override
                  public void run() {
                      if (finalView != null) {
                          finalView.setVisibility(VISIBLE);
                      }
                      if (onCompleteRunnable != null) {
                          onCompleteRunnable.run();
                      }
                  }
              };
              dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0],
                      finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
                      duration, this);
          }
      }
  
      public void setFinalTransitionTransform() {
          if (isSwitchingState()) {
              mCurrentScale = getScaleX();
              setScaleX(mStateTransitionAnimation.getFinalScale());
              setScaleY(mStateTransitionAnimation.getFinalScale());
          }
      }
      public void resetTransitionTransform() {
          if (isSwitchingState()) {
              setScaleX(mCurrentScale);
              setScaleY(mCurrentScale);
          }
      }
  
      /**
       * Return the current CellInfo describing our current drag; this method exists
       * so that Launcher can sync this object with the correct info when the activity is created/
       * destroyed
       *
       */
      public CellLayout.CellInfo getDragInfo() {
          return mDragInfo;
      }
  
      /**
       * Calculate the nearest cell where the given object would be dropped.
       *
       * pixelX and pixelY should be in the coordinate system of layout
       */
      @Thunk int[] findNearestArea(int pixelX, int pixelY,
              int spanX, int spanY, CellLayout layout, int[] recycle) {
          return layout.findNearestArea(
                  pixelX, pixelY, spanX, spanY, recycle);
      }
  
      void setup(DragController dragController) {
          mSpringLoadedDragController = new SpringLoadedDragController(mLauncher);
          mDragController = dragController;
  
          // hardware layers on children are enabled on startup, but should be disabled until
          // needed
          updateChildrenLayersEnabled();
      }
  
      /**
       * Called at the end of a drag which originated on the workspace.
       */
      public void onDropCompleted(final View target, final DragObject d,
              final boolean success) {
          if (success) {
              if (target != this && mDragInfo != null) {
                  removeWorkspaceItem(mDragInfo.cell);
              }
          } else if (mDragInfo != null) {
              final CellLayout cellLayout = mLauncher.getCellLayout(
                      mDragInfo.container, mDragInfo.screenId);
              if (cellLayout != null) {
                  cellLayout.onDropChild(mDragInfo.cell);
              } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
                  throw new RuntimeException("Invalid state: cellLayout == null in "
                          + "Workspace#onDropCompleted. Please file a bug. ");
              }
          }
          View cell = getHomescreenIconByItemId(d.originalDragInfo.id);
          if (d.cancelled && cell != null) {
              cell.setVisibility(VISIBLE);
          }
          mDragInfo = null;
      }
  
      /**
       * For opposite operation. See {@link #addInScreen}.
       */
      public void removeWorkspaceItem(View v) {
          CellLayout parentCell = getParentCellLayoutForView(v);
          if (parentCell != null) {
              parentCell.removeView(v);
          } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
              // When an app is uninstalled using the drop target, we wait until resume to remove
              // the icon. We also remove all the corresponding items from the workspace at
              // {@link Launcher#bindComponentsRemoved}. That call can come before or after
              // {@link Launcher#mOnResumeCallbacks} depending on how busy the worker thread is.
              Log.e(TAG, "mDragInfo.cell has null parent");
          }
          if (v instanceof DropTarget) {
              mDragController.removeDropTarget((DropTarget) v);
          }
      }
  
      /**
       * Removed widget from workspace by appWidgetId
       * @param appWidgetId
       */
      public void removeWidget(int appWidgetId) {
          mapOverItems((info, view) -> {
              if (info instanceof LauncherAppWidgetInfo) {
                  LauncherAppWidgetInfo appWidgetInfo = (LauncherAppWidgetInfo) info;
                  if (appWidgetInfo.appWidgetId == appWidgetId) {
                      mLauncher.removeItem(view, appWidgetInfo, true);
                      return true;
                  }
              }
              return false;
          });
      }
  
      /**
       * Removes all folder listeners
       */
      public void removeFolderListeners() {
          mapOverItems(new ItemOperator() {
              @Override
              public boolean evaluate(ItemInfo info, View view) {
                  if (view instanceof FolderIcon) {
                      ((FolderIcon) view).removeListeners();
                  }
                  return false;
              }
          });
      }
  
      public boolean isDropEnabled() {
          return true;
      }
  
      @Override
      protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
          // We don't dispatch restoreInstanceState to our children using this code path.
          // Some pages will be restored immediately as their items are bound immediately, and
          // others we will need to wait until after their items are bound.
          mSavedStates = container;
      }
  
      public void restoreInstanceStateForChild(int child) {
          if (mSavedStates != null) {
              mRestoredPages.add(child);
              CellLayout cl = (CellLayout) getChildAt(child);
              if (cl != null) {
                  cl.restoreInstanceState(mSavedStates);
              }
          }
      }
  
      public void restoreInstanceStateForRemainingPages() {
          int count = getChildCount();
          for (int i = 0; i < count; i++) {
              if (!mRestoredPages.contains(i)) {
                  restoreInstanceStateForChild(i);
              }
          }
          mRestoredPages.clear();
          mSavedStates = null;
      }
  
      @Override
      public boolean scrollLeft() {
          boolean result = false;
          if (!mIsSwitchingState && workspaceInScrollableState()) {
              result = super.scrollLeft();
          }
          Folder openFolder = Folder.getOpen(mLauncher);
          if (openFolder != null) {
              openFolder.completeDragExit();
          }
          return result;
      }
  
      @Override
      public boolean scrollRight() {
          boolean result = false;
          if (!mIsSwitchingState && workspaceInScrollableState()) {
              result = super.scrollRight();
          }
          Folder openFolder = Folder.getOpen(mLauncher);
          if (openFolder != null) {
              openFolder.completeDragExit();
          }
          return result;
      }
  
      /**
       * Returns a specific CellLayout
       */
      CellLayout getParentCellLayoutForView(View v) {
          for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
              if (layout.getShortcutsAndWidgets().indexOfChild(v) > -1) {
                  return layout;
              }
          }
          return null;
      }
  
      /**
       * Returns a list of all the CellLayouts on the Homescreen.
       */
      private CellLayout[] getWorkspaceAndHotseatCellLayouts() {
          int screenCount = getChildCount();
          final CellLayout[] layouts;
          if (mLauncher.getHotseat() != null) {
              layouts = new CellLayout[screenCount + 1];
              layouts[screenCount] = mLauncher.getHotseat();
          } else {
              layouts = new CellLayout[screenCount];
          }
          for (int screen = 0; screen < screenCount; screen++) {
              layouts[screen] = (CellLayout) getChildAt(screen);
          }
          return layouts;
      }
  
      /**
       * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
       * animation.
       *
       * @param packageName The package name of the app to match.
       * @param user The user of the app to match.
       */
      public View getFirstMatchForAppClose(String packageName, UserHandle user) {
          final int curPage = getCurrentPage();
          final CellLayout currentPage = (CellLayout) getPageAt(curPage);
          final Workspace.ItemOperator packageAndUser = (ItemInfo info, View view) -> info != null
                  && info.getTargetComponent() != null
                  && TextUtils.equals(info.getTargetComponent().getPackageName(), packageName)
                  && info.user.equals(user);
          final Workspace.ItemOperator packageAndUserAndApp = (ItemInfo info, View view) ->
                  packageAndUser.evaluate(info, view) && info.itemType == ITEM_TYPE_APPLICATION;
          final Workspace.ItemOperator packageAndUserAndAppInFolder = (info, view) -> {
              if (info instanceof FolderInfo) {
                  FolderInfo folderInfo = (FolderInfo) info;
                  for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
                      if (packageAndUserAndApp.evaluate(shortcutInfo, view)) {
                          return true;
                      }
                  }
              }
              return false;
          };
  
          // Order: App icons, app in folder. Items in hotseat get returned first.
          if (ADAPTIVE_ICON_WINDOW_ANIM.get()) {
              return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
                      packageAndUserAndApp, packageAndUserAndAppInFolder);
          } else {
              // Do not use Folder as a criteria, since it'll cause a crash when trying to draw
              // FolderAdaptiveIcon as the background.
              return getFirstMatch(new CellLayout[] { getHotseat(), currentPage },
                      packageAndUserAndApp);
          }
      }
  
      public View getHomescreenIconByItemId(final int id) {
          return getFirstMatch((info, v) -> info != null && info.id == id);
      }
  
      public LauncherAppWidgetHostView getWidgetForAppWidgetId(final int appWidgetId) {
          return (LauncherAppWidgetHostView) getFirstMatch((info, v) ->
                  (info instanceof LauncherAppWidgetInfo) &&
                          ((LauncherAppWidgetInfo) info).appWidgetId == appWidgetId);
      }
  
      public View getFirstMatch(final ItemOperator operator) {
          final View[] value = new View[1];
          mapOverItems(new ItemOperator() {
              @Override
              public boolean evaluate(ItemInfo info, View v) {
                  if (operator.evaluate(info, v)) {
                      value[0] = v;
                      return true;
                  }
                  return false;
              }
          });
          return value[0];
      }
  
      /**
       * @param cellLayouts List of CellLayouts to scan, in order of preference.
       * @param operators List of operators, in order starting from best matching operator.
       * @return
       */
      private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
          // This array is filled with the first match for each operator.
          final View[] matches = new View[operators.length];
          // For efficiency, the outer loop should be CellLayout.
          for (CellLayout cellLayout : cellLayouts) {
              mapOverCellLayout(cellLayout, (info, v) -> {
                  for (int i = 0; i < operators.length; ++i) {
                      if (matches[i] == null && operators[i].evaluate(info, v)) {
                          matches[i] = v;
                          if (i == 0) {
                              // We can return since this is the best match possible.
                              return true;
                          }
                      }
                  }
                  return false;
              });
              if (matches[0] != null) {
                  break;
              }
          }
          for (View match : matches) {
              if (match != null) {
                  return match;
              }
          }
          return null;
      }
  
      void clearDropTargets() {
          mapOverItems(new ItemOperator() {
              @Override
              public boolean evaluate(ItemInfo info, View v) {
                  if (v instanceof DropTarget) {
                      mDragController.removeDropTarget((DropTarget) v);
                  }
                  // not done, process all the shortcuts
                  return false;
              }
          });
      }
  
      /**
       * Removes items that match the {@param matcher}. When applications are removed
       * as a part of an update, this is called to ensure that other widgets and application
       * shortcuts are not removed.
       */
      public void removeItemsByMatcher(final ItemInfoMatcher matcher) {
          for (final CellLayout layoutParent: getWorkspaceAndHotseatCellLayouts()) {
              final ViewGroup layout = layoutParent.getShortcutsAndWidgets();
  
              IntSparseArrayMap<View> idToViewMap = new IntSparseArrayMap<>();
              ArrayList<ItemInfo> items = new ArrayList<>();
              for (int j = 0; j < layout.getChildCount(); j++) {
                  final View view = layout.getChildAt(j);
                  if (view.getTag() instanceof ItemInfo) {
                      ItemInfo item = (ItemInfo) view.getTag();
                      items.add(item);
                      idToViewMap.put(item.id, view);
                  }
              }
  
              for (ItemInfo itemToRemove : matcher.filterItemInfos(items)) {
                  View child = idToViewMap.get(itemToRemove.id);
  
                  if (child != null) {
                      // Note: We can not remove the view directly from CellLayoutChildren as this
                      // does not re-mark the spaces as unoccupied.
                      layoutParent.removeViewInLayout(child);
                      if (child instanceof DropTarget) {
                          mDragController.removeDropTarget((DropTarget) child);
                      }
                  } else if (itemToRemove.container >= 0) {
                      // The item may belong to a folder.
                      View parent = idToViewMap.get(itemToRemove.container);
                      if (parent instanceof FolderIcon) {
                          FolderInfo folderInfo = (FolderInfo) parent.getTag();
                          folderInfo.remove((WorkspaceItemInfo) itemToRemove, false);
                          if (((FolderIcon) parent).getFolder().isOpen()) {
                              ((FolderIcon) parent).getFolder().close(false /* animate */);
                          }
                      }
                  }
              }
          }
  
          // Strip all the empty screens
          stripEmptyScreens();
      }
  
      public interface ItemOperator {
          /**
           * Process the next itemInfo, possibly with side-effect on the next item.
           *
           * @param info info for the shortcut
           * @param view view for the shortcut
           * @return true if done, false to continue the map
           */
          boolean evaluate(ItemInfo info, View view);
      }
  
      /**
       * Map the operator over the shortcuts and widgets, return the first-non-null value.
       *
       * @param op the operator to map over the shortcuts
       */
      public void mapOverItems(ItemOperator op) {
          for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
              if (mapOverCellLayout(layout, op)) {
                  return;
              }
          }
      }
  
      private boolean mapOverCellLayout(CellLayout layout, ItemOperator op) {
          // TODO(b/128460496) Potential race condition where layout is not yet loaded
          if (layout == null) {
              return false;
          }
          ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
          // map over all the shortcuts on the workspace
          final int itemCount = container.getChildCount();
          for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
              View item = container.getChildAt(itemIdx);
              if (op.evaluate((ItemInfo) item.getTag(), item)) {
                  return true;
              }
          }
          return false;
      }
  
      void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
          final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
          ItemOperator op = (info, v) -> {
              if (v instanceof BubbleTextView && updates.contains(info)) {
                  WorkspaceItemInfo si = (WorkspaceItemInfo) info;
                  BubbleTextView shortcut = (BubbleTextView) v;
                  Drawable oldIcon = shortcut.getIcon();
                  boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
                          && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
                  shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
              } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
                  ((FolderIcon) v).updatePreviewItems(updates::contains);
              }
  
              // Iterate all items
              return false;
          };
  
          mapOverItems(op);
          Folder openFolder = Folder.getOpen(mLauncher);
          if (openFolder != null) {
              openFolder.iterateOverItems(op);
          }
      }
  
      public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
          final PackageUserKey packageUserKey = new PackageUserKey(null, null);
          Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
                  || updatedDots.test(packageUserKey);
  
          ItemOperator op = (info, v) -> {
              if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
                  if (matcher.test(info)) {
                      ((BubbleTextView) v).applyDotState(info, true /* animate */);
                  }
              } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
                  FolderInfo fi = (FolderInfo) info;
                  if (fi.contents.stream().anyMatch(matcher)) {
                      FolderDotInfo folderDotInfo = new FolderDotInfo();
                      for (WorkspaceItemInfo si : fi.contents) {
                          folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
                      }
                      ((FolderIcon) v).setDotInfo(folderDotInfo);
                  }
              }
  
              // process all the shortcuts
              return false;
          };
  
          mapOverItems(op);
          Folder folder = Folder.getOpen(mLauncher);
          if (folder != null) {
              folder.iterateOverItems(op);
          }
      }
  
      public void removeAbandonedPromise(String packageName, UserHandle user) {
          HashSet<String> packages = new HashSet<>(1);
          packages.add(packageName);
          ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packages, user);
          mLauncher.getModelWriter().deleteItemsFromDatabase(matcher);
          removeItemsByMatcher(matcher);
      }
  
      public void updateRestoreItems(final HashSet<ItemInfo> updates) {
          ItemOperator op = (info, v) -> {
              if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
                      && updates.contains(info)) {
                  ((BubbleTextView) v).applyPromiseState(false /* promiseStateChanged */);
              } else if (v instanceof PendingAppWidgetHostView
                      && info instanceof LauncherAppWidgetInfo
                      && updates.contains(info)) {
                  ((PendingAppWidgetHostView) v).applyState();
              } else if (v instanceof FolderIcon && info instanceof FolderInfo) {
                  ((FolderIcon) v).updatePreviewItems(updates::contains);
              }
              // process all the shortcuts
              return false;
          };
          mapOverItems(op);
          Folder folder = Folder.getOpen(mLauncher);
          if (folder != null) {
              folder.iterateOverItems(op);
          }
      }
  
      public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> changedInfo) {
          if (!changedInfo.isEmpty()) {
              DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo,
                      mLauncher.getAppWidgetHost());
  
              LauncherAppWidgetInfo item = changedInfo.get(0);
              final AppWidgetProviderInfo widgetInfo;
              if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
                  widgetInfo = AppWidgetManagerCompat
                          .getInstance(mLauncher).findProvider(item.providerName, item.user);
              } else {
                  widgetInfo = AppWidgetManagerCompat.getInstance(mLauncher)
                          .getLauncherAppWidgetInfo(item.appWidgetId);
              }
  
              if (widgetInfo != null) {
                  // Re-inflate the widgets which have changed status
                  widgetRefresh.run();
              } else {
                  // widgetRefresh will automatically run when the packages are updated.
                  // For now just update the progress bars
                  mapOverItems(new ItemOperator() {
                      @Override
                      public boolean evaluate(ItemInfo info, View view) {
                          if (view instanceof PendingAppWidgetHostView
                                  && changedInfo.contains(info)) {
                              ((LauncherAppWidgetInfo) info).installProgress = 100;
                              ((PendingAppWidgetHostView) view).applyState();
                          }
                          // process all the shortcuts
                          return false;
                      }
                  });
              }
          }
      }
  
      public boolean isOverlayShown() {
          return mOverlayShown;
      }
  
      void moveToDefaultScreen() {
          int page = DEFAULT_PAGE;
          if (!workspaceInModalState() && getNextPage() != page) {
              snapToPage(page);
          }
          View child = getChildAt(page);
          if (child != null) {
              child.requestFocus();
          }
      }
  
      @Override
      public int getExpectedHeight() {
          return getMeasuredHeight() <= 0 || !mIsLayoutValid
                  ? mLauncher.getDeviceProfile().heightPx : getMeasuredHeight();
      }
  
      @Override
      public int getExpectedWidth() {
          return getMeasuredWidth() <= 0 || !mIsLayoutValid
                  ? mLauncher.getDeviceProfile().widthPx : getMeasuredWidth();
      }
  
      @Override
      protected boolean canAnnouncePageDescription() {
          // Disable announcements while overscrolling potentially to overlay screen because if we end
          // up on the overlay screen, it will take care of announcing itself.
          return Float.compare(mOverlayTranslation, 0f) == 0;
      }
  
      @Override
      protected String getCurrentPageDescription() {
          int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
          return getPageDescription(page);
      }
  
      private String getPageDescription(int page) {
          int nScreens = getChildCount();
          int extraScreenId = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
          if (extraScreenId >= 0 && nScreens > 1) {
              if (page == extraScreenId) {
                  return getContext().getString(R.string.workspace_new_page);
              }
              nScreens--;
          }
          if (nScreens == 0) {
              // When the workspace is not loaded, we do not know how many screen will be bound.
              return getContext().getString(R.string.all_apps_home_button_label);
          }
          return getContext().getString(R.string.workspace_scroll_format, page + 1, nScreens);
      }
  
      @Override
      public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
          target.gridX = info.cellX;
          target.gridY = info.cellY;
          target.pageIndex = getCurrentPage();
          targetParent.containerType = ContainerType.WORKSPACE;
          if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
              target.rank = info.rank;
              targetParent.containerType = ContainerType.HOTSEAT;
          } else if (info.container >= 0) {
              targetParent.containerType = ContainerType.FOLDER;
          }
      }
  
      /**
       * Used as a workaround to ensure that the AppWidgetService receives the
       * PACKAGE_ADDED broadcast before updating widgets.
       */
      private class DeferredWidgetRefresh implements Runnable, ProviderChangedListener {
          private final ArrayList<LauncherAppWidgetInfo> mInfos;
          private final LauncherAppWidgetHost mHost;
          private final Handler mHandler;
  
          private boolean mRefreshPending;
  
          DeferredWidgetRefresh(ArrayList<LauncherAppWidgetInfo> infos,
              LauncherAppWidgetHost host) {
              mInfos = infos;
              mHost = host;
              mHandler = mLauncher.mHandler;
              mRefreshPending = true;
  
              mHost.addProviderChangeListener(this);
              // Force refresh after 10 seconds, if we don't get the provider changed event.
              // This could happen when the provider is no longer available in the app.
              Message msg = Message.obtain(mHandler, this);
              msg.obj = DeferredWidgetRefresh.class;
              mHandler.sendMessageDelayed(msg, 10000);
          }
  
          @Override
          public void run() {
              mHost.removeProviderChangeListener(this);
              mHandler.removeCallbacks(this);
  
              if (!mRefreshPending) {
                  return;
              }
  
              mRefreshPending = false;
  
              ArrayList<PendingAppWidgetHostView> views = new ArrayList<>(mInfos.size());
              mapOverItems((info, view) -> {
                  if (view instanceof PendingAppWidgetHostView && mInfos.contains(info)) {
                      views.add((PendingAppWidgetHostView) view);
                  }
                  // process all children
                  return false;
              });
              for (PendingAppWidgetHostView view : views) {
                  view.reInflate();
              }
          }
  
          @Override
          public void notifyWidgetProvidersChanged() {
              run();
          }
      }
  
      private class StateTransitionListener extends AnimatorListenerAdapter
              implements AnimatorUpdateListener {
  
          private final LauncherState mToState;
  
          StateTransitionListener(LauncherState toState) {
              mToState = toState;
          }
  
          @Override
          public void onAnimationUpdate(ValueAnimator anim) {
              mTransitionProgress = anim.getAnimatedFraction();
          }
  
          @Override
          public void onAnimationStart(Animator animation) {
              onStartStateTransition(mToState);
          }
  
          @Override
          public void onAnimationEnd(Animator animation) {
              onEndStateTransition();
          }
      }
  }
  