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