Storage roots in fragment, sectioned.

Move storage roots into a fragment, since it's not a drawer on
tablets.  Cluster and sort roots when displaying.  SectionedListAdapter
to make clustered roots easier to manage.  Add docs for root types.

Move roots cache into separate class to make it easier to share.

Change-Id: Ia0b92eade059e816324641f600c08026c0e268c9
diff --git a/api/current.txt b/api/current.txt
index 5fd52e3..2b05c3c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20301,6 +20301,7 @@
     field public static final java.lang.String FLAGS = "flags";
     field public static final java.lang.String LAST_MODIFIED = "last_modified";
     field public static final java.lang.String MIME_TYPE = "mime_type";
+    field public static final java.lang.String SUMMARY = "summary";
   }
 
   public static abstract interface DocumentsContract.RootColumns {
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 9c2bb49..289531e 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -267,11 +267,43 @@
          * Type: INTEGER (int)
          */
         public static final String FLAGS = "flags";
+
+        /**
+         * Summary for this document, or {@code null} to omit.
+         * <p>
+         * Type: STRING
+         */
+        public static final String SUMMARY = "summary";
     }
 
+    /**
+     * Root that represents a cloud-based storage service.
+     *
+     * @see RootColumns#ROOT_TYPE
+     */
     public static final int ROOT_TYPE_SERVICE = 1;
+
+    /**
+     * Root that represents a shortcut to content that may be available
+     * elsewhere through another storage root.
+     *
+     * @see RootColumns#ROOT_TYPE
+     */
     public static final int ROOT_TYPE_SHORTCUT = 2;
+
+    /**
+     * Root that represents a physical storage device.
+     *
+     * @see RootColumns#ROOT_TYPE
+     */
     public static final int ROOT_TYPE_DEVICE = 3;
+
+    /**
+     * Root that represents a physical storage device that should only be
+     * displayed to advanced users.
+     *
+     * @see RootColumns#ROOT_TYPE
+     */
     public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
 
     /**
diff --git a/packages/DocumentsUI/res/layout/activity.xml b/packages/DocumentsUI/res/layout/activity.xml
index d4a01d3..ff28e41 100644
--- a/packages/DocumentsUI/res/layout/activity.xml
+++ b/packages/DocumentsUI/res/layout/activity.xml
@@ -25,20 +25,20 @@
         android:orientation="vertical">
 
         <FrameLayout
-            android:id="@+id/directory"
+            android:id="@+id/container_directory"
             android:layout_width="match_parent"
             android:layout_height="0dip"
             android:layout_weight="1" />
 
         <FrameLayout
-            android:id="@+id/save"
+            android:id="@+id/container_save"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
     </LinearLayout>
 
-    <ListView
-        android:id="@+id/roots_list"
+    <FrameLayout
+        android:id="@+id/container_roots"
         android:layout_width="250dp"
         android:layout_height="match_parent"
         android:layout_gravity="start"
diff --git a/packages/DocumentsUI/res/layout/fragment_roots.xml b/packages/DocumentsUI/res/layout/fragment_roots.xml
new file mode 100644
index 0000000..d772892
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/fragment_roots.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
diff --git a/packages/DocumentsUI/res/layout/item_root_header.xml b/packages/DocumentsUI/res/layout/item_root_header.xml
new file mode 100644
index 0000000..2b9a46f
--- /dev/null
+++ b/packages/DocumentsUI/res/layout/item_root_header.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/title"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingTop="8dp"
+    android:paddingBottom="8dp"
+    android:singleLine="true"
+    android:ellipsize="marquee"
+    android:textAllCaps="true"
+    android:textAppearance="?android:attr/textAppearanceSmall"
+    android:textAlignment="viewStart" />
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 3eda207..2ff5d03 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -41,4 +41,8 @@
 
     <string name="root_recent">Recent</string>
 
+    <string name="root_type_service">Services</string>
+    <string name="root_type_shortcut">Shortcuts</string>
+    <string name="root_type_device">Devices</string>
+
 </resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
new file mode 100644
index 0000000..e19505f
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -0,0 +1,90 @@
+/*
+ * 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.documentsui;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.DocumentColumns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.android.documentsui.model.Document;
+
+/**
+ * Dialog to create a new directory.
+ */
+public class CreateDirectoryFragment extends DialogFragment {
+    private static final String TAG_CREATE_DIRECTORY = "create_directory";
+
+    public static void show(FragmentManager fm) {
+        final CreateDirectoryFragment dialog = new CreateDirectoryFragment();
+        dialog.show(fm, TAG_CREATE_DIRECTORY);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Context context = getActivity();
+        final ContentResolver resolver = context.getContentResolver();
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
+
+        final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
+        final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
+
+        builder.setTitle(R.string.menu_create_dir);
+        builder.setView(view);
+
+        builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                final String displayName = text1.getText().toString();
+
+                final ContentValues values = new ContentValues();
+                values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
+                values.put(DocumentColumns.DISPLAY_NAME, displayName);
+
+                final DocumentsActivity activity = (DocumentsActivity) getActivity();
+                final Document cwd = activity.getCurrentDirectory();
+
+                final Uri childUri = resolver.insert(cwd.uri, values);
+                if (childUri != null) {
+                    // Navigate into newly created child
+                    final Document childDoc = Document.fromUri(resolver, childUri);
+                    activity.onDocumentPicked(childDoc);
+                } else {
+                    Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
+                }
+            }
+        });
+        builder.setNegativeButton(android.R.string.cancel, null);
+
+        return builder.create();
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index f6f3f9c..1443f26 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -87,13 +87,13 @@
         fragment.setArguments(args);
 
         final FragmentTransaction ft = fm.beginTransaction();
-        ft.replace(R.id.directory, fragment);
+        ft.replace(R.id.container_directory, fragment);
         ft.commitAllowingStateLoss();
     }
 
     public static DirectoryFragment get(FragmentManager fm) {
         // TODO: deal with multiple directories shown at once
-        return (DirectoryFragment) fm.findFragmentById(R.id.directory);
+        return (DirectoryFragment) fm.findFragmentById(R.id.container_directory);
     }
 
     @Override
@@ -360,7 +360,7 @@
                 // TODO: load thumbnails async
                 icon.setImageURI(doc.uri);
             } else {
-                icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
+                icon.setImageDrawable(RootsCache.resolveDocumentIcon(
                         context, doc.uri.getAuthority(), doc.mimeType));
             }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 0cbd1cb..6067581 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -19,25 +19,13 @@
 import android.app.ActionBar;
 import android.app.ActionBar.OnNavigationListener;
 import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
 import android.app.FragmentManager;
 import android.content.ClipData;
-import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ProviderInfo;
-import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
@@ -47,37 +35,24 @@
 import android.support.v4.widget.DrawerLayout;
 import android.support.v4.widget.DrawerLayout.DrawerListener;
 import android.util.Log;
-import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.ArrayAdapter;
 import android.widget.BaseAdapter;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.ListView;
 import android.widget.SearchView;
 import android.widget.SearchView.OnQueryTextListener;
 import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.documentsui.model.Document;
-import com.android.documentsui.model.DocumentsProviderInfo;
-import com.android.documentsui.model.DocumentsProviderInfo.Icon;
 import com.android.documentsui.model.Root;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -86,8 +61,6 @@
 
     // TODO: share backend root cache with recents provider
 
-    private static final String TAG_CREATE_DIRECTORY = "create_directory";
-
     private static final int ACTION_OPEN = 1;
     private static final int ACTION_CREATE = 2;
 
@@ -95,24 +68,12 @@
 
     private SearchView mSearchView;
 
+    private View mRootsContainer;
     private DrawerLayout mDrawerLayout;
     private ActionBarDrawerToggle mDrawerToggle;
 
     private Root mCurrentRoot;
 
-    /** Map from authority to cached info */
-    private static HashMap<String, DocumentsProviderInfo> sProviders = Maps.newHashMap();
-    /** Map from (authority+rootId) to cached info */
-    private static HashMap<Pair<String, String>, Root> sRoots = Maps.newHashMap();
-
-    // TODO: remove once adapter split by type
-    private static ArrayList<Root> sRootsList = Lists.newArrayList();
-
-    private static Root sRecentOpenRoot;
-
-    private RootsAdapter mRootsAdapter;
-    private ListView mRootsList;
-
     private final DisplayState mDisplayState = new DisplayState();
 
     private LinkedList<Document> mStack = new LinkedList<Document>();
@@ -153,11 +114,11 @@
             SaveFragment.show(getFragmentManager(), mimeType, title);
         }
 
+        RootsFragment.show(getFragmentManager());
+
+        mRootsContainer = findViewById(R.id.container_roots);
+
         mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
-        mRootsAdapter = new RootsAdapter(this, sRootsList);
-        mRootsList = (ListView) findViewById(R.id.roots_list);
-        mRootsList.setAdapter(mRootsAdapter);
-        mRootsList.setOnItemClickListener(mRootsListener);
 
         mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
                 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
@@ -165,9 +126,7 @@
         mDrawerLayout.setDrawerListener(mDrawerListener);
         mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
 
-        mDrawerLayout.openDrawer(mRootsList);
-
-        updateRoots();
+        mDrawerLayout.openDrawer(mRootsContainer);
 
         // Restore last stack for calling package
         // TODO: move into async loader
@@ -186,7 +145,7 @@
 
         // Start in recents if no restored stack
         if (mStack.isEmpty()) {
-            onRootPicked(sRecentOpenRoot);
+            onRootPicked(RootsCache.getRecentOpenRoot(this), false);
         }
 
         updateDirectoryFragment();
@@ -228,7 +187,7 @@
         actionBar.setDisplayShowHomeEnabled(true);
         actionBar.setDisplayHomeAsUpEnabled(true);
 
-        if (mDrawerLayout.isDrawerOpen(mRootsList)) {
+        if (mDrawerLayout.isDrawerOpen(mRootsContainer)) {
             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
             actionBar.setIcon(new ColorDrawable());
 
@@ -334,7 +293,7 @@
         if (size > 1) {
             mStack.pop();
             updateDirectoryFragment();
-        } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsList)) {
+        } else if (size == 1 && !mDrawerLayout.isDrawerOpen(mRootsContainer)) {
             // TODO: open root drawer once we can capture back key
             super.onBackPressed();
         } else {
@@ -434,11 +393,15 @@
         dumpStack();
     }
 
-    public void onRootPicked(Root root) {
+    public void onRootPicked(Root root, boolean closeDrawer) {
         // Clear entire backstack and start in new root
         mStack.clear();
         mCurrentRoot = root;
         onDocumentPicked(Document.fromRoot(getContentResolver(), root));
+
+        if (closeDrawer) {
+            mDrawerLayout.closeDrawers();
+        }
     }
 
     public void onDocumentPicked(Document doc) {
@@ -511,7 +474,7 @@
         if (cwd != null) {
             final String authority = cwd.uri.getAuthority();
             final String rootId = DocumentsContract.getRootId(cwd.uri);
-            mCurrentRoot = sRoots.get(Pair.create(authority, rootId));
+            mCurrentRoot = RootsCache.findRoot(this, authority, rootId);
         }
     }
 
@@ -577,172 +540,10 @@
         public static final int SORT_ORDER_DATE = 1;
     }
 
-    public static Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
-        // Custom icons take precedence
-        final DocumentsProviderInfo info = sProviders.get(authority);
-        if (info != null) {
-            for (Icon icon : info.customIcons) {
-                if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
-                    return icon.icon;
-                }
-            }
-        }
-
-        if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
-            return context.getResources().getDrawable(R.drawable.ic_dir);
-        } else {
-            final PackageManager pm = context.getPackageManager();
-            final Intent intent = new Intent(Intent.ACTION_VIEW);
-            intent.setType(mimeType);
-
-            final ResolveInfo activityInfo = pm.resolveActivity(
-                    intent, PackageManager.MATCH_DEFAULT_ONLY);
-            if (activityInfo != null) {
-                return activityInfo.loadIcon(pm);
-            } else {
-                return null;
-            }
-        }
-    }
-
-    /**
-     * Gather roots from all known storage providers.
-     */
-    private void updateRoots() {
-        sProviders.clear();
-        sRoots.clear();
-        sRootsList.clear();
-
-        final Context context = this;
-        final PackageManager pm = getPackageManager();
-
-        // Create special roots, like recents
-        {
-            final Root root = Root.buildRecentOpen(context);
-            sRootsList.add(root);
-            sRecentOpenRoot = root;
-        }
-
-        // Query for other storage backends
-        final List<ProviderInfo> providers = pm.queryContentProviders(
-                null, -1, PackageManager.GET_META_DATA);
-        for (ProviderInfo providerInfo : providers) {
-            if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
-                    DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
-                final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
-                        this, providerInfo);
-                if (info == null) {
-                    Log.w(TAG, "Missing info for " + providerInfo);
-                    continue;
-                }
-
-                sProviders.put(info.providerInfo.authority, info);
-
-                // TODO: remove deprecated customRoots flag
-                // TODO: populate roots on background thread, and cache results
-                final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
-                final Cursor cursor = getContentResolver().query(uri, null, null, null, null);
-                try {
-                    while (cursor.moveToNext()) {
-                        final Root root = Root.fromCursor(this, info, cursor);
-                        sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
-                        sRootsList.add(root);
-                    }
-                } finally {
-                    cursor.close();
-                }
-            }
-        }
-    }
-
-    private OnItemClickListener mRootsListener = new OnItemClickListener() {
-        @Override
-        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            final Root root = mRootsAdapter.getItem(position);
-            onRootPicked(root);
-            mDrawerLayout.closeDrawers();
-        }
-    };
-
     private void dumpStack() {
         Log.d(TAG, "Current stack:");
         for (Document doc : mStack) {
             Log.d(TAG, "--> " + doc);
         }
     }
-
-    public static class RootsAdapter extends ArrayAdapter<Root> {
-        public RootsAdapter(Context context, List<Root> list) {
-            super(context, android.R.layout.simple_list_item_1, list);
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            if (convertView == null) {
-                convertView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.item_root, parent, false);
-            }
-
-            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
-            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
-            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
-
-            final Root root = getItem(position);
-            icon.setImageDrawable(root.icon);
-            title.setText(root.title);
-
-            summary.setText(root.summary);
-            summary.setVisibility(root.summary != null ? View.VISIBLE : View.GONE);
-
-            return convertView;
-        }
-    }
-
-    public static class CreateDirectoryFragment extends DialogFragment {
-        public static void show(FragmentManager fm) {
-            final CreateDirectoryFragment dialog = new CreateDirectoryFragment();
-            dialog.show(fm, TAG_CREATE_DIRECTORY);
-        }
-
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            final Context context = getActivity();
-            final ContentResolver resolver = context.getContentResolver();
-
-            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
-            final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
-
-            final View view = dialogInflater.inflate(R.layout.dialog_create_dir, null, false);
-            final EditText text1 = (EditText)view.findViewById(android.R.id.text1);
-
-            builder.setTitle(R.string.menu_create_dir);
-            builder.setView(view);
-
-            builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
-                @Override
-                public void onClick(DialogInterface dialog, int which) {
-                    final String displayName = text1.getText().toString();
-
-                    final ContentValues values = new ContentValues();
-                    values.put(DocumentColumns.MIME_TYPE, DocumentsContract.MIME_TYPE_DIRECTORY);
-                    values.put(DocumentColumns.DISPLAY_NAME, displayName);
-
-                    final DocumentsActivity activity = (DocumentsActivity) getActivity();
-                    final Document cwd = activity.getCurrentDirectory();
-
-                    final Uri childUri = resolver.insert(cwd.uri, values);
-                    if (childUri != null) {
-                        // Navigate into newly created child
-                        final Document childDoc = Document.fromUri(resolver, childUri);
-                        activity.onDocumentPicked(childDoc);
-                    } else {
-                        Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
-                    }
-                }
-            });
-            builder.setNegativeButton(android.R.string.cancel, null);
-
-            return builder.create();
-        }
-    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
new file mode 100644
index 0000000..1b56a20
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -0,0 +1,167 @@
+/*
+ * 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.documentsui;
+
+import static com.android.documentsui.DocumentsActivity.TAG;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.documentsui.model.DocumentsProviderInfo;
+import com.android.documentsui.model.DocumentsProviderInfo.Icon;
+import com.android.documentsui.model.Root;
+import com.android.internal.annotations.GuardedBy;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Cache of known storage backends and their roots.
+ */
+public class RootsCache {
+
+    // TODO: cache roots in local provider to avoid spinning up backends
+
+    private static boolean sCached = false;
+
+    /** Map from authority to cached info */
+    private static HashMap<String, DocumentsProviderInfo> sProviders = Maps.newHashMap();
+    /** Map from (authority+rootId) to cached info */
+    private static HashMap<Pair<String, String>, Root> sRoots = Maps.newHashMap();
+
+    public static ArrayList<Root> sRootsList = Lists.newArrayList();
+
+    private static Root sRecentOpenRoot;
+
+    /**
+     * Gather roots from all known storage providers.
+     */
+    private static void ensureCache(Context context) {
+        if (sCached) return;
+        sCached = true;
+
+        sProviders.clear();
+        sRoots.clear();
+        sRootsList.clear();
+
+        {
+            // Create special root for recents
+            final Root root = Root.buildRecentOpen(context);
+            sRootsList.add(root);
+            sRecentOpenRoot = root;
+        }
+
+        // Query for other storage backends
+        final PackageManager pm = context.getPackageManager();
+        final List<ProviderInfo> providers = pm.queryContentProviders(
+                null, -1, PackageManager.GET_META_DATA);
+        for (ProviderInfo providerInfo : providers) {
+            if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
+                    DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
+                final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
+                        context, providerInfo);
+                if (info == null) {
+                    Log.w(TAG, "Missing info for " + providerInfo);
+                    continue;
+                }
+
+                sProviders.put(info.providerInfo.authority, info);
+
+                // TODO: remove deprecated customRoots flag
+                // TODO: populate roots on background thread, and cache results
+                final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
+                final Cursor cursor = context.getContentResolver()
+                        .query(uri, null, null, null, null);
+                try {
+                    while (cursor.moveToNext()) {
+                        final Root root = Root.fromCursor(context, info, cursor);
+                        sRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
+                        sRootsList.add(root);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    @GuardedBy("ActivityThread")
+    public static DocumentsProviderInfo findProvider(Context context, String authority) {
+        ensureCache(context);
+        return sProviders.get(authority);
+    }
+
+    @GuardedBy("ActivityThread")
+    public static Root findRoot(Context context, String authority, String rootId) {
+        ensureCache(context);
+        return sRoots.get(Pair.create(authority, rootId));
+    }
+
+    @GuardedBy("ActivityThread")
+    public static Root getRecentOpenRoot(Context context) {
+        ensureCache(context);
+        return sRecentOpenRoot;
+    }
+
+    @GuardedBy("ActivityThread")
+    public static Collection<Root> getRoots() {
+        return sRootsList;
+    }
+
+    @GuardedBy("ActivityThread")
+    public static Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
+        // Custom icons take precedence
+        ensureCache(context);
+        final DocumentsProviderInfo info = sProviders.get(authority);
+        if (info != null) {
+            for (Icon icon : info.customIcons) {
+                if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
+                    return icon.icon;
+                }
+            }
+        }
+
+        if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
+            return context.getResources().getDrawable(R.drawable.ic_dir);
+        } else {
+            final PackageManager pm = context.getPackageManager();
+            final Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setType(mimeType);
+
+            final ResolveInfo activityInfo = pm.resolveActivity(
+                    intent, PackageManager.MATCH_DEFAULT_ONLY);
+            if (activityInfo != null) {
+                return activityInfo.loadIcon(pm);
+            } else {
+                return null;
+            }
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
new file mode 100644
index 0000000..3e645bc
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -0,0 +1,186 @@
+/*
+ * 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.documentsui;
+
+import static com.android.documentsui.DocumentsActivity.TAG;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.documentsui.SectionedListAdapter.SectionAdapter;
+import com.android.documentsui.model.Root;
+import com.android.documentsui.model.Root.RootComparator;
+
+import java.util.Collection;
+
+/**
+ * Display list of known storage backend roots.
+ */
+public class RootsFragment extends Fragment {
+
+    private ListView mList;
+    private SectionedRootsAdapter mAdapter;
+
+    public static void show(FragmentManager fm) {
+        final RootsFragment fragment = new RootsFragment();
+
+        final FragmentTransaction ft = fm.beginTransaction();
+        ft.replace(R.id.container_roots, fragment);
+        ft.commitAllowingStateLoss();
+    }
+
+    public static RootsFragment get(FragmentManager fm) {
+        return (RootsFragment) fm.findFragmentById(R.id.container_roots);
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        final Context context = inflater.getContext();
+
+        final View view = inflater.inflate(R.layout.fragment_roots, container, false);
+        mList = (ListView) view.findViewById(android.R.id.list);
+
+        mAdapter = new SectionedRootsAdapter(context, RootsCache.getRoots());
+        mList.setAdapter(mAdapter);
+        mList.setOnItemClickListener(mItemListener);
+
+        return view;
+    }
+
+    private OnItemClickListener mItemListener = new OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            final Root root = (Root) mAdapter.getItem(position);
+            ((DocumentsActivity) getActivity()).onRootPicked(root, true);
+        }
+    };
+
+    public static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
+        private int mHeaderId;
+
+        public RootsAdapter(Context context, int headerId) {
+            super(context, 0);
+            mHeaderId = headerId;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.item_root, parent, false);
+            }
+
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
+
+            final Root root = getItem(position);
+            icon.setImageDrawable(root.icon);
+            title.setText(root.title);
+
+            summary.setText(root.summary);
+            summary.setVisibility(root.summary != null ? View.VISIBLE : View.GONE);
+
+            return convertView;
+        }
+
+        @Override
+        public View getHeaderView(View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.item_root_header, parent, false);
+            }
+
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            title.setText(mHeaderId);
+
+            return convertView;
+        }
+    }
+
+    public static class SectionedRootsAdapter extends SectionedListAdapter {
+        private final RootsAdapter mServices;
+        private final RootsAdapter mShortcuts;
+        private final RootsAdapter mDevices;
+        private final RootsAdapter mDevicesAdvanced;
+
+        public SectionedRootsAdapter(Context context, Collection<Root> roots) {
+            mServices = new RootsAdapter(context, R.string.root_type_service);
+            mShortcuts = new RootsAdapter(context, R.string.root_type_shortcut);
+            mDevices = new RootsAdapter(context, R.string.root_type_device);
+            mDevicesAdvanced = new RootsAdapter(context, R.string.root_type_device);
+
+            for (Root root : roots) {
+                Log.d(TAG, "Found rootType=" + root.rootType);
+                switch (root.rootType) {
+                    case DocumentsContract.ROOT_TYPE_SERVICE:
+                        mServices.add(root);
+                        break;
+                    case DocumentsContract.ROOT_TYPE_SHORTCUT:
+                        mShortcuts.add(root);
+                        break;
+                    case DocumentsContract.ROOT_TYPE_DEVICE:
+                        mDevices.add(root);
+                        mDevicesAdvanced.add(root);
+                        break;
+                    case DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED:
+                        mDevicesAdvanced.add(root);
+                        break;
+                }
+            }
+
+            final RootComparator comp = new RootComparator();
+            mServices.sort(comp);
+            mShortcuts.sort(comp);
+            mDevices.sort(comp);
+            mDevicesAdvanced.sort(comp);
+
+            // TODO: switch to hide advanced items by default
+            setShowAdvanced(true);
+        }
+
+        public void setShowAdvanced(boolean showAdvanced) {
+            clearSections();
+            if (mServices.getCount() > 0) {
+                addSection(mServices);
+            }
+            if (mShortcuts.getCount() > 0) {
+                addSection(mShortcuts);
+            }
+
+            final RootsAdapter devices = showAdvanced ? mDevicesAdvanced : mDevices;
+            if (devices.getCount() > 0) {
+                addSection(devices);
+            }
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index cdc399d..304f6e3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -49,7 +49,7 @@
         fragment.setArguments(args);
 
         final FragmentTransaction ft = fm.beginTransaction();
-        ft.replace(R.id.save, fragment, TAG);
+        ft.replace(R.id.container_save, fragment, TAG);
         ft.commitAllowingStateLoss();
     }
 
@@ -65,7 +65,7 @@
         final View view = inflater.inflate(R.layout.fragment_save, container, false);
 
         final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
-        icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
+        icon.setImageDrawable(RootsCache.resolveDocumentIcon(
                 context, null, getArguments().getString(EXTRA_MIME_TYPE)));
 
         mDisplayName = (EditText) view.findViewById(android.R.id.title);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java b/packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java
new file mode 100644
index 0000000..aacce65
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/SectionedListAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * 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.documentsui;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Adapter that combines multiple adapters as sections, asking each section to
+ * provide a header, and correctly handling item types across child adapters.
+ */
+public class SectionedListAdapter extends BaseAdapter {
+    private ArrayList<SectionAdapter> mSections = Lists.newArrayList();
+
+    public interface SectionAdapter extends ListAdapter {
+        public View getHeaderView(View convertView, ViewGroup parent);
+    }
+
+    public void clearSections() {
+        mSections.clear();
+        notifyDataSetChanged();
+    }
+
+    public void addSection(SectionAdapter adapter) {
+        mSections.add(adapter);
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getCount() {
+        int count = 0;
+        final int size = mSections.size();
+        for (int i = 0; i < size; i++) {
+            count += mSections.get(i).getCount() + 1;
+        }
+        return count;
+    }
+
+    @Override
+    public Object getItem(int position) {
+        final int size = mSections.size();
+        for (int i = 0; i < size; i++) {
+            final SectionAdapter section = mSections.get(i);
+            final int sectionSize = section.getCount() + 1;
+
+            // Check if position inside this section
+            if (position == 0) {
+                return section;
+            } else if (position < sectionSize) {
+                return section.getItem(position - 1);
+            }
+
+            // Otherwise jump into next section
+            position -= sectionSize;
+        }
+        throw new IllegalStateException("Unknown position " + position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        final int size = mSections.size();
+        for (int i = 0; i < size; i++) {
+            final SectionAdapter section = mSections.get(i);
+            final int sectionSize = section.getCount() + 1;
+
+            // Check if position inside this section
+            if (position == 0) {
+                return section.getHeaderView(convertView, parent);
+            } else if (position < sectionSize) {
+                return section.getView(position - 1, convertView, parent);
+            }
+
+            // Otherwise jump into next section
+            position -= sectionSize;
+        }
+        throw new IllegalStateException("Unknown position " + position);
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        final int size = mSections.size();
+        for (int i = 0; i < size; i++) {
+            final SectionAdapter section = mSections.get(i);
+            final int sectionSize = section.getCount() + 1;
+
+            // Check if position inside this section
+            if (position == 0) {
+                return false;
+            } else if (position < sectionSize) {
+                return section.isEnabled(position);
+            }
+
+            // Otherwise jump into next section
+            position -= sectionSize;
+        }
+        throw new IllegalStateException("Unknown position " + position);
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        int type = 1;
+        final int size = mSections.size();
+        for (int i = 0; i < size; i++) {
+            final SectionAdapter section = mSections.get(i);
+            final int sectionSize = section.getCount() + 1;
+
+            // Check if position inside this section
+            if (position == 0) {
+                return 0;
+            } else if (position < sectionSize) {
+                return type + section.getItemViewType(position - 1);
+            }
+
+            // Otherwise jump into next section
+            position -= sectionSize;
+            type += section.getViewTypeCount();
+        }
+        throw new IllegalStateException("Unknown position " + position);
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        int count = 1;
+        final int size = mSections.size();
+        for (int i = 0; i < size; i++) {
+            count += mSections.get(i).getViewTypeCount();
+        }
+        return count;
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
index 94b9093..ed69690 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
@@ -155,7 +155,7 @@
             if (leftDir != rightDir) {
                 return leftDir ? -1 : 1;
             } else {
-                return lhs.displayName.compareToIgnoreCase(rhs.displayName);
+                return Root.compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
             }
         }
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
index ef3b8d7..9d816d7 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
@@ -29,6 +29,8 @@
 import com.android.documentsui.R;
 import com.android.documentsui.RecentsProvider;
 
+import java.util.Comparator;
+
 /**
  * Representation of a root under a storage backend.
  */
@@ -89,4 +91,22 @@
 
         return root;
     }
+
+    public static class RootComparator implements Comparator<Root> {
+        @Override
+        public int compare(Root lhs, Root rhs) {
+            final int score = compareToIgnoreCaseNullable(lhs.title, rhs.title);
+            if (score != 0) {
+                return score;
+            } else {
+                return compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
+            }
+        }
+    }
+
+    public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
+        if (lhs == null) return -1;
+        if (rhs == null) return 1;
+        return lhs.compareToIgnoreCase(rhs);
+    }
 }