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 
18 package com.android.mail.ui;
19 
20 import com.android.mail.R;
21 import com.android.mail.providers.Folder;
22 import com.android.mail.providers.UIProvider.FolderCapabilities;
23 import com.android.mail.utils.Utils;
24 import com.google.common.base.Objects;
25 import com.google.common.collect.Lists;
26 
27 import android.content.Context;
28 import android.database.Cursor;
29 import android.net.Uri;
30 import android.text.TextUtils;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.BaseAdapter;
35 import android.widget.CheckedTextView;
36 import android.widget.CompoundButton;
37 import android.widget.ImageView;
38 import android.widget.TextView;
39 
40 import java.util.ArrayDeque;
41 import java.util.ArrayList;
42 import java.util.Deque;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.PriorityQueue;
47 import 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  */
53 public 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 
FolderRow(Folder folder, boolean isSelected)61         public FolderRow(Folder folder, boolean isSelected) {
62             mFolder = folder;
63             mIsSelected = isSelected;
64         }
65 
getFolder()66         public Folder getFolder() {
67             return mFolder;
68         }
69 
isSelected()70         public boolean isSelected() {
71             return mIsSelected;
72         }
73 
setIsSelected(boolean isSelected)74         public void setIsSelected(boolean isSelected) {
75             mIsSelected = isSelected;
76         }
77 
78         @Override
compareTo(FolderRow another)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 
FolderSelectorAdapter(Context context, Cursor folders, Set<String> selected, int layout)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 
FolderSelectorAdapter(Context context, Cursor folders, int layout, Folder excludedFolder)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 
createFolderRows(Cursor folders, Set<String> selected)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 
TreeNode(FolderRow wrappedObject)160         TreeNode(FolderRow wrappedObject) {
161             mWrappedObject = wrappedObject;
162         }
163 
addChild(final TreeNode child)164         void addChild(final TreeNode child) {
165             mChildren.add(child);
166         }
167 
pollChild()168         TreeNode pollChild() {
169             return mChildren.poll();
170         }
171 
172         @Override
compareTo(TreeNode o)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      */
folderSort(final List<FolderRow> folderList)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      */
meetsRequirements(Folder folder)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
getCount()286     public int getCount() {
287         return mFolderRows.size();
288     }
289 
290     @Override
getItem(int position)291     public Object getItem(int position) {
292         return mFolderRows.get(position);
293     }
294 
295     @Override
getItemId(int position)296     public long getItemId(int position) {
297         return position;
298     }
299 
300     @Override
getItemViewType(int position)301     public int getItemViewType(int position) {
302         return SeparatedFolderListAdapter.TYPE_ITEM;
303     }
304 
305     @Override
getViewTypeCount()306     public int getViewTypeCount() {
307         return 1;
308     }
309 
310     @Override
getView(int position, View convertView, ViewGroup parent)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