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