1/*******************************************************************************
2 *      Copyright (C) 2012 Google Inc.
3 *      Licensed to The Android Open Source Project.
4 *
5 *      Licensed under the Apache License, Version 2.0 (the "License");
6 *      you may not use this file except in compliance with the License.
7 *      You may obtain a copy of the License at
8 *
9 *           http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *      Unless required by applicable law or agreed to in writing, software
12 *      distributed under the License is distributed on an "AS IS" BASIS,
13 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *      See the License for the specific language governing permissions and
15 *      limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.ui;
19
20import com.android.mail.R;
21import com.android.mail.providers.Folder;
22import com.android.mail.providers.UIProvider.FolderCapabilities;
23import com.android.mail.utils.Utils;
24import com.google.common.base.Objects;
25import com.google.common.collect.Lists;
26
27import android.content.Context;
28import android.database.Cursor;
29import android.net.Uri;
30import android.text.TextUtils;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.BaseAdapter;
35import android.widget.CheckedTextView;
36import android.widget.CompoundButton;
37import android.widget.ImageView;
38import android.widget.TextView;
39
40import java.util.ArrayDeque;
41import java.util.ArrayList;
42import java.util.Deque;
43import java.util.HashMap;
44import java.util.List;
45import java.util.Map;
46import java.util.PriorityQueue;
47import java.util.Set;
48
49/**
50 * An adapter for translating a cursor of {@link Folder} to a set of selectable views to be used for
51 * applying folders to one or more conversations.
52 */
53public class FolderSelectorAdapter extends BaseAdapter {
54
55    public static class FolderRow implements Comparable<FolderRow> {
56        private final Folder mFolder;
57        private boolean mIsSelected;
58        // Filled in during folderSort
59        public String mPathName;
60
61        public FolderRow(Folder folder, boolean isSelected) {
62            mFolder = folder;
63            mIsSelected = isSelected;
64        }
65
66        public Folder getFolder() {
67            return mFolder;
68        }
69
70        public boolean isSelected() {
71            return mIsSelected;
72        }
73
74        public void setIsSelected(boolean isSelected) {
75            mIsSelected = isSelected;
76        }
77
78        @Override
79        public int compareTo(FolderRow another) {
80            // TODO: this should sort the system folders in the appropriate order
81            if (equals(another)) {
82                return 0;
83            } else {
84                return mFolder.name.compareToIgnoreCase(another.mFolder.name);
85            }
86        }
87
88    }
89
90    protected final List<FolderRow> mFolderRows = Lists.newArrayList();
91    private final LayoutInflater mInflater;
92    private final int mLayout;
93    private Folder mExcludedFolder;
94
95    public FolderSelectorAdapter(Context context, Cursor folders,
96            Set<String> selected, int layout) {
97        mInflater = LayoutInflater.from(context);
98        mLayout = layout;
99        createFolderRows(folders, selected);
100    }
101
102    public FolderSelectorAdapter(Context context, Cursor folders,
103            int layout, Folder excludedFolder) {
104        mInflater = LayoutInflater.from(context);
105        mLayout = layout;
106        mExcludedFolder = excludedFolder;
107        createFolderRows(folders, null);
108    }
109
110    protected void createFolderRows(Cursor folders, Set<String> selected) {
111        if (folders == null) {
112            return;
113        }
114        final List<FolderRow> allFolders = new ArrayList<FolderRow>(folders.getCount());
115
116        // Rows corresponding to user created, unchecked folders.
117        final List<FolderRow> userFolders = new ArrayList<FolderRow>();
118        // Rows corresponding to system created, unchecked folders.
119        final List<FolderRow> systemFolders = new ArrayList<FolderRow>();
120
121        if (folders.moveToFirst()) {
122            do {
123                final Folder folder = new Folder(folders);
124                final boolean isSelected = selected != null
125                        && selected.contains(
126                        folder.folderUri.getComparisonUri().toString());
127                final FolderRow row = new FolderRow(folder, isSelected);
128                allFolders.add(row);
129
130                // Add system folders here since we want the original unsorted order (for now..)
131                if (meetsRequirements(folder) && !Objects.equal(folder, mExcludedFolder) &&
132                        folder.isProviderFolder()) {
133                    systemFolders.add(row);
134                }
135            } while (folders.moveToNext());
136        }
137        // Need to do the foldersort first with all folders present to avoid dropping orphans
138        folderSort(allFolders);
139
140        // Divert the folders to the appropriate sections
141        for (final FolderRow row : allFolders) {
142            final Folder folder = row.getFolder();
143            if (meetsRequirements(folder) && !Objects.equal(folder, mExcludedFolder) &&
144                    !folder.isProviderFolder()) {
145                userFolders.add(row);
146            }
147        }
148        mFolderRows.addAll(systemFolders);
149        mFolderRows.addAll(userFolders);
150    }
151
152    /**
153     * Wrapper class to construct a hierarchy tree of FolderRow objects for sorting
154     */
155    private static class TreeNode implements Comparable<TreeNode> {
156        public FolderRow mWrappedObject;
157        final public PriorityQueue<TreeNode> mChildren = new PriorityQueue<TreeNode>();
158        public boolean mAddedToList = false;
159
160        TreeNode(FolderRow wrappedObject) {
161            mWrappedObject = wrappedObject;
162        }
163
164        void addChild(final TreeNode child) {
165            mChildren.add(child);
166        }
167
168        TreeNode pollChild() {
169            return mChildren.poll();
170        }
171
172        @Override
173        public int compareTo(TreeNode o) {
174            // mWrappedObject is always non-null here because we set it before we add this object
175            // to a sorted collection, otherwise we wouldn't have known what collection to add it to
176            return mWrappedObject.compareTo(o.mWrappedObject);
177        }
178    }
179
180    /**
181     * Sorts the folder list according to hierarchy.
182     * If no parent information exists this basically just turns into a heap sort
183     *
184     * How this works:
185     * When the first part of this algorithm completes, we want to have a tree of TreeNode objects
186     * mirroring the hierarchy of mailboxes/folders in the user's account, but we don't have any
187     * guarantee that we'll see the parents before their respective children.
188     * First we check the nodeMap to see if we've already pre-created (see below) a TreeNode for
189     * the current FolderRow, and if not then we create one now.
190     * Then for each folder, we check to see if the parent TreeNode has already been created. We
191     * special case the root node. If we don't find the parent node, then we pre-create one to fill
192     * in later (see above) when we eventually find the parent's entry.
193     * Whenever we create a new TreeNode we add it to the nodeMap keyed on the folder's provider
194     * Uri, so that we can find it later either to add children or to retrieve a half-created node.
195     * It should be noted that it is only valid to add a child node after the mWrappedObject
196     * member variable has been set.
197     * Finally we do a depth-first traversal of the constructed tree to re-fill the folderList in
198     * hierarchical order.
199     * @param folderList List of {@link Folder} objects to sort
200     */
201    private void folderSort(final List<FolderRow> folderList) {
202        final TreeNode root = new TreeNode(null);
203        // Make double-sure we don't accidentally add the root node to the final list
204        root.mAddedToList = true;
205        // Map from folder Uri to TreeNode containing said folder
206        final Map<Uri, TreeNode> nodeMap = new HashMap<Uri, TreeNode>(folderList.size());
207        nodeMap.put(Uri.EMPTY, root);
208
209        for (final FolderRow folderRow : folderList) {
210            final Folder folder = folderRow.mFolder;
211            // Find-and-complete or create the TreeNode wrapper
212            TreeNode node = nodeMap.get(folder.folderUri.getComparisonUri());
213            if (node == null) {
214                node = new TreeNode(folderRow);
215                nodeMap.put(folder.folderUri.getComparisonUri(), node);
216            } else {
217                node.mWrappedObject = folderRow;
218            }
219            // Special case the top level folders
220            if (Utils.isEmpty(folderRow.mFolder.parent)) {
221                root.addChild(node);
222            } else {
223                // Find or half-create the parent TreeNode wrapper
224                TreeNode parentNode = nodeMap.get(folder.parent);
225                if (parentNode == null) {
226                    parentNode = new TreeNode(null);
227                    nodeMap.put(folder.parent, parentNode);
228                }
229                parentNode.addChild(node);
230            }
231        }
232
233        folderList.clear();
234
235        // Depth-first traversal of the constructed tree. Flattens the tree back into the
236        // folderList list and sets mPathName in the FolderRow objects
237        final Deque<TreeNode> stack = new ArrayDeque<TreeNode>(10);
238        stack.push(root);
239        TreeNode currentNode;
240        while ((currentNode = stack.poll()) != null) {
241            final TreeNode parentNode = stack.peek();
242            // If parentNode is null then currentNode is the root node (not a real folder)
243            // If mAddedToList is true it means we've seen this node before and just want to
244            // iterate the children.
245            if (parentNode != null && !currentNode.mAddedToList) {
246                final String pathName;
247                // If the wrapped object is null then the parent is the root
248                if (parentNode.mWrappedObject == null ||
249                        TextUtils.isEmpty(parentNode.mWrappedObject.mPathName)) {
250                    pathName = currentNode.mWrappedObject.mFolder.name;
251                } else {
252                    /**
253                     * This path name is re-split at / characters in
254                     * {@link HierarchicalFolderSelectorAdapter#truncateHierarchy}
255                     */
256                    pathName = parentNode.mWrappedObject.mPathName + "/"
257                            + currentNode.mWrappedObject.mFolder.name;
258                }
259                currentNode.mWrappedObject.mPathName = pathName;
260                folderList.add(currentNode.mWrappedObject);
261                // Mark this node as done so we don't re-add it
262                currentNode.mAddedToList = true;
263            }
264            final TreeNode childNode = currentNode.pollChild();
265            if (childNode != null) {
266                // If we have children to deal with, re-push the current node as the parent...
267                stack.push(currentNode);
268                // ... then add the child node and loop around to deal with it...
269                stack.push(childNode);
270            }
271            // ... otherwise we're done with currentNode
272        }
273    }
274
275    /**
276     * Return whether the supplied folder meets the requirements to be displayed
277     * in the folder list.
278     */
279    protected boolean meetsRequirements(Folder folder) {
280        // We only want to show the non-Trash folders that can accept moved messages
281        return folder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES) &&
282                !folder.isTrash() && !Objects.equal(folder, mExcludedFolder);
283    }
284
285    @Override
286    public int getCount() {
287        return mFolderRows.size();
288    }
289
290    @Override
291    public Object getItem(int position) {
292        return mFolderRows.get(position);
293    }
294
295    @Override
296    public long getItemId(int position) {
297        return position;
298    }
299
300    @Override
301    public int getItemViewType(int position) {
302        return SeparatedFolderListAdapter.TYPE_ITEM;
303    }
304
305    @Override
306    public int getViewTypeCount() {
307        return 1;
308    }
309
310    @Override
311    public View getView(int position, View convertView, ViewGroup parent) {
312        final View view;
313        if (convertView == null) {
314            view = mInflater.inflate(mLayout, parent, false);
315        } else {
316            view = convertView;
317        }
318        final FolderRow row = (FolderRow) getItem(position);
319        final Folder folder = row.getFolder();
320        final String folderDisplay = !TextUtils.isEmpty(row.mPathName) ?
321                row.mPathName : folder.name;
322        final CheckedTextView checkBox = (CheckedTextView) view.findViewById(R.id.checkbox);
323        if (checkBox != null) {
324            // Suppress the checkbox selection, and handle the toggling of the
325            // folder on the parent list item's click handler.
326            checkBox.setClickable(false);
327            checkBox.setText(folderDisplay);
328            checkBox.setChecked(row.isSelected());
329        }
330        final TextView display = (TextView) view.findViewById(R.id.folder_name);
331        if (display != null) {
332            display.setText(folderDisplay);
333        }
334
335        final ImageView folderIcon = (ImageView) view.findViewById(R.id.folder_icon);
336        Folder.setIcon(folder, folderIcon);
337        return view;
338    }
339}
340