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