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