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