Merge "Check for null before clearing selection set."
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index b17de1e..9da3337 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -39,8 +39,6 @@
 import com.android.mail.providers.UIProvider;
 import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
 import com.android.mail.providers.UIProvider.ConversationOperations;
-import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.ConversationPositionTracker;
 import com.android.mail.utils.LogUtils;
 import com.google.common.annotations.VisibleForTesting;
 
@@ -82,6 +80,9 @@
     // The index of the Uri whose data is reflected in the cached row
     // Updates/Deletes to this Uri are cached
     private static int sUriColumnIndex;
+    // The listeners registered for this cursor
+    private static ArrayList<ConversationListener> sListeners =
+        new ArrayList<ConversationListener>();
     // The ConversationProvider instance
     @VisibleForTesting
     static ConversationProvider sProvider;
@@ -110,10 +111,6 @@
     private boolean mCursorObserverRegistered = false;
     // Whether or not sync from underlying provider should be deferred
     private static boolean sDeferSync = false;
-    // The adapter using this cursor
-    private static AnimatedAdapter sAdapter;
-    // The position tracker that's watching this cursor
-    private static ConversationPositionTracker sTracker;
 
     // The current position of the cursor
     private int mPosition = -1;
@@ -142,6 +139,7 @@
             sUnderlyingCursor.close();
         }
         sUnderlyingCursor = cursor;
+        sListeners.clear();
         sRefreshRequired = false;
         sRefreshReady = false;
         sRefreshTask = null;
@@ -166,22 +164,6 @@
     }
 
     /**
-     * Set the adapter using this ConversationCursor
-     * @param adapter the adapter
-     */
-    public void setAdapter(AnimatedAdapter adapter) {
-        sAdapter = adapter;
-    }
-
-    /**
-     * Set the position tracker for this cursor; we notify it at every sync
-     * @param tracker the tracker
-     */
-    public static void setTracker(ConversationPositionTracker tracker) {
-        sTracker = tracker;
-    }
-
-    /**
      * Create a ConversationCursor; this should be called by the ListActivity using that cursor
      * @param activity the activity creating the cursor
      * @param messageListColumn the column used for individual cursor items
@@ -431,6 +413,28 @@
     }
 
     /**
+     * Add a listener for this cursor; we'll notify it when our data changes
+     */
+    public void addListener(ConversationListener listener) {
+        synchronized (sListeners) {
+            if (!sListeners.contains(listener)) {
+                sListeners.add(listener);
+            } else {
+                LogUtils.i(TAG, "Ignoring duplicate add of listener");
+            }
+        }
+    }
+
+    /**
+     * Remove a listener for this cursor
+     */
+    public void removeListener(ConversationListener listener) {
+        synchronized(sListeners) {
+            sListeners.remove(listener);
+        }
+    }
+
+    /**
      * Generate a forwarding Uri to ConversationProvider from an original Uri.  We do this by
      * changing the authority to ours, but otherwise leaving the Uri intact.
      * NOTE: This won't handle query parameters, so the functionality will need to be added if
@@ -601,8 +605,12 @@
         if (DEBUG) {
             LogUtils.i(TAG, "[Notify: onRefreshRequired()]");
         }
-        if (!sDeferSync && sAdapter != null) {
-            sAdapter.onRefreshRequired();
+        if (!sDeferSync) {
+            synchronized(sListeners) {
+                for (ConversationListener listener: sListeners) {
+                    listener.onRefreshRequired();
+                }
+            }
         }
     }
 
@@ -613,8 +621,10 @@
         if (DEBUG) {
             LogUtils.i(TAG, "[Notify: onRefreshReady()]");
         }
-        if (sAdapter != null) {
-            sAdapter.onRefreshReady();
+        synchronized(sListeners) {
+            for (ConversationListener listener: sListeners) {
+                listener.onRefreshReady();
+            }
         }
     }
 
@@ -625,8 +635,10 @@
         if (DEBUG) {
             LogUtils.i(TAG, "[Notify: onDataSetChanged()]");
         }
-        if (sAdapter != null) {
-            sAdapter.notifyDataSetChanged();
+        synchronized(sListeners) {
+            for (ConversationListener listener: sListeners) {
+                listener.onDataSetChanged();
+            }
         }
     }
 
@@ -652,9 +664,6 @@
             sRefreshReady = false;
         }
         notifyDataChanged();
-        if (sTracker != null) {
-            sTracker.updateCursor(sConversationCursor);
-        }
     }
 
     public boolean isRefreshRequired() {
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index ab64a70..3b30c9e 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -118,6 +118,8 @@
 
     private static final String EXTRA_BODY = "body";
 
+    private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
+
     // Extra that we can get passed from other activities
     private static final String EXTRA_TO = "to";
     private static final String EXTRA_CC = "cc";
@@ -534,13 +536,19 @@
     }
 
     private void initFromSpinner(Bundle bundle, int action) {
+        String accountString = null;
         if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
             action = COMPOSE;
         }
         mFromSpinner.asyncInitFromSpinner(action, mAccount);
-        if (bundle != null && bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
-            mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
-                    bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
+        if (bundle != null) {
+            if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
+                mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
+                        bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
+            } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
+                accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
+                mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
+            }
         }
         if (mReplyFromAccount == null) {
             if (mDraft != null) {
@@ -553,7 +561,9 @@
             mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
                     mAccount.name, true, false);
         }
+
         mFromSpinner.setCurrentAccount(mReplyFromAccount);
+
         if (mFromSpinner.getCount() > 1) {
             // If there is only 1 account, just show that account.
             // Otherwise, give the user the ability to choose which account to
@@ -1032,7 +1042,9 @@
     private void showCcBcc(Bundle state) {
         if (state != null && state.containsKey(EXTRA_SHOW_CC_BCC)) {
             boolean show = state.getBoolean(EXTRA_SHOW_CC_BCC);
-            mCcBccView.show(false, show, show);
+            if (show) {
+                mCcBccView.show(false, show, show);
+            }
         }
     }
 
diff --git a/src/com/android/mail/compose/FromAddressSpinner.java b/src/com/android/mail/compose/FromAddressSpinner.java
index 2abafd6..ec8cd11 100644
--- a/src/com/android/mail/compose/FromAddressSpinner.java
+++ b/src/com/android/mail/compose/FromAddressSpinner.java
@@ -71,6 +71,17 @@
         }
     }
 
+    public ReplyFromAccount getMatchingReplyFromAccount(String accountString) {
+        if (!TextUtils.isEmpty(accountString)) {
+            for (ReplyFromAccount acct : mReplyFromAccounts) {
+                if (accountString.equals(acct.name)) {
+                    return acct;
+                }
+            }
+        }
+        return null;
+    }
+
     public ReplyFromAccount getCurrentAccount() {
         return mAccount;
     }
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index ae5bf13..a0eff82 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -45,11 +45,14 @@
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MotionEvent;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
 import android.widget.Toast;
 
 import com.android.mail.ConversationListContext;
 import com.android.mail.R;
 import com.android.mail.browse.ConversationCursor;
+import com.android.mail.browse.ConversationCursor.ConversationListener;
 import com.android.mail.browse.SelectedConversationsActionMenu;
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.providers.Account;
@@ -73,6 +76,8 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
 
 
 /**
@@ -91,7 +96,8 @@
  * In the Gmail codebase, this was called BaseActivityController
  * </p>
  */
-public abstract class AbstractActivityController implements ActivityController {
+public abstract class AbstractActivityController implements ActivityController,
+        ConversationListener, OnScrollListener {
     // Keys for serialization of various information in Bundles.
     private static final String SAVED_ACCOUNT = "saved-account";
     private static final String SAVED_FOLDER = "saved-folder";
@@ -108,6 +114,8 @@
     /** Tag used when loading a folder list fragment. */
     protected static final String TAG_FOLDER_LIST = "tag-folder-list";
 
+    private static final long CONVERSATION_LIST_THROTTLE_MS = 4000L;
+
     /** Are we on a tablet device or not. */
     public final boolean IS_TABLET_DEVICE;
 
@@ -138,6 +146,12 @@
     private final Set<Uri> mCurrentAccountUris = Sets.newHashSet();
     protected Settings mCachedSettings;
     protected ConversationCursor mConversationListCursor;
+    protected boolean mConversationListenerAdded = false;
+
+    private boolean mIsConversationListScrolling = false;
+    private long mConversationListRefreshTime = 0;
+    private Timer mConversationListTimer = new Timer();
+    private RefreshTimerTask mConversationListRefreshTask;
 
     /**
      * Selected conversations, if any.
@@ -254,18 +268,6 @@
         }
         return null;
     }
-    /**
-     * Get the animated adapter for this activity. If the conversation list fragment
-     * is not attached, this method returns null
-     * @return
-     */
-    protected AnimatedAdapter getConversationListAdapter() {
-        final Fragment fragment = getConversationListFragment();
-        if (fragment != null) {
-            return ((ConversationListFragment) fragment).getAnimatedAdapter();
-        }
-        return null;
-    }
 
     /**
      * Returns the conversation view fragment attached with this activity. If no such fragment
@@ -328,6 +330,7 @@
             // Current account is different from the new account, restart loaders and show
             // the account Inbox.
             mAccount = account;
+            cancelRefreshTask();
             onSettingsChanged(mAccount.settings);
             mActionBarView.setAccount(mAccount);
             loadAccountInbox();
@@ -408,6 +411,7 @@
             // the list is shown to the user, this could fire in one pane if the user goes directly
             // to a conversation
             updateRecentFolderList();
+            cancelRefreshTask();
         }
     }
 
@@ -1004,7 +1008,6 @@
     protected void setCurrentConversation(Conversation conversation) {
         mCurrentConversation = conversation;
         mTracker.initialize(mCurrentConversation);
-        ConversationCursor.setTracker(mTracker);
     }
 
     /**
@@ -1411,6 +1414,126 @@
     protected abstract DestructiveActionListener getFolderDestructiveActionListener();
 
     @Override
+    public void onRefreshRequired() {
+        if (mIsConversationListScrolling) {
+            LogUtils.d(LOG_TAG, "onRefreshRequired: delay until scrolling done");
+            return;
+        }
+        // Refresh the query in the background
+        long now = System.currentTimeMillis();
+        long sinceLastRefresh = now - mConversationListRefreshTime;
+//        if (sinceLastRefresh > CONVERSATION_LIST_THROTTLE_MS) {
+            if (getConversationListCursor().isRefreshRequired()) {
+                getConversationListCursor().refresh();
+                mTracker.updateCursor(mConversationListCursor);
+                mConversationListRefreshTime = now;
+            }
+//        } else {
+//            long delay = CONVERSATION_LIST_THROTTLE_MS - sinceLastRefresh;
+//            LogUtils.d(LOG_TAG, "onRefreshRequired: delay for %sms", delay);
+//            mConversationListRefreshTask = new RefreshTimerTask(this, mHandler);
+//            mConversationListTimer.schedule(mConversationListRefreshTask, delay);
+//        }
+    }
+
+    /**
+     * Called when the {@link ConversationCursor} is changed or has new data in it.
+     * <p>
+     * {@inheritDoc}
+     */
+    @Override
+    public void onRefreshReady() {
+        final ArrayList<Integer> deletedRows = mConversationListCursor.getRefreshDeletions();
+        // If we have any deletions from the server, and the conversations are in the list view,
+        // remove them from a selected set, if any
+        if (!deletedRows.isEmpty() && !mSelectedSet.isEmpty()) {
+            mSelectedSet.delete(deletedRows);
+        }
+        // If we have any deletions from the server, animate them away
+        final ConversationListFragment convList = getConversationListFragment();
+        if (!deletedRows.isEmpty() && convList != null) {
+            final AnimatedAdapter adapter = convList.getAnimatedAdapter();
+            if (adapter != null) {
+                adapter.delete(deletedRows, this);
+            }
+        } if (!mIsConversationListScrolling) {
+            // Swap cursors
+            mConversationListCursor.sync();
+            refreshAdapter();
+        }
+        mTracker.updateCursor(mConversationListCursor);
+    }
+
+    @Override
+    public void onDataSetChanged() {
+        refreshAdapter();
+    }
+
+    private void refreshAdapter() {
+        final ConversationListFragment convList = getConversationListFragment();
+        if (convList != null) {
+            final AnimatedAdapter adapter = convList.getAnimatedAdapter();
+            if (adapter != null) {
+                adapter.notifyDataSetChanged();
+            }
+        }
+    }
+
+    /**
+     * This class handles throttled refresh of the conversation list
+     */
+    static class RefreshTimerTask extends TimerTask {
+        final Handler mHandler;
+        final AbstractActivityController mController;
+
+        RefreshTimerTask(AbstractActivityController controller, Handler handler) {
+            mHandler = handler;
+            mController = controller;
+        }
+
+        @Override
+        public void run() {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    LogUtils.d(LOG_TAG, "Delay done... calling onRefreshRequired");
+                    mController.onRefreshRequired();
+                }});
+        }
+    }
+
+    /**
+     * Cancel the refresh task, if it's running
+     */
+    private void cancelRefreshTask () {
+        if (mConversationListRefreshTask != null) {
+            mConversationListRefreshTask.cancel();
+            mConversationListRefreshTask = null;
+        }
+    }
+
+    @Override
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        boolean isScrolling = (scrollState != OnScrollListener.SCROLL_STATE_IDLE);
+        if (!isScrolling) {
+            ConversationCursor cc = getConversationListCursor();
+            if (cc.isRefreshRequired()) {
+                LogUtils.d(LOG_TAG, "Stop scrolling: refresh");
+                cc.refresh();
+            } else if (cc.isRefreshReady()) {
+                LogUtils.d(LOG_TAG, "Stop scrolling: try sync");
+                onRefreshReady();
+            }
+        }
+        mIsConversationListScrolling = isScrolling;
+    }
+
+    @Override
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+    }
+
+    @Override
     public void onSetEmpty() {
     }
 
@@ -1454,18 +1577,11 @@
         }
     }
 
-    private void checkRefreshConversationList() {
-        if (mConversationListCursor != null && mConversationListCursor.isRefreshReady()) {
-            AnimatedAdapter adapter = getConversationListAdapter();
-            if (adapter != null) {
-                adapter.onRefreshReady();
-            }
-        }
-    }
-
     @Override
     public void onActionComplete() {
-        checkRefreshConversationList();
+        if (getConversationListCursor().isRefreshReady()) {
+            refreshAdapter();
+        }
     }
 
     @Override
@@ -1572,13 +1688,24 @@
             mConversationListCursor = data;
 
             // Call the method that updates things when values in the cursor change
-            checkRefreshConversationList();
+            if (mConversationListCursor.isRefreshReady()) {
+                onRefreshReady();
+            }
 
             // Register the AbstractActivityController as a listener to changes in
             // data in the cursor.
             final ConversationListFragment convList = getConversationListFragment();
             if (convList != null) {
                 convList.onCursorUpdated();
+                if (!mConversationListenerAdded) {
+                    // TODO(mindyp): when we move to the cursor loader, we need
+                    // to add/remove the listener when we create/ destroy loaders.
+                    mConversationListCursor
+                            .addListener(AbstractActivityController.this);
+                    convList.getListView().setOnScrollListener(
+                            AbstractActivityController.this);
+                    mConversationListenerAdded = true;
+                }
             }
             // Shown for search results in two-pane mode only.
             if (shouldShowFirstConversation()) {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index a68c15b..0172109 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -24,21 +24,20 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.AbsListView.OnScrollListener;
 import android.widget.SimpleCursorAdapter;
 
 import com.android.mail.R;
 import com.android.mail.browse.ConversationCursor;
-import com.android.mail.browse.ConversationCursor.ConversationListener;
 import com.android.mail.browse.ConversationItemView;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.Settings;
 import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.SwipeableListView.SwipeCompleteListener;
 import com.android.mail.ui.UndoBarView.OnUndoCancelListener;
 import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableList;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -46,8 +45,7 @@
 import java.util.HashSet;
 
 public class AnimatedAdapter extends SimpleCursorAdapter implements
-        android.animation.Animator.AnimatorListener, OnUndoCancelListener, ConversationListener,
-        OnScrollListener {
+        android.animation.Animator.AnimatorListener, OnUndoCancelListener {
     private final static int TYPE_VIEW_CONVERSATION = 0;
     private final static int TYPE_VIEW_DELETING = 1;
     private final static int TYPE_VIEW_UNDOING = 2;
@@ -59,6 +57,7 @@
     private Context mContext;
     private ConversationSelectionSet mBatchConversations;
     private ActionCompleteListener mActionCompleteListener;
+    private boolean mUndo = false;
     private ArrayList<Integer> mLastDeletingItems = new ArrayList<Integer>();
     private ViewMode mViewMode;
     private View mFooter;
@@ -70,10 +69,6 @@
     private DragListener mDragListener;
     private HashMap<Long, LeaveBehindItem> mLeaveBehindItems = new HashMap<Long, LeaveBehindItem>();
 
-    private ConversationCursor mConversationCursor;
-    // TODO: Hook this up (set list view's onScrollListener)
-    private boolean mIsConversationListScrolling = false;
-
     /**
      * Used only for debugging.
      */
@@ -96,10 +91,6 @@
         mCachedSettings = settings;
         mDragListener = dragListener;
         mSwipeEnabled = account.supportsCapability(UIProvider.AccountCapabilities.ARCHIVE);
-        mConversationCursor = cursor;
-        if (cursor != null) {
-            cursor.setAdapter(this);
-        }
     }
 
     @Override
@@ -453,54 +444,4 @@
         mLeaveBehindItems.remove(item.id);
         notifyDataSetChanged();
     }
-
-    @Override
-    public void onRefreshRequired() {
-        if (mConversationCursor.isRefreshRequired()) {
-            mConversationCursor.refresh();
-        }
-    }
-
-    /**
-     * Called when the {@link ConversationCursor} is changed or has new data in it.
-     * <p>
-     * {@inheritDoc}
-     */
-    @Override
-    public void onRefreshReady() {
-        // Note: mIsConversationListScrolling will always be false until the listener is hooked up
-        if (!mIsConversationListScrolling) {
-            // Swap cursors
-            mConversationCursor.sync();
-            notifyDataSetChanged();
-        }
-    }
-
-    @Override
-    public void onDataSetChanged() {
-        notifyDataSetChanged();
-    }
-
-    // TODO: Hook this up (currently not used)
-    @Override
-    public void onScrollStateChanged(AbsListView view, int scrollState) {
-        if (mConversationCursor != null) {
-            boolean isScrolling = (scrollState != OnScrollListener.SCROLL_STATE_IDLE);
-            if (!isScrolling) {
-                if (mConversationCursor.isRefreshRequired()) {
-                    LogUtils.d(LOG_TAG, "Stop scrolling: refresh");
-                    mConversationCursor.refresh();
-                } else if (mConversationCursor.isRefreshReady()) {
-                    LogUtils.d(LOG_TAG, "Stop scrolling: try sync");
-                    onRefreshReady();
-                }
-            }
-            mIsConversationListScrolling = isScrolling;
-        }
-    }
-
-    @Override
-    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
-            int totalItemCount) {
-    }
 }
diff --git a/src/com/android/mail/ui/ConversationCursorLoader.java b/src/com/android/mail/ui/ConversationCursorLoader.java
index c50b697..cf25a8e 100644
--- a/src/com/android/mail/ui/ConversationCursorLoader.java
+++ b/src/com/android/mail/ui/ConversationCursorLoader.java
@@ -53,12 +53,12 @@
     @Override
     protected void onStartLoading() {
         forceLoad();
-        ConversationCursor.resume();
+        //ConversationCursor.resume();
     }
 
     @Override
     protected void onStopLoading() {
         cancelLoad();
-        ConversationCursor.pause();
+        //ConversationCursor.pause();
     }
 }
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index ba7658e..c8beeb9 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -25,7 +25,6 @@
 import android.os.Handler;
 import android.os.Parcelable;
 import android.view.LayoutInflater;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
@@ -296,7 +295,8 @@
     public void onDestroyView() {
         mListSavedState = mListView.onSaveInstanceState();
 
-        // Clear the adapter.
+        // Set a null cursor in the dapter, and clear the list's adapter
+        mListAdapter.swapCursor(null);
         mListView.setAdapter(null);
 
         mActivity.unsetViewModeListener(this);