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