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