Home | History | Annotate | Download | only in ui
      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