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