Update logic for adding/removing folders.

The logic is:
we only want to add/ remove if the folder was changed
So, track only the operations a user makes (aka checks a box/ unchecks a box)
And use this to determine what to add/ remove from a conversation.

Change-Id: I37d9c042e2db5f1a48c5c8a79c52039989f236d1
diff --git a/src/com/android/mail/providers/Folder.java b/src/com/android/mail/providers/Folder.java
index 6c1a518..b2a3995 100644
--- a/src/com/android/mail/providers/Folder.java
+++ b/src/com/android/mail/providers/Folder.java
@@ -43,9 +43,10 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -261,8 +262,8 @@
         return null;
     }
 
-    public static List<Folder> forFoldersString(String foldersString) {
-        final List<Folder> folders = Lists.newArrayList();
+    public static ArrayList<Folder> forFoldersString(String foldersString) {
+        final ArrayList<Folder> folders = Lists.newArrayList();
         if (foldersString == null) {
             return folders;
         }
@@ -277,6 +278,25 @@
         return folders;
     }
 
+
+    public static HashMap<Uri, Folder> hashMapForFoldersString(String rawFolders) {
+        final HashMap<Uri, Folder> folders = new HashMap<Uri, Folder>();
+        if (TextUtils.isEmpty(rawFolders)) {
+            return folders;
+        }
+        try {
+            JSONArray array = new JSONArray(rawFolders);
+            Folder f;
+            for (int i = 0; i < array.length(); i++) {
+                f = new Folder(array.getJSONObject(i));
+                folders.put(f.uri, f);
+            }
+        } catch (JSONException e) {
+            LogUtils.wtf(LOG_TAG, e, "Unable to create list of folders from serialzied jsonarray");
+        }
+        return folders;
+    }
+
     /**
      * Return a serialized String for this account.
      */
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 65255dc..ca4606c 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -85,6 +85,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Set;
 import java.util.TimerTask;
 
@@ -1635,21 +1636,21 @@
     // Called from the FolderSelectionDialog after a user is done selecting folders to assign the
     // conversations to.
     @Override
-    public final void assignFolder(Collection<Folder> folders, Collection<Conversation> target,
-            boolean batch, boolean showUndo) {
-        // Actions are destructive only when the current folder can be assigned to (which is the
-        // same as being able to un-assign a conversation from the folder) and when the list of
-        // folders contains the current folder.
-        final boolean isDestructive =
-                mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) &&
-                !Folder.containerIncludes(folders, mFolder);
+    public final void assignFolder(Collection<FolderOperation> folderOps,
+            Collection<Conversation> target, boolean batch, boolean showUndo) {
+        // Actions are destructive only when the current folder can be assigned
+        // to (which is the same as being able to un-assign a conversation from the folder) and
+        // when the list of folders contains the current folder.
+        final boolean isDestructive = mFolder
+                .supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
+                && FolderOperation.isDestructive(folderOps, mFolder);
         LogUtils.d(LOG_TAG, "onFolderChangesCommit: isDestructive = %b", isDestructive);
         if (isDestructive) {
             for (final Conversation c : target) {
                 c.localDeleteOnUpdate = true;
             }
         }
-        final DestructiveAction folderChange = getFolderChange(target, folders, isDestructive,
+        final DestructiveAction folderChange = getFolderChange(target, folderOps, isDestructive,
                 batch, showUndo);
         // Update the UI elements depending no their visibility and availability
         // TODO(viki): Consolidate this into a single method requestDelete.
@@ -1859,8 +1860,10 @@
             return;
         }
         final Collection<Conversation> conversations = mSelectedSet.values();
-        final Collection<Folder> dropTarget = Folder.listOf(folder);
-        // Drag and drop is destructive: we remove conversations from the current folder.
+        final Collection<FolderOperation> dropTarget = FolderOperation.listOf(new FolderOperation(
+                folder, true));
+        // Drag and drop is destructive: we remove conversations from the
+        // current folder.
         final DestructiveAction action = getFolderChange(conversations, dropTarget, true, true,
                 true);
         delete(conversations, action);
@@ -2011,7 +2014,7 @@
      */
     private class FolderDestruction implements DestructiveAction {
         private final Collection<Conversation> mTarget;
-        private final ArrayList<Folder> mFolderList = new ArrayList<Folder>();
+        private final ArrayList<FolderOperation> mFolderOps = new ArrayList<FolderOperation>();
         private final boolean mIsDestructive;
         /** Whether this destructive action has already been performed */
         private boolean mCompleted;
@@ -2023,10 +2026,10 @@
          * @param target
          */
         private FolderDestruction(final Collection<Conversation> target,
-                final Collection<Folder> folders, boolean isDestructive, boolean isBatch,
+                final Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
                 boolean showUndo) {
             mTarget = ImmutableList.copyOf(target);
-            mFolderList.addAll(folders);
+            mFolderOps.addAll(folders);
             mIsDestructive = isDestructive;
             mIsSelectedSet = isBatch;
             mShowUndo = showUndo;
@@ -2041,19 +2044,32 @@
                 UndoOperation undoOp = new UndoOperation(mTarget.size(), R.id.change_folder);
                 onUndoAvailable(undoOp);
             }
-            mConversationListCursor.updateStrings(
-                    mContext,
-                    mTarget,
-                    Conversation.UPDATE_FOLDER_COLUMNS,
-                    new String[] {
-                            Folder.getUriString(mFolderList),
-                            Folder.getSerializedFolderString(mFolder, mFolderList)
-                    });
+            // For each conversation, for each operation, add/ remove the
+            // appropriate folders.
+            for (Conversation target : mTarget) {
+                HashMap<Uri, Folder> targetFolders = Folder
+                        .hashMapForFoldersString(target.rawFolders);
+                for (FolderOperation op : mFolderOps) {
+                    if (op.mAdd) {
+                        targetFolders.put(op.mFolder.uri, op.mFolder);
+                    } else {
+                        targetFolders.remove(op.mFolder.uri);
+                    }
+                }
+                target.folderList = Folder.getUriString(targetFolders.values());
+                target.rawFolders = Folder.getSerializedFolderString(mFolder,
+                        targetFolders.values());
+                mConversationListCursor.updateStrings(mContext, Conversation.listOf(target),
+                        Conversation.UPDATE_FOLDER_COLUMNS, new String[] {
+                                target.folderList, target.rawFolders
+                        });
+            }
             refreshConversationList();
             if (mIsSelectedSet) {
                 mSelectedSet.clear();
             }
         }
+
         /**
          * Returns true if this action has been performed, false otherwise.
          * @return
@@ -2068,7 +2084,8 @@
     }
 
     private final DestructiveAction getFolderChange(Collection<Conversation> target,
-            Collection<Folder> folders, boolean isDestructive, boolean isBatch, boolean showUndo) {
+            Collection<FolderOperation> folders, boolean isDestructive, boolean isBatch,
+            boolean showUndo) {
         final DestructiveAction da = new FolderDestruction(target, folders, isDestructive, isBatch,
                 showUndo);
         registerDestructiveAction(da);
diff --git a/src/com/android/mail/ui/ConversationUpdater.java b/src/com/android/mail/ui/ConversationUpdater.java
index 5f6b93f..5f59770 100644
--- a/src/com/android/mail/ui/ConversationUpdater.java
+++ b/src/com/android/mail/ui/ConversationUpdater.java
@@ -18,7 +18,6 @@
 package com.android.mail.ui;
 
 import com.android.mail.providers.Conversation;
-import com.android.mail.providers.Folder;
 import com.android.mail.providers.UIProvider;
 
 import java.util.Collection;
@@ -80,7 +79,7 @@
      * @param batch whether this is a batch operation
      * @param showUndo whether to show the undo bar
      */
-    public void assignFolder(Collection<Folder> folders, Collection<Conversation> target,
+    public void assignFolder(Collection<FolderOperation> folders, Collection<Conversation> target,
             boolean batch, boolean showUndo);
 
     /**
diff --git a/src/com/android/mail/ui/FolderOperation.java b/src/com/android/mail/ui/FolderOperation.java
new file mode 100644
index 0000000..20614e2
--- /dev/null
+++ b/src/com/android/mail/ui/FolderOperation.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import com.android.mail.providers.Folder;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+public class FolderOperation {
+    /** An immutable, empty conversation list */
+    public static final Collection<FolderOperation> EMPTY = Collections.emptyList();
+    public Folder mFolder;
+    public boolean mAdd;
+
+    public FolderOperation(Folder folder, Boolean operation) {
+        mAdd = operation;
+        mFolder = folder;
+    }
+
+    /**
+     * Get all the unique folders associated with a set of folder operations.
+     * @param ops
+     * @return
+     */
+    public final static ArrayList<Folder> getFolders(Collection<FolderOperation> ops) {
+        HashSet<Folder> folders = new HashSet<Folder>();
+        for (FolderOperation op : ops) {
+            folders.add(op.mFolder);
+        }
+        return new ArrayList<Folder>(folders);
+    }
+
+    /**
+     * Returns a collection of a single FolderOperation. This method always
+     * returns a valid collection even if the input folder is null.
+     *
+     * @param in a FolderOperation, possibly null.
+     * @return a collection of the folder.
+     */
+    public static Collection<FolderOperation> listOf(FolderOperation in) {
+        final Collection<FolderOperation> target = (in == null) ? EMPTY : ImmutableList.of(in);
+        return target;
+    }
+
+    /**
+     * Return if a set of folder operations removes the specified folder, making
+     * it a destructive operation.
+     */
+    public static boolean isDestructive(Collection<FolderOperation> folderOps, Folder folder) {
+        for (FolderOperation op : folderOps) {
+            if (Objects.equal(op.mFolder.uri, folder.uri) && !op.mAdd) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/mail/ui/FoldersSelectionDialog.java b/src/com/android/mail/ui/FoldersSelectionDialog.java
index 66afbed..b6e4b9d 100644
--- a/src/com/android/mail/ui/FoldersSelectionDialog.java
+++ b/src/com/android/mail/ui/FoldersSelectionDialog.java
@@ -21,8 +21,8 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
-import android.content.DialogInterface.OnMultiChoiceClickListener;
 import android.database.Cursor;
+import android.net.Uri;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.AdapterView;
@@ -35,14 +35,12 @@
 import com.android.mail.ui.FolderSelectorAdapter.FolderRow;
 import com.android.mail.utils.Utils;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Map.Entry;
 
-public class FoldersSelectionDialog implements OnClickListener, OnMultiChoiceClickListener {
+public class FoldersSelectionDialog implements OnClickListener {
     private AlertDialog mDialog;
     private ConversationUpdater mUpdater;
     private HashMap<Folder, Boolean> mCheckedState;
@@ -50,6 +48,7 @@
     private SeparatedFolderListAdapter mAdapter;
     private final Collection<Conversation> mTarget;
     private boolean mBatch;
+    private HashMap<Uri, FolderOperation> mOperations;
 
     public FoldersSelectionDialog(final Context context, Account account,
             final ConversationUpdater updater, Collection<Conversation> target, boolean isBatch) {
@@ -59,6 +58,7 @@
 
         // Mapping of a folder's uri to its checked state
         mCheckedState = new HashMap<Folder, Boolean>();
+        mOperations = new HashMap<Uri, FolderOperation>();
         AlertDialog.Builder builder = new AlertDialog.Builder(context);
         builder.setTitle(R.string.folder_selection_dialog_title);
         builder.setPositiveButton(R.string.ok, this);
@@ -143,46 +143,31 @@
                 Object item = mAdapter.getItem(i);
                 if (item instanceof FolderRow) {
                    ((FolderRow)item).setIsPresent(false);
+                   Folder folder = ((FolderRow)item).getFolder();
+                   mOperations.put(folder.uri, new FolderOperation(folder, false));
                 }
             }
             mCheckedState.clear();
         }
         row.setIsPresent(add);
         mAdapter.notifyDataSetChanged();
-        mCheckedState.put(row.getFolder(), add);
+        Folder folder = row.getFolder();
+        mCheckedState.put(folder, add);
+        mOperations.put(folder.uri, new FolderOperation(folder, add));
     }
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
         switch (which) {
             case DialogInterface.BUTTON_POSITIVE:
-                final Collection<Folder> folders = new ArrayList<Folder>();
-                for (Entry<Folder, Boolean> entry : mCheckedState.entrySet()) {
-                    if (entry.getValue()) {
-                        folders.add(entry.getKey());
-                    }
-                }
                 if (mUpdater != null) {
-                    mUpdater.assignFolder(folders, mTarget, mBatch, true);
+                    mUpdater.assignFolder(mOperations.values(), mTarget, mBatch, true);
                 }
                 break;
             case DialogInterface.BUTTON_NEGATIVE:
                 break;
             default:
-                onClick(dialog, which, true);
                 break;
         }
     }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which, boolean isChecked) {
-        final FolderRow row = (FolderRow) mAdapter.getItem(which);
-        if (mSingle) {
-            // Clear any other checked items.
-            mCheckedState.clear();
-            isChecked = true;
-        }
-        mCheckedState.put(row.getFolder(), isChecked);
-        mDialog.getListView().setItemChecked(which, false);
-    }
 }
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 9f6a58d..93237c3 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -21,6 +21,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.net.Uri;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
@@ -39,6 +40,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 
 public class SwipeableListView extends ListView implements Callback{
     private SwipeHelper mSwipeHelper;
@@ -184,27 +186,33 @@
                     conversations.add(conversation);
                 }
             }
-            undoOp = new UndoOperation(
-                    conversationViews != null ? (conversations.size() + 1) : 1, mSwipeAction);
+            undoOp = new UndoOperation(conversationViews != null ? (conversations.size() + 1) : 1,
+                    mSwipeAction);
             handleLeaveBehind(target, undoOp, context);
             adapter.delete(conversations, new DestructiveAction() {
                 @Override
                 public void performAction() {
-                    ConversationCursor cc = (ConversationCursor)adapter.getCursor();
+                    ConversationCursor cc = (ConversationCursor) adapter.getCursor();
                     switch (mSwipeAction) {
                         case R.id.archive:
                             cc.archive(context, conversations);
                             break;
                         case R.id.change_folder:
-                            Collection<Folder> folders = getFolders(conversations);
-                            cc.updateStrings(
-                                    context,
-                                    conversations,
-                                    Conversation.UPDATE_FOLDER_COLUMNS,
-                                    new String[] {
-                                            Folder.getUriString(folders),
-                                            Folder.getSerializedFolderString(mFolder, folders)
-                                    });
+                            FolderOperation folderOp = new FolderOperation(mFolder, false);
+                            // For each conversation, for each operation, remove
+                            // the current folder.
+                            for (Conversation target : conversations) {
+                                HashMap<Uri, Folder> targetFolders = Folder
+                                        .hashMapForFoldersString(target.rawFolders);
+                                targetFolders.remove(folderOp.mFolder.uri);
+                                target.folderList = Folder.getUriString(targetFolders.values());
+                                target.rawFolders = Folder.getSerializedFolderString(mFolder,
+                                        targetFolders.values());
+                                cc.updateStrings(context, Conversation.listOf(target),
+                                        Conversation.UPDATE_FOLDER_COLUMNS, new String[] {
+                                                target.folderList, target.rawFolders
+                                        });
+                            }
                             break;
                         case R.id.delete:
                             cc.delete(context, conversations);
@@ -241,15 +249,15 @@
         ConversationCursor cc = (ConversationCursor)adapter.getCursor();
         switch (mSwipeAction) {
             case R.id.change_folder:
-                Collection<Conversation> convs = Conversation.listOf(conv);
-                Collection<Folder> folders = getFolders(convs);
-                cc.mostlyDestructiveUpdate(
-                        context,
-                        convs,
-                        Conversation.UPDATE_FOLDER_COLUMNS,
-                        new String[] {
-                                Folder.getUriString(folders),
-                                Folder.getSerializedFolderString(mFolder, folders)
+                FolderOperation folderOp = new FolderOperation(mFolder, false);
+                HashMap<Uri, Folder> targetFolders = Folder
+                        .hashMapForFoldersString(conv.rawFolders);
+                targetFolders.remove(folderOp.mFolder.uri);
+                conv.folderList = Folder.getUriString(targetFolders.values());
+                conv.rawFolders = Folder.getSerializedFolderString(mFolder, targetFolders.values());
+                cc.mostlyDestructiveUpdate(context, Conversation.listOf(conv),
+                        Conversation.UPDATE_FOLDER_COLUMNS, new String[] {
+                                conv.folderList, conv.rawFolders
                         });
                 break;
             case R.id.archive: