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