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