• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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