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.launcher2;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.content.Context;
26import android.content.res.Resources;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.text.InputType;
30import android.text.Selection;
31import android.text.Spannable;
32import android.util.AttributeSet;
33import android.view.ActionMode;
34import android.view.KeyEvent;
35import android.view.LayoutInflater;
36import android.view.Menu;
37import android.view.MenuItem;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.accessibility.AccessibilityEvent;
41import android.view.accessibility.AccessibilityManager;
42import android.view.animation.AccelerateInterpolator;
43import android.view.animation.DecelerateInterpolator;
44import android.view.inputmethod.EditorInfo;
45import android.view.inputmethod.InputMethodManager;
46import android.widget.LinearLayout;
47import android.widget.TextView;
48
49import com.android.launcher.R;
50import com.android.launcher2.FolderInfo.FolderListener;
51
52import java.util.ArrayList;
53
54/**
55 * Represents a set of icons chosen by the user or generated by the system.
56 */
57public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
58        View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
59        View.OnFocusChangeListener {
60
61    private static final String TAG = "Launcher.Folder";
62
63    protected DragController mDragController;
64    protected Launcher mLauncher;
65    protected FolderInfo mInfo;
66
67    static final int STATE_NONE = -1;
68    static final int STATE_SMALL = 0;
69    static final int STATE_ANIMATING = 1;
70    static final int STATE_OPEN = 2;
71
72    private int mExpandDuration;
73    protected CellLayout mContent;
74    private final LayoutInflater mInflater;
75    private final IconCache mIconCache;
76    private int mState = STATE_NONE;
77    private static final int FULL_GROW = 0;
78    private static final int PARTIAL_GROW = 1;
79    private static final int REORDER_ANIMATION_DURATION = 230;
80    private static final int ON_EXIT_CLOSE_DELAY = 800;
81    private int mMode = PARTIAL_GROW;
82    private boolean mRearrangeOnClose = false;
83    private FolderIcon mFolderIcon;
84    private int mMaxCountX;
85    private int mMaxCountY;
86    private int mMaxNumItems;
87    private Rect mNewSize = new Rect();
88    private Rect mIconRect = new Rect();
89    private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
90    private Drawable mIconDrawable;
91    boolean mItemsInvalidated = false;
92    private ShortcutInfo mCurrentDragInfo;
93    private View mCurrentDragView;
94    boolean mSuppressOnAdd = false;
95    private int[] mTargetCell = new int[2];
96    private int[] mPreviousTargetCell = new int[2];
97    private int[] mEmptyCell = new int[2];
98    private Alarm mReorderAlarm = new Alarm();
99    private Alarm mOnExitAlarm = new Alarm();
100    private int mFolderNameHeight;
101    private Rect mTempRect = new Rect();
102    private boolean mDragInProgress = false;
103    private boolean mDeleteFolderOnDropCompleted = false;
104    private boolean mSuppressFolderDeletion = false;
105    private boolean mItemAddedBackToSelfViaIcon = false;
106    FolderEditText mFolderName;
107
108    private boolean mIsEditingName = false;
109    private InputMethodManager mInputMethodManager;
110
111    private static String sDefaultFolderName;
112    private static String sHintText;
113
114    /**
115     * Used to inflate the Workspace from XML.
116     *
117     * @param context The application's context.
118     * @param attrs The attribtues set containing the Workspace's customization values.
119     */
120    public Folder(Context context, AttributeSet attrs) {
121        super(context, attrs);
122        setAlwaysDrawnWithCacheEnabled(false);
123        mInflater = LayoutInflater.from(context);
124        mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache();
125
126        Resources res = getResources();
127        mMaxCountX = res.getInteger(R.integer.folder_max_count_x);
128        mMaxCountY = res.getInteger(R.integer.folder_max_count_y);
129        mMaxNumItems = res.getInteger(R.integer.folder_max_num_items);
130        if (mMaxCountX < 0 || mMaxCountY < 0 || mMaxNumItems < 0) {
131            mMaxCountX = LauncherModel.getCellCountX();
132            mMaxCountY = LauncherModel.getCellCountY();
133            mMaxNumItems = mMaxCountX * mMaxCountY;
134        }
135
136        mInputMethodManager = (InputMethodManager)
137                mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
138
139        mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration);
140
141        if (sDefaultFolderName == null) {
142            sDefaultFolderName = res.getString(R.string.folder_name);
143        }
144        if (sHintText == null) {
145            sHintText = res.getString(R.string.folder_hint_text);
146        }
147        mLauncher = (Launcher) context;
148        // We need this view to be focusable in touch mode so that when text editing of the folder
149        // name is complete, we have something to focus on, thus hiding the cursor and giving
150        // reliable behvior when clicking the text field (since it will always gain focus on click).
151        setFocusableInTouchMode(true);
152    }
153
154    @Override
155    protected void onFinishInflate() {
156        super.onFinishInflate();
157        mContent = (CellLayout) findViewById(R.id.folder_content);
158        mContent.setGridSize(0, 0);
159        mContent.getChildrenLayout().setMotionEventSplittingEnabled(false);
160        mFolderName = (FolderEditText) findViewById(R.id.folder_name);
161        mFolderName.setFolder(this);
162        mFolderName.setOnFocusChangeListener(this);
163
164        // We find out how tall the text view wants to be (it is set to wrap_content), so that
165        // we can allocate the appropriate amount of space for it.
166        int measureSpec = MeasureSpec.UNSPECIFIED;
167        mFolderName.measure(measureSpec, measureSpec);
168        mFolderNameHeight = mFolderName.getMeasuredHeight();
169
170        // We disable action mode for now since it messes up the view on phones
171        mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback);
172        mFolderName.setOnEditorActionListener(this);
173        mFolderName.setSelectAllOnFocus(true);
174        mFolderName.setInputType(mFolderName.getInputType() |
175                InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
176    }
177
178    private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
179        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
180            return false;
181        }
182
183        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
184            return false;
185        }
186
187        public void onDestroyActionMode(ActionMode mode) {
188        }
189
190        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
191            return false;
192        }
193    };
194
195    public void onClick(View v) {
196        Object tag = v.getTag();
197        if (tag instanceof ShortcutInfo) {
198            // refactor this code from Folder
199            ShortcutInfo item = (ShortcutInfo) tag;
200            int[] pos = new int[2];
201            v.getLocationOnScreen(pos);
202            item.intent.setSourceBounds(new Rect(pos[0], pos[1],
203                    pos[0] + v.getWidth(), pos[1] + v.getHeight()));
204            mLauncher.startActivitySafely(item.intent, item);
205        }
206    }
207
208    public boolean onLongClick(View v) {
209        Object tag = v.getTag();
210        if (tag instanceof ShortcutInfo) {
211            ShortcutInfo item = (ShortcutInfo) tag;
212            if (!v.isInTouchMode()) {
213                return false;
214            }
215
216            mLauncher.dismissFolderCling(null);
217
218            mLauncher.getWorkspace().onDragStartedWithItem(v);
219            mLauncher.getWorkspace().beginDragShared(v, this);
220            mIconDrawable = ((TextView) v).getCompoundDrawables()[1];
221
222            mCurrentDragInfo = item;
223            mEmptyCell[0] = item.cellX;
224            mEmptyCell[1] = item.cellY;
225            mCurrentDragView = v;
226
227            mContent.removeView(mCurrentDragView);
228            mInfo.remove(mCurrentDragInfo);
229            mDragInProgress = true;
230            mItemAddedBackToSelfViaIcon = false;
231        }
232        return true;
233    }
234
235    public boolean isEditingName() {
236        return mIsEditingName;
237    }
238
239    public void startEditingFolderName() {
240        mFolderName.setHint("");
241        mIsEditingName = true;
242    }
243
244    public void dismissEditingName() {
245        mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
246        doneEditingFolderName(true);
247    }
248
249    public void doneEditingFolderName(boolean commit) {
250        mFolderName.setHint(sHintText);
251        // Convert to a string here to ensure that no other state associated with the text field
252        // gets saved.
253        String newTitle = mFolderName.getText().toString();
254        mInfo.setTitle(newTitle);
255        LauncherModel.updateItemInDatabase(mLauncher, mInfo);
256
257        if (commit) {
258            sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
259                    String.format(mContext.getString(R.string.folder_renamed), newTitle));
260        }
261        // In order to clear the focus from the text field, we set the focus on ourself. This
262        // ensures that every time the field is clicked, focus is gained, giving reliable behavior.
263        requestFocus();
264
265        Selection.setSelection((Spannable) mFolderName.getText(), 0, 0);
266        mIsEditingName = false;
267    }
268
269    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
270        if (actionId == EditorInfo.IME_ACTION_DONE) {
271            dismissEditingName();
272            return true;
273        }
274        return false;
275    }
276
277    public View getEditTextRegion() {
278        return mFolderName;
279    }
280
281    public Drawable getDragDrawable() {
282        return mIconDrawable;
283    }
284
285    /**
286     * We need to handle touch events to prevent them from falling through to the workspace below.
287     */
288    @Override
289    public boolean onTouchEvent(MotionEvent ev) {
290        return true;
291    }
292
293    public void setDragController(DragController dragController) {
294        mDragController = dragController;
295    }
296
297    void setFolderIcon(FolderIcon icon) {
298        mFolderIcon = icon;
299    }
300
301    @Override
302    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
303        // When the folder gets focus, we don't want to announce the list of items.
304        return true;
305    }
306
307    /**
308     * @return the FolderInfo object associated with this folder
309     */
310    FolderInfo getInfo() {
311        return mInfo;
312    }
313
314    void bind(FolderInfo info) {
315        mInfo = info;
316        ArrayList<ShortcutInfo> children = info.contents;
317        ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>();
318        setupContentForNumItems(children.size());
319        int count = 0;
320        for (int i = 0; i < children.size(); i++) {
321            ShortcutInfo child = (ShortcutInfo) children.get(i);
322            if (!createAndAddShortcut(child)) {
323                overflow.add(child);
324            } else {
325                count++;
326            }
327        }
328
329        // We rearrange the items in case there are any empty gaps
330        setupContentForNumItems(count);
331
332        // If our folder has too many items we prune them from the list. This is an issue
333        // when upgrading from the old Folders implementation which could contain an unlimited
334        // number of items.
335        for (ShortcutInfo item: overflow) {
336            mInfo.remove(item);
337            LauncherModel.deleteItemFromDatabase(mLauncher, item);
338        }
339
340        mItemsInvalidated = true;
341        updateTextViewFocus();
342        mInfo.addListener(this);
343
344        if (!sDefaultFolderName.contentEquals(mInfo.title)) {
345            mFolderName.setText(mInfo.title);
346        } else {
347            mFolderName.setText("");
348        }
349    }
350
351    /**
352     * Creates a new UserFolder, inflated from R.layout.user_folder.
353     *
354     * @param context The application's context.
355     *
356     * @return A new UserFolder.
357     */
358    static Folder fromXml(Context context) {
359        return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null);
360    }
361
362    /**
363     * This method is intended to make the UserFolder to be visually identical in size and position
364     * to its associated FolderIcon. This allows for a seamless transition into the expanded state.
365     */
366    private void positionAndSizeAsIcon() {
367        if (!(getParent() instanceof DragLayer)) return;
368
369        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
370
371        if (mMode == PARTIAL_GROW) {
372            setScaleX(0.8f);
373            setScaleY(0.8f);
374            setAlpha(0f);
375        } else {
376            mLauncher.getDragLayer().getDescendantRectRelativeToSelf(mFolderIcon, mIconRect);
377            lp.width = mIconRect.width();
378            lp.height = mIconRect.height();
379            lp.x = mIconRect.left;
380            lp.y = mIconRect.top;
381            mContent.setAlpha(0);
382        }
383        mState = STATE_SMALL;
384    }
385
386    public void animateOpen() {
387        positionAndSizeAsIcon();
388
389        if (!(getParent() instanceof DragLayer)) return;
390
391        ObjectAnimator oa;
392        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
393
394        centerAboutIcon();
395        if (mMode == PARTIAL_GROW) {
396            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1);
397            PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
398            PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
399            oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
400        } else {
401            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mNewSize.width());
402            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mNewSize.height());
403            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mNewSize.left);
404            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mNewSize.top);
405            oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y);
406            oa.addUpdateListener(new AnimatorUpdateListener() {
407                public void onAnimationUpdate(ValueAnimator animation) {
408                    requestLayout();
409                }
410            });
411
412            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f);
413            ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha);
414            alphaOa.setDuration(mExpandDuration);
415            alphaOa.setInterpolator(new AccelerateInterpolator(2.0f));
416            alphaOa.start();
417        }
418
419        oa.addListener(new AnimatorListenerAdapter() {
420            @Override
421            public void onAnimationStart(Animator animation) {
422                sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
423                        String.format(mContext.getString(R.string.folder_opened),
424                        mContent.getCountX(), mContent.getCountY()));
425                mState = STATE_ANIMATING;
426            }
427            @Override
428            public void onAnimationEnd(Animator animation) {
429                mState = STATE_OPEN;
430
431                Cling cling = mLauncher.showFirstRunFoldersCling();
432                if (cling != null) {
433                    cling.bringToFront();
434                }
435                setFocusOnFirstChild();
436            }
437        });
438        oa.setDuration(mExpandDuration);
439        oa.start();
440    }
441
442    private void sendCustomAccessibilityEvent(int type, String text) {
443        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
444            AccessibilityEvent event = AccessibilityEvent.obtain(type);
445            onInitializeAccessibilityEvent(event);
446            event.getText().add(text);
447            AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event);
448        }
449    }
450
451    private void setFocusOnFirstChild() {
452        View firstChild = mContent.getChildAt(0, 0);
453        if (firstChild != null) {
454            firstChild.requestFocus();
455        }
456    }
457
458    public void animateClosed() {
459        if (!(getParent() instanceof DragLayer)) return;
460
461        ObjectAnimator oa;
462        if (mMode == PARTIAL_GROW) {
463            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0);
464            PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f);
465            PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f);
466            oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
467        } else {
468            DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
469
470            PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mIconRect.width());
471            PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mIconRect.height());
472            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mIconRect.left);
473            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mIconRect.top);
474            oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y);
475            oa.addUpdateListener(new AnimatorUpdateListener() {
476                public void onAnimationUpdate(ValueAnimator animation) {
477                    requestLayout();
478                }
479            });
480
481            PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f);
482            ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha);
483            alphaOa.setDuration(mExpandDuration);
484            alphaOa.setInterpolator(new DecelerateInterpolator(2.0f));
485            alphaOa.start();
486        }
487
488        oa.addListener(new AnimatorListenerAdapter() {
489            @Override
490            public void onAnimationEnd(Animator animation) {
491                onCloseComplete();
492                mState = STATE_SMALL;
493            }
494            @Override
495            public void onAnimationStart(Animator animation) {
496                sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
497                        mContext.getString(R.string.folder_closed));
498                mState = STATE_ANIMATING;
499            }
500        });
501        oa.setDuration(mExpandDuration);
502        oa.start();
503    }
504
505    void notifyDataSetChanged() {
506        // recreate all the children if the data set changes under us. We may want to do this more
507        // intelligently (ie just removing the views that should no longer exist)
508        mContent.removeAllViewsInLayout();
509        bind(mInfo);
510    }
511
512    public boolean acceptDrop(DragObject d) {
513        final ItemInfo item = (ItemInfo) d.dragInfo;
514        final int itemType = item.itemType;
515        return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
516                    itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
517                    !isFull());
518    }
519
520    protected boolean findAndSetEmptyCells(ShortcutInfo item) {
521        int[] emptyCell = new int[2];
522        if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) {
523            item.cellX = emptyCell[0];
524            item.cellY = emptyCell[1];
525            return true;
526        } else {
527            return false;
528        }
529    }
530
531    protected boolean createAndAddShortcut(ShortcutInfo item) {
532        final TextView textView =
533            (TextView) mInflater.inflate(R.layout.application, this, false);
534        textView.setCompoundDrawablesWithIntrinsicBounds(null,
535                new FastBitmapDrawable(item.getIcon(mIconCache)), null, null);
536        textView.setText(item.title);
537        textView.setTag(item);
538
539        textView.setOnClickListener(this);
540        textView.setOnLongClickListener(this);
541
542        // We need to check here to verify that the given item's location isn't already occupied
543        // by another item. If it is, we need to find the next available slot and assign
544        // it that position. This is an issue when upgrading from the old Folders implementation
545        // which could contain an unlimited number of items.
546        if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0
547                || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) {
548            if (!findAndSetEmptyCells(item)) {
549                return false;
550            }
551        }
552
553        CellLayout.LayoutParams lp =
554            new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY);
555        boolean insert = false;
556        textView.setOnKeyListener(new FolderKeyEventListener());
557        mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true);
558        return true;
559    }
560
561    public void onDragEnter(DragObject d) {
562        mPreviousTargetCell[0] = -1;
563        mPreviousTargetCell[1] = -1;
564        mOnExitAlarm.cancelAlarm();
565    }
566
567    OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
568        public void onAlarm(Alarm alarm) {
569            realTimeReorder(mEmptyCell, mTargetCell);
570        }
571    };
572
573    boolean readingOrderGreaterThan(int[] v1, int[] v2) {
574        if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) {
575            return true;
576        } else {
577            return false;
578        }
579    }
580
581    private void realTimeReorder(int[] empty, int[] target) {
582        boolean wrap;
583        int startX;
584        int endX;
585        int startY;
586        int delay = 0;
587        float delayAmount = 30;
588        if (readingOrderGreaterThan(target, empty)) {
589            wrap = empty[0] >= mContent.getCountX() - 1;
590            startY = wrap ? empty[1] + 1 : empty[1];
591            for (int y = startY; y <= target[1]; y++) {
592                startX = y == empty[1] ? empty[0] + 1 : 0;
593                endX = y < target[1] ? mContent.getCountX() - 1 : target[0];
594                for (int x = startX; x <= endX; x++) {
595                    View v = mContent.getChildAt(x,y);
596                    if (mContent.animateChildToPosition(v, empty[0], empty[1],
597                            REORDER_ANIMATION_DURATION, delay)) {
598                        empty[0] = x;
599                        empty[1] = y;
600                        delay += delayAmount;
601                        delayAmount *= 0.9;
602                    }
603                }
604            }
605        } else {
606            wrap = empty[0] == 0;
607            startY = wrap ? empty[1] - 1 : empty[1];
608            for (int y = startY; y >= target[1]; y--) {
609                startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1;
610                endX = y > target[1] ? 0 : target[0];
611                for (int x = startX; x >= endX; x--) {
612                    View v = mContent.getChildAt(x,y);
613                    if (mContent.animateChildToPosition(v, empty[0], empty[1],
614                            REORDER_ANIMATION_DURATION, delay)) {
615                        empty[0] = x;
616                        empty[1] = y;
617                        delay += delayAmount;
618                        delayAmount *= 0.9;
619                    }
620                }
621            }
622        }
623    }
624
625    public void onDragOver(DragObject d) {
626        float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null);
627        mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 1, 1, mTargetCell);
628
629        if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) {
630            mReorderAlarm.cancelAlarm();
631            mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
632            mReorderAlarm.setAlarm(150);
633            mPreviousTargetCell[0] = mTargetCell[0];
634            mPreviousTargetCell[1] = mTargetCell[1];
635        }
636    }
637
638    // This is used to compute the visual center of the dragView. The idea is that
639    // the visual center represents the user's interpretation of where the item is, and hence
640    // is the appropriate point to use when determining drop location.
641    private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset,
642            DragView dragView, float[] recycle) {
643        float res[];
644        if (recycle == null) {
645            res = new float[2];
646        } else {
647            res = recycle;
648        }
649
650        // These represent the visual top and left of drag view if a dragRect was provided.
651        // If a dragRect was not provided, then they correspond to the actual view left and
652        // top, as the dragRect is in that case taken to be the entire dragView.
653        // R.dimen.dragViewOffsetY.
654        int left = x - xOffset;
655        int top = y - yOffset;
656
657        // In order to find the visual center, we shift by half the dragRect
658        res[0] = left + dragView.getDragRegion().width() / 2;
659        res[1] = top + dragView.getDragRegion().height() / 2;
660
661        return res;
662    }
663
664    OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
665        public void onAlarm(Alarm alarm) {
666            completeDragExit();
667        }
668    };
669
670    public void completeDragExit() {
671        mLauncher.closeFolder();
672        mCurrentDragInfo = null;
673        mCurrentDragView = null;
674        mSuppressOnAdd = false;
675        mRearrangeOnClose = true;
676    }
677
678    public void onDragExit(DragObject d) {
679        // We only close the folder if this is a true drag exit, ie. not because a drop
680        // has occurred above the folder.
681        if (!d.dragComplete) {
682            mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
683            mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
684        }
685        mReorderAlarm.cancelAlarm();
686    }
687
688    public void onDropCompleted(View target, DragObject d, boolean success) {
689        if (success) {
690            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) {
691                replaceFolderWithFinalItem();
692            }
693        } else {
694            // The drag failed, we need to return the item to the folder
695            mFolderIcon.onDrop(d);
696
697            // We're going to trigger a "closeFolder" which may occur before this item has
698            // been added back to the folder -- this could cause the folder to be deleted
699            if (mOnExitAlarm.alarmPending()) {
700                mSuppressFolderDeletion = true;
701            }
702        }
703
704        if (target != this) {
705            if (mOnExitAlarm.alarmPending()) {
706                mOnExitAlarm.cancelAlarm();
707                completeDragExit();
708            }
709        }
710        mDeleteFolderOnDropCompleted = false;
711        mDragInProgress = false;
712        mItemAddedBackToSelfViaIcon = false;
713        mCurrentDragInfo = null;
714        mCurrentDragView = null;
715        mSuppressOnAdd = false;
716
717        // Reordering may have occured, and we need to save the new item locations. We do this once
718        // at the end to prevent unnecessary database operations.
719        updateItemLocationsInDatabase();
720    }
721
722    private void updateItemLocationsInDatabase() {
723        ArrayList<View> list = getItemsInReadingOrder();
724        for (int i = 0; i < list.size(); i++) {
725            View v = list.get(i);
726            ItemInfo info = (ItemInfo) v.getTag();
727            LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0,
728                        info.cellX, info.cellY);
729        }
730    }
731
732    public void notifyDrop() {
733        if (mDragInProgress) {
734            mItemAddedBackToSelfViaIcon = true;
735        }
736    }
737
738    public boolean isDropEnabled() {
739        return true;
740    }
741
742    public DropTarget getDropTargetDelegate(DragObject d) {
743        return null;
744    }
745
746    private void setupContentDimensions(int count) {
747        ArrayList<View> list = getItemsInReadingOrder();
748
749        int countX = mContent.getCountX();
750        int countY = mContent.getCountY();
751        boolean done = false;
752
753        while (!done) {
754            int oldCountX = countX;
755            int oldCountY = countY;
756            if (countX * countY < count) {
757                // Current grid is too small, expand it
758                if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) {
759                    countX++;
760                } else if (countY < mMaxCountY) {
761                    countY++;
762                }
763                if (countY == 0) countY++;
764            } else if ((countY - 1) * countX >= count && countY >= countX) {
765                countY = Math.max(0, countY - 1);
766            } else if ((countX - 1) * countY >= count) {
767                countX = Math.max(0, countX - 1);
768            }
769            done = countX == oldCountX && countY == oldCountY;
770        }
771        mContent.setGridSize(countX, countY);
772        arrangeChildren(list);
773    }
774
775    public boolean isFull() {
776        return getItemCount() >= mMaxNumItems;
777    }
778
779    private void centerAboutIcon() {
780        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
781
782        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
783        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight()
784                + mFolderNameHeight;
785        DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
786
787        parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect);
788
789        int centerX = mTempRect.centerX();
790        int centerY = mTempRect.centerY();
791        int centeredLeft = centerX - width / 2;
792        int centeredTop = centerY - height / 2;
793
794        // We first fetch the currently visible CellLayoutChildren
795        CellLayout currentPage = mLauncher.getWorkspace().getCurrentDropLayout();
796        CellLayoutChildren boundingLayout = currentPage.getChildrenLayout();
797        Rect bounds = new Rect();
798        parent.getDescendantRectRelativeToSelf(boundingLayout, bounds);
799
800        // We need to bound the folder to the currently visible CellLayoutChildren
801        int left = Math.min(Math.max(bounds.left, centeredLeft),
802                bounds.left + bounds.width() - width);
803        int top = Math.min(Math.max(bounds.top, centeredTop),
804                bounds.top + bounds.height() - height);
805        // If the folder doesn't fit within the bounds, center it about the desired bounds
806        if (width >= bounds.width()) {
807            left = bounds.left + (bounds.width() - width) / 2;
808        }
809        if (height >= bounds.height()) {
810            top = bounds.top + (bounds.height() - height) / 2;
811        }
812
813        int folderPivotX = width / 2 + (centeredLeft - left);
814        int folderPivotY = height / 2 + (centeredTop - top);
815        setPivotX(folderPivotX);
816        setPivotY(folderPivotY);
817        int folderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
818                (1.0f * folderPivotX / width));
819        int folderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
820                (1.0f * folderPivotY / height));
821        mFolderIcon.setPivotX(folderIconPivotX);
822        mFolderIcon.setPivotY(folderIconPivotY);
823
824        if (mMode == PARTIAL_GROW) {
825            lp.width = width;
826            lp.height = height;
827            lp.x = left;
828            lp.y = top;
829        } else {
830            mNewSize.set(left, top, left + width, top + height);
831        }
832    }
833
834    private void setupContentForNumItems(int count) {
835        setupContentDimensions(count);
836
837        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
838        if (lp == null) {
839            lp = new DragLayer.LayoutParams(0, 0);
840            lp.customPosition = true;
841            setLayoutParams(lp);
842        }
843        centerAboutIcon();
844    }
845
846    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
847        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
848        int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight()
849                + mFolderNameHeight;
850
851        int contentWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(),
852                MeasureSpec.EXACTLY);
853        int contentHeightSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredHeight(),
854                MeasureSpec.EXACTLY);
855        mContent.measure(contentWidthSpec, contentHeightSpec);
856
857        mFolderName.measure(contentWidthSpec,
858                MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY));
859        setMeasuredDimension(width, height);
860    }
861
862    private void arrangeChildren(ArrayList<View> list) {
863        int[] vacant = new int[2];
864        if (list == null) {
865            list = getItemsInReadingOrder();
866        }
867        mContent.removeAllViews();
868
869        for (int i = 0; i < list.size(); i++) {
870            View v = list.get(i);
871            mContent.getVacantCell(vacant, 1, 1);
872            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
873            lp.cellX = vacant[0];
874            lp.cellY = vacant[1];
875            ItemInfo info = (ItemInfo) v.getTag();
876            if (info.cellX != vacant[0] || info.cellY != vacant[1]) {
877                info.cellX = vacant[0];
878                info.cellY = vacant[1];
879                LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0,
880                        info.cellX, info.cellY);
881            }
882            boolean insert = false;
883            mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true);
884        }
885        mItemsInvalidated = true;
886    }
887
888    public int getItemCount() {
889        return mContent.getChildrenLayout().getChildCount();
890    }
891
892    public View getItemAt(int index) {
893        return mContent.getChildrenLayout().getChildAt(index);
894    }
895
896    private void onCloseComplete() {
897        DragLayer parent = (DragLayer) getParent();
898        parent.removeView(this);
899        mDragController.removeDropTarget((DropTarget) this);
900        clearFocus();
901        mFolderIcon.requestFocus();
902
903        if (mRearrangeOnClose) {
904            setupContentForNumItems(getItemCount());
905            mRearrangeOnClose = false;
906        }
907        if (getItemCount() <= 1) {
908            if (!mDragInProgress && !mSuppressFolderDeletion) {
909                replaceFolderWithFinalItem();
910            } else if (mDragInProgress) {
911                mDeleteFolderOnDropCompleted = true;
912            }
913        }
914        mSuppressFolderDeletion = false;
915    }
916
917    private void replaceFolderWithFinalItem() {
918        ItemInfo finalItem = null;
919
920        if (getItemCount() == 1) {
921            finalItem = mInfo.contents.get(0);
922        }
923
924        // Remove the folder completely
925        CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screen);
926        cellLayout.removeView(mFolderIcon);
927        if (mFolderIcon instanceof DropTarget) {
928            mDragController.removeDropTarget((DropTarget) mFolderIcon);
929        }
930        mLauncher.removeFolder(mInfo);
931
932        if (finalItem != null) {
933            LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
934                    mInfo.screen, mInfo.cellX, mInfo.cellY);
935        }
936        LauncherModel.deleteItemFromDatabase(mLauncher, mInfo);
937
938        // Add the last remaining child to the workspace in place of the folder
939        if (finalItem != null) {
940            View child = mLauncher.createShortcut(R.layout.application, cellLayout,
941                    (ShortcutInfo) finalItem);
942
943            mLauncher.getWorkspace().addInScreen(child, mInfo.container, mInfo.screen, mInfo.cellX,
944                    mInfo.cellY, mInfo.spanX, mInfo.spanY);
945        }
946    }
947
948    // This method keeps track of the last item in the folder for the purposes
949    // of keyboard focus
950    private void updateTextViewFocus() {
951        View lastChild = getItemAt(getItemCount() - 1);
952        getItemAt(getItemCount() - 1);
953        if (lastChild != null) {
954            mFolderName.setNextFocusDownId(lastChild.getId());
955            mFolderName.setNextFocusRightId(lastChild.getId());
956            mFolderName.setNextFocusLeftId(lastChild.getId());
957            mFolderName.setNextFocusUpId(lastChild.getId());
958        }
959    }
960
961    public void onDrop(DragObject d) {
962        ShortcutInfo item;
963        if (d.dragInfo instanceof ApplicationInfo) {
964            // Came from all apps -- make a copy
965            item = ((ApplicationInfo) d.dragInfo).makeShortcut();
966            item.spanX = 1;
967            item.spanY = 1;
968        } else {
969            item = (ShortcutInfo) d.dragInfo;
970        }
971        // Dragged from self onto self, currently this is the only path possible, however
972        // we keep this as a distinct code path.
973        if (item == mCurrentDragInfo) {
974            ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag();
975            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams();
976            si.cellX = lp.cellX = mEmptyCell[0];
977            si.cellX = lp.cellY = mEmptyCell[1];
978            mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true);
979            if (d.dragView.hasDrawn()) {
980                mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView);
981            } else {
982                mCurrentDragView.setVisibility(VISIBLE);
983            }
984            mItemsInvalidated = true;
985            setupContentDimensions(getItemCount());
986            mSuppressOnAdd = true;
987        }
988        mInfo.add(item);
989    }
990
991    public void onAdd(ShortcutInfo item) {
992        mItemsInvalidated = true;
993        // If the item was dropped onto this open folder, we have done the work associated
994        // with adding the item to the folder, as indicated by mSuppressOnAdd being set
995        if (mSuppressOnAdd) return;
996        if (!findAndSetEmptyCells(item)) {
997            // The current layout is full, can we expand it?
998            setupContentForNumItems(getItemCount() + 1);
999            findAndSetEmptyCells(item);
1000        }
1001        createAndAddShortcut(item);
1002        LauncherModel.addOrMoveItemInDatabase(
1003                mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
1004    }
1005
1006    public void onRemove(ShortcutInfo item) {
1007        mItemsInvalidated = true;
1008        // If this item is being dragged from this open folder, we have already handled
1009        // the work associated with removing the item, so we don't have to do anything here.
1010        if (item == mCurrentDragInfo) return;
1011        View v = getViewForInfo(item);
1012        mContent.removeView(v);
1013        if (mState == STATE_ANIMATING) {
1014            mRearrangeOnClose = true;
1015        } else {
1016            setupContentForNumItems(getItemCount());
1017        }
1018        if (getItemCount() <= 1) {
1019            replaceFolderWithFinalItem();
1020        }
1021    }
1022
1023    private View getViewForInfo(ShortcutInfo item) {
1024        for (int j = 0; j < mContent.getCountY(); j++) {
1025            for (int i = 0; i < mContent.getCountX(); i++) {
1026                View v = mContent.getChildAt(i, j);
1027                if (v.getTag() == item) {
1028                    return v;
1029                }
1030            }
1031        }
1032        return null;
1033    }
1034
1035    public void onItemsChanged() {
1036        updateTextViewFocus();
1037    }
1038
1039    public void onTitleChanged(CharSequence title) {
1040    }
1041
1042    public ArrayList<View> getItemsInReadingOrder() {
1043        return getItemsInReadingOrder(true);
1044    }
1045
1046    public ArrayList<View> getItemsInReadingOrder(boolean includeCurrentDragItem) {
1047        if (mItemsInvalidated) {
1048            mItemsInReadingOrder.clear();
1049            for (int j = 0; j < mContent.getCountY(); j++) {
1050                for (int i = 0; i < mContent.getCountX(); i++) {
1051                    View v = mContent.getChildAt(i, j);
1052                    if (v != null) {
1053                        ShortcutInfo info = (ShortcutInfo) v.getTag();
1054                        if (info != mCurrentDragInfo || includeCurrentDragItem) {
1055                            mItemsInReadingOrder.add(v);
1056                        }
1057                    }
1058                }
1059            }
1060            mItemsInvalidated = false;
1061        }
1062        return mItemsInReadingOrder;
1063    }
1064
1065    public void getLocationInDragLayer(int[] loc) {
1066        mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
1067    }
1068
1069    public void onFocusChange(View v, boolean hasFocus) {
1070        if (v == mFolderName && hasFocus) {
1071            startEditingFolderName();
1072        }
1073    }
1074}
1075