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.folder; 18 19 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; 20 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; 21 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT; 22 import static com.android.launcher3.LauncherState.NORMAL; 23 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.AnimatorSet; 28 import android.annotation.SuppressLint; 29 import android.appwidget.AppWidgetHostView; 30 import android.content.Context; 31 import android.graphics.Canvas; 32 import android.graphics.Path; 33 import android.graphics.Rect; 34 import android.text.InputType; 35 import android.text.Selection; 36 import android.text.TextUtils; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.util.Pair; 40 import android.view.FocusFinder; 41 import android.view.KeyEvent; 42 import android.view.MotionEvent; 43 import android.view.View; 44 import android.view.ViewDebug; 45 import android.view.accessibility.AccessibilityEvent; 46 import android.view.animation.AnimationUtils; 47 import android.view.inputmethod.EditorInfo; 48 import android.widget.TextView; 49 50 import com.android.launcher3.AbstractFloatingView; 51 import com.android.launcher3.Alarm; 52 import com.android.launcher3.AppInfo; 53 import com.android.launcher3.BubbleTextView; 54 import com.android.launcher3.CellLayout; 55 import com.android.launcher3.DeviceProfile; 56 import com.android.launcher3.DragSource; 57 import com.android.launcher3.DropTarget; 58 import com.android.launcher3.ExtendedEditText; 59 import com.android.launcher3.FolderInfo; 60 import com.android.launcher3.FolderInfo.FolderListener; 61 import com.android.launcher3.ItemInfo; 62 import com.android.launcher3.Launcher; 63 import com.android.launcher3.LauncherSettings; 64 import com.android.launcher3.OnAlarmListener; 65 import com.android.launcher3.PagedView; 66 import com.android.launcher3.R; 67 import com.android.launcher3.ShortcutAndWidgetContainer; 68 import com.android.launcher3.Workspace; 69 import com.android.launcher3.Workspace.ItemOperator; 70 import com.android.launcher3.WorkspaceItemInfo; 71 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter; 72 import com.android.launcher3.config.FeatureFlags; 73 import com.android.launcher3.dragndrop.DragController; 74 import com.android.launcher3.dragndrop.DragController.DragListener; 75 import com.android.launcher3.dragndrop.DragLayer; 76 import com.android.launcher3.dragndrop.DragOptions; 77 import com.android.launcher3.logging.LoggerUtils; 78 import com.android.launcher3.pageindicators.PageIndicatorDots; 79 import com.android.launcher3.userevent.nano.LauncherLogProto; 80 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 81 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 82 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 83 import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType; 84 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 85 import com.android.launcher3.util.Thunk; 86 import com.android.launcher3.views.ClipPathView; 87 import com.android.launcher3.widget.PendingAddShortcutInfo; 88 89 import java.util.ArrayList; 90 import java.util.Collections; 91 import java.util.Comparator; 92 import java.util.List; 93 94 /** 95 * Represents a set of icons chosen by the user or generated by the system. 96 */ 97 public class Folder extends AbstractFloatingView implements ClipPathView, DragSource, 98 View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, 99 View.OnFocusChangeListener, DragListener, ExtendedEditText.OnBackKeyListener { 100 private static final String TAG = "Launcher.Folder"; 101 102 /** 103 * We avoid measuring {@link #mContent} with a 0 width or height, as this 104 * results in CellLayout being measured as UNSPECIFIED, which it does not support. 105 */ 106 private static final int MIN_CONTENT_DIMEN = 5; 107 108 static final int STATE_NONE = -1; 109 static final int STATE_SMALL = 0; 110 static final int STATE_ANIMATING = 1; 111 static final int STATE_OPEN = 2; 112 113 /** 114 * Time for which the scroll hint is shown before automatically changing page. 115 */ 116 public static final int SCROLL_HINT_DURATION = 500; 117 public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150; 118 119 public static final int SCROLL_NONE = -1; 120 public static final int SCROLL_LEFT = 0; 121 public static final int SCROLL_RIGHT = 1; 122 123 /** 124 * Fraction of icon width which behave as scroll region. 125 */ 126 private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; 127 128 private static final int FOLDER_NAME_ANIMATION_DURATION = 633; 129 130 private static final int REORDER_DELAY = 250; 131 private static final int ON_EXIT_CLOSE_DELAY = 400; 132 private static final Rect sTempRect = new Rect(); 133 private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10; 134 135 private final Alarm mReorderAlarm = new Alarm(); 136 private final Alarm mOnExitAlarm = new Alarm(); 137 private final Alarm mOnScrollHintAlarm = new Alarm(); 138 @Thunk final Alarm mScrollPauseAlarm = new Alarm(); 139 140 @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); 141 142 private AnimatorSet mCurrentAnimator; 143 144 protected final Launcher mLauncher; 145 protected DragController mDragController; 146 public FolderInfo mInfo; 147 148 @Thunk FolderIcon mFolderIcon; 149 150 @Thunk FolderPagedView mContent; 151 public ExtendedEditText mFolderName; 152 private PageIndicatorDots mPageIndicator; 153 154 protected View mFooter; 155 private int mFooterHeight; 156 157 // Cell ranks used for drag and drop 158 @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; 159 160 private Path mClipPath; 161 162 @ViewDebug.ExportedProperty(category = "launcher", 163 mapping = { 164 @ViewDebug.IntToString(from = STATE_NONE, to = "STATE_NONE"), 165 @ViewDebug.IntToString(from = STATE_SMALL, to = "STATE_SMALL"), 166 @ViewDebug.IntToString(from = STATE_ANIMATING, to = "STATE_ANIMATING"), 167 @ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"), 168 }) 169 @Thunk int mState = STATE_NONE; 170 @ViewDebug.ExportedProperty(category = "launcher") 171 private boolean mRearrangeOnClose = false; 172 boolean mItemsInvalidated = false; 173 private View mCurrentDragView; 174 private boolean mIsExternalDrag; 175 private boolean mDragInProgress = false; 176 private boolean mDeleteFolderOnDropCompleted = false; 177 private boolean mSuppressFolderDeletion = false; 178 private boolean mItemAddedBackToSelfViaIcon = false; 179 private boolean mIsEditingName = false; 180 181 @ViewDebug.ExportedProperty(category = "launcher") 182 private boolean mDestroyed; 183 184 // Folder scrolling 185 private int mScrollAreaOffset; 186 187 @Thunk int mScrollHintDir = SCROLL_NONE; 188 @Thunk int mCurrentScrollDir = SCROLL_NONE; 189 190 /** 191 * Used to inflate the Workspace from XML. 192 * 193 * @param context The application's context. 194 * @param attrs The attributes set containing the Workspace's customization values. 195 */ Folder(Context context, AttributeSet attrs)196 public Folder(Context context, AttributeSet attrs) { 197 super(context, attrs); 198 setAlwaysDrawnWithCacheEnabled(false); 199 200 mLauncher = Launcher.getLauncher(context); 201 // We need this view to be focusable in touch mode so that when text editing of the folder 202 // name is complete, we have something to focus on, thus hiding the cursor and giving 203 // reliable behavior when clicking the text field (since it will always gain focus on click). 204 setFocusableInTouchMode(true); 205 } 206 207 @Override onFinishInflate()208 protected void onFinishInflate() { 209 super.onFinishInflate(); 210 mContent = findViewById(R.id.folder_content); 211 mContent.setFolder(this); 212 213 mPageIndicator = findViewById(R.id.folder_page_indicator); 214 mFolderName = findViewById(R.id.folder_name); 215 mFolderName.setOnBackKeyListener(this); 216 mFolderName.setOnFocusChangeListener(this); 217 mFolderName.setOnEditorActionListener(this); 218 mFolderName.setSelectAllOnFocus(true); 219 mFolderName.setInputType(mFolderName.getInputType() 220 & ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT 221 & ~InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS 222 | InputType.TYPE_TEXT_FLAG_CAP_WORDS); 223 mFolderName.forceDisableSuggestions(true); 224 225 mFooter = findViewById(R.id.folder_footer); 226 227 // We find out how tall footer wants to be (it is set to wrap_content), so that 228 // we can allocate the appropriate amount of space for it. 229 int measureSpec = MeasureSpec.UNSPECIFIED; 230 mFooter.measure(measureSpec, measureSpec); 231 mFooterHeight = mFooter.getMeasuredHeight(); 232 } 233 onLongClick(View v)234 public boolean onLongClick(View v) { 235 // Return if global dragging is not enabled 236 if (!mLauncher.isDraggingEnabled()) return true; 237 return startDrag(v, new DragOptions()); 238 } 239 startDrag(View v, DragOptions options)240 public boolean startDrag(View v, DragOptions options) { 241 Object tag = v.getTag(); 242 if (tag instanceof WorkspaceItemInfo) { 243 WorkspaceItemInfo item = (WorkspaceItemInfo) tag; 244 245 mEmptyCellRank = item.rank; 246 mCurrentDragView = v; 247 248 mDragController.addDragListener(this); 249 if (options.isAccessibleDrag) { 250 mDragController.addDragListener(new AccessibleDragListenerAdapter( 251 mContent, CellLayout.FOLDER_ACCESSIBILITY_DRAG) { 252 253 @Override 254 protected void enableAccessibleDrag(boolean enable) { 255 super.enableAccessibleDrag(enable); 256 mFooter.setImportantForAccessibility(enable 257 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS 258 : IMPORTANT_FOR_ACCESSIBILITY_AUTO); 259 } 260 }); 261 } 262 263 mLauncher.getWorkspace().beginDragShared(v, this, options); 264 } 265 return true; 266 } 267 268 @Override onDragStart(DropTarget.DragObject dragObject, DragOptions options)269 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 270 if (dragObject.dragSource != this) { 271 return; 272 } 273 274 mContent.removeItem(mCurrentDragView); 275 if (dragObject.dragInfo instanceof WorkspaceItemInfo) { 276 mItemsInvalidated = true; 277 278 // We do not want to get events for the item being removed, as they will get handled 279 // when the drop completes 280 try (SuppressInfoChanges s = new SuppressInfoChanges()) { 281 mInfo.remove((WorkspaceItemInfo) dragObject.dragInfo, true); 282 } 283 } 284 mDragInProgress = true; 285 mItemAddedBackToSelfViaIcon = false; 286 } 287 288 @Override onDragEnd()289 public void onDragEnd() { 290 if (mIsExternalDrag && mDragInProgress) { 291 completeDragExit(); 292 } 293 mDragInProgress = false; 294 mDragController.removeDragListener(this); 295 } 296 isEditingName()297 public boolean isEditingName() { 298 return mIsEditingName; 299 } 300 startEditingFolderName()301 public void startEditingFolderName() { 302 post(new Runnable() { 303 @Override 304 public void run() { 305 mFolderName.setHint(""); 306 mIsEditingName = true; 307 } 308 }); 309 } 310 311 312 @Override onBackKey()313 public boolean onBackKey() { 314 // Convert to a string here to ensure that no other state associated with the text field 315 // gets saved. 316 String newTitle = mFolderName.getText().toString(); 317 mInfo.title = newTitle; 318 mFolderIcon.onTitleChanged(newTitle); 319 mLauncher.getModelWriter().updateItemInDatabase(mInfo); 320 321 if (TextUtils.isEmpty(mInfo.title)) { 322 mFolderName.setHint(R.string.folder_hint_text); 323 } else { 324 mFolderName.setHint(null); 325 } 326 327 sendCustomAccessibilityEvent( 328 this, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 329 getContext().getString(R.string.folder_renamed, newTitle)); 330 331 // This ensures that focus is gained every time the field is clicked, which selects all 332 // the text and brings up the soft keyboard if necessary. 333 mFolderName.clearFocus(); 334 335 Selection.setSelection(mFolderName.getText(), 0, 0); 336 mIsEditingName = false; 337 return true; 338 } 339 onEditorAction(TextView v, int actionId, KeyEvent event)340 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 341 if (actionId == EditorInfo.IME_ACTION_DONE) { 342 mFolderName.dispatchBackKey(); 343 return true; 344 } 345 return false; 346 } 347 getFolderIcon()348 public FolderIcon getFolderIcon() { 349 return mFolderIcon; 350 } 351 setDragController(DragController dragController)352 public void setDragController(DragController dragController) { 353 mDragController = dragController; 354 } 355 setFolderIcon(FolderIcon icon)356 public void setFolderIcon(FolderIcon icon) { 357 mFolderIcon = icon; 358 } 359 360 @Override onAttachedToWindow()361 protected void onAttachedToWindow() { 362 // requestFocus() causes the focus onto the folder itself, which doesn't cause visual 363 // effect but the next arrow key can start the keyboard focus inside of the folder, not 364 // the folder itself. 365 requestFocus(); 366 super.onAttachedToWindow(); 367 } 368 369 @Override dispatchPopulateAccessibilityEvent(AccessibilityEvent event)370 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 371 // When the folder gets focus, we don't want to announce the list of items. 372 return true; 373 } 374 375 @Override focusSearch(int direction)376 public View focusSearch(int direction) { 377 // When the folder is focused, further focus search should be within the folder contents. 378 return FocusFinder.getInstance().findNextFocus(this, null, direction); 379 } 380 381 /** 382 * @return the FolderInfo object associated with this folder 383 */ getInfo()384 public FolderInfo getInfo() { 385 return mInfo; 386 } 387 bind(FolderInfo info)388 void bind(FolderInfo info) { 389 mInfo = info; 390 ArrayList<WorkspaceItemInfo> children = info.contents; 391 Collections.sort(children, ITEM_POS_COMPARATOR); 392 updateItemLocationsInDatabaseBatch(); 393 394 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 395 if (lp == null) { 396 lp = new DragLayer.LayoutParams(0, 0); 397 lp.customPosition = true; 398 setLayoutParams(lp); 399 } 400 mItemsInvalidated = true; 401 mInfo.addListener(this); 402 403 if (!TextUtils.isEmpty(mInfo.title)) { 404 mFolderName.setText(mInfo.title); 405 mFolderName.setHint(null); 406 } else { 407 mFolderName.setText(""); 408 mFolderName.setHint(R.string.folder_hint_text); 409 } 410 // In case any children didn't come across during loading, clean up the folder accordingly 411 mFolderIcon.post(() -> { 412 if (getItemCount() <= 1) { 413 replaceFolderWithFinalItem(); 414 } 415 }); 416 } 417 418 419 /** 420 * Show suggested folder title. 421 */ showSuggestedTitle(CharSequence suggestName)422 public void showSuggestedTitle(CharSequence suggestName) { 423 if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && mInfo.contents.size() == 2) { 424 if (!TextUtils.isEmpty(suggestName)) { 425 mFolderName.setHint(suggestName); 426 mFolderName.setText(suggestName); 427 mFolderName.showKeyboard(); 428 mInfo.title = suggestName; 429 } 430 animateOpen(); 431 } 432 } 433 434 /** 435 * Creates a new UserFolder, inflated from R.layout.user_folder. 436 * 437 * @param launcher The main activity. 438 * 439 * @return A new UserFolder. 440 */ 441 @SuppressLint("InflateParams") fromXml(Launcher launcher)442 static Folder fromXml(Launcher launcher) { 443 return (Folder) launcher.getLayoutInflater() 444 .inflate(R.layout.user_folder_icon_normalized, null); 445 } 446 startAnimation(final AnimatorSet a)447 private void startAnimation(final AnimatorSet a) { 448 final Workspace workspace = mLauncher.getWorkspace(); 449 final CellLayout currentCellLayout = 450 (CellLayout) workspace.getChildAt(workspace.getCurrentPage()); 451 final boolean useHardware = shouldUseHardwareLayerForAnimation(currentCellLayout); 452 final boolean wasHardwareAccelerated = currentCellLayout.isHardwareLayerEnabled(); 453 454 a.addListener(new AnimatorListenerAdapter() { 455 @Override 456 public void onAnimationStart(Animator animation) { 457 if (useHardware) { 458 currentCellLayout.enableHardwareLayer(true); 459 } 460 mState = STATE_ANIMATING; 461 mCurrentAnimator = a; 462 } 463 464 @Override 465 public void onAnimationEnd(Animator animation) { 466 if (useHardware) { 467 currentCellLayout.enableHardwareLayer(wasHardwareAccelerated); 468 } 469 mCurrentAnimator = null; 470 } 471 }); 472 a.start(); 473 } 474 shouldUseHardwareLayerForAnimation(CellLayout currentCellLayout)475 private boolean shouldUseHardwareLayerForAnimation(CellLayout currentCellLayout) { 476 int folderCount = 0; 477 final ShortcutAndWidgetContainer container = currentCellLayout.getShortcutsAndWidgets(); 478 for (int i = container.getChildCount() - 1; i >= 0; --i) { 479 final View child = container.getChildAt(i); 480 if (child instanceof AppWidgetHostView) return false; 481 if (child instanceof FolderIcon) ++folderCount; 482 } 483 return folderCount >= MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION; 484 } 485 486 /** 487 * Opens the folder as part of a drag operation 488 */ beginExternalDrag()489 public void beginExternalDrag() { 490 mIsExternalDrag = true; 491 mDragInProgress = true; 492 493 // Since this folder opened by another controller, it might not get onDrop or 494 // onDropComplete. Perform cleanup once drag-n-drop ends. 495 mDragController.addDragListener(this); 496 497 ArrayList<WorkspaceItemInfo> items = new ArrayList<>(mInfo.contents); 498 mEmptyCellRank = items.size(); 499 items.add(null); // Add an empty spot at the end 500 501 animateOpen(items, mEmptyCellRank / mContent.itemsPerPage()); 502 } 503 504 /** 505 * Opens the user folder described by the specified tag. The opening of the folder 506 * is animated relative to the specified View. If the View is null, no animation 507 * is played. 508 */ animateOpen()509 public void animateOpen() { 510 animateOpen(mInfo.contents, 0); 511 } 512 513 /** 514 * Opens the user folder described by the specified tag. The opening of the folder 515 * is animated relative to the specified View. If the View is null, no animation 516 * is played. 517 */ animateOpen(List<WorkspaceItemInfo> items, int pageNo)518 private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) { 519 Folder openFolder = getOpen(mLauncher); 520 if (openFolder != null && openFolder != this) { 521 // Close any open folder before opening a folder. 522 openFolder.close(true); 523 } 524 525 mContent.bindItems(items); 526 centerAboutIcon(); 527 mItemsInvalidated = true; 528 updateTextViewFocus(); 529 530 mIsOpen = true; 531 532 DragLayer dragLayer = mLauncher.getDragLayer(); 533 // Just verify that the folder hasn't already been added to the DragLayer. 534 // There was a one-off crash where the folder had a parent already. 535 if (getParent() == null) { 536 dragLayer.addView(this); 537 mDragController.addDropTarget(this); 538 } else { 539 if (FeatureFlags.IS_DOGFOOD_BUILD) { 540 Log.e(TAG, "Opening folder (" + this + ") which already has a parent:" 541 + getParent()); 542 } 543 } 544 545 mContent.completePendingPageChanges(); 546 mContent.snapToPageImmediately(pageNo); 547 548 // This is set to true in close(), but isn't reset to false until onDropCompleted(). This 549 // leads to an inconsistent state if you drag out of the folder and drag back in without 550 // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice. 551 mDeleteFolderOnDropCompleted = false; 552 553 if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { 554 mCurrentAnimator.cancel(); 555 } 556 AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator(); 557 anim.addListener(new AnimatorListenerAdapter() { 558 @Override 559 public void onAnimationStart(Animator animation) { 560 mFolderIcon.setIconVisible(false); 561 mFolderIcon.drawLeaveBehindIfExists(); 562 } 563 @Override 564 public void onAnimationEnd(Animator animation) { 565 mState = STATE_OPEN; 566 announceAccessibilityChanges(); 567 568 mLauncher.getUserEventDispatcher().logActionOnItem( 569 Touch.TAP, 570 Direction.NONE, 571 ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY); 572 573 mContent.setFocusOnFirstChild(); 574 } 575 }); 576 577 // Footer animation 578 if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) { 579 int footerWidth = mContent.getDesiredWidth() 580 - mFooter.getPaddingLeft() - mFooter.getPaddingRight(); 581 582 float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString()); 583 float translation = (footerWidth - textWidth) / 2; 584 mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation); 585 mPageIndicator.prepareEntryAnimation(); 586 587 // Do not update the flag if we are in drag mode. The flag will be updated, when we 588 // actually drop the icon. 589 final boolean updateAnimationFlag = !mDragInProgress; 590 anim.addListener(new AnimatorListenerAdapter() { 591 592 @SuppressLint("InlinedApi") 593 @Override 594 public void onAnimationEnd(Animator animation) { 595 mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION) 596 .translationX(0) 597 .setInterpolator(AnimationUtils.loadInterpolator( 598 mLauncher, android.R.interpolator.fast_out_slow_in)); 599 mPageIndicator.playEntryAnimation(); 600 601 if (updateAnimationFlag) { 602 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, 603 mLauncher.getModelWriter()); 604 } 605 } 606 }); 607 } else { 608 mFolderName.setTranslationX(0); 609 } 610 611 mPageIndicator.stopAllAnimations(); 612 startAnimation(anim); 613 614 // Make sure the folder picks up the last drag move even if the finger doesn't move. 615 if (mDragController.isDragging()) { 616 mDragController.forceTouchMove(); 617 } 618 mContent.verifyVisibleHighResIcons(mContent.getNextPage()); 619 } 620 621 @Override isOfType(int type)622 protected boolean isOfType(int type) { 623 return (type & TYPE_FOLDER) != 0; 624 } 625 626 @Override handleClose(boolean animate)627 protected void handleClose(boolean animate) { 628 mIsOpen = false; 629 630 if (!animate && mCurrentAnimator != null && mCurrentAnimator.isRunning()) { 631 mCurrentAnimator.cancel(); 632 } 633 634 if (isEditingName()) { 635 mFolderName.dispatchBackKey(); 636 } 637 638 if (mFolderIcon != null) { 639 mFolderIcon.clearLeaveBehindIfExists(); 640 } 641 642 if (animate) { 643 animateClosed(); 644 } else { 645 closeComplete(false); 646 post(this::announceAccessibilityChanges); 647 } 648 649 // Notify the accessibility manager that this folder "window" has disappeared and no 650 // longer occludes the workspace items 651 mLauncher.getDragLayer().sendAccessibilityEvent( 652 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 653 } 654 animateClosed()655 private void animateClosed() { 656 if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { 657 mCurrentAnimator.cancel(); 658 } 659 AnimatorSet a = new FolderAnimationManager(this, false /* isOpening */).getAnimator(); 660 a.addListener(new AnimatorListenerAdapter() { 661 @Override 662 public void onAnimationEnd(Animator animation) { 663 closeComplete(true); 664 announceAccessibilityChanges(); 665 } 666 }); 667 startAnimation(a); 668 } 669 670 @Override getAccessibilityTarget()671 protected Pair<View, String> getAccessibilityTarget() { 672 return Pair.create(mContent, mIsOpen ? mContent.getAccessibilityDescription() 673 : getContext().getString(R.string.folder_closed)); 674 } 675 closeComplete(boolean wasAnimated)676 private void closeComplete(boolean wasAnimated) { 677 // TODO: Clear all active animations. 678 DragLayer parent = (DragLayer) getParent(); 679 if (parent != null) { 680 parent.removeView(this); 681 } 682 mDragController.removeDropTarget(this); 683 clearFocus(); 684 if (mFolderIcon != null) { 685 mFolderIcon.setVisibility(View.VISIBLE); 686 mFolderIcon.setIconVisible(true); 687 mFolderIcon.mFolderName.setTextVisibility(true); 688 if (wasAnimated) { 689 mFolderIcon.animateBgShadowAndStroke(); 690 mFolderIcon.onFolderClose(mContent.getCurrentPage()); 691 if (mFolderIcon.hasDot()) { 692 mFolderIcon.animateDotScale(0f, 1f); 693 } 694 mFolderIcon.requestFocus(); 695 } 696 } 697 698 if (mRearrangeOnClose) { 699 rearrangeChildren(); 700 mRearrangeOnClose = false; 701 } 702 if (getItemCount() <= 1) { 703 if (!mDragInProgress && !mSuppressFolderDeletion) { 704 replaceFolderWithFinalItem(); 705 } else if (mDragInProgress) { 706 mDeleteFolderOnDropCompleted = true; 707 } 708 } else if (!mDragInProgress) { 709 mContent.unbindItems(); 710 } 711 mSuppressFolderDeletion = false; 712 clearDragInfo(); 713 mState = STATE_SMALL; 714 mContent.setCurrentPage(0); 715 } 716 717 @Override acceptDrop(DragObject d)718 public boolean acceptDrop(DragObject d) { 719 final ItemInfo item = d.dragInfo; 720 final int itemType = item.itemType; 721 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 722 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT || 723 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)); 724 } 725 onDragEnter(DragObject d)726 public void onDragEnter(DragObject d) { 727 mPrevTargetRank = -1; 728 mOnExitAlarm.cancelAlarm(); 729 // Get the area offset such that the folder only closes if half the drag icon width 730 // is outside the folder area 731 mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset; 732 } 733 734 OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { 735 public void onAlarm(Alarm alarm) { 736 mContent.realTimeReorder(mEmptyCellRank, mTargetRank); 737 mEmptyCellRank = mTargetRank; 738 } 739 }; 740 isLayoutRtl()741 public boolean isLayoutRtl() { 742 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 743 } 744 getTargetRank(DragObject d, float[] recycle)745 private int getTargetRank(DragObject d, float[] recycle) { 746 recycle = d.getVisualCenter(recycle); 747 return mContent.findNearestArea( 748 (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop()); 749 } 750 751 @Override onDragOver(DragObject d)752 public void onDragOver(DragObject d) { 753 if (mScrollPauseAlarm.alarmPending()) { 754 return; 755 } 756 final float[] r = new float[2]; 757 mTargetRank = getTargetRank(d, r); 758 759 if (mTargetRank != mPrevTargetRank) { 760 mReorderAlarm.cancelAlarm(); 761 mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); 762 mReorderAlarm.setAlarm(REORDER_DELAY); 763 mPrevTargetRank = mTargetRank; 764 765 if (d.stateAnnouncer != null) { 766 d.stateAnnouncer.announce(getContext().getString(R.string.move_to_position, 767 mTargetRank + 1)); 768 } 769 } 770 771 float x = r[0]; 772 int currentPage = mContent.getNextPage(); 773 774 float cellOverlap = mContent.getCurrentCellLayout().getCellWidth() 775 * ICON_OVERSCROLL_WIDTH_FACTOR; 776 boolean isOutsideLeftEdge = x < cellOverlap; 777 boolean isOutsideRightEdge = x > (getWidth() - cellOverlap); 778 779 if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) { 780 showScrollHint(SCROLL_LEFT, d); 781 } else if (currentPage < (mContent.getPageCount() - 1) 782 && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) { 783 showScrollHint(SCROLL_RIGHT, d); 784 } else { 785 mOnScrollHintAlarm.cancelAlarm(); 786 if (mScrollHintDir != SCROLL_NONE) { 787 mContent.clearScrollHint(); 788 mScrollHintDir = SCROLL_NONE; 789 } 790 } 791 } 792 showScrollHint(int direction, DragObject d)793 private void showScrollHint(int direction, DragObject d) { 794 // Show scroll hint on the right 795 if (mScrollHintDir != direction) { 796 mContent.showScrollHint(direction); 797 mScrollHintDir = direction; 798 } 799 800 // Set alarm for when the hint is complete 801 if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) { 802 mCurrentScrollDir = direction; 803 mOnScrollHintAlarm.cancelAlarm(); 804 mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); 805 mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); 806 807 mReorderAlarm.cancelAlarm(); 808 mTargetRank = mEmptyCellRank; 809 } 810 } 811 812 OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { 813 public void onAlarm(Alarm alarm) { 814 completeDragExit(); 815 } 816 }; 817 completeDragExit()818 public void completeDragExit() { 819 if (mIsOpen) { 820 close(true); 821 mRearrangeOnClose = true; 822 } else if (mState == STATE_ANIMATING) { 823 mRearrangeOnClose = true; 824 } else { 825 rearrangeChildren(); 826 clearDragInfo(); 827 } 828 } 829 clearDragInfo()830 private void clearDragInfo() { 831 mCurrentDragView = null; 832 mIsExternalDrag = false; 833 } 834 onDragExit(DragObject d)835 public void onDragExit(DragObject d) { 836 // We only close the folder if this is a true drag exit, ie. not because 837 // a drop has occurred above the folder. 838 if (!d.dragComplete) { 839 mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener); 840 mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); 841 } 842 mReorderAlarm.cancelAlarm(); 843 844 mOnScrollHintAlarm.cancelAlarm(); 845 mScrollPauseAlarm.cancelAlarm(); 846 if (mScrollHintDir != SCROLL_NONE) { 847 mContent.clearScrollHint(); 848 mScrollHintDir = SCROLL_NONE; 849 } 850 } 851 852 /** 853 * When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we 854 * need to complete all transient states based on timers. 855 */ 856 @Override prepareAccessibilityDrop()857 public void prepareAccessibilityDrop() { 858 if (mReorderAlarm.alarmPending()) { 859 mReorderAlarm.cancelAlarm(); 860 mReorderAlarmListener.onAlarm(mReorderAlarm); 861 } 862 } 863 864 @Override onDropCompleted(final View target, final DragObject d, final boolean success)865 public void onDropCompleted(final View target, final DragObject d, 866 final boolean success) { 867 if (success) { 868 if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) { 869 replaceFolderWithFinalItem(); 870 } 871 } else { 872 // The drag failed, we need to return the item to the folder 873 WorkspaceItemInfo info = (WorkspaceItemInfo) d.dragInfo; 874 View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info) 875 ? mCurrentDragView : mContent.createNewView(info); 876 ArrayList<View> views = getIconsInReadingOrder(); 877 views.add(info.rank, icon); 878 mContent.arrangeChildren(views); 879 mItemsInvalidated = true; 880 881 try (SuppressInfoChanges s = new SuppressInfoChanges()) { 882 mFolderIcon.onDrop(d, true /* itemReturnedOnFailedDrop */); 883 } 884 } 885 886 if (target != this) { 887 if (mOnExitAlarm.alarmPending()) { 888 mOnExitAlarm.cancelAlarm(); 889 if (!success) { 890 mSuppressFolderDeletion = true; 891 } 892 mScrollPauseAlarm.cancelAlarm(); 893 completeDragExit(); 894 } 895 } 896 897 mDeleteFolderOnDropCompleted = false; 898 mDragInProgress = false; 899 mItemAddedBackToSelfViaIcon = false; 900 mCurrentDragView = null; 901 902 // Reordering may have occured, and we need to save the new item locations. We do this once 903 // at the end to prevent unnecessary database operations. 904 updateItemLocationsInDatabaseBatch(); 905 // Use the item count to check for multi-page as the folder UI may not have 906 // been refreshed yet. 907 if (getItemCount() <= mContent.itemsPerPage()) { 908 // Show the animation, next time something is added to the folder. 909 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, 910 mLauncher.getModelWriter()); 911 } 912 } 913 updateItemLocationsInDatabaseBatch()914 private void updateItemLocationsInDatabaseBatch() { 915 FolderGridOrganizer verifier = new FolderGridOrganizer( 916 mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo); 917 918 ArrayList<ItemInfo> items = new ArrayList<>(); 919 int total = mInfo.contents.size(); 920 for (int i = 0; i < total; i++) { 921 WorkspaceItemInfo itemInfo = mInfo.contents.get(i); 922 if (verifier.updateRankAndPos(itemInfo, i)) { 923 items.add(itemInfo); 924 } 925 } 926 927 if (!items.isEmpty()) { 928 mLauncher.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0); 929 } 930 } 931 notifyDrop()932 public void notifyDrop() { 933 if (mDragInProgress) { 934 mItemAddedBackToSelfViaIcon = true; 935 } 936 } 937 isDropEnabled()938 public boolean isDropEnabled() { 939 return mState != STATE_ANIMATING; 940 } 941 centerAboutIcon()942 private void centerAboutIcon() { 943 DeviceProfile grid = mLauncher.getDeviceProfile(); 944 945 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 946 DragLayer parent = mLauncher.getDragLayer(); 947 int width = getFolderWidth(); 948 int height = getFolderHeight(); 949 950 parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); 951 int centerX = sTempRect.centerX(); 952 int centerY = sTempRect.centerY(); 953 int centeredLeft = centerX - width / 2; 954 int centeredTop = centerY - height / 2; 955 956 // We need to bound the folder to the currently visible workspace area 957 if (mLauncher.getStateManager().getState().overviewUi) { 958 parent.getDescendantRectRelativeToSelf(mLauncher.getOverviewPanel(), sTempRect); 959 } else { 960 mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect); 961 } 962 int left = Math.min(Math.max(sTempRect.left, centeredLeft), 963 sTempRect.right- width); 964 int top = Math.min(Math.max(sTempRect.top, centeredTop), 965 sTempRect.bottom - height); 966 967 int distFromEdgeOfScreen = mLauncher.getWorkspace().getPaddingLeft() + getPaddingLeft(); 968 969 if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) { 970 // Center the folder if it is very close to being centered anyway, by virtue of 971 // filling the majority of the viewport. ie. remove it from the uncanny valley 972 // of centeredness. 973 left = (grid.availableWidthPx - width) / 2; 974 } else if (width >= sTempRect.width()) { 975 // If the folder doesn't fit within the bounds, center it about the desired bounds 976 left = sTempRect.left + (sTempRect.width() - width) / 2; 977 } 978 if (height >= sTempRect.height()) { 979 // Folder height is greater than page height, center on page 980 top = sTempRect.top + (sTempRect.height() - height) / 2; 981 } else { 982 // Folder height is less than page height, so bound it to the absolute open folder 983 // bounds if necessary 984 Rect folderBounds = grid.getAbsoluteOpenFolderBounds(); 985 left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width)); 986 top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height)); 987 } 988 989 int folderPivotX = width / 2 + (centeredLeft - left); 990 int folderPivotY = height / 2 + (centeredTop - top); 991 setPivotX(folderPivotX); 992 setPivotY(folderPivotY); 993 994 lp.width = width; 995 lp.height = height; 996 lp.x = left; 997 lp.y = top; 998 } 999 getContentAreaHeight()1000 protected int getContentAreaHeight() { 1001 DeviceProfile grid = mLauncher.getDeviceProfile(); 1002 int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y 1003 - mFooterHeight; 1004 int height = Math.min(maxContentAreaHeight, 1005 mContent.getDesiredHeight()); 1006 return Math.max(height, MIN_CONTENT_DIMEN); 1007 } 1008 getContentAreaWidth()1009 private int getContentAreaWidth() { 1010 return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN); 1011 } 1012 getFolderWidth()1013 private int getFolderWidth() { 1014 return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 1015 } 1016 getFolderHeight()1017 private int getFolderHeight() { 1018 return getFolderHeight(getContentAreaHeight()); 1019 } 1020 getFolderHeight(int contentAreaHeight)1021 private int getFolderHeight(int contentAreaHeight) { 1022 return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; 1023 } 1024 onMeasure(int widthMeasureSpec, int heightMeasureSpec)1025 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1026 int contentWidth = getContentAreaWidth(); 1027 int contentHeight = getContentAreaHeight(); 1028 1029 int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); 1030 int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); 1031 1032 mContent.setFixedSize(contentWidth, contentHeight); 1033 mContent.measure(contentAreaWidthSpec, contentAreaHeightSpec); 1034 1035 if (mContent.getChildCount() > 0) { 1036 int cellIconGap = (mContent.getPageAt(0).getCellWidth() 1037 - mLauncher.getDeviceProfile().iconSizePx) / 2; 1038 mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap, 1039 mFooter.getPaddingTop(), 1040 mContent.getPaddingRight() + cellIconGap, 1041 mFooter.getPaddingBottom()); 1042 } 1043 mFooter.measure(contentAreaWidthSpec, 1044 MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); 1045 1046 int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth; 1047 int folderHeight = getFolderHeight(contentHeight); 1048 setMeasuredDimension(folderWidth, folderHeight); 1049 } 1050 1051 /** 1052 * Rearranges the children based on their rank. 1053 */ rearrangeChildren()1054 public void rearrangeChildren() { 1055 mContent.arrangeChildren(getIconsInReadingOrder()); 1056 mItemsInvalidated = true; 1057 } 1058 getItemCount()1059 public int getItemCount() { 1060 return mInfo.contents.size(); 1061 } 1062 replaceFolderWithFinalItem()1063 @Thunk void replaceFolderWithFinalItem() { 1064 // Add the last remaining child to the workspace in place of the folder 1065 Runnable onCompleteRunnable = new Runnable() { 1066 @Override 1067 public void run() { 1068 int itemCount = getItemCount(); 1069 if (itemCount <= 1) { 1070 View newIcon = null; 1071 1072 if (itemCount == 1) { 1073 // Move the item from the folder to the workspace, in the position of the 1074 // folder 1075 CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, 1076 mInfo.screenId); 1077 WorkspaceItemInfo finalItem = mInfo.contents.remove(0); 1078 newIcon = mLauncher.createShortcut(cellLayout, finalItem); 1079 mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem, 1080 mInfo.container, mInfo.screenId, mInfo.cellX, mInfo.cellY); 1081 } 1082 1083 // Remove the folder 1084 mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */); 1085 if (mFolderIcon instanceof DropTarget) { 1086 mDragController.removeDropTarget((DropTarget) mFolderIcon); 1087 } 1088 1089 if (newIcon != null) { 1090 // We add the child after removing the folder to prevent both from existing 1091 // at the same time in the CellLayout. We need to add the new item with 1092 // addInScreenFromBind() to ensure that hotseat items are placed correctly. 1093 mLauncher.getWorkspace().addInScreenFromBind(newIcon, mInfo); 1094 1095 // Focus the newly created child 1096 newIcon.requestFocus(); 1097 } 1098 } 1099 } 1100 }; 1101 View finalChild = mContent.getLastItem(); 1102 if (finalChild != null) { 1103 mFolderIcon.performDestroyAnimation(onCompleteRunnable); 1104 } else { 1105 onCompleteRunnable.run(); 1106 } 1107 mDestroyed = true; 1108 } 1109 isDestroyed()1110 public boolean isDestroyed() { 1111 return mDestroyed; 1112 } 1113 1114 // This method keeps track of the first and last item in the folder for the purposes 1115 // of keyboard focus updateTextViewFocus()1116 public void updateTextViewFocus() { 1117 final View firstChild = mContent.getFirstItem(); 1118 final View lastChild = mContent.getLastItem(); 1119 if (firstChild != null && lastChild != null) { 1120 mFolderName.setNextFocusDownId(lastChild.getId()); 1121 mFolderName.setNextFocusRightId(lastChild.getId()); 1122 mFolderName.setNextFocusLeftId(lastChild.getId()); 1123 mFolderName.setNextFocusUpId(lastChild.getId()); 1124 // Hitting TAB from the folder name wraps around to the first item on the current 1125 // folder page, and hitting SHIFT+TAB from that item wraps back to the folder name. 1126 mFolderName.setNextFocusForwardId(firstChild.getId()); 1127 // When clicking off the folder when editing the name, this Folder gains focus. When 1128 // pressing an arrow key from that state, give the focus to the first item. 1129 this.setNextFocusDownId(firstChild.getId()); 1130 this.setNextFocusRightId(firstChild.getId()); 1131 this.setNextFocusLeftId(firstChild.getId()); 1132 this.setNextFocusUpId(firstChild.getId()); 1133 // When pressing shift+tab in the above state, give the focus to the last item. 1134 setOnKeyListener(new OnKeyListener() { 1135 @Override 1136 public boolean onKey(View v, int keyCode, KeyEvent event) { 1137 boolean isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB && 1138 event.hasModifiers(KeyEvent.META_SHIFT_ON); 1139 if (isShiftPlusTab && Folder.this.isFocused()) { 1140 return lastChild.requestFocus(); 1141 } 1142 return false; 1143 } 1144 }); 1145 } else { 1146 setOnKeyListener(null); 1147 } 1148 } 1149 1150 @Override onDrop(DragObject d, DragOptions options)1151 public void onDrop(DragObject d, DragOptions options) { 1152 // If the icon was dropped while the page was being scrolled, we need to compute 1153 // the target location again such that the icon is placed of the final page. 1154 if (!mContent.rankOnCurrentPage(mEmptyCellRank)) { 1155 // Reorder again. 1156 mTargetRank = getTargetRank(d, null); 1157 1158 // Rearrange items immediately. 1159 mReorderAlarmListener.onAlarm(mReorderAlarm); 1160 1161 mOnScrollHintAlarm.cancelAlarm(); 1162 mScrollPauseAlarm.cancelAlarm(); 1163 } 1164 mContent.completePendingPageChanges(); 1165 1166 PendingAddShortcutInfo pasi = d.dragInfo instanceof PendingAddShortcutInfo 1167 ? (PendingAddShortcutInfo) d.dragInfo : null; 1168 WorkspaceItemInfo pasiSi = pasi != null ? pasi.activityInfo.createWorkspaceItemInfo() : null; 1169 if (pasi != null && pasiSi == null) { 1170 // There is no WorkspaceItemInfo, so we have to go through a configuration activity. 1171 pasi.container = mInfo.id; 1172 pasi.rank = mEmptyCellRank; 1173 1174 mLauncher.addPendingItem(pasi, pasi.container, pasi.screenId, null, pasi.spanX, 1175 pasi.spanY); 1176 d.deferDragViewCleanupPostAnimation = false; 1177 mRearrangeOnClose = true; 1178 } else { 1179 final WorkspaceItemInfo si; 1180 if (pasiSi != null) { 1181 si = pasiSi; 1182 } else if (d.dragInfo instanceof AppInfo) { 1183 // Came from all apps -- make a copy. 1184 si = ((AppInfo) d.dragInfo).makeWorkspaceItem(); 1185 } else { 1186 // WorkspaceItemInfo 1187 si = (WorkspaceItemInfo) d.dragInfo; 1188 } 1189 1190 View currentDragView; 1191 if (mIsExternalDrag) { 1192 currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); 1193 1194 // Actually move the item in the database if it was an external drag. Call this 1195 // before creating the view, so that WorkspaceItemInfo is updated appropriately. 1196 mLauncher.getModelWriter().addOrMoveItemInDatabase( 1197 si, mInfo.id, 0, si.cellX, si.cellY); 1198 mIsExternalDrag = false; 1199 } else { 1200 currentDragView = mCurrentDragView; 1201 mContent.addViewForRank(currentDragView, si, mEmptyCellRank); 1202 } 1203 1204 if (d.dragView.hasDrawn()) { 1205 // Temporarily reset the scale such that the animation target gets calculated 1206 // correctly. 1207 float scaleX = getScaleX(); 1208 float scaleY = getScaleY(); 1209 setScaleX(1.0f); 1210 setScaleY(1.0f); 1211 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView, null); 1212 setScaleX(scaleX); 1213 setScaleY(scaleY); 1214 } else { 1215 d.deferDragViewCleanupPostAnimation = false; 1216 currentDragView.setVisibility(VISIBLE); 1217 } 1218 1219 mItemsInvalidated = true; 1220 rearrangeChildren(); 1221 1222 // Temporarily suppress the listener, as we did all the work already here. 1223 try (SuppressInfoChanges s = new SuppressInfoChanges()) { 1224 mInfo.add(si, mEmptyCellRank, false); 1225 } 1226 1227 // We only need to update the locations if it doesn't get handled in 1228 // #onDropCompleted. 1229 if (d.dragSource != this) { 1230 updateItemLocationsInDatabaseBatch(); 1231 } 1232 } 1233 1234 // Clear the drag info, as it is no longer being dragged. 1235 mDragInProgress = false; 1236 1237 if (mContent.getPageCount() > 1) { 1238 // The animation has already been shown while opening the folder. 1239 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher.getModelWriter()); 1240 } 1241 1242 mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); 1243 if (d.stateAnnouncer != null) { 1244 d.stateAnnouncer.completeAction(R.string.item_moved); 1245 } 1246 } 1247 1248 // This is used so the item doesn't immediately appear in the folder when added. In one case 1249 // we need to create the illusion that the item isn't added back to the folder yet, to 1250 // to correspond to the animation of the icon back into the folder. This is hideItem(WorkspaceItemInfo info)1251 public void hideItem(WorkspaceItemInfo info) { 1252 View v = getViewForInfo(info); 1253 if (v != null) { 1254 v.setVisibility(INVISIBLE); 1255 } 1256 } showItem(WorkspaceItemInfo info)1257 public void showItem(WorkspaceItemInfo info) { 1258 View v = getViewForInfo(info); 1259 if (v != null) { 1260 v.setVisibility(VISIBLE); 1261 } 1262 } 1263 1264 @Override onAdd(WorkspaceItemInfo item, int rank)1265 public void onAdd(WorkspaceItemInfo item, int rank) { 1266 FolderGridOrganizer verifier = new FolderGridOrganizer( 1267 mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo); 1268 verifier.updateRankAndPos(item, rank); 1269 mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX, 1270 item.cellY); 1271 updateItemLocationsInDatabaseBatch(); 1272 1273 if (mContent.areViewsBound()) { 1274 mContent.createAndAddViewForRank(item, rank); 1275 } 1276 mItemsInvalidated = true; 1277 } 1278 onRemove(WorkspaceItemInfo item)1279 public void onRemove(WorkspaceItemInfo item) { 1280 mItemsInvalidated = true; 1281 View v = getViewForInfo(item); 1282 mContent.removeItem(v); 1283 if (mState == STATE_ANIMATING) { 1284 mRearrangeOnClose = true; 1285 } else { 1286 rearrangeChildren(); 1287 } 1288 if (getItemCount() <= 1) { 1289 if (mIsOpen) { 1290 close(true); 1291 } else { 1292 replaceFolderWithFinalItem(); 1293 } 1294 } 1295 } 1296 getViewForInfo(final WorkspaceItemInfo item)1297 private View getViewForInfo(final WorkspaceItemInfo item) { 1298 return mContent.iterateOverItems((info, view) -> info == item); 1299 } 1300 1301 @Override onItemsChanged(boolean animate)1302 public void onItemsChanged(boolean animate) { 1303 updateTextViewFocus(); 1304 } 1305 1306 /** 1307 * Utility methods to iterate over items of the view 1308 */ iterateOverItems(ItemOperator op)1309 public void iterateOverItems(ItemOperator op) { 1310 mContent.iterateOverItems(op); 1311 } 1312 1313 /** 1314 * Returns the sorted list of all the icons in the folder 1315 */ getIconsInReadingOrder()1316 public ArrayList<View> getIconsInReadingOrder() { 1317 if (mItemsInvalidated) { 1318 mItemsInReadingOrder.clear(); 1319 mContent.iterateOverItems((i, v) -> !mItemsInReadingOrder.add(v)); 1320 mItemsInvalidated = false; 1321 } 1322 return mItemsInReadingOrder; 1323 } 1324 getItemsOnPage(int page)1325 public List<BubbleTextView> getItemsOnPage(int page) { 1326 ArrayList<View> allItems = getIconsInReadingOrder(); 1327 int lastPage = mContent.getPageCount() - 1; 1328 int totalItemsInFolder = allItems.size(); 1329 int itemsPerPage = mContent.itemsPerPage(); 1330 int numItemsOnCurrentPage = page == lastPage 1331 ? totalItemsInFolder - (itemsPerPage * page) 1332 : itemsPerPage; 1333 1334 int startIndex = page * itemsPerPage; 1335 int endIndex = Math.min(startIndex + numItemsOnCurrentPage, allItems.size()); 1336 1337 List<BubbleTextView> itemsOnCurrentPage = new ArrayList<>(numItemsOnCurrentPage); 1338 for (int i = startIndex; i < endIndex; ++i) { 1339 itemsOnCurrentPage.add((BubbleTextView) allItems.get(i)); 1340 } 1341 return itemsOnCurrentPage; 1342 } 1343 onFocusChange(View v, boolean hasFocus)1344 public void onFocusChange(View v, boolean hasFocus) { 1345 if (v == mFolderName) { 1346 if (hasFocus) { 1347 startEditingFolderName(); 1348 } else { 1349 if (isEditingName()) { 1350 logEditFolderLabel(); 1351 } 1352 mFolderName.dispatchBackKey(); 1353 } 1354 } 1355 } 1356 1357 @Override getHitRectRelativeToDragLayer(Rect outRect)1358 public void getHitRectRelativeToDragLayer(Rect outRect) { 1359 getHitRect(outRect); 1360 outRect.left -= mScrollAreaOffset; 1361 outRect.right += mScrollAreaOffset; 1362 } 1363 1364 @Override fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent)1365 public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { 1366 target.gridX = info.cellX; 1367 target.gridY = info.cellY; 1368 target.pageIndex = mContent.getCurrentPage(); 1369 targetParent.containerType = ContainerType.FOLDER; 1370 } 1371 1372 private class OnScrollHintListener implements OnAlarmListener { 1373 1374 private final DragObject mDragObject; 1375 OnScrollHintListener(DragObject object)1376 OnScrollHintListener(DragObject object) { 1377 mDragObject = object; 1378 } 1379 1380 /** 1381 * Scroll hint has been shown long enough. Now scroll to appropriate page. 1382 */ 1383 @Override onAlarm(Alarm alarm)1384 public void onAlarm(Alarm alarm) { 1385 if (mCurrentScrollDir == SCROLL_LEFT) { 1386 mContent.scrollLeft(); 1387 mScrollHintDir = SCROLL_NONE; 1388 } else if (mCurrentScrollDir == SCROLL_RIGHT) { 1389 mContent.scrollRight(); 1390 mScrollHintDir = SCROLL_NONE; 1391 } else { 1392 // This should not happen 1393 return; 1394 } 1395 mCurrentScrollDir = SCROLL_NONE; 1396 1397 // Pause drag event until the scrolling is finished 1398 mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); 1399 mScrollPauseAlarm.setAlarm(RESCROLL_DELAY); 1400 } 1401 } 1402 1403 private class OnScrollFinishedListener implements OnAlarmListener { 1404 1405 private final DragObject mDragObject; 1406 OnScrollFinishedListener(DragObject object)1407 OnScrollFinishedListener(DragObject object) { 1408 mDragObject = object; 1409 } 1410 1411 /** 1412 * Page scroll is complete. 1413 */ 1414 @Override onAlarm(Alarm alarm)1415 public void onAlarm(Alarm alarm) { 1416 // Reorder immediately on page change. 1417 onDragOver(mDragObject); 1418 } 1419 } 1420 1421 // Compares item position based on rank and position giving priority to the rank. 1422 public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() { 1423 1424 @Override 1425 public int compare(ItemInfo lhs, ItemInfo rhs) { 1426 if (lhs.rank != rhs.rank) { 1427 return lhs.rank - rhs.rank; 1428 } else if (lhs.cellY != rhs.cellY) { 1429 return lhs.cellY - rhs.cellY; 1430 } else { 1431 return lhs.cellX - rhs.cellX; 1432 } 1433 } 1434 }; 1435 1436 /** 1437 * Temporary resource held while we don't want to handle info changes 1438 */ 1439 private class SuppressInfoChanges implements AutoCloseable { 1440 SuppressInfoChanges()1441 SuppressInfoChanges() { 1442 mInfo.removeListener(Folder.this); 1443 } 1444 1445 @Override close()1446 public void close() { 1447 mInfo.addListener(Folder.this); 1448 updateTextViewFocus(); 1449 } 1450 } 1451 1452 /** 1453 * Returns a folder which is already open or null 1454 */ getOpen(Launcher launcher)1455 public static Folder getOpen(Launcher launcher) { 1456 return getOpenView(launcher, TYPE_FOLDER); 1457 } 1458 1459 @Override logActionCommand(int command)1460 public void logActionCommand(int command) { 1461 mLauncher.getUserEventDispatcher().logActionCommand( 1462 command, getFolderIcon(), getLogContainerType()); 1463 } 1464 1465 @Override getLogContainerType()1466 public int getLogContainerType() { 1467 return ContainerType.FOLDER; 1468 } 1469 1470 @Override onBackPressed()1471 public boolean onBackPressed() { 1472 if (isEditingName()) { 1473 mFolderName.dispatchBackKey(); 1474 } else { 1475 super.onBackPressed(); 1476 } 1477 return true; 1478 } 1479 1480 @Override onControllerInterceptTouchEvent(MotionEvent ev)1481 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 1482 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 1483 DragLayer dl = mLauncher.getDragLayer(); 1484 1485 if (isEditingName()) { 1486 if (!dl.isEventOverView(mFolderName, ev)) { 1487 mFolderName.dispatchBackKey(); 1488 return true; 1489 } 1490 return false; 1491 } else if (!dl.isEventOverView(this, ev)) { 1492 if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) { 1493 // Do not close the container if in drag and drop. 1494 if (!dl.isEventOverView(mLauncher.getDropTargetBar(), ev)) { 1495 return true; 1496 } 1497 } else { 1498 mLauncher.getUserEventDispatcher().logActionTapOutside( 1499 LoggerUtils.newContainerTarget(ContainerType.FOLDER)); 1500 close(true); 1501 return true; 1502 } 1503 } 1504 } 1505 return false; 1506 } 1507 1508 /** 1509 * Alternative to using {@link #getClipToOutline()} as it only works with derivatives of 1510 * rounded rect. 1511 */ 1512 @Override setClipPath(Path clipPath)1513 public void setClipPath(Path clipPath) { 1514 mClipPath = clipPath; 1515 invalidate(); 1516 } 1517 1518 @Override draw(Canvas canvas)1519 public void draw(Canvas canvas) { 1520 if (mClipPath != null) { 1521 int count = canvas.save(); 1522 canvas.clipPath(mClipPath); 1523 super.draw(canvas); 1524 canvas.restoreToCount(count); 1525 } else { 1526 super.draw(canvas); 1527 } 1528 } 1529 logEditFolderLabel()1530 private void logEditFolderLabel() { 1531 LauncherLogProto.LauncherEvent ev = new LauncherLogProto.LauncherEvent(); 1532 LauncherLogProto.Action action = new LauncherLogProto.Action(); 1533 action.type = LauncherLogProto.Action.Type.SOFT_KEYBOARD; 1534 ev.action = action; 1535 1536 LauncherLogProto.Target edittext_target = new LauncherLogProto.Target(); 1537 edittext_target.type = LauncherLogProto.Target.Type.ITEM; 1538 edittext_target.itemType = LauncherLogProto.ItemType.EDITTEXT; 1539 1540 LauncherLogProto.Target folder_target = new LauncherLogProto.Target(); 1541 folder_target.type = LauncherLogProto.Target.Type.CONTAINER; 1542 folder_target.containerType = LauncherLogProto.ContainerType.FOLDER; 1543 folder_target.pageIndex = mInfo.screenId; 1544 folder_target.gridX = mInfo.cellX; 1545 folder_target.gridY = mInfo.cellY; 1546 folder_target.cardinality = mInfo.contents.size(); 1547 1548 LauncherLogProto.Target parent_target = new LauncherLogProto.Target(); 1549 parent_target.type = LauncherLogProto.Target.Type.CONTAINER; 1550 switch (mInfo.container) { 1551 case CONTAINER_HOTSEAT: 1552 parent_target.containerType = LauncherLogProto.ContainerType.HOTSEAT; 1553 break; 1554 case CONTAINER_DESKTOP: 1555 parent_target.containerType = LauncherLogProto.ContainerType.WORKSPACE; 1556 break; 1557 default: 1558 Log.e(TAG, String.format("Expected container to be either %s or %s but found %s.", 1559 CONTAINER_HOTSEAT, CONTAINER_DESKTOP, mInfo.container)); 1560 } 1561 ev.srcTarget = new LauncherLogProto.Target[]{edittext_target, folder_target, parent_target}; 1562 mLauncher.getUserEventDispatcher().dispatchUserEvent(ev, null); 1563 } 1564 } 1565