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