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 60 public FolderRow(Folder folder, boolean isSelected) { 61 mFolder = folder; 62 mIsSelected = isSelected; 63 } 64 65 public Folder getFolder() { 66 return mFolder; 67 } 68 69 public boolean isSelected() { 70 return mIsSelected; 71 } 72 73 public void setIsSelected(boolean isSelected) { 74 mIsSelected = isSelected; 75 } 76 77 @Override 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 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 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 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 159 TreeNode(FolderRow wrappedObject) { 160 mWrappedObject = wrappedObject; 161 } 162 163 void addChild(final TreeNode child) { 164 mChildren.add(child); 165 } 166 167 TreeNode pollChild() { 168 return mChildren.poll(); 169 } 170 171 @Override 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 */ 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 */ 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 285 public int getCount() { 286 return mFolderRows.size(); 287 } 288 289 @Override 290 public Object getItem(int position) { 291 return mFolderRows.get(position); 292 } 293 294 @Override 295 public long getItemId(int position) { 296 return position; 297 } 298 299 @Override 300 public int getItemViewType(int position) { 301 return SeparatedFolderListAdapter.TYPE_ITEM; 302 } 303 304 @Override 305 public int getViewTypeCount() { 306 return 1; 307 } 308 309 @Override 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