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);
}
}