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 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