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