Fist cut of nested folders

1. Rename FolderListFragment.FolderListSelectionListener to
   FolderSelector.

2. Allow special views to be tapped; all existing special views
   disallow taps.

3. Allow nested folders in ConversationListFragment. The adapter is
   responsible for populating the nested folders above the
   conversations.

4. Add a nested folder special item for the conversation list. This
   reuses current resources and is particularly ugly.  The ugliness
   will be fixed once we have a UX spec and real assets.

5. The child folders are loaded through an ObjectCursorLoader in the
   ConversationListFragment.

Change-Id: I5eb566d7a1f87c1a11fc6961378d00650a27007d
diff --git a/res/layout/nested_folder.xml b/res/layout/nested_folder.xml
new file mode 100644
index 0000000..d93765b
--- /dev/null
+++ b/res/layout/nested_folder.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- View that displays folders that are contained in a folder. These are shown at the top of
+     the conversation list. Email has them, Gmail doesn't currently. -->
+<com.android.mail.ui.NestedFolderView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:minHeight="@dimen/folder_list_item_minimum_height"
+        android:background="@drawable/folder_item" >
+
+    <!--This is a rough layout. We don't have UX specs yet, so all the values are hardcoded.
+        Also, it looks totally ugly. The ugliness is intentional.-->
+    <RelativeLayout
+            android:id="@+id/swipeable_content"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            >
+
+        <ImageView
+                android:id="@+id/nested_folder_icon"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginLeft="16dp"
+                android:layout_marginRight="16dp"
+                android:src="@drawable/ic_menu_folders_holo_light"
+                android:layout_alignParentLeft="true"
+                android:contentDescription="@string/folder_icon_desc"
+                android:layout_centerVertical="true"
+                />
+        <TextView
+                android:layout_width="wrap_content"
+                android:id="@+id/nested_folder_name"
+                android:includeFontPadding="false"
+                android:maxLines="2"
+                android:ellipsize="end"
+                android:textColor="@color/folder_item_text_color"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:layout_centerVertical="true"
+                android:layout_height="match_parent"
+                android:layout_toRightOf="@id/nested_folder_icon" />
+
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:id="@+id/nested_folder_unread"
+                style="@style/UnreadCount"
+                android:layout_alignBaseline="@id/nested_folder_name"
+                android:layout_alignParentRight="true"
+                android:layout_marginRight="16dp"
+                />
+    </RelativeLayout>
+</com.android.mail.ui.NestedFolderView>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4c6ed01..86e05e9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -911,4 +911,6 @@
     <string name="drawer_close">Close navigation drawer</string>
 
     <string name="conversation_photo_welcome_text">Touch a sender image to select that conversation.</string>
+    <!-- Content description for the folder icon for nested folders. -->
+    <string name="folder_icon_desc">Folder icon</string>
 </resources>
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index 580679c..993d5d4 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -58,8 +58,8 @@
  */
 public interface ActivityController extends LayoutListener,
         ModeChangeListener, ConversationListCallbacks,
-        FolderChangeListener, ConversationSetObserver, ConversationListener,
-        FolderListFragment.FolderListSelectionListener, HelpCallback, UndoListener,
+        FolderChangeListener, ConversationSetObserver, ConversationListener, FolderSelector,
+        HelpCallback, UndoListener,
         ConversationUpdater, ErrorListener, FolderController, AccountController,
         ConversationPositionTracker.Callbacks, ConversationListFooterView.FooterViewClickListener,
         RecentFolderController, UpOrBackController {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index fd387a6..1eba5a0 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -23,6 +23,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.Handler;
@@ -37,6 +38,7 @@
 import com.android.mail.browse.ConversationItemView;
 import com.android.mail.browse.ConversationItemViewCoordinates;
 import com.android.mail.browse.SwipeableConversationItemView;
+import com.android.mail.content.ObjectCursor;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.AccountObserver;
 import com.android.mail.providers.Conversation;
@@ -46,10 +48,13 @@
 import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
+
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -134,10 +139,11 @@
 
     /**
      * The next action to perform. Do not read or write this. All accesses should
-     * be in {@link #performAndSetNextAction(DestructiveAction)} which commits the
-     * previous action, if any.
+     * be in {@link #performAndSetNextAction(SwipeableListView.ListItemsRemovedListener)} which
+     * commits the previous action, if any.
      */
     private ListItemsRemovedListener mPendingDestruction;
+
     /**
      * A destructive action that refreshes the list and performs no other action.
      */
@@ -169,33 +175,38 @@
         }
     };
 
-    private final List<ConversationSpecialItemView> mSpecialViews;
-    private final SparseArray<ConversationSpecialItemView> mSpecialViewPositions;
+    /**
+     * A list of all views that are not conversations. These include temporary views from
+     * {@link #mFleetingViews} and child folders from {@link #mFolderViews}.
+     */
+    private final SparseArray<ConversationSpecialItemView> mSpecialViews;
 
     private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache =
             new SparseArray<ConversationItemViewCoordinates>();
 
-    private final void setAccount(Account newAccount) {
+    /**
+     * Temporary views insert at specific positions relative to conversations. These can be
+     * related to showing new features (on-boarding) or showing information about new mailboxes
+     * that have been added by the system.
+     */
+    private final List<ConversationSpecialItemView> mFleetingViews;
+
+    /** List of all child folders for this folder. */
+    private List<NestedFolderView> mFolderViews;
+
+    private void setAccount(Account newAccount) {
         mAccount = newAccount;
         mPriorityMarkersEnabled = mAccount.settings.priorityArrowsEnabled;
         mSwipeEnabled = mAccount.supportsCapability(UIProvider.AccountCapabilities.UNDO);
     }
 
-    /**
-     * Used only for debugging.
-     */
     private static final String LOG_TAG = LogTag.getLogTag();
     private static final int INCREASE_WAIT_COUNT = 2;
 
     public AnimatedAdapter(Context context, ConversationCursor cursor,
             ConversationSelectionSet batch, ControllableActivity activity,
-            SwipeableListView listView) {
-        this(context, cursor, batch, activity, listView, null);
-    }
-
-    public AnimatedAdapter(Context context, ConversationCursor cursor,
-            ConversationSelectionSet batch, ControllableActivity activity,
-            SwipeableListView listView, final List<ConversationSpecialItemView> specialViews) {
+            SwipeableListView listView, final List<ConversationSpecialItemView> specialViews,
+            final ObjectCursor<Folder> childFolders) {
         super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
         mContext = context;
         mBatchConversations = batch;
@@ -203,27 +214,61 @@
         mActivity = activity;
         mShowFooter = false;
         mListView = listView;
+        mFolderViews = getNestedFolders(childFolders);
+
         mHandler = new Handler();
         if (sDismissAllShortDelay == -1) {
-            sDismissAllShortDelay =
-                    context.getResources()
-                        .getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
-            sDismissAllLongDelay =
-                    context.getResources()
-                        .getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
+            final Resources r = context.getResources();
+            sDismissAllShortDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_short_delay);
+            sDismissAllLongDelay = r.getInteger(R.integer.dismiss_all_leavebehinds_long_delay);
         }
-        mSpecialViews =
-                specialViews == null ? new ArrayList<ConversationSpecialItemView>(0)
-                        : new ArrayList<ConversationSpecialItemView>(specialViews);
-        mSpecialViewPositions = new SparseArray<ConversationSpecialItemView>(mSpecialViews.size());
+        if (specialViews != null) {
+            mFleetingViews = new ArrayList<ConversationSpecialItemView>(specialViews);
+        } else {
+            mFleetingViews = new ArrayList<ConversationSpecialItemView>(0);
+        }
+        /** Total number of special views */
+        final int size = mFleetingViews.size() + mFolderViews.size();
+        mSpecialViews = new SparseArray<ConversationSpecialItemView>(size);
 
-        for (final ConversationSpecialItemView view : mSpecialViews) {
+        // Only set the adapter in teaser views. Folder views don't care about the adapter.
+        for (final ConversationSpecialItemView view : mFleetingViews) {
             view.setAdapter(this);
         }
-
         updateSpecialViews();
     }
 
+    /**
+     * Returns a list containing views for all the nested folders.
+     * @param cursor cursor containing the folders nested within the current folder
+     * @return a list, possibly empty of the views representing the folders.
+     */
+    private List<NestedFolderView> getNestedFolders (final ObjectCursor<Folder> cursor) {
+        if (cursor == null || !cursor.moveToFirst()) {
+            // The cursor has nothing valid.  Return an empty list.
+            return ImmutableList.of();
+        }
+
+        final LayoutInflater inflater = LayoutInflater.from(mContext);
+        final List<NestedFolderView> folders = new ArrayList<NestedFolderView>(cursor.getCount());
+        do {
+            final NestedFolderView view =
+                    (NestedFolderView) inflater.inflate(R.layout.nested_folder, null);
+            view.setFolder(cursor.getModel());
+            folders.add(view);
+        } while (cursor.moveToNext());
+        return folders;
+    }
+
+    /**
+     * Updates the list of folders for the current list with the cursor provided here.
+     * @param childFolders A cursor containing child folders for the current folder.
+     */
+    public void updateNestedFolders (ObjectCursor<Folder> childFolders) {
+        mFolderViews = getNestedFolders(childFolders);
+        notifyDataSetChanged();
+    }
+
     public void cancelDismissCounter() {
         cancelLeaveBehindFadeInAnimation();
         mHandler.removeCallbacks(mCountDown);
@@ -245,8 +290,8 @@
 
     @Override
     public int getCount() {
-        // mSpecialViewPositions only contains the views that are currently being displayed
-        final int specialViewCount = mSpecialViewPositions.size();
+        // mSpecialViews only contains the views that are currently being displayed
+        final int specialViewCount = mSpecialViews.size();
 
         final int count = super.getCount() + specialViewCount;
         return mShowFooter ? count + 1 : count;
@@ -339,7 +384,7 @@
             // types. In a future release, use position/id map to try to make
             // this cleaner / faster to determine if the view is animating.
             return TYPE_VIEW_DONT_RECYCLE;
-        } else if (mSpecialViewPositions.get(position) != null) {
+        } else if (mSpecialViews.get(position) != null) {
             // Don't recycle the special views
             return TYPE_VIEW_DONT_RECYCLE;
         }
@@ -412,12 +457,12 @@
         }
 
         // Check if this is a special view
-        final View specialView = (View) mSpecialViewPositions.get(position);
+        final View specialView = (View) mSpecialViews.get(position);
         if (specialView != null) {
             return specialView;
         }
 
-        ConversationCursor cursor = (ConversationCursor) getItem(position);
+        final ConversationCursor cursor = (ConversationCursor) getItem(position);
         final Conversation conv = cursor.getConversation();
 
         // Notify the provider of this change in the position of Conversation cursor
@@ -616,7 +661,7 @@
     @Override
     public long getItemId(int position) {
         if (mShowFooter && position == getCount() - 1
-                || mSpecialViewPositions.get(position) != null) {
+                || mSpecialViews.get(position) != null) {
             return -1;
         }
         final int cursorPos = position - getPositionOffset(position);
@@ -668,9 +713,7 @@
 
     @Override
     public View newView(Context context, Cursor cursor, ViewGroup parent) {
-        SwipeableConversationItemView view = new SwipeableConversationItemView(context,
-                mAccount.name);
-        return view;
+        return new SwipeableConversationItemView(context, mAccount.name);
     }
 
     @Override
@@ -700,8 +743,8 @@
     public Object getItem(int position) {
         if (mShowFooter && position == getCount() - 1) {
             return mFooter;
-        } else if (mSpecialViewPositions.get(position) != null) {
-            return mSpecialViewPositions.get(position);
+        } else if (mSpecialViews.get(position) != null) {
+            return mSpecialViews.get(position);
         }
         return super.getItem(position - getPositionOffset(position));
     }
@@ -739,7 +782,7 @@
      * @param next The next action that is to be performed, possibly null (if no next action is
      * needed).
      */
-    private final void performAndSetNextAction(ListItemsRemovedListener next) {
+    private void performAndSetNextAction(ListItemsRemovedListener next) {
         if (mPendingDestruction != null) {
             mPendingDestruction.onListItemsRemoved();
         }
@@ -763,28 +806,21 @@
 
     @Override
     public boolean areAllItemsEnabled() {
-        // The animating positions are not enabled.
+        // The animating items and some special views are not enabled.
         return false;
     }
 
     @Override
     public boolean isEnabled(final int position) {
-        if (mSpecialViewPositions.get(position) != null) {
-            // This is a special view
-            return false;
+        final ConversationSpecialItemView view = mSpecialViews.get(position);
+        if (view != null) {
+            final boolean enabled = view.acceptsUserTaps();
+            LogUtils.d(LOG_TAG, "AA.isEnabled(%d) = %b", position, enabled);
+            return enabled;
         }
-
         return !isPositionDeleting(position) && !isPositionUndoing(position);
     }
 
-    public void showFooter() {
-        setFooterVisibility(true);
-    }
-
-    public void hideFooter() {
-        setFooterVisibility(false);
-    }
-
     public void setFooterVisibility(boolean show) {
         if (mShowFooter != show) {
             mShowFooter = show;
@@ -836,8 +872,8 @@
     public void onRestoreInstanceState(Bundle outState) {
         if (outState.containsKey(LAST_DELETING_ITEMS)) {
             final long[] lastDeleting = outState.getLongArray(LAST_DELETING_ITEMS);
-            for (int i = 0; i < lastDeleting.length; i++) {
-                mLastDeletingItems.add(lastDeleting[i]);
+            for (final long aLastDeleting : lastDeleting) {
+                mLastDeletingItems.add(aLastDeleting);
             }
         }
         if (outState.containsKey(LEAVE_BEHIND_ITEM_DATA)) {
@@ -910,24 +946,40 @@
         }
     }
 
+    /**
+     * Updates special (non-conversation view) when either {@link #mFolderViews} or
+     * {@link #mFleetingViews} changed
+     */
     private void updateSpecialViews() {
-        mSpecialViewPositions.clear();
+        // We recreate all the special views using mFolderViews and mFleetingViews (in that order).
+        mSpecialViews.clear();
 
-        for (int i = 0; i < mSpecialViews.size(); i++) {
-            final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+        int folderCount = 0;
+        // Nested folders are added initially. They don't specify positions: we put them at the
+        // very top.
+        for (final NestedFolderView view : mFolderViews) {
+            mSpecialViews.put(folderCount, view);
+            folderCount++;
+        }
+
+        // Fleeting (temporary) views go after this. They specify a position,which is 0-indexed and
+        // has to be adjusted for the number of folders above it.
+        for (final ConversationSpecialItemView specialView : mFleetingViews) {
             specialView.onUpdate(mAccount.name, mFolder, getConversationCursor());
 
             if (specialView.getShouldDisplayInList()) {
-                int position = specialView.getPosition();
+                // If the special view asks for position 0, it wants to be at the top. However,
+                // if there are already 3 folders above it, the real position it needs is 0+3 (4th
+                // from top, since everything is 0-indexed).
+                int position = (specialView.getPosition() + folderCount);
 
                 // insert the special view into the position, but if there is
                 // already an item occupying that position, move that item back
                 // one position, and repeat
                 ConversationSpecialItemView insert = specialView;
                 while (insert != null) {
-                    final ConversationSpecialItemView kickedOut = mSpecialViewPositions.get(
-                            position);
-                    mSpecialViewPositions.put(position, insert);
+                    final ConversationSpecialItemView kickedOut = mSpecialViews.get(position);
+                    mSpecialViews.put(position, insert);
                     insert = kickedOut;
                     position++;
                 }
@@ -968,9 +1020,8 @@
     public int getPositionOffset(final int position) {
         int offset = 0;
 
-        for (int i = 0; i < mSpecialViewPositions.size(); i++) {
-            final int key = mSpecialViewPositions.keyAt(i);
-            final ConversationSpecialItemView specialView = mSpecialViewPositions.get(key);
+        for (int i = 0, size = mSpecialViews.size(); i < size; i++) {
+            final int key = mSpecialViews.keyAt(i);
             if (key <= position) {
                 offset++;
             }
@@ -980,14 +1031,15 @@
     }
 
     public void cleanup() {
-        for (final ConversationSpecialItemView view : mSpecialViews) {
+        // Only clean up teaser views. Folder views don't care about clean up.
+        for (final ConversationSpecialItemView view : mFleetingViews) {
             view.cleanup();
         }
     }
 
     public void onConversationSelected() {
-        for (int i = 0; i < mSpecialViews.size(); i++) {
-            final ConversationSpecialItemView specialView = mSpecialViews.get(i);
+        // Only notify teaser views. Folder views don't care about selected conversations.
+        for (final ConversationSpecialItemView specialView : mFleetingViews) {
             specialView.onConversationSelected();
         }
     }
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index fae8665..9bd563b 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -72,7 +72,7 @@
      * fragment so that activity controllers can track the last folder list
      * pushed for hierarchical folders.
      */
-    FolderListFragment.FolderListSelectionListener getFolderListSelectionListener();
+    FolderSelector getFolderSelector();
 
     /**
      * Get the folder currently being accessed by the activity.
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 16fc684..b11cadd 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -21,15 +21,15 @@
 
 import android.app.Activity;
 import android.app.ListFragment;
+import android.app.LoaderManager;
 import android.content.Context;
+import android.content.Loader;
 import android.content.res.Resources;
 import android.database.DataSetObserver;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.text.format.DateUtils;
-import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -46,6 +46,8 @@
 import com.android.mail.browse.ConversationItemViewModel;
 import com.android.mail.browse.ConversationListFooterView;
 import com.android.mail.browse.ToggleableItem;
+import com.android.mail.content.ObjectCursor;
+import com.android.mail.content.ObjectCursorLoader;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.AccountObserver;
 import com.android.mail.providers.Conversation;
@@ -147,6 +149,52 @@
     private int mConversationCursorHash;
 
     /**
+     * If the current list is for a folder with children, this set of loader callbacks will
+     * create a loader for all the child folders, and will return an {@link ObjectCursor} over the
+     * list.
+     */
+    private final class ChildFolderLoads
+            implements LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
+        /** Load all child folders for the current folder. */
+        private static final int LOADER_CHIDREN = 0;
+        public static final String CHILD_URI = "arg-child-uri";
+        private final String[] projection = UIProvider.FOLDERS_PROJECTION;
+
+        @Override
+        public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
+            if (id != LOADER_CHIDREN) {
+                throw new IllegalStateException("ChildFolderLoads loading ID=" + id);
+            }
+            final Uri childUri = Uri.parse(args.getString(CHILD_URI));
+            return new ObjectCursorLoader<Folder>(
+                    getActivity(), childUri, projection, Folder.FACTORY);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
+            if (data != null && data.getCount() >= 0 && mListAdapter != null) {
+                mListAdapter.updateNestedFolders(data);
+            }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
+            // Do nothing.
+        }
+    }
+
+    /** Callbacks to handle creating a loader and receiving child folders from it. */
+    private final ChildFolderLoads mChildCallback = new ChildFolderLoads();
+
+    /**
+     * Include all the folders at the cursor provided here in the conversation list.
+     * @param cursor The cursor containing child folders for the current folder.
+     */
+    private void showChildFolders(ObjectCursor<Folder> cursor) {
+
+    }
+
+    /**
      * Constructor needs to be public to handle orientation changes and activity
      * lifecycle events.
      */
@@ -265,26 +313,35 @@
         mFooterView.setClickListener(mActivity);
         mConversationListView.setActivity(mActivity);
         final ConversationCursor conversationCursor = getConversationListCursor();
+        final LoaderManager manager = getLoaderManager();
+
+        // If this a parent folder, load all the child folders.
+        if (mViewContext.folder.hasChildren) {
+            final Uri childUri = mViewContext.folder.childFoldersListUri;
+            final Bundle args = new Bundle();
+            args.putString(ChildFolderLoads.CHILD_URI, childUri.toString());
+            manager.initLoader(ChildFolderLoads.LOADER_CHIDREN, args, mChildCallback);
+        }
 
         final ConversationListHelper helper = mActivity.getConversationListHelper();
         final List<ConversationSpecialItemView> specialItemViews = helper != null ?
                 ImmutableList.copyOf(helper.makeConversationListSpecialViews(
-                        getActivity(), mAccount, mActivity.getFolderListSelectionListener()))
+                        activity, mAccount, mActivity.getFolderSelector()))
                 : null;
         if (specialItemViews != null) {
             // Attach to the LoaderManager
             for (final ConversationSpecialItemView view : specialItemViews) {
-                view.bindLoaderManager(getLoaderManager());
+                view.bindLoaderManager(manager);
             }
         }
 
         mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
-                        mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
+                        mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
         mListAdapter.addFooter(mFooterView);
         mListView.setAdapter(mListAdapter);
         mSelectedSet = mActivity.getSelectedSet();
         mListView.setSelectionSet(mSelectedSet);
-        mListAdapter.hideFooter();
+        mListAdapter.setFooterVisibility(false);
         mFolderObserver = new FolderObserver(){
             @Override
             public void onChanged(Folder newFolder) {
@@ -521,19 +578,22 @@
      */
     @Override
     public void onListItemClick(ListView l, View view, int position, long id) {
-        // Ignore anything that is not a conversation item. Could be a footer.
-        // If we are using a keyboard, the highlighted item is the parent;
-        // otherwise, this is a direct call from the ConverationItemView
-        if (!(view instanceof ToggleableItem)) {
-            return;
-        }
-        boolean showSenderImage = (mAccount.settings.convListIcon ==
-                ConversationListIcon.SENDER_IMAGE);
-        if (!showSenderImage && !mSelectedSet.isEmpty()) {
-            ToggleableItem v = (ToggleableItem) view;
-            v.toggleSelectedState();
+        if (view instanceof NestedFolderView) {
+            final FolderSelector selector = mActivity.getFolderSelector();
+            selector.onFolderSelected(((NestedFolderView) view).getFolder());
+        } else if (view instanceof ToggleableItem) {
+            final boolean showSenderImage =
+                    (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE);
+            if (!showSenderImage && !mSelectedSet.isEmpty()) {
+                ((ToggleableItem) view).toggleSelectedState();
+            } else {
+                viewConversation(position);
+            }
         } else {
-            viewConversation(position);
+            // Ignore anything that is not a conversation item. Could be a footer.
+            // If we are using a keyboard, the highlighted item is the parent;
+            // otherwise, this is a direct call from the ConverationItemView
+            return;
         }
         // When a new list item is clicked, commit any existing leave behind
         // items. Wait until we have opened the desired conversation to cause
diff --git a/src/com/android/mail/ui/ConversationListHelper.java b/src/com/android/mail/ui/ConversationListHelper.java
index 05c27d0..f7a6862 100644
--- a/src/com/android/mail/ui/ConversationListHelper.java
+++ b/src/com/android/mail/ui/ConversationListHelper.java
@@ -21,7 +21,6 @@
 import android.content.Context;
 
 import com.android.mail.providers.Account;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 
 import java.util.ArrayList;
 
@@ -30,7 +29,7 @@
      * Creates a list of newly created special views.
      */
     public ArrayList<ConversationSpecialItemView> makeConversationListSpecialViews(Context context,
-            Account account, FolderListSelectionListener listener) {
+            Account account, FolderSelector listener) {
         // TODO: Move conversation photo teaser view here once
         // getConversationListIcon() is moved out of Persistence
         return Lists.newArrayList();
diff --git a/src/com/android/mail/ui/ConversationPhotoTeaserView.java b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
index 84b710d..9d5f437 100644
--- a/src/com/android/mail/ui/ConversationPhotoTeaserView.java
+++ b/src/com/android/mail/ui/ConversationPhotoTeaserView.java
@@ -135,6 +135,12 @@
     }
 
     @Override
+    public boolean acceptsUserTaps() {
+        // No, we don't allow user taps.
+        return false;
+    }
+
+    @Override
     public void dismiss() {
         setDismissed();
         startDestroyAnimation();
diff --git a/src/com/android/mail/ui/ConversationSpecialItemView.java b/src/com/android/mail/ui/ConversationSpecialItemView.java
index 097641e..e0b221e 100644
--- a/src/com/android/mail/ui/ConversationSpecialItemView.java
+++ b/src/com/android/mail/ui/ConversationSpecialItemView.java
@@ -34,8 +34,17 @@
      */
     void onUpdate(String account, Folder folder, ConversationCursor cursor);
 
+    /**
+     * Returns whether this view is to be displayed in the list or not. A view can be added freely
+     * and it might decide to disable itself by returning false here.
+     * @return true if this view should be displayed, false otherwise.
+     */
     boolean getShouldDisplayInList();
 
+    /**
+     * Returns the position (0 indexed) where this element expects to be inserted.
+     * @return
+     */
     int getPosition();
 
     void setAdapter(AnimatedAdapter adapter);
@@ -51,4 +60,7 @@
      * Called when a regular conversation item was clicked.
      */
     void onConversationSelected();
+
+    /** Returns whether this special view is enabled (= accepts user taps). */
+    boolean acceptsUserTaps();
 }
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index a8a718a..120240c 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -103,7 +103,7 @@
     /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
     private ArrayList<Integer> mExcludedFolderTypes;
     /** Object that changes folders on our behalf. */
-    private FolderListSelectionListener mFolderChanger;
+    private FolderSelector mFolderChanger;
     /** Object that changes accounts on our behalf */
     private AccountController mAccountController;
 
@@ -312,7 +312,7 @@
                 setSelectedAccount(newAccount);
             }
         };
-        mFolderChanger = mActivity.getFolderListSelectionListener();
+        mFolderChanger = mActivity.getFolderSelector();
         if (accountController != null) {
             // Current account and its observer.
             setSelectedAccount(mAccountObserver.initialize(accountController));
@@ -1196,10 +1196,6 @@
         }
     }
 
-    public interface FolderListSelectionListener {
-        public void onFolderSelected(Folder folder);
-    }
-
     /**
      * Get whether the FolderListFragment is currently showing the hierarchy
      * under a single parent.
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
index 6a121fb..8b8d0a8 100644
--- a/src/com/android/mail/ui/FolderSelectionActivity.java
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -37,7 +37,6 @@
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Folder;
 import com.android.mail.providers.FolderWatcher;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
@@ -53,7 +52,7 @@
  */
 public class FolderSelectionActivity extends Activity implements OnClickListener,
         DialogInterface.OnClickListener, FolderChangeListener, ControllableActivity,
-        FolderListSelectionListener {
+        FolderSelector {
     public static final String EXTRA_ACCOUNT_SHORTCUT = "account-shortcut";
 
     private static final String LOG_TAG = LogTag.getLogTag();
@@ -364,7 +363,7 @@
     }
 
     @Override
-    public FolderListSelectionListener getFolderListSelectionListener() {
+    public FolderSelector getFolderSelector() {
         return this;
     }
 
diff --git a/src/com/android/mail/ui/FolderSelector.java b/src/com/android/mail/ui/FolderSelector.java
new file mode 100644
index 0000000..6523a34
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSelector.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.mail.ui;
+
+import com.android.mail.providers.Folder;
+
+/**
+ * Interface that permits elements to implement selecting a folder.
+ * The single method {@link #onFolderSelected(com.android.mail.providers.Folder)} defines what
+ * happens when a folder is selected.
+ */
+public interface FolderSelector {
+    /**
+     * Selects the folder provided as an argument here.  This corresponds to the user
+     * selecting a folder in the UI element, either for creating a widget/shortcut (as in the
+     * case of {@link FolderSelectionActivity} or for viewing the contents of
+     * the folder (as in the case of {@link AbstractActivityController}.
+     * @param folder
+     */
+    public void onFolderSelected(Folder folder);
+}
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index bc0c693..2cf2256 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -36,7 +36,6 @@
 
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.providers.Folder;
-import com.android.mail.ui.FolderListFragment.FolderListSelectionListener;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
 import com.android.mail.utils.StorageLowState;
 import com.android.mail.utils.Utils;
@@ -308,7 +307,7 @@
     }
 
     @Override
-    public FolderListSelectionListener getFolderListSelectionListener() {
+    public FolderSelector getFolderSelector() {
         return mController;
     }
 
diff --git a/src/com/android/mail/ui/NestedFolderView.java b/src/com/android/mail/ui/NestedFolderView.java
new file mode 100644
index 0000000..e9c3afb
--- /dev/null
+++ b/src/com/android/mail/ui/NestedFolderView.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.mail.ui;
+
+import com.android.mail.R;
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Folder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * For folders that might contain other folders, we show the nested folders within this view.
+ * Tapping on this opens the folder.
+ */
+public class NestedFolderView extends LinearLayout implements ConversationSpecialItemView,
+        SwipeableItemView {
+    protected static final String LOG_TAG = LogTag.getLogTag();
+    /**
+     * The actual view that is displayed and is perhaps swiped away. We don't allow swiping,
+     * but this is required by the {@link SwipeableItemView} interface.
+     */
+    private View mSwipeableContent;
+    /** The folder this view represents */
+    private Folder mFolder;
+
+    public NestedFolderView(Context context) {
+        super(context);
+    }
+
+    public NestedFolderView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NestedFolderView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSwipeableContent = findViewById(R.id.swipeable_content);
+    }
+
+    @Override
+    public void onUpdate(String s, Folder folder, ConversationCursor conversationCursor) {
+        // Do nothing. We don't care about the change to the conversation cursor here.
+        // Nested folders only care if they were removed from the parent folder,
+        // so supposing we should check for that here.
+    }
+
+    /**
+     * Sets the folder associated with this view. This method is meant to be called infrequently,
+     * since we assume that a new view will be created when the unread count changes.
+     * @param folder the folder that this view represents.
+     */
+    public void setFolder(Folder folder) {
+        mFolder = folder;
+        // Since we assume that setFolder will be called infrequently (once currently),
+        // we don't bother saving the textviews for folder name and folder unread count.  If we
+        // find that setFolder gets called repeatedly, it might be prudent to remember the
+        // references to these textviews, making setFolder slightly faster.
+        TextView t = (TextView) findViewById(R.id.nested_folder_name);
+        t.setText(folder.name);
+        t = (TextView) findViewById(R.id.nested_folder_unread);
+        t.setText("" + folder.unreadCount);
+    }
+
+    /**
+     * Returns the folder associated with this view
+     * @return a folder that this view represents.
+     */
+    public Folder getFolder() {
+        return mFolder;
+    }
+
+    @Override
+    public boolean getShouldDisplayInList() {
+        // Nested folders once created are always displayed in the list.
+        return true;
+    }
+
+    @Override
+    public int getPosition() {
+        // We only have one element, and that's always at the top for now.
+        return 0;
+    }
+
+    @Override
+    public void setAdapter(AnimatedAdapter animatedAdapter) {
+        // Do nothing, since the adapter creates these views.
+    }
+
+    @Override
+    public void bindLoaderManager(LoaderManager loaderManager) {
+        // Do nothing. We don't need the loader manager.
+    }
+
+    @Override
+    public void cleanup() {
+        // Do nothing.
+    }
+
+    @Override
+    public void onConversationSelected() {
+        // Do nothing. We don't care if conversations are selected.
+    }
+
+    @Override
+    public boolean acceptsUserTaps() {
+        return true;
+    }
+
+    @Override
+    public SwipeableView getSwipeableView() {
+        return SwipeableView.from(mSwipeableContent);
+    }
+
+    @Override
+    public boolean canChildBeDismissed() {
+        // The folders can never be dismissed, return false.
+        return false;
+    }
+
+    @Override
+    public void dismiss() {
+        /** How did this happen? We returned false in {@link #canChildBeDismissed()} so this
+         * method should never be called. */
+        LogUtils.wtf(LOG_TAG, "NestedFolderView.dismiss() called. Not expected.");
+    }
+
+    @Override
+    public float getMinAllowScrollDistance() {
+        return -1;
+    }
+}
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index ed62708..3e8ff2b 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -201,7 +201,8 @@
         final int transition = mConversationListNeverShown
                 ? FragmentTransaction.TRANSIT_FRAGMENT_FADE
                 : FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
-        Fragment conversationListFragment = ConversationListFragment.newInstance(listContext);
+        final Fragment conversationListFragment =
+                ConversationListFragment.newInstance(listContext);
 
         if (!inInbox(mAccount, listContext)) {
             // Maintain fragment transaction history so we can get back to the
diff --git a/src/com/android/mail/ui/SwipeableItemView.java b/src/com/android/mail/ui/SwipeableItemView.java
index 9bf7f34..3a4a13f 100644
--- a/src/com/android/mail/ui/SwipeableItemView.java
+++ b/src/com/android/mail/ui/SwipeableItemView.java
@@ -29,6 +29,11 @@
 
     public void dismiss();
 
+    /**
+     * Returns the minimum allowed displacement in the Y axis that is considered a scroll. After
+     * this displacement, all future events are considered scroll events rather than swipes.
+     * @return
+     */
     public float getMinAllowScrollDistance();
 
     public static class SwipeableView {
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index 5294a61..f67f30a 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -70,7 +70,8 @@
         FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
         // Use cross fading animation.
         fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
-        Fragment conversationListFragment = ConversationListFragment.newInstance(mConvListContext);
+        final Fragment conversationListFragment =
+                ConversationListFragment.newInstance(mConvListContext);
         fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment,
                 TAG_CONVERSATION_LIST);
         fragmentTransaction.commitAllowingStateLoss();
@@ -167,26 +168,18 @@
             mViewMode.enterConversationListMode();
         }
 
-        if (folder.hasChildren && !folder.equals(getHierarchyFolder())) {
-            // Replace this fragment with a new FolderListFragment
-            // showing this folder's children if we are not already looking
-            // at the child view for this folder.
-            createFolderTree(folder);
+        if (folder.hasChildren) {
             // Show the up affordance when digging into child folders.
             mActionBarView.setBackButton();
-        } else {
-            setHierarchyFolder(folder);
         }
+        setHierarchyFolder(folder);
         super.onFolderSelected(folder);
     }
 
     private void goUpFolderHierarchy(Folder current) {
-        Folder parent = current.parent;
-        if (parent.parent != null) {
-            createFolderTree(parent.parent);
-            // Show the up affordance when digging into child folders.
-            mActionBarView.setBackButton();
-        } else {
+        // If the current folder is a child, up should show the parent folder.
+        final Folder parent = current.parent;
+        if (parent != null) {
             onFolderSelected(parent);
         }
     }