AnimatedAdapter.java revision 07fe8df87bde8732398434e55cce366a8528c181
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.animation.Animator;
21import android.animation.Animator.AnimatorListener;
22import android.animation.AnimatorListenerAdapter;
23import android.animation.AnimatorSet;
24import android.animation.ObjectAnimator;
25import android.content.Context;
26import android.database.Cursor;
27import android.os.Bundle;
28import android.os.Handler;
29import android.util.SparseArray;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.SimpleCursorAdapter;
34
35import com.android.mail.R;
36import com.android.mail.browse.ConversationCursor;
37import com.android.mail.browse.ConversationItemView;
38import com.android.mail.browse.ConversationItemViewCoordinates;
39import com.android.mail.browse.SwipeableConversationItemView;
40import com.android.mail.providers.Account;
41import com.android.mail.providers.AccountObserver;
42import com.android.mail.providers.Conversation;
43import com.android.mail.providers.Folder;
44import com.android.mail.providers.UIProvider;
45import com.android.mail.providers.UIProvider.ConversationListIcon;
46import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
47import com.android.mail.utils.LogTag;
48import com.android.mail.utils.LogUtils;
49import com.google.common.collect.Maps;
50
51import java.util.ArrayList;
52import java.util.Collection;
53import java.util.HashMap;
54import java.util.HashSet;
55import java.util.Iterator;
56import java.util.List;
57import java.util.Map.Entry;
58
59public class AnimatedAdapter extends SimpleCursorAdapter {
60    private static int sDismissAllShortDelay = -1;
61    private static int sDismissAllLongDelay = -1;
62    private static final String LAST_DELETING_ITEMS = "last_deleting_items";
63    private static final String LEAVE_BEHIND_ITEM_DATA = "leave_behind_item_data";
64    private static final String LEAVE_BEHIND_ITEM_ID = "leave_behind_item_id";
65    private final static int TYPE_VIEW_CONVERSATION = 0;
66    private final static int TYPE_VIEW_FOOTER = 1;
67    private final static int TYPE_VIEW_DONT_RECYCLE = -1;
68    private final HashSet<Long> mDeletingItems = new HashSet<Long>();
69    private final ArrayList<Long> mLastDeletingItems = new ArrayList<Long>();
70    private final HashSet<Long> mUndoingItems = new HashSet<Long>();
71    private final HashSet<Long> mSwipeDeletingItems = new HashSet<Long>();
72    private final HashSet<Long> mSwipeUndoingItems = new HashSet<Long>();
73    private final HashMap<Long, SwipeableConversationItemView> mAnimatingViews =
74            new HashMap<Long, SwipeableConversationItemView>();
75    private final HashMap<Long, LeaveBehindItem> mFadeLeaveBehindItems =
76            new HashMap<Long, LeaveBehindItem>();
77    /** The current account */
78    private Account mAccount;
79    private final Context mContext;
80    private final ConversationSelectionSet mBatchConversations;
81    private Runnable mCountDown;
82    private final Handler mHandler;
83    protected long mLastLeaveBehind = -1;
84
85    private final AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
86
87        @Override
88        public void onAnimationStart(Animator animation) {
89            if (!mUndoingItems.isEmpty()) {
90                mDeletingItems.clear();
91                mLastDeletingItems.clear();
92                mSwipeDeletingItems.clear();
93            }
94        }
95
96        @Override
97        public void onAnimationEnd(Animator animation) {
98            Object obj;
99            if (animation instanceof AnimatorSet) {
100                AnimatorSet set = (AnimatorSet) animation;
101                obj = ((ObjectAnimator) set.getChildAnimations().get(0)).getTarget();
102            } else {
103                obj = ((ObjectAnimator) animation).getTarget();
104            }
105            updateAnimatingConversationItems(obj, mSwipeDeletingItems);
106            updateAnimatingConversationItems(obj, mDeletingItems);
107            updateAnimatingConversationItems(obj, mSwipeUndoingItems);
108            updateAnimatingConversationItems(obj, mUndoingItems);
109            if (hasFadeLeaveBehinds() && obj instanceof LeaveBehindItem) {
110                LeaveBehindItem objItem = (LeaveBehindItem) obj;
111                clearLeaveBehind(objItem.getConversationId());
112                objItem.commit();
113                if (!hasFadeLeaveBehinds()) {
114                    // Cancel any existing animations on the remaining leave behind
115                    // item and start fading in text immediately.
116                    LeaveBehindItem item = getLastLeaveBehindItem();
117                    if (item != null) {
118                        boolean cancelled = item.cancelFadeInTextAnimationIfNotStarted();
119                        if (cancelled) {
120                            item.startFadeInTextAnimation(0 /* delay start */);
121                        }
122                    }
123                }
124                // The view types have changed, since the animating views are gone.
125                notifyDataSetChanged();
126            }
127
128            if (!isAnimating()) {
129                mActivity.onAnimationEnd(AnimatedAdapter.this);
130            }
131        }
132
133    };
134
135    /**
136     * The next action to perform. Do not read or write this. All accesses should
137     * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the
138     * previous action, if any.
139     */
140    private ListItemsRemovedListener mPendingDestruction;
141    /**
142     * A destructive action that refreshes the list and performs no other action.
143     */
144    private final ListItemsRemovedListener mRefreshAction = new ListItemsRemovedListener() {
145        @Override
146        public void onListItemsRemoved() {
147            notifyDataSetChanged();
148        }
149    };
150
151    public interface Listener {
152        void onAnimationEnd(AnimatedAdapter adapter);
153    }
154
155    private View mFooter;
156    private boolean mShowFooter;
157    private Folder mFolder;
158    private final SwipeableListView mListView;
159    private boolean mSwipeEnabled;
160    private final HashMap<Long, LeaveBehindItem> mLeaveBehindItems = Maps.newHashMap();
161    /** True if priority inbox markers are enabled, false otherwise. */
162    private boolean mPriorityMarkersEnabled;
163    private final ControllableActivity mActivity;
164    private final AccountObserver mAccountListener = new AccountObserver() {
165        @Override
166        public void onChanged(Account newAccount) {
167            setAccount(newAccount);
168            notifyDataSetChanged();
169        }
170    };
171
172    private final List<ConversationSpecialItemView> mSpecialViews;
173    private final SparseArray<ConversationSpecialItemView> mSpecialViewPositions;
174
175    private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache =
176            new SparseArray<ConversationItemViewCoordinates>();
177
178    private final void setAccount(Account newAccount) {
179        mAccount = newAccount;
180        mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
181        mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO);
182    }
183
184    /**
185     * Used only for debugging.
186     */
187    private static final String LOG_TAG = LogTag.getLogTag();
188    private static final int INCREASE_WAIT_COUNT = 2;
189
190    public AnimatedAdapter(Context context, ConversationCursor cursor,
191            ConversationSelectionSet batch, ControllableActivity activity,
192            SwipeableListView listView) {
193        this(context, cursor, batch, activity, listView, null);
194    }
195
196    public AnimatedAdapter(Context context, ConversationCursor cursor,
197            ConversationSelectionSet batch, ControllableActivity activity,
198            SwipeableListView listView, final List<ConversationSpecialItemView> specialViews) {
199        super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
200        mContext = context;
201        mBatchConversations = batch;
202        setAccount(mAccountListener.initialize(activity.getAccountController()));
203        mActivity = activity;
204        mShowFooter = false;
205        mListView = listView;
206        mHandler = new Handler();
207        if (sDismissAllShortDelay == -1) {
208            sDismissAllShortDelay =
209                    context.getResources()
210                        .getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
211            sDismissAllLongDelay =
212                    context.getResources()
213                        .getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
214        }
215        mSpecialViews =
216                specialViews == null ? new ArrayList<ConversationSpecialItemView>(0)
217                        : new ArrayList<ConversationSpecialItemView>(specialViews);
218        mSpecialViewPositions = new SparseArray<ConversationSpecialItemView>(mSpecialViews.size());
219
220        for (final ConversationSpecialItemView view : mSpecialViews) {
221            view.setAdapter(this);
222        }
223
224        updateSpecialViews();
225    }
226
227    public void cancelDismissCounter() {
228        cancelLeaveBehindFadeInAnimation();
229        mHandler.removeCallbacks(mCountDown);
230    }
231
232    public void startDismissCounter() {
233        if (mLeaveBehindItems.size() > INCREASE_WAIT_COUNT) {
234            mHandler.postDelayed(mCountDown, sDismissAllLongDelay);
235        } else {
236            mHandler.postDelayed(mCountDown, sDismissAllShortDelay);
237        }
238    }
239
240    public final void destroy() {
241        // Set a null cursor in the adapter
242        swapCursor(null);
243        mAccountListener.unregisterAndDestroy();
244    }
245
246    @Override
247    public int getCount() {
248        // mSpecialViewPositions only contains the views that are currently being displayed
249        final int specialViewCount = mSpecialViewPositions.size();
250
251        final int count = super.getCount() + specialViewCount;
252        return mShowFooter ? count + 1 : count;
253    }
254
255    /**
256     * Add a conversation to the undo set, but only if its deletion is still cached. If the
257     * deletion has already been written through and the cursor doesn't have it anymore, we can't
258     * handle it here, and should instead rely on the cursor refresh to restore the item.
259     * @param item id for the conversation that is being undeleted.
260     * @return true if the conversation is still cached and therefore we will handle the undo.
261     */
262    private boolean addUndoingItem(final long item) {
263        if (getConversationCursor().getUnderlyingPosition(item) >= 0) {
264            mUndoingItems.add(item);
265            return true;
266        }
267        return false;
268    }
269
270    public void setUndo(boolean undo) {
271        if (undo) {
272            boolean itemAdded = false;
273            if (!mLastDeletingItems.isEmpty()) {
274                for (Long item : mLastDeletingItems) {
275                    itemAdded |= addUndoingItem(item);
276                }
277                mLastDeletingItems.clear();
278            }
279            if (mLastLeaveBehind != -1) {
280                itemAdded |= addUndoingItem(mLastLeaveBehind);
281                mLastLeaveBehind = -1;
282            }
283            // Start animation, only if we're handling the undo.
284            if (itemAdded) {
285                notifyDataSetChanged();
286                performAndSetNextAction(mRefreshAction);
287            }
288        }
289    }
290
291    public void setSwipeUndo(boolean undo) {
292        if (undo) {
293            if (!mLastDeletingItems.isEmpty()) {
294                mSwipeUndoingItems.addAll(mLastDeletingItems);
295                mLastDeletingItems.clear();
296            }
297            if (mLastLeaveBehind != -1) {
298                mSwipeUndoingItems.add(mLastLeaveBehind);
299                mLastLeaveBehind = -1;
300            }
301            // Start animation
302            notifyDataSetChanged();
303            performAndSetNextAction(mRefreshAction);
304        }
305    }
306
307    public View createConversationItemView(SwipeableConversationItemView view, Context context,
308            Conversation conv) {
309        if (view == null) {
310            view = new SwipeableConversationItemView(context, mAccount.name);
311        }
312        view.bind(conv, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
313                mSwipeEnabled, mPriorityMarkersEnabled, this);
314        return view;
315    }
316
317    @Override
318    public boolean hasStableIds() {
319        return true;
320    }
321
322    @Override
323    public int getViewTypeCount() {
324        // TYPE_VIEW_CONVERSATION, TYPE_VIEW_DELETING, TYPE_VIEW_UNDOING, and
325        // TYPE_VIEW_FOOTER, TYPE_VIEW_LEAVEBEHIND.
326        return 5;
327    }
328
329    @Override
330    public int getItemViewType(int position) {
331        // Try to recycle views.
332        if (mShowFooter && position == getCount() - 1) {
333            return TYPE_VIEW_FOOTER;
334        } else if (hasLeaveBehinds() || isAnimating()) {
335            // Setting as type -1 means the recycler won't take this view and
336            // return it in get view. This is a bit of a "hammer" in that it
337            // won't let even safe views be recycled here,
338            // but its safer and cheaper than trying to determine individual
339            // types. In a future release, use position/id map to try to make
340            // this cleaner / faster to determine if the view is animating.
341            return TYPE_VIEW_DONT_RECYCLE;
342        } else if (mSpecialViewPositions.get(position) != null) {
343            // Don't recycle the special views
344            return TYPE_VIEW_DONT_RECYCLE;
345        }
346        return TYPE_VIEW_CONVERSATION;
347    }
348
349    /**
350     * Deletes the selected conversations from the conversation list view with a
351     * translation and then a shrink. These conversations <b>must</b> have their
352     * {@link Conversation#position} set to the position of these conversations
353     * among the list. This will only remove the element from the list. The job
354     * of deleting the actual element is left to the the listener. This listener
355     * will be called when the animations are complete and is required to delete
356     * the conversation.
357     * @param conversations
358     * @param listener
359     */
360    public void swipeDelete(Collection<Conversation> conversations,
361            ListItemsRemovedListener listener) {
362        delete(conversations, listener, mSwipeDeletingItems);
363    }
364
365
366    /**
367     * Deletes the selected conversations from the conversation list view by
368     * shrinking them away. These conversations <b>must</b> have their
369     * {@link Conversation#position} set to the position of these conversations
370     * among the list. This will only remove the element from the list. The job
371     * of deleting the actual element is left to the the listener. This listener
372     * will be called when the animations are complete and is required to delete
373     * the conversation.
374     * @param conversations
375     * @param listener
376     */
377    public void delete(Collection<Conversation> conversations, ListItemsRemovedListener listener) {
378        delete(conversations, listener, mDeletingItems);
379    }
380
381    private void delete(Collection<Conversation> conversations, ListItemsRemovedListener listener,
382            HashSet<Long> list) {
383        // Clear out any remaining items and add the new ones
384        mLastDeletingItems.clear();
385        // Since we are deleting new items, clear any remaining undo items
386        mUndoingItems.clear();
387
388        final int startPosition = mListView.getFirstVisiblePosition();
389        final int endPosition = mListView.getLastVisiblePosition();
390
391        // Only animate visible items
392        for (Conversation c: conversations) {
393            if (c.position >= startPosition && c.position <= endPosition) {
394                mLastDeletingItems.add(c.id);
395                list.add(c.id);
396            }
397        }
398
399        if (list.isEmpty()) {
400            // If we have no deleted items on screen, skip the animation
401            listener.onListItemsRemoved();
402        } else {
403            performAndSetNextAction(listener);
404        }
405        notifyDataSetChanged();
406    }
407
408    @Override
409    public View getView(int position, View convertView, ViewGroup parent) {
410        if (mShowFooter && position == getCount() - 1) {
411            return mFooter;
412        }
413
414        // Check if this is a special view
415        final View specialView = (View) mSpecialViewPositions.get(position);
416        if (specialView != null) {
417            return specialView;
418        }
419
420        ConversationCursor cursor = (ConversationCursor) getItem(position);
421        final Conversation conv = cursor.getConversation();
422
423        // Notify the provider of this change in the position of Conversation cursor
424        cursor.notifyUIPositionChange();
425
426        if (isPositionUndoing(conv.id)) {
427            return getUndoingView(position - getPositionOffset(position), conv, parent,
428                    false /* don't show swipe background */);
429        } if (isPositionUndoingSwipe(conv.id)) {
430            return getUndoingView(position - getPositionOffset(position), conv, parent,
431                    true /* show swipe background */);
432        } else if (isPositionDeleting(conv.id)) {
433            return getDeletingView(position - getPositionOffset(position), conv, parent, false);
434        } else if (isPositionSwipeDeleting(conv.id)) {
435            return getDeletingView(position - getPositionOffset(position), conv, parent, true);
436        }
437        if (hasFadeLeaveBehinds()) {
438            if(isPositionFadeLeaveBehind(conv)) {
439                LeaveBehindItem fade  = getFadeLeaveBehindItem(position, conv);
440                fade.startShrinkAnimation(mActivity.getViewMode(), mAnimatorListener);
441                return fade;
442            }
443        }
444        if (hasLeaveBehinds()) {
445            if (isPositionLeaveBehind(conv)) {
446                final LeaveBehindItem fadeIn = getLeaveBehindItem(conv);
447                if (conv.id == mLastLeaveBehind) {
448                    // If it looks like the person is doing a lot of rapid
449                    // swipes, wait patiently before animating
450                    if (mLeaveBehindItems.size() > INCREASE_WAIT_COUNT) {
451                        if (fadeIn.isAnimating()) {
452                            fadeIn.increaseFadeInDelay(sDismissAllLongDelay);
453                        } else {
454                            fadeIn.startFadeInTextAnimation(sDismissAllLongDelay);
455                        }
456                    } else {
457                        // Otherwise, assume they are just doing 1 and wait less time
458                        fadeIn.startFadeInTextAnimation(sDismissAllShortDelay /* delay start */);
459                    }
460                }
461                return fadeIn;
462            }
463        }
464
465        if (convertView != null && !(convertView instanceof SwipeableConversationItemView)) {
466            LogUtils.w(LOG_TAG, "Incorrect convert view received; nulling it out");
467            convertView = newView(mContext, cursor, parent);
468        } else if (convertView != null) {
469            ((SwipeableConversationItemView) convertView).reset();
470        }
471        return createConversationItemView((SwipeableConversationItemView) convertView, mContext,
472                conv);
473    }
474
475    private boolean hasLeaveBehinds() {
476        return !mLeaveBehindItems.isEmpty();
477    }
478
479    private boolean hasFadeLeaveBehinds() {
480        return !mFadeLeaveBehindItems.isEmpty();
481    }
482
483    public LeaveBehindItem setupLeaveBehind(Conversation target, ToastBarOperation undoOp,
484            int deletedRow, int viewHeight) {
485        cancelLeaveBehindFadeInAnimation();
486        mLastLeaveBehind = target.id;
487        fadeOutLeaveBehindItems();
488
489        final LeaveBehindItem leaveBehind = (LeaveBehindItem) LayoutInflater.from(mContext)
490                .inflate(R.layout.swipe_leavebehind, mListView, false);
491        leaveBehind.bind(deletedRow, mAccount, this, undoOp, target, mFolder, viewHeight);
492        mLeaveBehindItems.put(target.id, leaveBehind);
493        mLastDeletingItems.add(target.id);
494        return leaveBehind;
495    }
496
497    public void fadeOutSpecificLeaveBehindItem(long id) {
498        if (mLastLeaveBehind == id) {
499            mLastLeaveBehind = -1;
500        }
501        startFadeOutLeaveBehindItemsAnimations();
502    }
503
504    // This should kick off a timer such that there is a minimum time each item
505    // shows up before being dismissed. That way if the user is swiping away
506    // items in rapid succession, their finger position is maintained.
507    public void fadeOutLeaveBehindItems() {
508        if (mCountDown == null) {
509            mCountDown = new Runnable() {
510                @Override
511                public void run() {
512                    startFadeOutLeaveBehindItemsAnimations();
513                }
514            };
515        } else {
516            mHandler.removeCallbacks(mCountDown);
517        }
518        // Clear all the text since these are no longer clickable
519        Iterator<Entry<Long, LeaveBehindItem>> i = mLeaveBehindItems.entrySet().iterator();
520        LeaveBehindItem item;
521        while (i.hasNext()) {
522            item = i.next().getValue();
523            Conversation conv = item.getData();
524            if (mLastLeaveBehind == -1 || conv.id != mLastLeaveBehind) {
525                item.cancelFadeInTextAnimation();
526                item.makeInert();
527            }
528        }
529        startDismissCounter();
530    }
531
532    protected void startFadeOutLeaveBehindItemsAnimations() {
533        final int startPosition = mListView.getFirstVisiblePosition();
534        final int endPosition = mListView.getLastVisiblePosition();
535
536        if (hasLeaveBehinds()) {
537            // If the item is visible, fade it out. Otherwise, just remove
538            // it.
539            Iterator<Entry<Long, LeaveBehindItem>> i = mLeaveBehindItems.entrySet().iterator();
540            LeaveBehindItem item;
541            while (i.hasNext()) {
542                item = i.next().getValue();
543                Conversation conv = item.getData();
544                if (mLastLeaveBehind == -1 || conv.id != mLastLeaveBehind) {
545                    if (conv.position >= startPosition && conv.position <= endPosition) {
546                        mFadeLeaveBehindItems.put(conv.id, item);
547                    } else {
548                        item.commit();
549                    }
550                    i.remove();
551                }
552            }
553            cancelLeaveBehindFadeInAnimation();
554        }
555        if (!mLastDeletingItems.isEmpty()) {
556            mLastDeletingItems.clear();
557        }
558        notifyDataSetChanged();
559    }
560
561    private void cancelLeaveBehindFadeInAnimation() {
562        LeaveBehindItem leaveBehind = getLastLeaveBehindItem();
563        if (leaveBehind != null) {
564            leaveBehind.cancelFadeInTextAnimation();
565        }
566    }
567
568    public SparseArray<ConversationItemViewCoordinates> getCoordinatesCache() {
569        return mCoordinatesCache;
570    }
571
572    public SwipeableListView getListView() {
573        return mListView;
574    }
575
576    public void commitLeaveBehindItems(boolean animate) {
577        // Remove any previously existing leave behinds.
578        boolean changed = false;
579        if (hasLeaveBehinds()) {
580            for (LeaveBehindItem item : mLeaveBehindItems.values()) {
581                if (animate) {
582                    mFadeLeaveBehindItems.put(item.getConversationId(), item);
583                } else {
584                    item.commit();
585                }
586            }
587            changed = true;
588            mLastLeaveBehind = -1;
589            mLeaveBehindItems.clear();
590        }
591        if (hasFadeLeaveBehinds() && !animate) {
592            // Find any fading leave behind items and commit them all, too.
593            for (LeaveBehindItem item : mFadeLeaveBehindItems.values()) {
594                item.commit();
595            }
596            mFadeLeaveBehindItems.clear();
597            changed = true;
598        }
599        if (!mLastDeletingItems.isEmpty()) {
600            mLastDeletingItems.clear();
601            changed = true;
602        }
603        if (changed) {
604            notifyDataSetChanged();
605        }
606    }
607
608    private LeaveBehindItem getLeaveBehindItem(Conversation target) {
609        return mLeaveBehindItems.get(target.id);
610    }
611
612    private LeaveBehindItem getFadeLeaveBehindItem(int position, Conversation target) {
613        return mFadeLeaveBehindItems.get(target.id);
614    }
615
616    @Override
617    public long getItemId(int position) {
618        if (mShowFooter && position == getCount() - 1
619                || mSpecialViewPositions.get(position) != null) {
620            return -1;
621        }
622        final int cursorPos = position - getPositionOffset(position);
623        // advance the cursor to the right position and read the cached conversation, if present
624        //
625        // (no need to have CursorAdapter check mDataValid because in our incarnation without
626        // FLAG_REGISTER_CONTENT_OBSERVER, mDataValid is effectively identical to mCursor being
627        // non-null)
628        final ConversationCursor cursor = getConversationCursor();
629        if (cursor != null && cursor.moveToPosition(cursorPos)) {
630            final Conversation conv = cursor.getCachedConversation();
631            if (conv != null) {
632                return conv.id;
633            }
634        }
635        return super.getItemId(cursorPos);
636    }
637
638    /**
639     * @param position The position in the cursor
640     */
641    private View getDeletingView(int position, Conversation conversation, ViewGroup parent,
642            boolean swipe) {
643        conversation.position = position;
644        SwipeableConversationItemView deletingView = mAnimatingViews.get(conversation.id);
645        if (deletingView == null) {
646            // The undo animation consists of fading in the conversation that
647            // had been destroyed.
648            deletingView = newConversationItemView(position, parent, conversation);
649            deletingView.startDeleteAnimation(mAnimatorListener, swipe);
650        }
651        return deletingView;
652    }
653
654    /**
655     * @param position The position in the cursor
656     */
657    private View getUndoingView(int position, Conversation conv, ViewGroup parent, boolean swipe) {
658        conv.position = position;
659        SwipeableConversationItemView undoView = mAnimatingViews.get(conv.id);
660        if (undoView == null) {
661            // The undo animation consists of fading in the conversation that
662            // had been destroyed.
663            undoView = newConversationItemView(position, parent, conv);
664            undoView.startUndoAnimation(mAnimatorListener, swipe);
665        }
666        return undoView;
667    }
668
669    @Override
670    public View newView(Context context, Cursor cursor, ViewGroup parent) {
671        SwipeableConversationItemView view = new SwipeableConversationItemView(context,
672                mAccount.name);
673        return view;
674    }
675
676    @Override
677    public void bindView(View view, Context context, Cursor cursor) {
678        // no-op. we only get here from newConversationItemView(), which will immediately bind
679        // on its own.
680    }
681
682    private SwipeableConversationItemView newConversationItemView(int position, ViewGroup parent,
683            Conversation conversation) {
684        SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView(
685                position, null, parent);
686        view.reset();
687        view.bind(conversation, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
688                mSwipeEnabled, mPriorityMarkersEnabled, this);
689        mAnimatingViews.put(conversation.id, view);
690        return view;
691    }
692
693    private int getCheckboxSetting() {
694        return mAccount != null ? mAccount.settings.convListIcon :
695            ConversationListIcon.DEFAULT;
696    }
697
698
699    @Override
700    public Object getItem(int position) {
701        if (mShowFooter && position == getCount() - 1) {
702            return mFooter;
703        } else if (mSpecialViewPositions.get(position) != null) {
704            return mSpecialViewPositions.get(position);
705        }
706        return super.getItem(position - getPositionOffset(position));
707    }
708
709    private boolean isPositionDeleting(long id) {
710        return mDeletingItems.contains(id);
711    }
712
713    private boolean isPositionSwipeDeleting(long id) {
714        return mSwipeDeletingItems.contains(id);
715    }
716
717    private boolean isPositionUndoing(long id) {
718        return mUndoingItems.contains(id);
719    }
720
721    private boolean isPositionUndoingSwipe(long id) {
722        return mSwipeUndoingItems.contains(id);
723    }
724
725    private boolean isPositionLeaveBehind(Conversation conv) {
726        return hasLeaveBehinds()
727                && mLeaveBehindItems.containsKey(conv.id)
728                && conv.isMostlyDead();
729    }
730
731    private boolean isPositionFadeLeaveBehind(Conversation conv) {
732        return hasFadeLeaveBehinds()
733                && mFadeLeaveBehindItems.containsKey(conv.id)
734                && conv.isMostlyDead();
735    }
736
737    /**
738     * Performs the pending destruction, if any and assigns the next pending action.
739     * @param next The next action that is to be performed, possibly null (if no next action is
740     * needed).
741     */
742    private final void performAndSetNextAction(ListItemsRemovedListener next) {
743        if (mPendingDestruction != null) {
744            mPendingDestruction.onListItemsRemoved();
745        }
746        mPendingDestruction = next;
747    }
748
749    private void updateAnimatingConversationItems(Object obj, HashSet<Long> items) {
750        if (!items.isEmpty()) {
751            if (obj instanceof ConversationItemView) {
752                final ConversationItemView target = (ConversationItemView) obj;
753                final long id = target.getConversation().id;
754                items.remove(id);
755                mAnimatingViews.remove(id);
756                if (items.isEmpty()) {
757                    performAndSetNextAction(null);
758                    notifyDataSetChanged();
759                }
760            }
761        }
762    }
763
764    @Override
765    public boolean areAllItemsEnabled() {
766        // The animating positions are not enabled.
767        return false;
768    }
769
770    @Override
771    public boolean isEnabled(final int position) {
772        if (mSpecialViewPositions.get(position) != null) {
773            // This is a special view
774            return false;
775        }
776
777        return !isPositionDeleting(position) && !isPositionUndoing(position);
778    }
779
780    public void showFooter() {
781        setFooterVisibility(true);
782    }
783
784    public void hideFooter() {
785        setFooterVisibility(false);
786    }
787
788    public void setFooterVisibility(boolean show) {
789        if (mShowFooter != show) {
790            mShowFooter = show;
791            notifyDataSetChanged();
792        }
793    }
794
795    public void addFooter(View footerView) {
796        mFooter = footerView;
797    }
798
799    public void setFolder(Folder folder) {
800        mFolder = folder;
801    }
802
803    public void clearLeaveBehind(long itemId) {
804        if (hasLeaveBehinds() && mLeaveBehindItems.containsKey(itemId)) {
805            mLeaveBehindItems.remove(itemId);
806        } else if (hasFadeLeaveBehinds()) {
807            mFadeLeaveBehindItems.remove(itemId);
808        } else {
809            LogUtils.d(LOG_TAG, "Trying to clear a non-existant leave behind");
810        }
811        if (mLastLeaveBehind == itemId) {
812            mLastLeaveBehind = -1;
813        }
814    }
815
816    public void onSaveInstanceState(Bundle outState) {
817        long[] lastDeleting = new long[mLastDeletingItems.size()];
818        for (int i = 0; i < lastDeleting.length; i++) {
819            lastDeleting[i] = mLastDeletingItems.get(i);
820        }
821        outState.putLongArray(LAST_DELETING_ITEMS, lastDeleting);
822        if (hasLeaveBehinds()) {
823            if (mLastLeaveBehind != -1) {
824                outState.putParcelable(LEAVE_BEHIND_ITEM_DATA,
825                        mLeaveBehindItems.get(mLastLeaveBehind).getLeaveBehindData());
826                outState.putLong(LEAVE_BEHIND_ITEM_ID, mLastLeaveBehind);
827            }
828            for (LeaveBehindItem item : mLeaveBehindItems.values()) {
829                if (mLastLeaveBehind == -1 || item.getData().id != mLastLeaveBehind) {
830                    item.commit();
831                }
832            }
833        }
834    }
835
836    public void onRestoreInstanceState(Bundle outState) {
837        if (outState.containsKey(LAST_DELETING_ITEMS)) {
838            final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS);
839            for (int i = 0; i < lastDeleting.length; i++) {
840                mLastDeletingItems.add(lastDeleting[i]);
841            }
842        }
843        if (outState.containsKey(LEAVE_BEHIND_ITEM_DATA)) {
844            LeaveBehindData left =
845                    (LeaveBehindData) outState.getParcelable(LEAVE_BEHIND_ITEM_DATA);
846            mLeaveBehindItems.put(outState.getLong(LEAVE_BEHIND_ITEM_ID),
847                    setupLeaveBehind(left.data, left.op, left.data.position, left.height));
848        }
849    }
850
851    /**
852     * Return if the adapter is in the process of animating anything.
853     */
854    public boolean isAnimating() {
855        return !mUndoingItems.isEmpty()
856                || !mSwipeUndoingItems.isEmpty()
857                || hasFadeLeaveBehinds()
858                || !mDeletingItems.isEmpty()
859                || !mSwipeDeletingItems.isEmpty();
860    }
861
862    @Override
863    public String toString() {
864        final String s = super.toString();
865        final StringBuilder sb = new StringBuilder("{");
866        sb.append(super.toString());
867        sb.append(" mUndoingItems=");
868        sb.append(mUndoingItems);
869        sb.append(" mSwipeUndoingItems=");
870        sb.append(mSwipeUndoingItems);
871        sb.append(" mDeletingItems=");
872        sb.append(mDeletingItems);
873        sb.append(" mSwipeDeletingItems=");
874        sb.append(mSwipeDeletingItems);
875        sb.append(" mLeaveBehindItems=");
876        sb.append(mLeaveBehindItems);
877        sb.append(" mFadeLeaveBehindItems=");
878        sb.append(mFadeLeaveBehindItems);
879        sb.append(" mLastDeletingItems=");
880        sb.append(mLastDeletingItems);
881        sb.append("}");
882        return sb.toString();
883    }
884
885    /**
886     * Get the ConversationCursor associated with this adapter.
887     */
888    public ConversationCursor getConversationCursor() {
889        return (ConversationCursor) getCursor();
890    }
891
892    /**
893     * Get the currently visible leave behind item.
894     */
895    public LeaveBehindItem getLastLeaveBehindItem() {
896        if (mLastLeaveBehind != -1) {
897            return mLeaveBehindItems.get(mLastLeaveBehind);
898        }
899        return null;
900    }
901
902    /**
903     * Cancel fading out the text displayed in the leave behind item currently
904     * shown.
905     */
906    public void cancelFadeOutLastLeaveBehindItemText() {
907        LeaveBehindItem item = getLastLeaveBehindItem();
908        if (item != null) {
909            item.cancelFadeOutText();
910        }
911    }
912
913    private void updateSpecialViews() {
914        mSpecialViewPositions.clear();
915
916        for (int i = 0; i < mSpecialViews.size(); i++) {
917            final ConversationSpecialItemView specialView = mSpecialViews.get(i);
918            specialView.onUpdate(mAccount.name, mFolder, getConversationCursor());
919
920            if (specialView.getShouldDisplayInList()) {
921                int position = specialView.getPosition();
922
923                // insert the special view into the position, but if there is
924                // already an item occupying that position, move that item back
925                // one position, and repeat
926                ConversationSpecialItemView insert = specialView;
927                while (insert != null) {
928                    final ConversationSpecialItemView kickedOut = mSpecialViewPositions.get(
929                            position);
930                    mSpecialViewPositions.put(position, insert);
931                    insert = kickedOut;
932                    position++;
933                }
934            }
935        }
936    }
937
938    @Override
939    public void notifyDataSetChanged() {
940        updateSpecialViews();
941        super.notifyDataSetChanged();
942    }
943
944    @Override
945    public void changeCursor(final Cursor cursor) {
946        super.changeCursor(cursor);
947        updateSpecialViews();
948    }
949
950    @Override
951    public void changeCursorAndColumns(final Cursor c, final String[] from, final int[] to) {
952        super.changeCursorAndColumns(c, from, to);
953        updateSpecialViews();
954    }
955
956    @Override
957    public Cursor swapCursor(final Cursor c) {
958        final Cursor oldCursor = super.swapCursor(c);
959        updateSpecialViews();
960
961        return oldCursor;
962    }
963
964    /**
965     * Gets the offset for the given position in the underlying cursor, based on any special views
966     * that may be above it.
967     */
968    public int getPositionOffset(final int position) {
969        int offset = 0;
970
971        for (int i = 0; i < mSpecialViewPositions.size(); i++) {
972            final int key = mSpecialViewPositions.keyAt(i);
973            final ConversationSpecialItemView specialView = mSpecialViewPositions.get(key);
974            if (key <= position) {
975                offset++;
976            }
977        }
978
979        return offset;
980    }
981
982    public void cleanup() {
983        for (final ConversationSpecialItemView view : mSpecialViews) {
984            view.cleanup();
985        }
986    }
987
988    public void onConversationSelected() {
989        for (int i = 0; i < mSpecialViews.size(); i++) {
990            final ConversationSpecialItemView specialView = mSpecialViews.get(i);
991            specialView.onConversationSelected();
992        }
993    }
994}
995