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