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