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