Merge "Refresh adapter after sync()"
diff --git a/proguard.flags b/proguard.flags
index c3f7930..8a0a1d0 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -43,4 +43,9 @@
   *** setListLeft(...);
   *** setListAlpha(...);
   *** setConversationLeft(...);
+}
+
+-keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
+  *** setAnimatedHeight(...);
+  *** setItemAlpha(...);
 }
\ No newline at end of file
diff --git a/res/drawable/conversation_read_selector.xml b/res/drawable/conversation_read_selector.xml
index 37f8fee..ebe2537 100644
--- a/res/drawable/conversation_read_selector.xml
+++ b/res/drawable/conversation_read_selector.xml
@@ -22,5 +22,7 @@
           android:drawable="@drawable/list_focused_holo" />
     <item android:state_selected="true"
           android:drawable="@drawable/list_selected_holo" />
+    <item android:state_activated="true"
+          android:drawable="@drawable/list_selected_holo" />
     <item android:drawable="@drawable/list_read_holo" />
 </selector>
diff --git a/res/drawable/conversation_unread_selector.xml b/res/drawable/conversation_unread_selector.xml
index b087ea9..8988638 100644
--- a/res/drawable/conversation_unread_selector.xml
+++ b/res/drawable/conversation_unread_selector.xml
@@ -22,5 +22,7 @@
           android:drawable="@drawable/list_focused_holo" />
     <item android:state_selected="true"
           android:drawable="@drawable/list_selected_holo" />
+    <item android:state_activated="true"
+          android:drawable="@drawable/list_selected_holo" />
     <item android:drawable="@drawable/list_unread_holo" />
 </selector>
diff --git a/res/layout/conversation_list.xml b/res/layout/conversation_list.xml
index 1a76303..3f4044f 100644
--- a/res/layout/conversation_list.xml
+++ b/res/layout/conversation_list.xml
@@ -20,7 +20,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/between_chrome"
     android:layout_width="match_parent"
-    android:layout_height="match_parent" >
+    android:layout_height="match_parent"
+    android:background="@color/conversation_list_background_color" >
 
    <!-- Note: intentionally not called "empty" because we call
         setEmptyView programmatically-->
diff --git a/res/layout/conversation_list_footer_view.xml b/res/layout/conversation_list_footer_view.xml
index a9f7f97..1b50630 100644
--- a/res/layout/conversation_list_footer_view.xml
+++ b/res/layout/conversation_list_footer_view.xml
@@ -21,7 +21,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="horizontal"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:background="@android:color/white">
 
     <LinearLayout android:id="@+id/network_error"
         android:layout_width="match_parent"
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
index 3ed2bc1..6a72d37 100644
--- a/res/values/animation_constants.xml
+++ b/res/values/animation_constants.xml
@@ -24,4 +24,5 @@
     <integer name="fade_duration">250</integer>
     <integer name="dialog_animationDefaultDur">220</integer>
     <integer name="dialog_animationShortDur">150</integer>
+    <integer name="undo_animation_duration">300</integer>
 </resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 78dfdac..63211f6 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -35,6 +35,7 @@
     <color name="light_text_color">#ff666666</color>
     <color name="default_folder_background_color">#f1f5ec</color>
     <color name="default_folder_foreground_color">#888888</color>
+    <color name="conversation_list_background_color">#bbbbbb</color>
 
     <!-- Compose colors -->
     <color name="compose_label_text">#aaaaaa</color>
diff --git a/res/values/constants.xml b/res/values/constants.xml
index 6421109..e00c550 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -41,6 +41,7 @@
     <integer name="conversation_view_weight">1</integer>
     <!-- Max unread count to show for a folder -->
     <integer name="maxUnreadCount">999</integer>
+    <integer name="swipeScrollSlop">2</integer>
    <!-- <integer name="widget_refresh_delay_ms">4000</integer>-->
     <integer name="widget_folder_refresh_delay_ms">500</integer>
     <item type="id" name="folder"/>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4ab697b..47e6c01 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -190,6 +190,8 @@
 
     <style name="ConversationListFade" parent="@android:style/Widget.Holo.Light.ListView">
         <item name="android:cacheColorHint">@android:color/transparent</item>
+        <item name="android:divider">#dddddd</item>
+        <item name="android:dividerHeight">1dip</item>
     </style>
 
     <style name="StarStyle">
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index c83a85a..7dd476a 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -19,6 +19,10 @@
 
 import com.google.common.annotations.VisibleForTesting;
 
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.Animator.AnimatorListener;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
@@ -48,6 +52,9 @@
 import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Checkable;
 import android.widget.ListView;
 
 import com.android.mail.R;
@@ -108,6 +115,7 @@
     private static int TOUCH_SLOP;
     private static int sDateBackgroundHeight;
     private static int sStandardScaledDimen;
+    private static int sUndoAnimationDuration;
     private static CharacterStyle sLightTextStyle;
     private static CharacterStyle sNormalTextStyle;
 
@@ -136,16 +144,15 @@
 
     private final Context mContext;
 
-    private String mAccount;
-    private ConversationItemViewModel mHeader;
+    public ConversationItemViewModel mHeader;
     private ViewMode mViewMode;
     private boolean mDownEvent;
     private boolean mChecked = false;
-    private static int sFadedColor = -1;
     private static int sFadedActivatedColor = -1;
     private ConversationSelectionSet mSelectedConversationSet;
     private Folder mDisplayedFolder;
     private boolean mPriorityMarkersEnabled;
+    private int mAnimatedHeight = -1;
     private static Bitmap MORE_FOLDERS;
 
     static {
@@ -290,7 +297,6 @@
         mContext = context.getApplicationContext();
         mTabletDevice = Utils.useTabletUI(mContext);
 
-        mAccount = account;
         Resources res = mContext.getResources();
 
         if (CHECKMARK_OFF == null) {
@@ -341,6 +347,7 @@
             TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop);
             sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
             sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
+            sUndoAnimationDuration = res.getInteger(R.integer.undo_animation_duration);
 
             // Initialize static color.
             sNormalTextStyle = new StyleSpan(Typeface.NORMAL);
@@ -348,17 +355,19 @@
         }
     }
 
-    public void bind(Cursor cursor, String account, ViewMode viewMode,
-            ConversationSelectionSet set, Folder folder) {
-        mAccount = account;
+    public void bind(Cursor cursor, ViewMode viewMode, ConversationSelectionSet set,
+            Folder folder) {
         mViewMode = viewMode;
-        mHeader = ConversationItemViewModel.forCursor(account, cursor);
+        mHeader = ConversationItemViewModel.forCursor(cursor);
         mSelectedConversationSet = set;
         mDisplayedFolder = folder;
         setContentDescription(mHeader.getContentDescription(mContext));
         requestLayout();
     }
 
+    /**
+     * Get the Conversation object associated with this view.
+     */
     public Conversation getConversation() {
         return mHeader.conversation;
     }
@@ -755,10 +764,14 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int width = measureWidth(widthMeasureSpec);
-        int height = measureHeight(heightMeasureSpec,
-                ConversationItemViewCoordinates.getMode(mContext, mViewMode));
-        setMeasuredDimension(width, height);
+        if (mAnimatedHeight == -1) {
+            int width = measureWidth(widthMeasureSpec);
+            int height = measureHeight(heightMeasureSpec,
+                    ConversationItemViewCoordinates.getMode(mContext, mViewMode));
+            setMeasuredDimension(width, height);
+        } else {
+            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
+        }
     }
 
     /**
@@ -983,7 +996,7 @@
         conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this)
                 : Conversation.NO_POSITION;
         if (mSelectedConversationSet != null) {
-            mSelectedConversationSet.toggle(conv);
+            mSelectedConversationSet.toggle(this, conv);
         }
         // We update the background after the checked state has changed now that
         // we have a selected background asset. Setting the background usually
@@ -993,9 +1006,16 @@
     }
 
     /**
+     * Return if the checkbox for this item is checked.
+     */
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    /**
      * Toggle the star on this view and update the conversation.
      */
-    private void toggleStar() {
+    public void toggleStar() {
         mHeader.starred = !mHeader.starred;
         mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
         postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
@@ -1005,52 +1025,112 @@
         mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
     }
 
-    private boolean touchCheckmark(float x, float y) {
+    private boolean isTouchInCheckmark(float x, float y) {
         // Everything before senders and include a touch slop.
         return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP;
     }
 
-    private boolean touchStar(float x, float y) {
+    private boolean isTouchInStar(float x, float y) {
         // Everything after the star and include a touch slop.
         return x > mCoordinates.starX - TOUCH_SLOP;
     }
 
+    /**
+     * ConversationItemView is given the first chance to handle touch events.
+     */
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        boolean handled = false;
+        boolean handled = true;
 
         int x = (int) event.getX();
         int y = (int) event.getY();
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mDownEvent = true;
-                if (touchCheckmark(x, y) || touchStar(x, y)) {
-                    handled = true;
-                }
+                // In order to allow the down event and subsequent move events
+                // to bubble to the swipe handler, we need to return that all
+                // down events are handled.
+                handled = true;
                 break;
-
             case MotionEvent.ACTION_CANCEL:
                 mDownEvent = false;
                 break;
-
             case MotionEvent.ACTION_UP:
                 if (mDownEvent) {
-                    if (touchCheckmark(x, y)) {
+                    // ConversationItemView gets the first chance to handle up
+                    // events if there was a down event and there was no move
+                    // event in between. In this case, ConversationItemView
+                    // received the down event, and then an up event in the
+                    // same location (+/- slop). Treat this as a click on the
+                    // view or on a specific part of the view.
+                    if (isTouchInCheckmark(x, y)) {
                         // Touch on the check mark
                         toggleCheckMark();
-                    } else if (touchStar(x, y)) {
+                    } else if (isTouchInStar(x, y)) {
                         // Touch on the star
                         toggleStar();
+                    } else {
+                        ListView list = (ListView)getParent();
+                        int pos = list.getPositionForView(this);
+                        list.performItemClick(this, pos, mHeader.conversation.id);
                     }
                     handled = true;
+                } else {
+                    // There was no down event that this view was made aware of,
+                    // therefore it cannot handle it.
+                    handled = false;
                 }
                 break;
         }
 
         if (!handled) {
+            // Let View try to handle it as well.
             handled = super.onTouchEvent(event);
         }
 
         return handled;
     }
+
+    /**
+     * Grow the height of the item and fade it in when bringing a conversation
+     * back from a destructive action.
+     *
+     * @param listener
+     */
+    public void startUndoAnimation(final AnimatorListener listener) {
+        setMinimumHeight(140);
+        final int start = 0 ;
+        final int end = 140;
+        ObjectAnimator undoAnimator = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
+        Animator fadeAnimator = ObjectAnimator.ofFloat(this, "itemAlpha", 0, 1.0f);
+        mAnimatedHeight = start;
+        undoAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
+        undoAnimator.setDuration(sUndoAnimationDuration);
+        AnimatorSet transitionSet = new AnimatorSet();
+        transitionSet.playTogether(undoAnimator, fadeAnimator);
+        transitionSet.addListener(listener);
+        transitionSet.start();
+    }
+
+    // Used by animator
+    @SuppressWarnings("unused")
+    public void setItemAlpha(float alpha) {
+        setAlpha(alpha);
+        invalidate();
+    }
+
+    // Used by animator
+    @SuppressWarnings("unused")
+    public void setAnimatedHeight(int height) {
+        mAnimatedHeight = height;
+        requestLayout();
+    }
+
+    /**
+     * Get the current position of this conversation item in the list.
+     */
+    public int getPosition() {
+        return mHeader != null && mHeader.conversation != null ?
+                mHeader.conversation.position : -1;
+    }
 }
diff --git a/src/com/android/mail/browse/ConversationItemViewModel.java b/src/com/android/mail/browse/ConversationItemViewModel.java
index e4490ea..b80490d 100644
--- a/src/com/android/mail/browse/ConversationItemViewModel.java
+++ b/src/com/android/mail/browse/ConversationItemViewModel.java
@@ -130,7 +130,7 @@
         }
     }
 
-    static ConversationItemViewModel forCursor(String account, Cursor cursor) {
+    static ConversationItemViewModel forCursor(Cursor cursor) {
         ConversationItemViewModel header = new ConversationItemViewModel();
         if (cursor != null) {
             header.faded = false;
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 17e3d2f..4731761 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -106,7 +106,7 @@
     @Override
     public void bindView(View view, Context context, Cursor cursor) {
         if (!isPositionAnimating(view) && !isPositionFooter(view)) {
-            ((ConversationItemView) view).bind(cursor, mSelectedAccount.name, mViewMode,
+            ((ConversationItemView) view).bind(cursor, mViewMode,
                     mBatchConversations, mFolder);
         }
     }
@@ -229,14 +229,24 @@
         }
         Conversation conversation = new Conversation((ConversationCursor) getItem(position));
         conversation.position = position;
-        final AnimatingItemView view = (convertView == null) ? new AnimatingItemView(mContext)
-                : (AnimatingItemView) convertView;
-        view.startAnimation(conversation, this, mUndo);
-        return view;
+        if (mUndo) {
+            // The undo animation consists of fading in the conversation that
+            // had been destroyed.
+            ConversationItemView convView = (ConversationItemView) super.getView(position, null,
+                    parent);
+            convView.startUndoAnimation(this);
+            return convView;
+        } else {
+            // Destroying a conversation just shows a blank shrinking item.
+            final AnimatingItemView view = new AnimatingItemView(mContext);
+            view.startAnimation(conversation, this);
+            return view;
+        }
     }
 
     private boolean isPositionAnimating(int position) {
-        return mDeletingItems.contains(position);
+        return mDeletingItems.contains(position)
+                || (mUndo && mLastDeletingItems.contains(position));
     }
 
     private boolean isPositionAnimating(View view) {
@@ -249,22 +259,36 @@
 
     @Override
     public void onAnimationStart(Animator animation) {
-        // TODO Auto-generated method stub
+        if (mUndo) {
+            mDeletingItems.clear();
+            mLastDeletingItems.clear();
+            mUndo = false;
+        }
     }
 
     @Override
     public void onAnimationEnd(Animator animation) {
-        if (!mDeletingItems.isEmpty()) {
-            // See if we have received all the animations we expected; if so,
-            // call the listener and reset it.
-            int position = ((AnimatingItemView)
-                    ((ObjectAnimator) animation).getTarget()).getData().position;
+        if (mUndo && !mLastDeletingItems.isEmpty()) {
+            // See if we have received all the animations we expected; if
+            // so, call the listener and reset it.
+            int position = ((ConversationItemView) ((ObjectAnimator) animation).getTarget())
+                    .getPosition();
+            mLastDeletingItems.remove(position);
+            if (mLastDeletingItems.isEmpty()) {
+                if (mActionCompleteListener != null) {
+                    mActionCompleteListener.onActionComplete();
+                    mActionCompleteListener = null;
+                }
+                mUndo = false;
+            }
+        } else if (!mDeletingItems.isEmpty()) {
+            // See if we have received all the animations we expected; if
+            // so, call the listener and reset it.
+            AnimatingItemView target = ((AnimatingItemView) ((ObjectAnimator) animation)
+                    .getTarget());
+            int position = target.getData().position;
             mDeletingItems.remove(position);
             if (mDeletingItems.isEmpty()) {
-                if (mUndo) {
-                    mLastDeletingItems.clear();
-                    mUndo = false;
-                }
                 if (mActionCompleteListener != null) {
                     mActionCompleteListener.onActionComplete();
                     mActionCompleteListener = null;
@@ -274,6 +298,16 @@
     }
 
     @Override
+    public boolean areAllItemsEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return !isPositionAnimating(position);
+    }
+
+    @Override
     public void onAnimationCancel(Animator animation) {
         onAnimationEnd(animation);
     }
diff --git a/src/com/android/mail/ui/AnimatingItemView.java b/src/com/android/mail/ui/AnimatingItemView.java
index 65fc181..482986e 100644
--- a/src/com/android/mail/ui/AnimatingItemView.java
+++ b/src/com/android/mail/ui/AnimatingItemView.java
@@ -17,35 +17,23 @@
 
 package com.android.mail.ui;
 
-import com.android.mail.R;
-
 import android.animation.Animator.AnimatorListener;
 import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.util.AttributeSet;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.LinearLayout;
 
-import com.android.mail.browse.ConversationItemView;
 import com.android.mail.providers.Conversation;
 
 public class AnimatingItemView extends LinearLayout {
+    public AnimatingItemView(Context context) {
+        super(context);
+    }
+
     private Conversation mData;
     private ObjectAnimator mAnimator;
     private int mAnimatedHeight;
 
-    public AnimatingItemView(Context context) {
-        this(context, null);
-    }
-
-    public AnimatingItemView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public AnimatingItemView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
-
     /**
      * Start the animation on an animating view.
      * @param item the conversation to animate
@@ -53,14 +41,12 @@
      * @param undo true if an operation is being undone. We animate the item away during delete.
      * Undoing populates the item.
      */
-    public void startAnimation(Conversation item, AnimatorListener listener, boolean undo) {
+    public void startAnimation(Conversation item, AnimatorListener listener) {
         mData = item;
         setMinimumHeight(140);
-        final int start = undo ? 0 : 140;
-        final int end = undo ? 140 : 0;
-        if (!undo) {
-            setBackgroundResource(R.drawable.list_activated_holo);
-        }
+        final int start = 140;
+        final int end = 0;
+
         mAnimator = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
         mAnimatedHeight = start;
         mAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
@@ -69,12 +55,6 @@
         mAnimator.start();
     }
 
-    public AnimatingItemView(Context context, Conversation item, AnimatorListener listener,
-            boolean undo) {
-        // The context stays the same when views are recycled.
-        this(context);
-        startAnimation(item, listener, undo);
-    }
 
     public Conversation getData() {
         return mData;
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 9f343b8..1d7983a 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -219,6 +219,7 @@
                 null);
         mListAdapter.addFooter(mFooterView);
         mListView.setAdapter(mListAdapter);
+        mListView.setSelectionSet(mActivity.getSelectedSet());
         mListAdapter.hideFooter();
         mListView.setSwipeCompleteListener(this);
         // Don't need to add ourselves to our own set observer.
@@ -511,6 +512,9 @@
                 break;
         }
         mListAdapter.notifyDataSetChanged();
+        if (!mActivity.getSelectedSet().isEmpty()) {
+            mActivity.getSelectedSet().clear();
+        }
         onUndoAvailable(new UndoOperation(conversations.size(), mSwipeAction));
     }
 
diff --git a/src/com/android/mail/ui/ConversationSelectionSet.java b/src/com/android/mail/ui/ConversationSelectionSet.java
index 4af5497..9f26fe3 100644
--- a/src/com/android/mail/ui/ConversationSelectionSet.java
+++ b/src/com/android/mail/ui/ConversationSelectionSet.java
@@ -23,6 +23,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.mail.browse.ConversationItemView;
 import com.android.mail.providers.Conversation;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -45,8 +46,8 @@
             Parcelable[] conversations = source.readParcelableArray(
                             Conversation.class.getClassLoader());
             for (Parcelable parceled : conversations) {
-                        Conversation conversation = (Conversation) parceled;
-                        result.put(conversation.id, conversation);
+                Conversation conversation = (Conversation) parceled;
+                result.put(conversation.id, conversation);
             }
             return result;
         }
@@ -60,6 +61,9 @@
     private final HashMap<Long, Conversation> mInternalMap =
             new HashMap<Long, Conversation>();
 
+    private final HashMap<Long, ConversationItemView> mInternalViewMap =
+            new HashMap<Long, ConversationItemView>();
+
     @VisibleForTesting
     final ArrayList<ConversationSetObserver> mObservers = new ArrayList<ConversationSetObserver>();
 
@@ -77,6 +81,7 @@
      */
     public synchronized void clear() {
         boolean initiallyNotEmpty = !mInternalMap.isEmpty();
+        mInternalViewMap.clear();
         mInternalMap.clear();
 
         if (mInternalMap.isEmpty() && initiallyNotEmpty) {
@@ -139,10 +144,25 @@
         return mInternalMap.isEmpty();
     }
 
-    /** @see java.util.HashMap#put */
     private synchronized void put(Long id, Conversation info) {
         final boolean initiallyEmpty = mInternalMap.isEmpty();
         mInternalMap.put(id, info);
+        // Fill out the view map with null. The sizes will match, but
+        // we won't have any views available yet to store.
+        mInternalViewMap.put(id, null);
+
+        ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers);
+        dispatchOnChange(observersCopy);
+        if (initiallyEmpty) {
+            dispatchOnBecomeUnempty(observersCopy);
+        }
+    }
+
+    /** @see java.util.HashMap#put */
+    private synchronized void put(Long id, ConversationItemView info) {
+        boolean initiallyEmpty = mInternalMap.isEmpty();
+        mInternalViewMap.put(id, info);
+        mInternalMap.put(id, info.mHeader.conversation);
 
         ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers);
         dispatchOnChange(observersCopy);
@@ -154,6 +174,8 @@
     /** @see java.util.HashMap#remove */
     private synchronized void remove(Long id) {
         final boolean initiallyNotEmpty = !mInternalMap.isEmpty();
+
+        mInternalViewMap.remove(id);
         mInternalMap.remove(id);
 
         ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers);
@@ -186,12 +208,12 @@
      * selected.
      * @param conversation
      */
-    public void toggle(Conversation conversation) {
+    public void toggle(ConversationItemView view, Conversation conversation) {
         long conversationId = conversation.id;
         if (containsKey(conversationId)) {
             remove(conversationId);
         } else {
-            put(conversationId, conversation);
+            put(conversationId, view);
         }
     }
 
@@ -221,4 +243,8 @@
         Conversation[] values = values().toArray(new Conversation[size()]);
         dest.writeParcelableArray(values, flags);
     }
+
+    public Collection<ConversationItemView> views() {
+        return mInternalViewMap.values();
+    }
 }
diff --git a/src/com/android/mail/ui/SwipeHelper.java b/src/com/android/mail/ui/SwipeHelper.java
index 3e5d917..b660804 100644
--- a/src/com/android/mail/ui/SwipeHelper.java
+++ b/src/com/android/mail/ui/SwipeHelper.java
@@ -19,6 +19,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -29,9 +30,13 @@
 import android.view.VelocityTracker;
 import android.view.View;
 
+import com.android.mail.browse.ConversationItemView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
 public class SwipeHelper {
     static final String TAG = "com.android.systemui.SwipeHelper";
-    private static final boolean DEBUG = false;
     private static final boolean DEBUG_INVALIDATE = false;
     private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
     private static final boolean CONSTRAIN_SWIPE = true;
@@ -62,18 +67,22 @@
 
     private float mInitialTouchPos;
     private boolean mDragging;
-    private View mCurrView;
+    private ConversationItemView mCurrView;
     private View mCurrAnimView;
     private boolean mCanCurrViewBeDimissed;
     private float mDensityScale;
+    private float mLastY;
+    private Collection<ConversationItemView> mAssociatedViews;
+    private final float mScrollSlop;
 
     public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
-            float pagingTouchSlop) {
+            float pagingTouchSlop, float scrollSlop) {
         mCallback = callback;
         mSwipeDirection = swipeDirection;
         mVelocityTracker = VelocityTracker.obtain();
         mDensityScale = densityScale;
         mPagingTouchSlop = pagingTouchSlop;
+        mScrollSlop = scrollSlop;
     }
 
     public void setDensityScale(float densityScale) {
@@ -103,6 +112,13 @@
         return anim;
     }
 
+    private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) {
+        ObjectAnimator anim = createTranslationAnimation(v, newPos);
+        anim.setInterpolator(sLinearInterpolator);
+        anim.setDuration(duration);
+        return anim;
+    }
+
     private float getPerpendicularVelocity(VelocityTracker vt) {
         return mSwipeDirection == X ? vt.getYVelocity() :
                 vt.getXVelocity();
@@ -169,11 +185,11 @@
 
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         final int action = ev.getAction();
-
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                mLastY = ev.getY();
                 mDragging = false;
-                mCurrView = mCallback.getChildAtPosition(ev);
+                mCurrView = (ConversationItemView)mCallback.getChildAtPosition(ev);
                 mVelocityTracker.clear();
                 if (mCurrView != null) {
                     mCurrAnimView = mCallback.getChildContentView(mCurrView);
@@ -184,60 +200,65 @@
                 break;
             case MotionEvent.ACTION_MOVE:
                 if (mCurrView != null) {
+                    // Check the movement direction.
+                    if (mLastY >= 0) {
+                        float currY = ev.getY();
+                        if (Math.abs(currY - mLastY) > mScrollSlop) {
+                            mLastY = ev.getY();
+                            return false;
+                        }
+                    }
                     mVelocityTracker.addMovement(ev);
                     float pos = getPos(ev);
                     float delta = pos - mInitialTouchPos;
                     if (Math.abs(delta) > mPagingTouchSlop) {
-                        mCallback.onBeginDrag(mCurrView);
-                        mDragging = true;
-                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
+                        if (mCallback.getSelectionSet().isEmpty()
+                                || (!mCallback.getSelectionSet().isEmpty()
+                                        && mCurrView.isChecked())) {
+                            mCallback.onBeginDrag(mCurrView);
+                            mDragging = true;
+                            mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
+                        }
                     }
                 }
+                mLastY = ev.getY();
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
                 mDragging = false;
                 mCurrView = null;
                 mCurrAnimView = null;
+                mLastY = -1;
                 break;
         }
         return mDragging;
     }
 
+    public void setAssociatedViews(Collection<ConversationItemView> associated) {
+        mAssociatedViews = associated;
+    }
+
+    public void clearAssociatedViews() {
+        mAssociatedViews = null;
+    }
+
     /**
      * @param view The view to be dismissed
-     * @param velocity The desired pixels/second speed at which the view should move
+     * @param velocity The desired pixels/second speed at which the view should
+     *            move
      */
-    public void dismissChild(final View view, float velocity) {
-        final View animView = mCallback.getChildContentView(view);
-        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
-        float newPos;
-
-        if (velocity < 0
-                || (velocity == 0 && getTranslation(animView) < 0)
-                // if we use the Menu to dismiss an item in landscape, animate up
-                || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
-            newPos = -getSize(animView);
-        } else {
-            newPos = getSize(animView);
-        }
-        int duration = MAX_ESCAPE_ANIMATION_DURATION;
-        if (velocity != 0) {
-            duration = Math.min(duration,
-                                (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
-                                        .abs(velocity)));
-        } else {
-            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
-        }
+    private void dismissChild(final View view, float velocity) {
+        final View animView = mCurrView;
+        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
+        float newPos = determinePos(animView, velocity);
+        int duration = determineDuration(animView, newPos, velocity);
 
         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
-        anim.setInterpolator(sLinearInterpolator);
-        anim.setDuration(duration);
+        ObjectAnimator anim = createDismissAnimation(animView, newPos, duration);
         anim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
                 mCallback.onChildDismissed(view);
-                animView.setLayerType(View.LAYER_TYPE_NONE, null);
+                mCurrView.setLayerType(View.LAYER_TYPE_NONE, null);
             }
         });
         anim.addUpdateListener(new AnimatorUpdateListener() {
@@ -251,6 +272,63 @@
         anim.start();
     }
 
+    private void dismissChildren(final Collection<ConversationItemView> views, float velocity) {
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator animation) {
+                mCallback.onChildrenDismissed(views);
+                mCurrView.setLayerType(View.LAYER_TYPE_NONE, null);
+            }
+        };
+        final View animView = mCurrView;
+        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
+        float newPos = determinePos(animView, velocity);
+        int duration = determineDuration(animView, newPos, velocity);
+        ArrayList<Animator> animations = new ArrayList<Animator>();
+        ObjectAnimator anim;
+        for (final ConversationItemView view : views) {
+            view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            anim = createDismissAnimation(view, newPos, duration);
+            anim.addUpdateListener(new AnimatorUpdateListener() {
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
+                        view.setAlpha(getAlphaForOffset(view));
+                    }
+                    invalidateGlobalRegion(view);
+                }
+            });
+            animations.add(anim);
+        }
+        AnimatorSet transitionSet = new AnimatorSet();
+        transitionSet.playTogether(animations);
+        transitionSet.addListener(listener);
+        transitionSet.start();
+    }
+
+    private int determineDuration(View animView, float newPos, float velocity) {
+        int duration = MAX_ESCAPE_ANIMATION_DURATION;
+        if (velocity != 0) {
+            duration = Math
+                    .min(duration,
+                            (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
+                                    .abs(velocity)));
+        } else {
+            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
+        }
+        return duration;
+    }
+
+    private float determinePos(View animView, float velocity) {
+        float newPos = 0;
+        if (velocity < 0 || (velocity == 0 && getTranslation(animView) < 0)
+        // if we use the Menu to dismiss an item in landscape, animate up
+                || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
+            newPos = -getSize(animView);
+        } else {
+            newPos = getSize(animView);
+        }
+        return newPos;
+    }
+
     public void snapChild(final View view, float velocity) {
         final View animView = mCallback.getChildContentView(view);
         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
@@ -272,7 +350,6 @@
         if (!mDragging) {
             return false;
         }
-
         mVelocityTracker.addMovement(ev);
         final int action = ev.getAction();
         switch (action) {
@@ -291,9 +368,21 @@
                             delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
                         }
                     }
-                    setTranslation(mCurrAnimView, delta);
+                    if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+                        for (View v : mAssociatedViews) {
+                            setTranslation(v, delta);
+                        }
+                    } else {
+                        setTranslation(mCurrAnimView, delta);
+                    }
                     if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
-                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
+                        if (mAssociatedViews != null  && mAssociatedViews.size() > 1) {
+                            for (View v : mAssociatedViews) {
+                                v.setAlpha(getAlphaForOffset(mCurrAnimView));
+                            }
+                        } else {
+                            mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
+                        }
                     }
                     invalidateGlobalRegion(mCurrView);
                 }
@@ -318,12 +407,23 @@
                             (childSwipedFastEnough || childSwipedFarEnough);
 
                     if (dismissChild) {
-                        // flingadingy
-                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
+                        if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+                            dismissChildren(mAssociatedViews, childSwipedFastEnough ?
+                                    velocity : 0f);
+                        } else {
+                            dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
+                        }
                     } else {
                         // snappity
                         mCallback.onDragCancelled(mCurrView);
-                        snapChild(mCurrView, velocity);
+
+                        if (mAssociatedViews != null && mAssociatedViews.size() > 1) {
+                            for (View v : mAssociatedViews) {
+                                snapChild(v, velocity);
+                            }
+                        } else {
+                            snapChild(mCurrView, velocity);
+                        }
                     }
                 }
                 break;
@@ -342,6 +442,10 @@
 
         void onChildDismissed(View v);
 
+        void onChildrenDismissed(Collection<ConversationItemView> v);
+
         void onDragCancelled(View v);
+
+        ConversationSelectionSet getSelectionSet();
     }
 }
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index b910c48..fbec5bd 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -28,18 +28,17 @@
 import com.android.mail.browse.ConversationItemView;
 import com.android.mail.providers.Conversation;
 import com.android.mail.ui.SwipeHelper.Callback;
-import com.android.mail.ui.UndoBarView.UndoListener;
 import com.android.mail.utils.LogUtils;
 
 import com.google.common.collect.ImmutableList;
 
+import java.util.ArrayList;
 import java.util.Collection;
 
 public class SwipeableListView extends ListView implements Callback {
     private SwipeHelper mSwipeHelper;
     private SwipeCompleteListener mSwipeCompleteListener;
-    // TODO(mindyp) disable for original droidfood build.
-    private boolean ENABLE_SWIPE = false;
+    private boolean ENABLE_SWIPE = true;
     private ListAdapter mDebugAdapter;
     private int mDebugLastCount;
 
@@ -48,6 +47,8 @@
 
     public static final String LOG_TAG = new LogUtils().getLogTag();
 
+    private ConversationSelectionSet mConvSelectionSet;
+
     public SwipeableListView(Context context) {
         this(context, null);
     }
@@ -59,13 +60,24 @@
     public SwipeableListView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         float densityScale = getResources().getDisplayMetrics().density;
-        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, densityScale);
+        float scrollSlop = context.getResources().getInteger(R.integer.swipeScrollSlop);
+        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, densityScale,
+                scrollSlop);
     }
 
     public void setSwipeCompleteListener(SwipeCompleteListener listener) {
         mSwipeCompleteListener = listener;
     }
 
+    public void setSelectionSet(ConversationSelectionSet set) {
+        mConvSelectionSet = set;
+    }
+
+    @Override
+    public ConversationSelectionSet getSelectionSet() {
+        return mConvSelectionSet;
+    }
+
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ENABLE_SWIPE) {
@@ -119,7 +131,6 @@
     public View getChildAtPosition(MotionEvent ev) {
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
-        int y = 0;
         int touchY = (int) ev.getY();
         int childIdx = 0;
         View slidingChild;
@@ -128,8 +139,9 @@
             if (slidingChild.getVisibility() == GONE) {
                 continue;
             }
-            y += slidingChild.getMeasuredHeight();
-            if (touchY < y) return slidingChild;
+            if (touchY >= slidingChild.getTop() && touchY <= slidingChild.getBottom()) {
+                return slidingChild;
+            }
         }
         return null;
     }
@@ -146,18 +158,32 @@
 
     @Override
     public void onChildDismissed(View v) {
-        if (v instanceof ConversationItemView) {
-            Conversation c = ((ConversationItemView) v).getConversation();
-            c.position = getPositionForView(v);
-            AnimatedAdapter adapter = ((AnimatedAdapter) getAdapter());
-            final ImmutableList<Conversation> conversations = ImmutableList.of(c);
-            adapter.delete(conversations, new ActionCompleteListener() {
-                @Override
-                public void onActionComplete() {
-                    mSwipeCompleteListener.onSwipeComplete(conversations);
-                }
-            });
+        dismissChildren(ImmutableList.of(getConversation(v)));
+    }
+
+    @Override
+    public void onChildrenDismissed(Collection<ConversationItemView> views) {
+        final ArrayList<Conversation> conversations = new ArrayList<Conversation>();
+        for (ConversationItemView view : views) {
+            conversations.add(getConversation(view));
         }
+        dismissChildren(conversations);
+    }
+
+    private Conversation getConversation(View view) {
+        Conversation c = ((ConversationItemView) view).getConversation();
+        c.position = getPositionForView(view);
+        return c;
+    }
+
+    private void dismissChildren(final Collection<Conversation> conversations) {
+        AnimatedAdapter adapter = ((AnimatedAdapter) getAdapter());
+        adapter.delete(conversations, new ActionCompleteListener() {
+            @Override
+            public void onActionComplete() {
+                mSwipeCompleteListener.onSwipeComplete(conversations);
+            }
+        });
     }
 
     @Override
@@ -165,12 +191,17 @@
         // We do this so the underlying ScrollView knows that it won't get
         // the chance to intercept events anymore
         requestDisallowInterceptTouchEvent(true);
-        v.setActivated(true);
+        // If there are selected conversations, we are dismissing an entire
+        // associated set.
+        // Otherwise, the SwipeHelper will just get rid of the single item it
+        // received touch events for.
+        mSwipeHelper.setAssociatedViews(mConvSelectionSet != null ? mConvSelectionSet.views()
+                : null);
     }
 
     @Override
     public void onDragCancelled(View v) {
-        v.setActivated(false);
+        mSwipeHelper.setAssociatedViews(null);
     }
 
     public interface SwipeCompleteListener {