AnimatedAdapter.java revision e7a4b6898f085782cf4c5f5c8c4091a245465f0d
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.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.database.Cursor;
25import android.os.Bundle;
26import android.os.Handler;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30import android.widget.SimpleCursorAdapter;
31
32import com.android.mail.R;
33import com.android.mail.browse.ConversationCursor;
34import com.android.mail.browse.ConversationItemView;
35import com.android.mail.browse.ConversationItemViewCoordinates;
36import com.android.mail.browse.SwipeableConversationItemView;
37import com.android.mail.providers.Account;
38import com.android.mail.providers.AccountObserver;
39import com.android.mail.providers.Conversation;
40import com.android.mail.providers.Folder;
41import com.android.mail.providers.UIProvider;
42import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
43import com.android.mail.utils.LogTag;
44import com.android.mail.utils.LogUtils;
45import com.google.common.collect.Maps;
46
47import java.util.ArrayList;
48import java.util.Collection;
49import java.util.HashMap;
50import java.util.HashSet;
51import java.util.Iterator;
52import java.util.Map.Entry;
53
54public class AnimatedAdapter extends SimpleCursorAdapter implements
55        android.animation.Animator.AnimatorListener {
56    private static int sDismissAllShortDelay = -1;
57    private static int sDismissAllLongDelay = -1;
58    private static final String LAST_DELETING_ITEMS = "last_deleting_items";
59    private static final String LEAVE_BEHIND_ITEM_DATA = "leave_behind_item_data";
60    private static final String LEAVE_BEHIND_ITEM_ID = "leave_behind_item_id";
61    private final static int TYPE_VIEW_CONVERSATION = 0;
62    private final static int TYPE_VIEW_FOOTER = 1;
63    private final static int TYPE_VIEW_DONT_RECYCLE = -1;
64    private final HashSet<Long> mDeletingItems = new HashSet<Long>();
65    private final ArrayList<Long> mLastDeletingItems = new ArrayList<Long>();
66    private final HashSet<Long> mUndoingItems = new HashSet<Long>();
67    private final HashSet<Long> mSwipeDeletingItems = new HashSet<Long>();
68    private final HashSet<Long> mSwipeUndoingItems = new HashSet<Long>();
69    private final HashMap<Long, SwipeableConversationItemView> mAnimatingViews =
70            new HashMap<Long, SwipeableConversationItemView>();
71    private final HashMap<Long, LeaveBehindItem> mFadeLeaveBehindItems =
72            new HashMap<Long, LeaveBehindItem>();
73    /** The current account */
74    private Account mAccount;
75    private final Context mContext;
76    private final ConversationSelectionSet mBatchConversations;
77    private Runnable mCountDown;
78    private Handler mHandler;
79    protected long mLastLeaveBehind = -1;
80
81    /**
82     * The next action to perform. Do not read or write this. All accesses should
83     * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the
84     * previous action, if any.
85     */
86    private ListItemsRemovedListener mPendingDestruction;
87    /**
88     * A destructive action that refreshes the list and performs no other action.
89     */
90    private final ListItemsRemovedListener mRefreshAction = new ListItemsRemovedListener() {
91        @Override
92        public void onListItemsRemoved() {
93            notifyDataSetChanged();
94        }
95    };
96
97    public interface Listener {
98        void onAnimationEnd(AnimatedAdapter adapter);
99    }
100
101    private View mFooter;
102    private boolean mShowFooter;
103    private Folder mFolder;
104    private final SwipeableListView mListView;
105    private boolean mSwipeEnabled;
106    private final HashMap<Long, LeaveBehindItem> mLeaveBehindItems = Maps.newHashMap();
107    /** True if priority inbox markers are enabled, false otherwise. */
108    private boolean mPriorityMarkersEnabled;
109    private ControllableActivity mActivity;
110    private final AccountObserver mAccountListener = new AccountObserver() {
111        @Override
112        public void onChanged(Account newAccount) {
113            setAccount(newAccount);
114            notifyDataSetChanged();
115        }
116    };
117
118    private final void setAccount(Account newAccount) {
119        mAccount = newAccount;
120        mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
121        mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO);
122    }
123
124    /**
125     * Used only for debugging.
126     */
127    private static final String LOG_TAG = LogTag.getLogTag();
128    private static final int INCREASE_WAIT_COUNT = 2;
129
130    public AnimatedAdapter(Context context, ConversationCursor cursor,
131            ConversationSelectionSet batch, ControllableActivity activity,
132            SwipeableListView listView) {
133        super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
134        mContext = context;
135        mBatchConversations = batch;
136        setAccount(mAccountListener.initialize(activity.getAccountController()));
137        mActivity = activity;
138        mShowFooter = false;
139        mListView = listView;
140        mHandler = new Handler();
141        if (sDismissAllShortDelay == -1) {
142            sDismissAllShortDelay =
143                    context.getResources()
144                        .getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
145            sDismissAllLongDelay =
146                    context.getResources()
147                        .getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
148        }
149    }
150
151    public void cancelDismissCounter() {
152        cancelLeaveBehindFadeInAnimation();
153        mHandler.removeCallbacks(mCountDown);
154    }
155
156    public void startDismissCounter() {
157        if (mLeaveBehindItems.size() > INCREASE_WAIT_COUNT) {
158            mHandler.postDelayed(mCountDown, sDismissAllLongDelay);
159        } else {
160            mHandler.postDelayed(mCountDown, sDismissAllShortDelay);
161        }
162    }
163
164    public final void destroy() {
165        // Set a null cursor in the adapter
166        swapCursor(null);
167        mAccountListener.unregisterAndDestroy();
168    }
169
170    @Override
171    public int getCount() {
172        final int count = super.getCount();
173        return mShowFooter ? count + 1 : count;
174    }
175
176    public void setUndo(boolean undo) {
177        if (undo) {
178            if (!mLastDeletingItems.isEmpty()) {
179                mUndoingItems.addAll(mLastDeletingItems);
180                mLastDeletingItems.clear();
181            }
182            if (mLastLeaveBehind != -1) {
183                mUndoingItems.add(mLastLeaveBehind);
184                mLastLeaveBehind = -1;
185            }
186            // Start animation
187            notifyDataSetChanged();
188            performAndSetNextAction(mRefreshAction);
189        }
190    }
191
192    public void setSwipeUndo(boolean undo) {
193        if (undo) {
194            if (!mLastDeletingItems.isEmpty()) {
195                mSwipeUndoingItems.addAll(mLastDeletingItems);
196                mLastDeletingItems.clear();
197            }
198            if (mLastLeaveBehind != -1) {
199                mSwipeUndoingItems.add(mLastLeaveBehind);
200                mLastLeaveBehind = -1;
201            }
202            // Start animation
203            notifyDataSetChanged();
204            performAndSetNextAction(mRefreshAction);
205        }
206    }
207
208    public View createConversationItemView(SwipeableConversationItemView view, Context context,
209            Conversation conv) {
210        if (view == null) {
211            view = new SwipeableConversationItemView(context, mAccount.name);
212        }
213        view.bind(conv, mActivity, mBatchConversations, mFolder,
214                mAccount != null ? !mAccount.settings.showCheckboxes : false, mSwipeEnabled,
215                mPriorityMarkersEnabled, this);
216        return view;
217    }
218
219    @Override
220    public boolean hasStableIds() {
221        return true;
222    }
223
224    @Override
225    public int getViewTypeCount() {
226        // TYPE_VIEW_CONVERSATION, TYPE_VIEW_DELETING, TYPE_VIEW_UNDOING, and
227        // TYPE_VIEW_FOOTER, TYPE_VIEW_LEAVEBEHIND.
228        return 5;
229    }
230
231    @Override
232    public int getItemViewType(int position) {
233        // Try to recycle views.
234        if (mShowFooter && position == super.getCount()) {
235            return TYPE_VIEW_FOOTER;
236        } else if (hasLeaveBehinds() || isAnimating()) {
237            // Setting as type -1 means the recycler won't take this view and
238            // return it in get view. This is a bit of a "hammer" in that it
239            // won't let even safe views be recycled here,
240            // but its safer and cheaper than trying to determine individual
241            // types. In a future release, use position/id map to try to make
242            // this cleaner / faster to determine if the view is animating.
243            return TYPE_VIEW_DONT_RECYCLE;
244        }
245        return TYPE_VIEW_CONVERSATION;
246    }
247
248    /**
249     * Deletes the selected conversations from the conversation list view with a
250     * translation and then a shrink. These conversations <b>must</b> have their
251     * {@link Conversation#position} set to the position of these conversations
252     * among the list. This will only remove the element from the list. The job
253     * of deleting the actual element is left to the the listener. This listener
254     * will be called when the animations are complete and is required to delete
255     * the conversation.
256     * @param conversations
257     * @param listener
258     */
259    public void swipeDelete(Collection<Conversation> conversations,
260            ListItemsRemovedListener listener) {
261        delete(conversations, listener, mSwipeDeletingItems);
262    }
263
264
265    /**
266     * Deletes the selected conversations from the conversation list view by
267     * shrinking them away. These conversations <b>must</b> have their
268     * {@link Conversation#position} set to the position of these conversations
269     * among the list. This will only remove the element from the list. The job
270     * of deleting the actual element is left to the the listener. This listener
271     * will be called when the animations are complete and is required to delete
272     * the conversation.
273     * @param conversations
274     * @param listener
275     */
276    public void delete(Collection<Conversation> conversations, ListItemsRemovedListener listener) {
277        delete(conversations, listener, mDeletingItems);
278    }
279
280    private void delete(Collection<Conversation> conversations, ListItemsRemovedListener listener,
281            HashSet<Long> list) {
282        // Clear out any remaining items and add the new ones
283        mLastDeletingItems.clear();
284        // Since we are deleting new items, clear any remaining undo items
285        mUndoingItems.clear();
286
287        final int startPosition = mListView.getFirstVisiblePosition();
288        final int endPosition = mListView.getLastVisiblePosition();
289
290        // Only animate visible items
291        for (Conversation c: conversations) {
292            if (c.position >= startPosition && c.position <= endPosition) {
293                mLastDeletingItems.add(c.id);
294                list.add(c.id);
295            }
296        }
297
298        if (list.isEmpty()) {
299            // If we have no deleted items on screen, skip the animation
300            listener.onListItemsRemoved();
301        } else {
302            performAndSetNextAction(listener);
303        }
304        notifyDataSetChanged();
305    }
306
307    @Override
308    public View getView(int position, View convertView, ViewGroup parent) {
309        if (mShowFooter && position == super.getCount()) {
310            return mFooter;
311        }
312        ConversationCursor cursor = (ConversationCursor) getItem(position);
313        Conversation conv = new Conversation(cursor);
314        if (isPositionUndoing(conv.id)) {
315            return getUndoingView(position, conv, parent, false /* don't show swipe background */);
316        } if (isPositionUndoingSwipe(conv.id)) {
317            return getUndoingView(position, conv, parent, true /* show swipe background */);
318        } else if (isPositionDeleting(conv.id)) {
319            return getDeletingView(position, conv, parent, false);
320        } else if (isPositionSwipeDeleting(conv.id)) {
321            return getDeletingView(position, conv, parent, true);
322        }
323        if (hasFadeLeaveBehinds()) {
324            if(isPositionFadeLeaveBehind(conv)) {
325                LeaveBehindItem fade  = getFadeLeaveBehindItem(position, conv);
326                fade.startShrinkAnimation(mActivity.getViewMode(), this);
327                return fade;
328            }
329        }
330        if (hasLeaveBehinds()) {
331            if (isPositionLeaveBehind(conv)) {
332                LeaveBehindItem fadeIn = getLeaveBehindItem(conv);
333                if (conv.id == mLastLeaveBehind) {
334                    // If it looks like the person is doing a lot of rapid
335                    // swipes, wait patiently before animating
336                    if (mLeaveBehindItems.size() > INCREASE_WAIT_COUNT) {
337                        if (fadeIn.isAnimating()) {
338                            fadeIn.increaseFadeInDelay(sDismissAllLongDelay);
339                        } else {
340                            fadeIn.startFadeInTextAnimation(sDismissAllLongDelay);
341                        }
342                    } else {
343                        // Otherwise, assume they are just doing 1 and wait less time
344                        fadeIn.startFadeInTextAnimation(sDismissAllShortDelay /* delay start */);
345                    }
346                }
347                return fadeIn;
348            }
349        }
350        if (convertView != null && !(convertView instanceof SwipeableConversationItemView)) {
351            LogUtils.w(LOG_TAG, "Incorrect convert view received; nulling it out");
352            convertView = newView(mContext, cursor, parent);
353        } else if (convertView != null) {
354            ((SwipeableConversationItemView) convertView).reset();
355        }
356        return createConversationItemView((SwipeableConversationItemView) convertView, mContext,
357                conv);
358    }
359
360    private boolean hasLeaveBehinds() {
361        return !mLeaveBehindItems.isEmpty();
362    }
363
364    private boolean hasFadeLeaveBehinds() {
365        return !mFadeLeaveBehindItems.isEmpty();
366    }
367
368    public LeaveBehindItem setupLeaveBehind(Conversation target, ToastBarOperation undoOp,
369            int deletedRow) {
370        cancelLeaveBehindFadeInAnimation();
371        mLastLeaveBehind = target.id;
372        fadeOutLeaveBehindItems();
373        boolean isWide = ConversationItemViewCoordinates.isWideMode(ConversationItemViewCoordinates
374                .getMode(mContext, mActivity.getViewMode()));
375        LeaveBehindItem leaveBehind = (LeaveBehindItem) LayoutInflater.from(mContext).inflate(
376                isWide? R.layout.swipe_leavebehind_wide : R.layout.swipe_leavebehind, null);
377        leaveBehind.bindOperations(deletedRow, mAccount, this, undoOp, target, mFolder);
378        mLeaveBehindItems.put(target.id, leaveBehind);
379        mLastDeletingItems.add(target.id);
380        return leaveBehind;
381    }
382
383    public void fadeOutSpecificLeaveBehindItem(long id) {
384        if (mLastLeaveBehind == id) {
385            mLastLeaveBehind = -1;
386        }
387        startFadeOutLeaveBehindItemsAnimations();
388    }
389
390    // This should kick off a timer such that there is a minimum time each item
391    // shows up before being dismissed. That way if the user is swiping away
392    // items in rapid succession, their finger position is maintained.
393    public void fadeOutLeaveBehindItems() {
394        if (mCountDown == null) {
395            mCountDown = new Runnable() {
396                @Override
397                public void run() {
398                    startFadeOutLeaveBehindItemsAnimations();
399                }
400            };
401        } else {
402            mHandler.removeCallbacks(mCountDown);
403        }
404        // Clear all the text since these are no longer clickable
405        Iterator<Entry<Long, LeaveBehindItem>> i = mLeaveBehindItems.entrySet().iterator();
406        LeaveBehindItem item;
407        while (i.hasNext()) {
408            item = i.next().getValue();
409            Conversation conv = item.getData();
410            if (mLastLeaveBehind == -1 || conv.id != mLastLeaveBehind) {
411                item.cancelFadeInTextAnimation();
412                item.makeInert();
413            }
414        }
415        startDismissCounter();
416    }
417
418    protected void startFadeOutLeaveBehindItemsAnimations() {
419        final int startPosition = mListView.getFirstVisiblePosition();
420        final int endPosition = mListView.getLastVisiblePosition();
421
422        if (hasLeaveBehinds()) {
423            // If the item is visible, fade it out. Otherwise, just remove
424            // it.
425            Iterator<Entry<Long, LeaveBehindItem>> i = mLeaveBehindItems.entrySet().iterator();
426            LeaveBehindItem item;
427            while (i.hasNext()) {
428                item = i.next().getValue();
429                Conversation conv = item.getData();
430                if (mLastLeaveBehind == -1 || conv.id != mLastLeaveBehind) {
431                    if (conv.position >= startPosition && conv.position <= endPosition) {
432                        mFadeLeaveBehindItems.put(conv.id, item);
433                    } else {
434                        item.commit();
435                    }
436                    i.remove();
437                }
438            }
439            cancelLeaveBehindFadeInAnimation();
440        }
441        if (!mLastDeletingItems.isEmpty()) {
442            mLastDeletingItems.clear();
443        }
444        notifyDataSetChanged();
445    }
446
447    private void cancelLeaveBehindFadeInAnimation() {
448        LeaveBehindItem leaveBehind = getLastLeaveBehindItem();
449        if (leaveBehind != null) {
450            leaveBehind.cancelFadeInTextAnimation();
451        }
452    }
453
454    public SwipeableListView getListView() {
455        return mListView;
456    }
457
458    public void commitLeaveBehindItems(boolean animate) {
459        // Remove any previously existing leave behinds.
460        boolean changed = false;
461        if (hasLeaveBehinds()) {
462            for (LeaveBehindItem item : mLeaveBehindItems.values()) {
463                if (animate) {
464                    mFadeLeaveBehindItems.put(item.getConversationId(), item);
465                } else {
466                    item.commit();
467                }
468            }
469            changed = true;
470            mLastLeaveBehind = -1;
471            mLeaveBehindItems.clear();
472        }
473        if (hasFadeLeaveBehinds() && !animate) {
474            // Find any fading leave behind items and commit them all, too.
475            for (LeaveBehindItem item : mFadeLeaveBehindItems.values()) {
476                item.commit();
477            }
478            mFadeLeaveBehindItems.clear();
479            changed = true;
480        }
481        if (!mLastDeletingItems.isEmpty()) {
482            mLastDeletingItems.clear();
483            changed = true;
484        }
485        if (changed) {
486            notifyDataSetChanged();
487        }
488    }
489
490    private LeaveBehindItem getLeaveBehindItem(Conversation target) {
491        return mLeaveBehindItems.get(target.id);
492    }
493
494    private LeaveBehindItem getFadeLeaveBehindItem(int position, Conversation target) {
495        return mFadeLeaveBehindItems.get(target.id);
496    }
497
498    @Override
499    public long getItemId(int position) {
500        if (mShowFooter && position == super.getCount()) {
501            return -1;
502        }
503        return super.getItemId(position);
504    }
505
506    private View getDeletingView(int position, Conversation conversation, ViewGroup parent,
507            boolean swipe) {
508        conversation.position = position;
509        SwipeableConversationItemView deletingView = mAnimatingViews.get(conversation.id);
510        if (deletingView == null) {
511            // The undo animation consists of fading in the conversation that
512            // had been destroyed.
513            deletingView = newConversationItemView(position, parent, conversation);
514            deletingView.startDeleteAnimation(this, swipe);
515        }
516        return deletingView;
517    }
518
519    private View getUndoingView(int position, Conversation conv, ViewGroup parent, boolean swipe) {
520        conv.position = position;
521        SwipeableConversationItemView undoView = mAnimatingViews.get(conv.id);
522        if (undoView == null) {
523            // The undo animation consists of fading in the conversation that
524            // had been destroyed.
525            undoView = newConversationItemView(position, parent, conv);
526            undoView.startUndoAnimation(mActivity.getViewMode(), this, swipe);
527        }
528        return undoView;
529    }
530
531    @Override
532    public View newView(Context context, Cursor cursor, ViewGroup parent) {
533        SwipeableConversationItemView view = new SwipeableConversationItemView(context,
534                mAccount.name);
535        return view;
536    }
537
538    @Override
539    public void bindView(View view, Context context, Cursor cursor) {
540        if (! (view instanceof SwipeableConversationItemView)) {
541            return;
542        }
543        ((SwipeableConversationItemView) view).bind(cursor, mActivity, mBatchConversations, mFolder,
544                mAccount != null ? !mAccount.settings.showCheckboxes : false,
545                        mSwipeEnabled, mPriorityMarkersEnabled, this);
546    }
547
548    private SwipeableConversationItemView newConversationItemView(int position, ViewGroup parent,
549            Conversation conversation) {
550        SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView(
551                position, null, parent);
552        view.reset();
553        view.bind(conversation, mActivity, mBatchConversations, mFolder,
554                mAccount != null ? !mAccount.settings.showCheckboxes : false, mSwipeEnabled,
555                mPriorityMarkersEnabled, this);
556        mAnimatingViews.put(conversation.id, view);
557        return view;
558    }
559
560    @Override
561    public Object getItem(int position) {
562        if (mShowFooter && position == super.getCount()) {
563            return mFooter;
564        }
565        return super.getItem(position);
566    }
567
568    private boolean isPositionDeleting(long id) {
569        return mDeletingItems.contains(id);
570    }
571
572    private boolean isPositionSwipeDeleting(long id) {
573        return mSwipeDeletingItems.contains(id);
574    }
575
576    private boolean isPositionUndoing(long id) {
577        return mUndoingItems.contains(id);
578    }
579
580    private boolean isPositionUndoingSwipe(long id) {
581        return mSwipeUndoingItems.contains(id);
582    }
583
584    private boolean isPositionLeaveBehind(Conversation conv) {
585        return hasLeaveBehinds()
586                && mLeaveBehindItems.containsKey(conv.id)
587                && conv.isMostlyDead();
588    }
589
590    private boolean isPositionFadeLeaveBehind(Conversation conv) {
591        return hasFadeLeaveBehinds()
592                && mFadeLeaveBehindItems.containsKey(conv.id)
593                && conv.isMostlyDead();
594    }
595
596    @Override
597    public void onAnimationStart(Animator animation) {
598        if (!mUndoingItems.isEmpty()) {
599            mDeletingItems.clear();
600            mLastDeletingItems.clear();
601            mSwipeDeletingItems.clear();
602        } else {
603            mUndoingItems.clear();
604        }
605    }
606
607    /**
608     * Performs the pending destruction, if any and assigns the next pending action.
609     * @param next The next action that is to be performed, possibly null (if no next action is
610     * needed).
611     */
612    private final void performAndSetNextAction(ListItemsRemovedListener next) {
613        if (mPendingDestruction != null) {
614            mPendingDestruction.onListItemsRemoved();
615        }
616        mPendingDestruction = next;
617    }
618
619    @Override
620    public void onAnimationEnd(Animator animation) {
621        Object obj;
622        if (animation instanceof AnimatorSet) {
623            AnimatorSet set = (AnimatorSet) animation;
624            obj = ((ObjectAnimator) set.getChildAnimations().get(0)).getTarget();
625        } else {
626            obj = ((ObjectAnimator) animation).getTarget();
627        }
628        updateAnimatingConversationItems(obj, mSwipeDeletingItems);
629        updateAnimatingConversationItems(obj, mDeletingItems);
630        updateAnimatingConversationItems(obj, mSwipeUndoingItems);
631        updateAnimatingConversationItems(obj, mUndoingItems);
632        if (hasFadeLeaveBehinds() && obj instanceof LeaveBehindItem) {
633            LeaveBehindItem objItem = (LeaveBehindItem) obj;
634            clearLeaveBehind(objItem.getConversationId());
635            objItem.commit();
636            if (!hasFadeLeaveBehinds()) {
637                // Cancel any existing animations on the remaining leave behind
638                // item and start fading in text immediately.
639                LeaveBehindItem item = getLastLeaveBehindItem();
640                if (item != null) {
641                    boolean cancelled = item.cancelFadeInTextAnimationIfNotStarted();
642                    if (cancelled) {
643                        item.startFadeInTextAnimation(0 /* delay start */);
644                    }
645                }
646            }
647            // The view types have changed, since the animating views are gone.
648            notifyDataSetChanged();
649        }
650
651        if (!isAnimating()) {
652            mActivity.onAnimationEnd(this);
653        }
654    }
655
656    private void updateAnimatingConversationItems(Object obj, HashSet<Long> items) {
657        if (!items.isEmpty()) {
658            if (obj instanceof ConversationItemView) {
659                final ConversationItemView target = (ConversationItemView) obj;
660                final long id = target.getConversation().id;
661                items.remove(id);
662                mAnimatingViews.remove(id);
663                if (items.isEmpty()) {
664                    performAndSetNextAction(null);
665                    notifyDataSetChanged();
666                }
667            }
668        }
669    }
670
671    @Override
672    public boolean areAllItemsEnabled() {
673        // The animating positions are not enabled.
674        return false;
675    }
676
677    @Override
678    public boolean isEnabled(int position) {
679        return !isPositionDeleting(position) && !isPositionUndoing(position);
680    }
681
682    @Override
683    public void onAnimationCancel(Animator animation) {
684        onAnimationEnd(animation);
685    }
686
687    @Override
688    public void onAnimationRepeat(Animator animation) {
689    }
690
691    public void showFooter() {
692        setFooterVisibility(true);
693    }
694
695    public void hideFooter() {
696        setFooterVisibility(false);
697    }
698
699    public void setFooterVisibility(boolean show) {
700        if (mShowFooter != show) {
701            mShowFooter = show;
702            notifyDataSetChanged();
703        }
704    }
705
706    public void addFooter(View footerView) {
707        mFooter = footerView;
708    }
709
710    public void setFolder(Folder folder) {
711        mFolder = folder;
712    }
713
714    public void clearLeaveBehind(long itemId) {
715        if (hasLeaveBehinds() && mLeaveBehindItems.containsKey(itemId)) {
716            mLeaveBehindItems.remove(itemId);
717        } else if (hasFadeLeaveBehinds()) {
718            mFadeLeaveBehindItems.remove(itemId);
719        } else {
720            LogUtils.d(LOG_TAG, "Trying to clear a non-existant leave behind");
721        }
722        if (mLastLeaveBehind == itemId) {
723            mLastLeaveBehind = -1;
724        }
725    }
726
727    public void onSaveInstanceState(Bundle outState) {
728        long[] lastDeleting = new long[mLastDeletingItems.size()];
729        for (int i = 0; i < lastDeleting.length; i++) {
730            lastDeleting[i] = mLastDeletingItems.get(i);
731        }
732        outState.putLongArray(LAST_DELETING_ITEMS, lastDeleting);
733        if (hasLeaveBehinds()) {
734            if (mLastLeaveBehind != -1) {
735                outState.putParcelable(LEAVE_BEHIND_ITEM_DATA,
736                        mLeaveBehindItems.get(mLastLeaveBehind).getLeaveBehindData());
737                outState.putLong(LEAVE_BEHIND_ITEM_ID, mLastLeaveBehind);
738            }
739            for (LeaveBehindItem item : mLeaveBehindItems.values()) {
740                if (mLastLeaveBehind == -1 || item.getData().id != mLastLeaveBehind) {
741                    item.commit();
742                }
743            }
744        }
745    }
746
747    public void onRestoreInstanceState(Bundle outState) {
748        if (outState.containsKey(LAST_DELETING_ITEMS)) {
749            final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS);
750            for (int i = 0; i < lastDeleting.length; i++) {
751                mLastDeletingItems.add(lastDeleting[i]);
752            }
753        }
754        if (outState.containsKey(LEAVE_BEHIND_ITEM_DATA)) {
755            LeaveBehindData left =
756                    (LeaveBehindData) outState.getParcelable(LEAVE_BEHIND_ITEM_DATA);
757            mLeaveBehindItems.put(outState.getLong(LEAVE_BEHIND_ITEM_ID),
758                    setupLeaveBehind(left.data, left.op, left.data.position));
759        }
760    }
761
762    /**
763     * Return if the adapter is in the process of animating anything.
764     */
765    public boolean isAnimating() {
766        return !mUndoingItems.isEmpty()
767                || !mSwipeUndoingItems.isEmpty()
768                || hasFadeLeaveBehinds()
769                || !mDeletingItems.isEmpty()
770                || !mSwipeDeletingItems.isEmpty();
771    }
772
773    /**
774     * Get the ConversationCursor associated with this adapter.
775     */
776    public ConversationCursor getConversationCursor() {
777        return (ConversationCursor) getCursor();
778    }
779
780    /**
781     * Get the currently visible leave behind item.
782     */
783    public LeaveBehindItem getLastLeaveBehindItem() {
784        if (mLastLeaveBehind != -1) {
785            return mLeaveBehindItems.get(mLastLeaveBehind);
786        }
787        return null;
788    }
789
790    /**
791     * Cancel fading out the text displayed in the leave behind item currently
792     * shown.
793     */
794    public void cancelFadeOutLastLeaveBehindItemText() {
795        LeaveBehindItem item = getLastLeaveBehindItem();
796        if (item != null) {
797            item.cancelFadeOutText();
798        }
799    }
800}
801