Setup multi folder select dialog.

This is a first pass; I expect to be doing a LOT of cleanup.
Change-Id: I5a478781776a47530c5f7862071dec829b585fbf
diff --git a/res/layout/apply_remove_folder_dialog.xml b/res/layout/apply_remove_folder_dialog.xml
new file mode 100644
index 0000000..3623415
--- /dev/null
+++ b/res/layout/apply_remove_folder_dialog.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/res/layout/select_dialog.xml
+**
+** Copyright 2006, Google Inc.
+**
+** 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.
+*/
+-->
+
+<!--
+    This layout file is used by the AlertDialog when displaying a list of items.
+    This layout file is inflated and used as the ListView to display the items.
+    Assign an ID so its state will be saved/restored.
+-->
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginTop="5px" />
diff --git a/res/layout/folders_view.xml b/res/layout/folders_view.xml
new file mode 100644
index 0000000..91f6888
--- /dev/null
+++ b/res/layout/folders_view.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2007 Google Inc.
+ *
+ * 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.
+ */
+-->
+
+<!-- Describes an individual toggleable label entry to be displayed in a list of labels in
+     a label selection UI. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/labels"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:height="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical"
+    android:padding="3dip">
+
+    <!-- Note: the checkbox is not focusable because the parent list item itself handles
+         the toggling -->
+    <CheckBox android:id="@+id/checkbox"
+        android:layout_height="wrap_content"
+        android:layout_width="0dip"
+        android:layout_weight="1"
+        android:layout_margin="4dip"
+        android:singleLine="false"
+        android:maxLines="2"
+        android:ellipsize="end"
+        android:drawablePadding="20dip"
+        android:focusable="false"
+        android:focusableInTouchMode="false"
+        android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+    <View
+        android:id="@+id/color_block"
+        android:layout_height="36dip"
+        android:layout_width="36dip"
+        android:layout_gravity="right|top"
+        android:layout_margin="4dip" />
+
+</LinearLayout>
diff --git a/src/com/android/mail/ui/ApplyRemoveFolderDialog.java b/src/com/android/mail/ui/ApplyRemoveFolderDialog.java
new file mode 100644
index 0000000..0eb2423
--- /dev/null
+++ b/src/com/android/mail/ui/ApplyRemoveFolderDialog.java
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ *      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 android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.FoldersSelectionDialog.CommitListener;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Apply labels to a conversation.
+ *
+ * Invoked by ConversationActivity and ConversationListActivity to display a
+ * list of available labels and allow the user to add or remove them from the
+ * current conversation. This class doesn't do any cursor manipulation.
+ */
+public class ApplyRemoveFolderDialog extends AlertDialog
+        implements OnCancelListener, DialogInterface.OnClickListener {
+
+    // All the labels available on this account
+    public static final String EXTRA_ALL_LABELS = "all-labels";
+
+    // All the labels applied to the current conversation
+    public static final String EXTRA_CURRENT_LABELS = "current-labels";
+
+    // The following two extras are set on the result and they contain
+    // all the labels that were added and removed by the user on this screen.
+    public static final String EXTRA_ADDED_LABELS = "added-labels";
+    public static final String EXTRA_REMOVED_LABELS = "removed-labels";
+
+    private Context mContext;
+    private ListView mListView;
+
+    private FolderSelectorAdapter mAdapter;
+
+    private CommitListener mCommitListener;
+
+    private ConversationsLabelHandler mLabelHandler;
+
+    public ApplyRemoveFolderDialog(Context context, CommitListener commitListener, Account account) {
+        super(context);
+        mContext = context;
+        setTitle("change label");
+        setOnCancelListener(this);
+        setButton(DialogInterface.BUTTON_POSITIVE,
+                mContext.getString(android.R.string.ok), this);
+        setButton(DialogInterface.BUTTON_NEGATIVE,
+            mContext.getString(android.R.string.cancel), this);
+        setInverseBackgroundForced(true);
+
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        mListView = (ListView) inflater.inflate(R.layout.apply_remove_folder_dialog, null);
+        setView(mListView, 0, 0, 0, 0);
+        mCommitListener = commitListener;
+        onPrepare(account);
+    }
+
+    /**
+     * Invoked before showing the dialog.  This method resets the change list and initializes
+     * the list adapter to reflect the labels found on the current conversations.
+     * @param account the account
+     * @param currentLabels the labels on this/these conversations.
+     */
+    public void onPrepare(Account account) {
+        Cursor folders = getContext().getContentResolver().query(Uri.parse(account.folderListUri),
+                UIProvider.FOLDERS_PROJECTION, null, null, null);
+        mAdapter = new FolderSelectorAdapter(getContext(), folders, new HashSet<String>());
+
+        // Handle toggling of labels on the list item itself.
+        // The clicks on the checkboxes are suppressed so that there is a consistent experience
+        // regardless on whether or not the touch area was the checkbox, or elsewhere in the item.
+        // This also allows the list items to be navigable and toggled using the trackball.
+        mListView.setItemsCanFocus(true);
+        mListView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                onSelectLabel(position);
+            }
+        });
+
+        mListView.setAdapter(mAdapter);
+        mLabelHandler = new ConversationsLabelHandler(mAdapter);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void onSelectLabel(int position) {
+        // Update the UI
+        mLabelHandler.update(mAdapter.getItem(position));
+    }
+
+    /////
+    // implements DialogInterface.OnClickListener
+    //
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        // Nothing to do
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        // If the user clicked the OK button, apply the change list
+        if (which == DialogInterface.BUTTON_POSITIVE) {
+            mCommitListener.onCommit(mLabelHandler.getUris());
+        }
+    }
+}
diff --git a/src/com/android/mail/ui/ConversationsLabelHandler.java b/src/com/android/mail/ui/ConversationsLabelHandler.java
new file mode 100644
index 0000000..618a6de
--- /dev/null
+++ b/src/com/android/mail/ui/ConversationsLabelHandler.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ *      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 java.util.ArrayList;
+
+/**
+ * Utility class that handles all changes to labels associated with a set of
+ * conversations.
+ *
+ * @author mindyp@google.com
+ */
+public class ConversationsLabelHandler {
+
+    private final FolderSelectorAdapter mAdapter;
+
+    private final ArrayList<String> mChangeList;
+
+    public ConversationsLabelHandler(FolderSelectorAdapter adapter) {
+        mAdapter = adapter;
+        mChangeList = new ArrayList<String>();
+    }
+
+    /**
+     * Call this to update the state of labels as a result of them being
+     * selected / de-selected.
+     *
+     * @param row The item being updated.
+     */
+    public void update(FolderSelectorAdapter.FolderRow row) {
+        // Update the UI
+        final boolean add = !row.isPresent();
+        final Folder folder = row.getFolder();
+
+        row.setIsPresent(add);
+        mAdapter.notifyDataSetChanged();
+
+        // Update the label
+
+        // Always add the change to our change list since this dialog could
+        // be used to apply labels to several selected conversations and the
+        // user might have to click on the same label (first + and then -) to
+        // remove a label on the set. The previous implementation turned this
+        // operation into a no-op but we can no longer do this now. The downside
+        // is that we might emit label changes to the provider that cancel each
+        // other out but the provider might be already smart enough not to emit
+        // a no-op label change anyway.
+        if (add) {
+            mChangeList.add(folder.uri);
+        } else {
+            int pos = mChangeList.indexOf(folder.uri);
+            if (pos >= 0) {
+                mChangeList.remove(pos);
+            }
+        }
+    }
+
+    /**
+     * Clear the state of the handler.
+     */
+    public void reset() {
+        mChangeList.clear();
+    }
+
+    public String getUris() {
+        StringBuilder folderUris = new StringBuilder();
+        boolean first = true;
+        for (String folderUri : mChangeList) {
+            if (first) {
+                first = false;
+            } else {
+                folderUris.append(',');
+            }
+            folderUris.append(folderUri);
+        }
+        return folderUris.toString();
+    }
+}
diff --git a/src/com/android/mail/ui/FolderSelectorAdapter.java b/src/com/android/mail/ui/FolderSelectorAdapter.java
new file mode 100644
index 0000000..094677e
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSelectorAdapter.java
@@ -0,0 +1,147 @@
+/*******************************************************************************
+ *      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.R;
+import com.android.mail.providers.Folder;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.PaintDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An adapter for translating a {@link LabelList} to a set of selectable views to be used for
+ * applying labels to one or more conversations.
+ */
+public class FolderSelectorAdapter extends BaseAdapter {
+
+    public static class FolderRow implements Comparable<FolderRow> {
+        private final Folder mFolder;
+        private boolean mIsPresent;
+
+        public FolderRow(Folder folder, boolean isPresent) {
+            mFolder = folder;
+            mIsPresent = isPresent;
+        }
+
+        public Folder getFolder() {
+            return mFolder;
+        }
+
+        public boolean isPresent() {
+            return mIsPresent;
+        }
+
+        public void setIsPresent(boolean isPresent) {
+            mIsPresent = isPresent;
+        }
+
+        @Override
+        public int compareTo(FolderRow another) {
+            if (equals(another)) {
+                return 0;
+            } else if (mIsPresent != another.mIsPresent) {
+                return mIsPresent ? -1 : 1;
+            } else {
+                return mFolder.name.compareToIgnoreCase(another.mFolder.name);
+            }
+        }
+
+    }
+
+    private List<FolderRow> mFolderRows = Lists.newArrayList();
+    private LayoutInflater mInflater;
+
+    private final Map<Integer, PaintDrawable> mColorBlockCache = Maps.newHashMap();
+
+    private static int DEFAULT_LABEL_BACKGROUND_COLOR = android.R.color.white;
+
+    public FolderSelectorAdapter(Context context, Cursor folders,
+            Set<String> initiallySelected) {
+        mInflater = LayoutInflater.from(context);
+
+        processLists(folders, initiallySelected);
+    }
+
+    private void processLists(Cursor folders, Set<String> initiallySelected) {
+        while (folders.moveToNext()) {
+            Folder folder = new Folder(folders);
+
+            FolderRow row = new FolderRow(folder, initiallySelected.contains(folder.name));
+            mFolderRows.add(row);
+        }
+        Collections.sort(mFolderRows);
+    }
+
+    @Override
+    public int getCount() {
+        return mFolderRows.size();
+    }
+
+    @Override
+    public FolderRow getItem(int position) {
+        return mFolderRows.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        View view = convertView;
+        CheckBox checkBox;
+        View colorBlock;
+
+        if (view == null) {
+            view = mInflater.inflate(R.layout.folders_view, parent, false);
+            checkBox = (CheckBox) view.findViewById(R.id.checkbox);
+            // Suppress the checkbox selection, and handle the toggling of the label
+            // on the parent list item's click handler.
+            checkBox.setClickable(false);
+            colorBlock = view.findViewById(R.id.color_block);
+            view.setTag(R.id.checkbox, checkBox);
+            view.setTag(R.id.color_block, colorBlock);
+        } else {
+            checkBox = (CheckBox) view.getTag(R.id.checkbox);
+            colorBlock = (View) view.getTag(R.id.color_block);
+        }
+
+        FolderRow row = getItem(position);
+        Folder folder = row.getFolder();
+
+        checkBox.setText(folder.name);
+        checkBox.setChecked(row.isPresent());
+
+        return view;
+    }
+
+}
diff --git a/src/com/android/mail/ui/FoldersSelectionDialog.java b/src/com/android/mail/ui/FoldersSelectionDialog.java
index 0d04700..a6488a8 100644
--- a/src/com/android/mail/ui/FoldersSelectionDialog.java
+++ b/src/com/android/mail/ui/FoldersSelectionDialog.java
@@ -63,44 +63,46 @@
         mCommitListener = commitListener;
         // Mapping of a folder's uri to its checked state
         mCheckedState = new HashMap<String, Boolean>();
-        AlertDialog.Builder builder = new AlertDialog.Builder(context);
-        builder.setTitle("Change folders");
-        builder.setPositiveButton(R.string.ok, this);
-        builder.setNegativeButton(R.string.cancel, this);
-
-        // Get all of our folders
-        // TODO: Should only be folders that allow messages to be moved there!!
-        Cursor foldersCursor = context.getContentResolver().query(Uri.parse(account.folderListUri),
-                UIProvider.FOLDERS_PROJECTION, null, null, null);
-        // Get the id, name, and a placeholder for check information
-        Object[] columnValues = new Object[FOLDER_DIALOG_PROJECTION.length];
-        mFolderDialogCursor = new MatrixCursor(FOLDER_DIALOG_PROJECTION);
-        int i = 0;
-        while (foldersCursor.moveToNext()) {
-            int flags = foldersCursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN);
-            if ((flags & UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) == 0) {
-                continue;
-            }
-            String uri = foldersCursor.getString(UIProvider.FOLDER_URI_COLUMN);
-            columnValues[FOLDERS_CURSOR_ID] = i++;
-            columnValues[FOLDERS_CURSOR_URI] = uri;
-            columnValues[FOLDERS_CURSOR_NAME] = foldersCursor
-                    .getString(UIProvider.FOLDER_NAME_COLUMN);
-            columnValues[FOLDERS_CURSOR_CHECKED] = 0; // 0 = unchecked
-            mFolderDialogCursor.addRow(columnValues);
-            mCheckedState.put(uri, false);
-        }
-        foldersCursor.close();
 
         if (!account.supportsCapability(UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
+            AlertDialog.Builder builder = new AlertDialog.Builder(context);
+            builder.setTitle("Change folders");
+            builder.setPositiveButton(R.string.ok, this);
+            builder.setNegativeButton(R.string.cancel, this);
+
+            // Get all of our folders
+            // TODO: Should only be folders that allow messages to be moved
+            // there!!
+            Cursor foldersCursor = context.getContentResolver().query(
+                    Uri.parse(account.folderListUri), UIProvider.FOLDERS_PROJECTION, null, null,
+                    null);
+            // Get the id, name, and a placeholder for check information
+            Object[] columnValues = new Object[FOLDER_DIALOG_PROJECTION.length];
+            mFolderDialogCursor = new MatrixCursor(FOLDER_DIALOG_PROJECTION);
+            int i = 0;
+            while (foldersCursor.moveToNext()) {
+                int flags = foldersCursor.getInt(UIProvider.FOLDER_CAPABILITIES_COLUMN);
+                if ((flags & UIProvider.FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) == 0) {
+                    continue;
+                }
+                String uri = foldersCursor.getString(UIProvider.FOLDER_URI_COLUMN);
+                columnValues[FOLDERS_CURSOR_ID] = i++;
+                columnValues[FOLDERS_CURSOR_URI] = uri;
+                columnValues[FOLDERS_CURSOR_NAME] = foldersCursor
+                        .getString(UIProvider.FOLDER_NAME_COLUMN);
+                columnValues[FOLDERS_CURSOR_CHECKED] = 1; // 0 = unchecked
+                mFolderDialogCursor.addRow(columnValues);
+                mCheckedState.put(uri, true);
+            }
+            foldersCursor.close();
             mSingle = true;
             builder.setSingleChoiceItems(mFolderDialogCursor, mCheckedItem,
                     UIProvider.FolderColumns.NAME, this);
+            mDialog = builder.create();
         } else {
-            builder.setMultiChoiceItems(mFolderDialogCursor, CHECKED_COLUMN_NAME,
-                    UIProvider.FolderColumns.NAME, this);
+            mSingle = false;
+            mDialog = new ApplyRemoveFolderDialog(context, commitListener, account);
         }
-        mDialog = builder.create();
     }
 
     public void show() {
@@ -144,10 +146,11 @@
     public void onClick(DialogInterface dialog, int which, boolean isChecked) {
         mFolderDialogCursor.moveToPosition(which);
         if (mSingle) {
+            // Clear any other checked items.
             mCheckedState.clear();
-            mCheckedState.put(mFolderDialogCursor.getString(FOLDERS_CURSOR_URI), true);
-        } else {
-            mCheckedState.put(mFolderDialogCursor.getString(FOLDERS_CURSOR_URI), isChecked);
+            isChecked = true;
         }
+        mCheckedState.put(mFolderDialogCursor.getString(FOLDERS_CURSOR_URI), isChecked);
+        mDialog.getListView().setItemChecked(which, false);
     }
 }