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