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