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