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