Merge "Stronger DocumentsProvider contract." into klp-dev
diff --git a/api/current.txt b/api/current.txt
index 6a83508..0418cd9 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -20814,40 +20814,46 @@
   }
 
   public final class DocumentsContract {
-    ctor public DocumentsContract();
-    method public static android.net.Uri buildContentsUri(java.lang.String, java.lang.String, java.lang.String);
-    method public static android.net.Uri buildContentsUri(android.net.Uri);
-    method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String, java.lang.String);
-    method public static android.net.Uri buildDocumentUri(android.net.Uri, java.lang.String);
-    method public static android.net.Uri buildRootUri(java.lang.String, java.lang.String);
-    method public static android.net.Uri buildRootsUri(java.lang.String);
-    method public static android.net.Uri buildSearchUri(java.lang.String, java.lang.String, java.lang.String, java.lang.String);
-    method public static android.net.Uri buildSearchUri(android.net.Uri, java.lang.String);
-    method public static android.net.Uri createDocument(android.content.ContentResolver, android.net.Uri, java.lang.String, java.lang.String);
+    method public static android.net.Uri buildDocumentUri(java.lang.String, java.lang.String);
     method public static java.lang.String getDocId(android.net.Uri);
     method public static android.net.Uri[] getOpenDocuments(android.content.Context);
-    method public static java.lang.String getRootId(android.net.Uri);
-    method public static java.lang.String getSearchQuery(android.net.Uri);
-    method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, android.net.Uri, android.graphics.Point);
-    method public static boolean isLocalOnly(android.net.Uri);
-    method public static void notifyRootsChanged(android.content.Context, java.lang.String);
-    method public static boolean renameDocument(android.content.ContentResolver, android.net.Uri, java.lang.String);
-    method public static android.net.Uri setLocalOnly(android.net.Uri);
-    field public static final java.lang.String EXTRA_HAS_MORE = "has_more";
-    field public static final java.lang.String EXTRA_REQUEST_MORE = "request_more";
-    field public static final java.lang.String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+    field public static final java.lang.String EXTRA_ERROR = "error";
+    field public static final java.lang.String EXTRA_INFO = "info";
+    field public static final java.lang.String EXTRA_LOADING = "loading";
   }
 
   public static abstract interface DocumentsContract.DocumentColumns implements android.provider.OpenableColumns {
     field public static final java.lang.String DOC_ID = "doc_id";
     field public static final java.lang.String FLAGS = "flags";
+    field public static final java.lang.String ICON = "icon";
     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 class DocumentsContract.Documents {
-    field public static final java.lang.String DOC_ID_ROOT = "0";
+  public static final class DocumentsContract.DocumentRoot implements android.os.Parcelable {
+    ctor public DocumentsContract.DocumentRoot();
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+    field public static final int FLAG_LOCAL_ONLY = 2; // 0x2
+    field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
+    field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
+    field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
+    field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
+    field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
+    field public long availableBytes;
+    field public java.lang.String docId;
+    field public int flags;
+    field public int icon;
+    field public java.lang.String[] mimeTypes;
+    field public java.lang.String recentDocId;
+    field public int rootType;
+    field public java.lang.String summary;
+    field public java.lang.String title;
+  }
+
+  public static final class DocumentsContract.Documents {
     field public static final int FLAG_PREFERS_GRID = 64; // 0x40
     field public static final int FLAG_SUPPORTS_CREATE = 1; // 0x1
     field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4
@@ -20855,25 +20861,32 @@
     field public static final int FLAG_SUPPORTS_SEARCH = 16; // 0x10
     field public static final int FLAG_SUPPORTS_THUMBNAIL = 8; // 0x8
     field public static final int FLAG_SUPPORTS_WRITE = 32; // 0x20
-    field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
+    field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.doc/dir";
   }
 
-  public static abstract interface DocumentsContract.RootColumns {
-    field public static final java.lang.String AVAILABLE_BYTES = "available_bytes";
-    field public static final java.lang.String ICON = "icon";
-    field public static final java.lang.String ROOT_ID = "root_id";
-    field public static final java.lang.String ROOT_TYPE = "root_type";
-    field public static final java.lang.String SUMMARY = "summary";
-    field public static final java.lang.String TITLE = "title";
-  }
-
-  public static class DocumentsContract.Roots {
-    field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
-    field public static final java.lang.String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
-    field public static final int ROOT_TYPE_DEVICE = 3; // 0x3
-    field public static final int ROOT_TYPE_DEVICE_ADVANCED = 4; // 0x4
-    field public static final int ROOT_TYPE_SERVICE = 1; // 0x1
-    field public static final int ROOT_TYPE_SHORTCUT = 2; // 0x2
+  public abstract class DocumentsProvider extends android.content.ContentProvider {
+    ctor public DocumentsProvider();
+    method public final android.os.Bundle callFromPackage(java.lang.String, java.lang.String, java.lang.String, android.os.Bundle);
+    method public java.lang.String createDocument(java.lang.String, java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
+    method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
+    method public void deleteDocument(java.lang.String) throws java.io.FileNotFoundException;
+    method public abstract java.util.List<android.provider.DocumentsContract.DocumentRoot> getDocumentRoots();
+    method public java.lang.String getType(java.lang.String) throws java.io.FileNotFoundException;
+    method public final java.lang.String getType(android.net.Uri);
+    method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
+    method public void notifyDocumentRootsChanged();
+    method public abstract android.os.ParcelFileDescriptor openDocument(java.lang.String, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method public android.content.res.AssetFileDescriptor openDocumentThumbnail(java.lang.String, android.graphics.Point, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String) throws java.io.FileNotFoundException;
+    method public final android.os.ParcelFileDescriptor openFile(android.net.Uri, java.lang.String, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle) throws java.io.FileNotFoundException;
+    method public final android.content.res.AssetFileDescriptor openTypedAssetFile(android.net.Uri, java.lang.String, android.os.Bundle, android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String);
+    method public abstract android.database.Cursor queryDocument(java.lang.String) throws java.io.FileNotFoundException;
+    method public abstract android.database.Cursor queryDocumentChildren(java.lang.String) throws java.io.FileNotFoundException;
+    method public android.database.Cursor querySearch(java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
+    method public void renameDocument(java.lang.String, java.lang.String) throws java.io.FileNotFoundException;
+    method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
   }
 
   public final deprecated class LiveFolders implements android.provider.BaseColumns {
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 024a521..4e8dd82 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -316,4 +316,11 @@
     public ContentProvider getLocalContentProvider() {
         return ContentProvider.coerceToLocalContentProvider(mContentProvider);
     }
+
+    /** {@hide} */
+    public static void closeQuietly(ContentProviderClient client) {
+        if (client != null) {
+            client.release();
+        }
+    }
 }
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index c99f09c..d7ca915 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2687,10 +2687,6 @@
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
 
-    /** {@hide} */
-    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
-    public static final String ACTION_MANAGE_DOCUMENT = "android.intent.action.MANAGE_DOCUMENT";
-
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
     // Standard intent categories (see addCategory()).
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index f474504..32b1b60 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -22,6 +22,7 @@
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -545,6 +546,13 @@
         mFdsKnown = false;
     }
 
+    /** {@hide} */
+    public void putParcelableList(String key, List<? extends Parcelable> value) {
+        unparcel();
+        mMap.put(key, value);
+        mFdsKnown = false;
+    }
+
     /**
      * Inserts a SparceArray of Parcelable values into the mapping of this
      * Bundle, replacing any existing value for the given key.  Either key
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 65c9220..ebb7eb8 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -19,9 +19,8 @@
 import static android.net.TrafficStats.KB_IN_BYTES;
 import static libcore.io.OsConstants.SEEK_SET;
 
-import android.content.ContentProvider;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -31,12 +30,16 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Point;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.os.Parcelable;
 import android.util.Log;
 
+import com.android.internal.util.Preconditions;
 import com.google.android.collect.Lists;
 
 import libcore.io.ErrnoException;
@@ -51,74 +54,49 @@
 /**
  * Defines the contract between a documents provider and the platform.
  * <p>
- * A document provider is a {@link ContentProvider} that presents a set of
- * documents in a hierarchical structure. The system provides UI that visualizes
- * all available document providers, offering users the ability to open existing
- * documents or create new documents.
- * <p>
- * Each provider expresses one or more "roots" which each serve as the top-level
- * of a tree. For example, a root could represent an account, or a physical
- * storage device. Under each root, documents are referenced by a unique
- * {@link DocumentColumns#DOC_ID}, and each root starts at the
- * {@link Documents#DOC_ID_ROOT} document.
- * <p>
- * Documents can be either an openable file (with a specific MIME type), or a
- * directory containing additional documents (with the
- * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
- * capabilities, as described by {@link DocumentColumns#FLAGS}. The same
- * {@link DocumentColumns#DOC_ID} can be included in multiple directories.
- * <p>
- * Document providers must be protected with the
- * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
- * only be requested by the system. The system-provided UI then issues narrow
- * Uri permission grants for individual documents when the user explicitly picks
- * documents.
+ * To create a document provider, extend {@link DocumentsProvider}, which
+ * provides a foundational implementation of this contract.
  *
- * @see Intent#ACTION_OPEN_DOCUMENT
- * @see Intent#ACTION_CREATE_DOCUMENT
+ * @see DocumentsProvider
  */
 public final class DocumentsContract {
     private static final String TAG = "Documents";
 
-    // content://com.example/roots/
-    // content://com.example/roots/sdcard/
-    // content://com.example/roots/sdcard/docs/0/
-    // content://com.example/roots/sdcard/docs/0/contents/
-    // content://com.example/roots/sdcard/docs/0/search/?query=pony
+    // content://com.example/docs/12/
+    // content://com.example/docs/12/children/
+    // content://com.example/docs/12/search/?query=pony
+
+    private DocumentsContract() {
+    }
 
     /** {@hide} */
     public static final String META_DATA_DOCUMENT_PROVIDER = "android.content.DOCUMENT_PROVIDER";
 
     /** {@hide} */
-    public static final String ACTION_DOCUMENT_CHANGED = "android.provider.action.DOCUMENT_CHANGED";
+    public static final String ACTION_MANAGE_DOCUMENTS = "android.provider.action.MANAGE_DOCUMENTS";
+
+    /** {@hide} */
+    public static final String
+            ACTION_DOCUMENT_ROOT_CHANGED = "android.provider.action.DOCUMENT_ROOT_CHANGED";
 
     /**
      * Constants for individual documents.
      */
-    public static class Documents {
+    public final static class Documents {
         private Documents() {
         }
 
         /**
          * MIME type of a document which is a directory that may contain additional
          * documents.
-         *
-         * @see #buildContentsUri(String, String, String)
          */
-        public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/doc";
-
-        /**
-         * {@link DocumentColumns#DOC_ID} value representing the root directory of a
-         * documents root.
-         */
-        public static final String DOC_ID_ROOT = "0";
+        public static final String MIME_TYPE_DIR = "vnd.android.doc/dir";
 
         /**
          * Flag indicating that a document is a directory that supports creation of
          * new files within it.
          *
          * @see DocumentColumns#FLAGS
-         * @see #createDocument(ContentResolver, Uri, String, String)
          */
         public static final int FLAG_SUPPORTS_CREATE = 1;
 
@@ -126,7 +104,6 @@
          * Flag indicating that a document is renamable.
          *
          * @see DocumentColumns#FLAGS
-         * @see #renameDocument(ContentResolver, Uri, String)
          */
         public static final int FLAG_SUPPORTS_RENAME = 1 << 1;
 
@@ -141,7 +118,6 @@
          * Flag indicating that a document can be represented as a thumbnail.
          *
          * @see DocumentColumns#FLAGS
-         * @see #getThumbnail(ContentResolver, Uri, Point)
          */
         public static final int FLAG_SUPPORTS_THUMBNAIL = 1 << 3;
 
@@ -153,7 +129,7 @@
         public static final int FLAG_SUPPORTS_SEARCH = 1 << 4;
 
         /**
-         * Flag indicating that a document is writable.
+         * Flag indicating that a document supports writing.
          *
          * @see DocumentColumns#FLAGS
          */
@@ -170,127 +146,89 @@
     }
 
     /**
-     * Optimal dimensions for a document thumbnail request, stored as a
-     * {@link Point} object. This is only a hint, and the returned thumbnail may
-     * have different dimensions.
-     *
-     * @see ContentProvider#openTypedAssetFile(Uri, String, Bundle)
-     */
-    public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
-
-    /**
      * Extra boolean flag included in a directory {@link Cursor#getExtras()}
-     * indicating that the document provider can provide additional data if
-     * requested, such as additional search results.
-     */
-    public static final String EXTRA_HAS_MORE = "has_more";
-
-    /**
-     * Extra boolean flag included in a {@link Cursor#respond(Bundle)} call to a
-     * directory to request that additional data should be fetched. When
-     * requested data is ready, the provider should send a change notification
-     * to cause a requery.
+     * indicating that a document provider is still loading data. For example, a
+     * provider has returned some results, but is still waiting on an
+     * outstanding network request.
      *
-     * @see Cursor#respond(Bundle)
      * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
      *      boolean)
      */
-    public static final String EXTRA_REQUEST_MORE = "request_more";
+    public static final String EXTRA_LOADING = "loading";
 
-    private static final String PATH_ROOTS = "roots";
+    /**
+     * Extra string included in a directory {@link Cursor#getExtras()}
+     * providing an informational message that should be shown to a user. For
+     * example, a provider may wish to indicate that not all documents are
+     * available.
+     */
+    public static final String EXTRA_INFO = "info";
+
+    /**
+     * Extra string included in a directory {@link Cursor#getExtras()} providing
+     * an error message that should be shown to a user. For example, a provider
+     * may wish to indicate that a network error occurred. The user may choose
+     * to retry, resulting in a new query.
+     */
+    public static final String EXTRA_ERROR = "error";
+
+    /** {@hide} */
+    public static final String METHOD_GET_ROOTS = "android:getRoots";
+    /** {@hide} */
+    public static final String METHOD_CREATE_DOCUMENT = "android:createDocument";
+    /** {@hide} */
+    public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument";
+    /** {@hide} */
+    public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument";
+
+    /** {@hide} */
+    public static final String EXTRA_AUTHORITY = "authority";
+    /** {@hide} */
+    public static final String EXTRA_PACKAGE_NAME = "packageName";
+    /** {@hide} */
+    public static final String EXTRA_URI = "uri";
+    /** {@hide} */
+    public static final String EXTRA_ROOTS = "roots";
+    /** {@hide} */
+    public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+
     private static final String PATH_DOCS = "docs";
-    private static final String PATH_CONTENTS = "contents";
+    private static final String PATH_CHILDREN = "children";
     private static final String PATH_SEARCH = "search";
 
     private static final String PARAM_QUERY = "query";
-    private static final String PARAM_LOCAL_ONLY = "localOnly";
-
-    /**
-     * Build Uri representing the roots offered by a document provider.
-     */
-    public static Uri buildRootsUri(String authority) {
-        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
-                .authority(authority).appendPath(PATH_ROOTS).build();
-    }
-
-    /**
-     * Build Uri representing a specific root offered by a document provider.
-     */
-    public static Uri buildRootUri(String authority, String rootId) {
-        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
-                .authority(authority).appendPath(PATH_ROOTS).appendPath(rootId).build();
-    }
 
     /**
      * Build Uri representing the given {@link DocumentColumns#DOC_ID} in a
      * document provider.
      */
-    public static Uri buildDocumentUri(String authority, String rootId, String docId) {
-        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
-                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
-                .build();
+    public static Uri buildDocumentUri(String authority, String docId) {
+        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(authority).appendPath(PATH_DOCS).appendPath(docId).build();
     }
 
     /**
      * Build Uri representing the contents of the given directory in a document
      * provider. The given document must be {@link Documents#MIME_TYPE_DIR}.
+     *
+     * @hide
      */
-    public static Uri buildContentsUri(String authority, String rootId, String docId) {
+    public static Uri buildChildrenUri(String authority, String docId) {
         return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
-                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
-                .appendPath(PATH_CONTENTS).build();
+                .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_CHILDREN).build();
     }
 
     /**
      * Build Uri representing a search for matching documents under a specific
      * directory in a document provider. The given document must have
      * {@link Documents#FLAG_SUPPORTS_SEARCH}.
+     *
+     * @hide
      */
-    public static Uri buildSearchUri(String authority, String rootId, String docId, String query) {
+    public static Uri buildSearchUri(String authority, String docId, String query) {
         return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
-                .appendPath(PATH_ROOTS).appendPath(rootId).appendPath(PATH_DOCS).appendPath(docId)
-                .appendPath(PATH_SEARCH).appendQueryParameter(PARAM_QUERY, query).build();
-    }
-
-    /**
-     * Convenience method for {@link #buildDocumentUri(String, String, String)},
-     * extracting authority and root from the given Uri.
-     */
-    public static Uri buildDocumentUri(Uri relatedUri, String docId) {
-        return buildDocumentUri(relatedUri.getAuthority(), getRootId(relatedUri), docId);
-    }
-
-    /**
-     * Convenience method for {@link #buildContentsUri(String, String, String)},
-     * extracting authority and root from the given Uri.
-     */
-    public static Uri buildContentsUri(Uri relatedUri) {
-        return buildContentsUri(
-                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri));
-    }
-
-    /**
-     * Convenience method for
-     * {@link #buildSearchUri(String, String, String, String)}, extracting
-     * authority and root from the given Uri.
-     */
-    public static Uri buildSearchUri(Uri relatedUri, String query) {
-        return buildSearchUri(
-                relatedUri.getAuthority(), getRootId(relatedUri), getDocId(relatedUri), query);
-    }
-
-    /**
-     * Extract the {@link RootColumns#ROOT_ID} from the given Uri.
-     */
-    public static String getRootId(Uri documentUri) {
-        final List<String> paths = documentUri.getPathSegments();
-        if (paths.size() < 2) {
-            throw new IllegalArgumentException("Not a root: " + documentUri);
-        }
-        if (!PATH_ROOTS.equals(paths.get(0))) {
-            throw new IllegalArgumentException("Not a root: " + documentUri);
-        }
-        return paths.get(1);
+                .appendPath(PATH_DOCS).appendPath(docId).appendPath(PATH_SEARCH)
+                .appendQueryParameter(PARAM_QUERY, query).build();
     }
 
     /**
@@ -298,68 +236,35 @@
      */
     public static String getDocId(Uri documentUri) {
         final List<String> paths = documentUri.getPathSegments();
-        if (paths.size() < 4) {
+        if (paths.size() < 2) {
             throw new IllegalArgumentException("Not a document: " + documentUri);
         }
-        if (!PATH_ROOTS.equals(paths.get(0))) {
+        if (!PATH_DOCS.equals(paths.get(0))) {
             throw new IllegalArgumentException("Not a document: " + documentUri);
         }
-        if (!PATH_DOCS.equals(paths.get(2))) {
-            throw new IllegalArgumentException("Not a document: " + documentUri);
-        }
-        return paths.get(3);
+        return paths.get(1);
     }
 
-    /**
-     * Return requested search query from the given Uri, as constructed by
-     * {@link #buildSearchUri(String, String, String, String)}.
-     */
+    /** {@hide} */
     public static String getSearchQuery(Uri documentUri) {
         return documentUri.getQueryParameter(PARAM_QUERY);
     }
 
     /**
-     * Mark the given Uri to indicate that only locally-available data should be
-     * returned. That is, no network connections should be initiated to provide
-     * the metadata or content.
-     */
-    public static Uri setLocalOnly(Uri documentUri) {
-        return documentUri.buildUpon()
-                .appendQueryParameter(PARAM_LOCAL_ONLY, String.valueOf(true)).build();
-    }
-
-    /**
-     * Return if the given Uri is requesting that only locally-available data be
-     * returned. That is, no network connections should be initiated to provide
-     * the metadata or content.
-     */
-    public static boolean isLocalOnly(Uri documentUri) {
-        return documentUri.getBooleanQueryParameter(PARAM_LOCAL_ONLY, false);
-    }
-
-    /**
      * Standard columns for document queries. Document providers <em>must</em>
      * support at least these columns when queried.
-     *
-     * @see DocumentsContract#buildDocumentUri(String, String, String)
-     * @see DocumentsContract#buildContentsUri(String, String, String)
-     * @see DocumentsContract#buildSearchUri(String, String, String, String)
      */
     public interface DocumentColumns extends OpenableColumns {
         /**
-         * The ID for a document under a storage backend root. Values
-         * <em>must</em> never change once returned. This field is read-only to
-         * document clients.
+         * Unique ID for a document. Values <em>must</em> never change once
+         * returned, since they may used for long-term Uri permission grants.
          * <p>
          * Type: STRING
          */
         public static final String DOC_ID = "doc_id";
 
         /**
-         * MIME type of a document, matching the value returned by
-         * {@link ContentResolver#getType(android.net.Uri)}. This field must be
-         * provided when a new document is created. This field is read-only to
-         * document clients.
+         * MIME type of a document.
          * <p>
          * Type: STRING
          *
@@ -369,10 +274,10 @@
 
         /**
          * Timestamp when a document was last modified, in milliseconds since
-         * January 1, 1970 00:00:00.0 UTC. This field is read-only to document
-         * clients. Document providers can update this field using events from
+         * January 1, 1970 00:00:00.0 UTC, or {@code null} if unknown. Document
+         * providers can update this field using events from
          * {@link OnCloseListener} or other reliable
-         * {@link ParcelFileDescriptor} transport.
+         * {@link ParcelFileDescriptor} transports.
          * <p>
          * Type: INTEGER (long)
          *
@@ -381,37 +286,37 @@
         public static final String LAST_MODIFIED = "last_modified";
 
         /**
-         * Flags that apply to a specific document. This field is read-only to
-         * document clients.
+         * Specific icon resource for a document, or {@code null} to resolve
+         * default using {@link #MIME_TYPE}.
          * <p>
          * Type: INTEGER (int)
          */
-        public static final String FLAGS = "flags";
+        public static final String ICON = "icon";
 
         /**
-         * Summary for this document, or {@code null} to omit. This field is
-         * read-only to document clients.
+         * Summary for a document, or {@code null} to omit.
          * <p>
          * Type: STRING
          */
         public static final String SUMMARY = "summary";
+
+        /**
+         * Flags that apply to a specific document.
+         * <p>
+         * Type: INTEGER (int)
+         */
+        public static final String FLAGS = "flags";
     }
 
     /**
-     * Constants for individual document roots.
+     * Metadata about a specific root of documents.
      */
-    public static class Roots {
-        private Roots() {
-        }
-
-        public static final String MIME_TYPE_DIR = "vnd.android.cursor.dir/root";
-        public static final String MIME_TYPE_ITEM = "vnd.android.cursor.item/root";
-
+    public final static class DocumentRoot implements Parcelable {
         /**
          * Root that represents a storage service, such as a cloud-based
          * service.
          *
-         * @see RootColumns#ROOT_TYPE
+         * @see #rootType
          */
         public static final int ROOT_TYPE_SERVICE = 1;
 
@@ -419,14 +324,14 @@
          * Root that represents a shortcut to content that may be available
          * elsewhere through another storage root.
          *
-         * @see RootColumns#ROOT_TYPE
+         * @see #rootType
          */
         public static final int ROOT_TYPE_SHORTCUT = 2;
 
         /**
          * Root that represents a physical storage device.
          *
-         * @see RootColumns#ROOT_TYPE
+         * @see #rootType
          */
         public static final int ROOT_TYPE_DEVICE = 3;
 
@@ -434,65 +339,154 @@
          * Root that represents a physical storage device that should only be
          * displayed to advanced users.
          *
-         * @see RootColumns#ROOT_TYPE
+         * @see #rootType
          */
         public static final int ROOT_TYPE_DEVICE_ADVANCED = 4;
-    }
-
-    /**
-     * Standard columns for document root queries.
-     *
-     * @see DocumentsContract#buildRootsUri(String)
-     * @see DocumentsContract#buildRootUri(String, String)
-     */
-    public interface RootColumns {
-        public static final String ROOT_ID = "root_id";
 
         /**
-         * Storage root type, use for clustering. This field is read-only to
-         * document clients.
-         * <p>
-         * Type: INTEGER (int)
+         * Flag indicating that at least one directory under this root supports
+         * creating content.
          *
-         * @see Roots#ROOT_TYPE_SERVICE
-         * @see Roots#ROOT_TYPE_DEVICE
+         * @see #flags
          */
-        public static final String ROOT_TYPE = "root_type";
+        public static final int FLAG_SUPPORTS_CREATE = 1;
 
         /**
-         * Icon resource ID for this storage root, or {@code null} to use the
-         * default {@link ProviderInfo#icon}. This field is read-only to
-         * document clients.
-         * <p>
-         * Type: INTEGER (int)
+         * Flag indicating that this root offers content that is strictly local
+         * on the device. That is, no network requests are made for the content.
+         *
+         * @see #flags
          */
-        public static final String ICON = "icon";
+        public static final int FLAG_LOCAL_ONLY = 1 << 1;
+
+        /** {@hide} */
+        public String authority;
 
         /**
-         * Title for this storage root, or {@code null} to use the default
-         * {@link ProviderInfo#labelRes}. This field is read-only to document
-         * clients.
-         * <p>
-         * Type: STRING
+         * Root type, use for clustering.
+         *
+         * @see #ROOT_TYPE_SERVICE
+         * @see #ROOT_TYPE_DEVICE
          */
-        public static final String TITLE = "title";
+        public int rootType;
 
         /**
-         * Summary for this storage root, or {@code null} to omit. This field is
-         * read-only to document clients.
-         * <p>
-         * Type: STRING
+         * Flags for this root.
+         *
+         * @see #FLAG_LOCAL_ONLY
          */
-        public static final String SUMMARY = "summary";
+        public int flags;
 
         /**
-         * Number of free bytes of available in this storage root, or
-         * {@code null} if unknown or unbounded. This field is read-only to
-         * document clients.
-         * <p>
-         * Type: INTEGER (long)
+         * Icon resource ID for this root.
          */
-        public static final String AVAILABLE_BYTES = "available_bytes";
+        public int icon;
+
+        /**
+         * Title for this root.
+         */
+        public String title;
+
+        /**
+         * Summary for this root. May be {@code null}.
+         */
+        public String summary;
+
+        /**
+         * Document which is a directory that represents the top of this root.
+         * Must not be {@code null}.
+         *
+         * @see DocumentColumns#DOC_ID
+         */
+        public String docId;
+
+        /**
+         * Document which is a directory representing recently modified
+         * documents under this root. This directory should return at most two
+         * dozen documents modified within the last 90 days. May be {@code null}
+         * if this root doesn't support recents.
+         *
+         * @see DocumentColumns#DOC_ID
+         */
+        public String recentDocId;
+
+        /**
+         * Number of free bytes of available in this root, or -1 if unknown or
+         * unbounded.
+         */
+        public long availableBytes;
+
+        /**
+         * Set of MIME type filters describing the content offered by this root,
+         * or {@code null} to indicate that all MIME types are supported. For
+         * example, a provider only supporting audio and video might set this to
+         * {@code ["audio/*", "video/*"]}.
+         */
+        public String[] mimeTypes;
+
+        public DocumentRoot() {
+        }
+
+        /** {@hide} */
+        public DocumentRoot(Parcel in) {
+            rootType = in.readInt();
+            flags = in.readInt();
+            icon = in.readInt();
+            title = in.readString();
+            summary = in.readString();
+            docId = in.readString();
+            recentDocId = in.readString();
+            availableBytes = in.readLong();
+            mimeTypes = in.readStringArray();
+        }
+
+        /** {@hide} */
+        public Drawable loadIcon(Context context) {
+            if (icon != 0) {
+                if (authority != null) {
+                    final PackageManager pm = context.getPackageManager();
+                    final ProviderInfo info = pm.resolveContentProvider(authority, 0);
+                    if (info != null) {
+                        return pm.getDrawable(info.packageName, icon, info.applicationInfo);
+                    }
+                } else {
+                    return context.getResources().getDrawable(icon);
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            Preconditions.checkNotNull(docId);
+
+            dest.writeInt(rootType);
+            dest.writeInt(flags);
+            dest.writeInt(icon);
+            dest.writeString(title);
+            dest.writeString(summary);
+            dest.writeString(docId);
+            dest.writeString(recentDocId);
+            dest.writeLong(availableBytes);
+            dest.writeStringArray(mimeTypes);
+        }
+
+        public static final Creator<DocumentRoot> CREATOR = new Creator<DocumentRoot>() {
+            @Override
+            public DocumentRoot createFromParcel(Parcel in) {
+                return new DocumentRoot(in);
+            }
+
+            @Override
+            public DocumentRoot[] newArray(int size) {
+                return new DocumentRoot[size];
+            }
+        };
     }
 
     /**
@@ -531,6 +525,7 @@
      * {@link Documents#FLAG_SUPPORTS_THUMBNAIL} set.
      *
      * @return decoded thumbnail, or {@code null} if problem was encountered.
+     * @hide
      */
     public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
         final Bundle openOpts = new Bundle();
@@ -588,44 +583,83 @@
         }
     }
 
-    /**
-     * Create a new document under a specific parent document with the given
-     * display name and MIME type.
-     *
-     * @param parentDocumentUri document with
-     *            {@link Documents#FLAG_SUPPORTS_CREATE}
-     * @param displayName name for new document
-     * @param mimeType MIME type for new document, which cannot be changed
-     * @return newly created document Uri, or {@code null} if failed
-     */
-    public static Uri createDocument(
-            ContentResolver resolver, Uri parentDocumentUri, String displayName, String mimeType) {
-        final ContentValues values = new ContentValues();
-        values.put(DocumentColumns.MIME_TYPE, mimeType);
-        values.put(DocumentColumns.DISPLAY_NAME, displayName);
-        return resolver.insert(parentDocumentUri, values);
+    /** {@hide} */
+    public static List<DocumentRoot> getDocumentRoots(ContentProviderClient client) {
+        try {
+            final Bundle out = client.call(METHOD_GET_ROOTS, null, null);
+            final List<DocumentRoot> roots = out.getParcelableArrayList(EXTRA_ROOTS);
+            return roots;
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to get roots", e);
+            return null;
+        }
     }
 
     /**
-     * Rename the document at the given URI. Given document must have
-     * {@link Documents#FLAG_SUPPORTS_RENAME} set.
+     * Create a new document under the given parent document with MIME type and
+     * display name.
      *
-     * @return if rename was successful.
+     * @param docId document with {@link Documents#FLAG_SUPPORTS_CREATE}
+     * @param mimeType MIME type of new document
+     * @param displayName name of new document
+     * @return newly created document, or {@code null} if failed
+     * @hide
      */
-    public static boolean renameDocument(
-            ContentResolver resolver, Uri documentUri, String displayName) {
-        final ContentValues values = new ContentValues();
-        values.put(DocumentColumns.DISPLAY_NAME, displayName);
-        return (resolver.update(documentUri, values, null, null) == 1);
+    public static String createDocument(
+            ContentProviderClient client, String docId, String mimeType, String displayName) {
+        final Bundle in = new Bundle();
+        in.putString(DocumentColumns.DOC_ID, docId);
+        in.putString(DocumentColumns.MIME_TYPE, mimeType);
+        in.putString(DocumentColumns.DISPLAY_NAME, displayName);
+
+        try {
+            final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
+            return out.getString(DocumentColumns.DOC_ID);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to create document", e);
+            return null;
+        }
     }
 
     /**
-     * Notify the system that roots have changed for the given storage provider.
-     * This signal is used to invalidate internal caches.
+     * Rename the given document.
+     *
+     * @param docId document with {@link Documents#FLAG_SUPPORTS_RENAME}
+     * @return document which may have changed due to rename, or {@code null} if
+     *         rename failed.
+     * @hide
      */
-    public static void notifyRootsChanged(Context context, String authority) {
-        final Intent intent = new Intent(ACTION_DOCUMENT_CHANGED);
-        intent.setData(buildRootsUri(authority));
-        context.sendBroadcast(intent);
+    public static String renameDocument(
+            ContentProviderClient client, String docId, String displayName) {
+        final Bundle in = new Bundle();
+        in.putString(DocumentColumns.DOC_ID, docId);
+        in.putString(DocumentColumns.DISPLAY_NAME, displayName);
+
+        try {
+            final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in);
+            return out.getString(DocumentColumns.DOC_ID);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to rename document", e);
+            return null;
+        }
+    }
+
+    /**
+     * Delete the given document.
+     *
+     * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE}
+     * @hide
+     */
+    public static boolean deleteDocument(ContentProviderClient client, String docId) {
+        final Bundle in = new Bundle();
+        in.putString(DocumentColumns.DOC_ID, docId);
+
+        try {
+            client.call(METHOD_DELETE_DOCUMENT, null, in);
+            return true;
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to delete document", e);
+            return false;
+        }
     }
 }
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
new file mode 100644
index 0000000..eeb8c41
--- /dev/null
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -0,0 +1,384 @@
+/*
+ * 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 android.provider;
+
+import static android.provider.DocumentsContract.ACTION_DOCUMENT_ROOT_CHANGED;
+import static android.provider.DocumentsContract.EXTRA_AUTHORITY;
+import static android.provider.DocumentsContract.EXTRA_ROOTS;
+import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE;
+import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
+import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
+import static android.provider.DocumentsContract.METHOD_GET_ROOTS;
+import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT;
+import static android.provider.DocumentsContract.getDocId;
+import static android.provider.DocumentsContract.getSearchQuery;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.DocumentRoot;
+import android.provider.DocumentsContract.Documents;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.FileNotFoundException;
+import java.util.List;
+
+/**
+ * Base class for a document provider. A document provider should extend this
+ * class and implement the abstract methods.
+ * <p>
+ * Each document provider expresses one or more "roots" which each serve as the
+ * top-level of a tree. For example, a root could represent an account, or a
+ * physical storage device. Under each root, documents are referenced by
+ * {@link DocumentColumns#DOC_ID}, which must not change once returned.
+ * <p>
+ * Documents can be either an openable file (with a specific MIME type), or a
+ * directory containing additional documents (with the
+ * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different
+ * capabilities, as described by {@link DocumentColumns#FLAGS}. The same
+ * {@link DocumentColumns#DOC_ID} can be included in multiple directories.
+ * <p>
+ * Document providers must be protected with the
+ * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can
+ * only be requested by the system. The system-provided UI then issues narrow
+ * Uri permission grants for individual documents when the user explicitly picks
+ * documents.
+ *
+ * @see Intent#ACTION_OPEN_DOCUMENT
+ * @see Intent#ACTION_CREATE_DOCUMENT
+ */
+public abstract class DocumentsProvider extends ContentProvider {
+    private static final String TAG = "DocumentsProvider";
+
+    private static final int MATCH_DOCUMENT = 1;
+    private static final int MATCH_CHILDREN = 2;
+    private static final int MATCH_SEARCH = 3;
+
+    private String mAuthority;
+
+    private UriMatcher mMatcher;
+
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        mAuthority = info.authority;
+
+        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+        mMatcher.addURI(mAuthority, "docs/*", MATCH_DOCUMENT);
+        mMatcher.addURI(mAuthority, "docs/*/children", MATCH_CHILDREN);
+        mMatcher.addURI(mAuthority, "docs/*/search", MATCH_SEARCH);
+
+        // Sanity check our setup
+        if (!info.exported) {
+            throw new SecurityException("Provider must be exported");
+        }
+        if (!info.grantUriPermissions) {
+            throw new SecurityException("Provider must grantUriPermissions");
+        }
+        if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
+                || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
+            throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
+        }
+
+        super.attachInfo(context, info);
+    }
+
+    /**
+     * Return list of all document roots provided by this document provider.
+     * When this list changes, a provider must call
+     * {@link #notifyDocumentRootsChanged()}.
+     */
+    public abstract List<DocumentRoot> getDocumentRoots();
+
+    /**
+     * Create and return a new document. A provider must allocate a new
+     * {@link DocumentColumns#DOC_ID} to represent the document, which must not
+     * change once returned.
+     *
+     * @param docId the parent directory to create the new document under.
+     * @param mimeType the MIME type associated with the new document.
+     * @param displayName the display name of the new document.
+     */
+    @SuppressWarnings("unused")
+    public String createDocument(String docId, String mimeType, String displayName)
+            throws FileNotFoundException {
+        throw new UnsupportedOperationException("Create not supported");
+    }
+
+    /**
+     * Rename the given document.
+     *
+     * @param docId the document to rename.
+     * @param displayName the new display name.
+     */
+    @SuppressWarnings("unused")
+    public void renameDocument(String docId, String displayName) throws FileNotFoundException {
+        throw new UnsupportedOperationException("Rename not supported");
+    }
+
+    /**
+     * Delete the given document.
+     *
+     * @param docId the document to delete.
+     */
+    @SuppressWarnings("unused")
+    public void deleteDocument(String docId) throws FileNotFoundException {
+        throw new UnsupportedOperationException("Delete not supported");
+    }
+
+    /**
+     * Return metadata for the given document. A provider should avoid making
+     * network requests to keep this request fast.
+     *
+     * @param docId the document to return.
+     */
+    public abstract Cursor queryDocument(String docId) throws FileNotFoundException;
+
+    /**
+     * Return the children of the given document which is a directory.
+     *
+     * @param docId the directory to return children for.
+     */
+    public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException;
+
+    /**
+     * Return documents that that match the given query, starting the search at
+     * the given directory.
+     *
+     * @param docId the directory to start search at.
+     */
+    @SuppressWarnings("unused")
+    public Cursor querySearch(String docId, String query) throws FileNotFoundException {
+        throw new UnsupportedOperationException("Search not supported");
+    }
+
+    /**
+     * Return MIME type for the given document. Must match the value of
+     * {@link DocumentColumns#MIME_TYPE} for this document.
+     */
+    public String getType(String docId) throws FileNotFoundException {
+        final Cursor cursor = queryDocument(docId);
+        try {
+            if (cursor.moveToFirst()) {
+                return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE));
+            } else {
+                return null;
+            }
+        } finally {
+            IoUtils.closeQuietly(cursor);
+        }
+    }
+
+    /**
+     * Open and return the requested document. A provider should return a
+     * reliable {@link ParcelFileDescriptor} to detect when the remote caller
+     * has finished reading or writing the document. A provider may return a
+     * pipe or socket pair if the mode is exclusively
+     * {@link ParcelFileDescriptor#MODE_READ_ONLY} or
+     * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like
+     * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on
+     * disk. If a provider blocks while downloading content, it should
+     * periodically check {@link CancellationSignal#isCanceled()} to abort
+     * abandoned open requests.
+     *
+     * @param docId the document to return.
+     * @param mode the mode to open with, such as 'r', 'w', or 'rw'.
+     * @param signal used by the caller to signal if the request should be
+     *            cancelled.
+     * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler,
+     *      OnCloseListener)
+     * @see ParcelFileDescriptor#createReliablePipe()
+     * @see ParcelFileDescriptor#createReliableSocketPair()
+     */
+    public abstract ParcelFileDescriptor openDocument(
+            String docId, String mode, CancellationSignal signal) throws FileNotFoundException;
+
+    /**
+     * Open and return a thumbnail of the requested document. A provider should
+     * return a thumbnail closely matching the hinted size, attempting to serve
+     * from a local cache if possible. A provider should never return images
+     * more than double the hinted size. If a provider performs expensive
+     * operations to download or generate a thumbnail, it should periodically
+     * check {@link CancellationSignal#isCanceled()} to abort abandoned
+     * thumbnail requests.
+     *
+     * @param docId the document to return.
+     * @param sizeHint hint of the optimal thumbnail dimensions.
+     * @param signal used by the caller to signal if the request should be
+     *            cancelled.
+     * @see Documents#FLAG_SUPPORTS_THUMBNAIL
+     */
+    @SuppressWarnings("unused")
+    public AssetFileDescriptor openDocumentThumbnail(
+            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+        throw new UnsupportedOperationException("Thumbnails not supported");
+    }
+
+    @Override
+    public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        try {
+            switch (mMatcher.match(uri)) {
+                case MATCH_DOCUMENT:
+                    return queryDocument(getDocId(uri));
+                case MATCH_CHILDREN:
+                    return queryDocumentChildren(getDocId(uri));
+                case MATCH_SEARCH:
+                    return querySearch(getDocId(uri), getSearchQuery(uri));
+                default:
+                    throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Failed during query", e);
+            return null;
+        }
+    }
+
+    @Override
+    public final String getType(Uri uri) {
+        try {
+            switch (mMatcher.match(uri)) {
+                case MATCH_DOCUMENT:
+                    return getType(getDocId(uri));
+                default:
+                    return null;
+            }
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Failed during getType", e);
+            return null;
+        }
+    }
+
+    @Override
+    public final Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException("Insert not supported");
+    }
+
+    @Override
+    public final int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Delete not supported");
+    }
+
+    @Override
+    public final int update(
+            Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Update not supported");
+    }
+
+    @Override
+    public final Bundle callFromPackage(
+            String callingPackage, String method, String arg, Bundle extras) {
+        if (!method.startsWith("android:")) {
+            // Let non-platform methods pass through
+            return super.callFromPackage(callingPackage, method, arg, extras);
+        }
+
+        // Platform operations require the caller explicitly hold manage
+        // permission; Uri permissions don't extend management operations.
+        getContext().enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_DOCUMENTS, "Document management");
+
+        final Bundle out = new Bundle();
+        try {
+            if (METHOD_GET_ROOTS.equals(method)) {
+                final List<DocumentRoot> roots = getDocumentRoots();
+                out.putParcelableList(EXTRA_ROOTS, roots);
+
+            } else if (METHOD_CREATE_DOCUMENT.equals(method)) {
+                final String docId = extras.getString(DocumentColumns.DOC_ID);
+                final String mimeType = extras.getString(DocumentColumns.MIME_TYPE);
+                final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
+
+                // TODO: issue Uri grant towards caller
+                final String newDocId = createDocument(docId, mimeType, displayName);
+                out.putString(DocumentColumns.DOC_ID, newDocId);
+
+            } else if (METHOD_RENAME_DOCUMENT.equals(method)) {
+                final String docId = extras.getString(DocumentColumns.DOC_ID);
+                final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME);
+                renameDocument(docId, displayName);
+
+            } else if (METHOD_DELETE_DOCUMENT.equals(method)) {
+                final String docId = extras.getString(DocumentColumns.DOC_ID);
+                deleteDocument(docId);
+
+            } else {
+                throw new UnsupportedOperationException("Method not supported " + method);
+            }
+        } catch (FileNotFoundException e) {
+            throw new IllegalStateException("Failed call " + method, e);
+        }
+        return out;
+    }
+
+    @Override
+    public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        return openDocument(getDocId(uri), mode, null);
+    }
+
+    @Override
+    public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        return openDocument(getDocId(uri), mode, signal);
+    }
+
+    @Override
+    public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
+            throws FileNotFoundException {
+        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
+            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
+            return openDocumentThumbnail(getDocId(uri), sizeHint, null);
+        } else {
+            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
+        }
+    }
+
+    @Override
+    public final AssetFileDescriptor openTypedAssetFile(
+            Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
+            throws FileNotFoundException {
+        if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
+            final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
+            return openDocumentThumbnail(getDocId(uri), sizeHint, signal);
+        } else {
+            return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
+        }
+    }
+
+    /**
+     * Notify system that {@link #getDocumentRoots()} has changed, usually due to an
+     * account or device change.
+     */
+    public void notifyDocumentRootsChanged() {
+        final Intent intent = new Intent(ACTION_DOCUMENT_ROOT_CHANGED);
+        intent.putExtra(EXTRA_AUTHORITY, mAuthority);
+        getContext().sendBroadcast(intent);
+    }
+}
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index d79f5c6..6cc92e3 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -35,9 +35,9 @@
             </intent-filter>
             <!-- data expected to point at existing root to manage -->
             <intent-filter>
-                <action android:name="android.intent.action.MANAGE_DOCUMENT" />
+                <action android:name="android.provider.action.MANAGE_DOCUMENTS" />
                 <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="vnd.android.cursor.item/root" />
+                <data android:mimeType="vnd.android.doc/dir" />
             </intent-filter>
         </activity>
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
index 575947f..6bc554f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -20,14 +20,14 @@
 import android.app.Dialog;
 import android.app.DialogFragment;
 import android.app.FragmentManager;
+import android.content.ContentProviderClient;
 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.DocumentColumns;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Documents;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -36,8 +36,6 @@
 
 import com.android.documentsui.model.Document;
 
-import java.io.FileNotFoundException;
-
 /**
  * Dialog to create a new directory.
  */
@@ -58,7 +56,7 @@
         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);
+        final EditText text1 = (EditText) view.findViewById(android.R.id.text1);
 
         builder.setTitle(R.string.menu_create_dir);
         builder.setView(view);
@@ -68,24 +66,25 @@
             public void onClick(DialogInterface dialog, int which) {
                 final String displayName = text1.getText().toString();
 
-                final ContentValues values = new ContentValues();
-                values.put(DocumentColumns.MIME_TYPE, Documents.MIME_TYPE_DIR);
-                values.put(DocumentColumns.DISPLAY_NAME, displayName);
-
                 final DocumentsActivity activity = (DocumentsActivity) getActivity();
                 final Document cwd = activity.getCurrentDirectory();
 
-                Uri childUri = resolver.insert(cwd.uri, values);
+                final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                        cwd.uri.getAuthority());
                 try {
+                    final String docId = DocumentsContract.createDocument(client,
+                            DocumentsContract.getDocId(cwd.uri), Documents.MIME_TYPE_DIR,
+                            displayName);
+
                     // Navigate into newly created child
+                    final Uri childUri = DocumentsContract.buildDocumentUri(
+                            cwd.uri.getAuthority(), docId);
                     final Document childDoc = Document.fromUri(resolver, childUri);
                     activity.onDocumentPicked(childDoc);
-                } catch (FileNotFoundException e) {
-                    childUri = null;
-                }
-
-                if (childUri == null) {
+                } catch (Exception e) {
                     Toast.makeText(context, R.string.save_error, Toast.LENGTH_SHORT).show();
+                } finally {
+                    ContentProviderClient.closeQuietly(client);
                 }
             }
         });
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index dd9aee5..783b6ff 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -20,8 +20,8 @@
 import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE;
 import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
 import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DATE;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_NAME;
+import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
 import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_SIZE;
 
 import android.app.Fragment;
@@ -32,7 +32,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.Loader;
-import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.net.Uri;
@@ -55,7 +54,6 @@
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.BaseAdapter;
-import android.widget.Button;
 import android.widget.GridView;
 import android.widget.ImageView;
 import android.widget.ListView;
@@ -64,7 +62,6 @@
 
 import com.android.documentsui.DocumentsActivity.DisplayState;
 import com.android.documentsui.model.Document;
-import com.android.documentsui.model.Root;
 import com.android.internal.util.Predicate;
 import com.google.android.collect.Lists;
 
@@ -81,7 +78,6 @@
     private View mEmptyView;
     private ListView mListView;
     private GridView mGridView;
-    private Button mMoreView;
 
     private AbsListView mCurrentView;
 
@@ -110,7 +106,8 @@
     }
 
     public static void showSearch(FragmentManager fm, Uri uri, String query) {
-        final Uri searchUri = DocumentsContract.buildSearchUri(uri, query);
+        final Uri searchUri = DocumentsContract.buildSearchUri(
+                uri.getAuthority(), DocumentsContract.getDocId(uri), query);
         show(fm, TYPE_SEARCH, searchUri);
     }
 
@@ -153,8 +150,6 @@
         mGridView.setOnItemClickListener(mItemListener);
         mGridView.setMultiChoiceModeListener(mMultiListener);
 
-        mMoreView = (Button) view.findViewById(R.id.more);
-
         mAdapter = new DocumentsAdapter();
 
         final Uri uri = getArguments().getParcelable(EXTRA_URI);
@@ -168,22 +163,19 @@
 
                 Uri contentsUri;
                 if (mType == TYPE_NORMAL) {
-                    contentsUri = DocumentsContract.buildContentsUri(uri);
+                    contentsUri = DocumentsContract.buildChildrenUri(
+                            uri.getAuthority(), DocumentsContract.getDocId(uri));
                 } else if (mType == TYPE_RECENT_OPEN) {
                     contentsUri = RecentsProvider.buildRecentOpen();
                 } else {
                     contentsUri = uri;
                 }
 
-                if (state.localOnly) {
-                    contentsUri = DocumentsContract.setLocalOnly(contentsUri);
-                }
-
                 final Comparator<Document> sortOrder;
-                if (state.sortOrder == SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN) {
-                    sortOrder = new Document.DateComparator();
-                } else if (state.sortOrder == SORT_ORDER_NAME) {
-                    sortOrder = new Document.NameComparator();
+                if (state.sortOrder == SORT_ORDER_LAST_MODIFIED || mType == TYPE_RECENT_OPEN) {
+                    sortOrder = new Document.LastModifiedComparator();
+                } else if (state.sortOrder == SORT_ORDER_DISPLAY_NAME) {
+                    sortOrder = new Document.DisplayNameComparator();
                 } else if (state.sortOrder == SORT_ORDER_SIZE) {
                     sortOrder = new Document.SizeComparator();
                 } else {
@@ -196,28 +188,6 @@
             @Override
             public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
                 mAdapter.swapDocuments(result.contents);
-
-                final Cursor cursor = result.cursor;
-                if (cursor != null && cursor.getExtras()
-                        .getBoolean(DocumentsContract.EXTRA_HAS_MORE, false)) {
-                    mMoreView.setText(R.string.more);
-                    mMoreView.setVisibility(View.VISIBLE);
-                    mMoreView.setOnClickListener(new View.OnClickListener() {
-                        @Override
-                        public void onClick(View v) {
-                            mMoreView.setText(R.string.loading);
-                            final Bundle bundle = new Bundle();
-                            bundle.putBoolean(DocumentsContract.EXTRA_REQUEST_MORE, true);
-                            try {
-                                cursor.respond(bundle);
-                            } catch (Exception e) {
-                                Log.w(TAG, "Failed to respond: " + e);
-                            }
-                        }
-                    });
-                } else {
-                    mMoreView.setVisibility(View.GONE);
-                }
             }
 
             @Override
@@ -489,8 +459,7 @@
                     task.execute(doc.uri);
                 }
             } else {
-                icon.setImageDrawable(roots.resolveDocumentIcon(
-                        context, doc.uri.getAuthority(), doc.mimeType));
+                icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, doc.mimeType));
             }
 
             title.setText(doc.displayName);
@@ -504,11 +473,7 @@
                     summary.setVisibility(View.INVISIBLE);
                 }
             } else if (mType == TYPE_RECENT_OPEN) {
-                final Root root = roots.findRoot(doc);
-                icon1.setVisibility(View.VISIBLE);
-                icon1.setImageDrawable(root.icon);
-                summary.setText(root.getDirectoryString());
-                summary.setVisibility(View.VISIBLE);
+                // TODO: resolve storage root
             }
 
             if (summaryGrid != null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index 14d6fd5..4ce5ef8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -26,7 +26,6 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.CancellationSignal;
-import android.provider.DocumentsContract.DocumentColumns;
 import android.util.Log;
 
 import com.android.documentsui.model.Document;
@@ -77,9 +76,10 @@
     }
 
     private void loadInBackgroundInternal(
-            DirectoryResult result, Uri uri, CancellationSignal signal) {
+            DirectoryResult result, Uri uri, CancellationSignal signal) throws RuntimeException {
+        // TODO: switch to using unstable CPC
         final ContentResolver resolver = getContext().getContentResolver();
-        final Cursor cursor = resolver.query(uri, null, null, null, getQuerySortOrder(), signal);
+        final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
         result.cursor = cursor;
         result.cursor.registerContentObserver(mObserver);
 
@@ -110,16 +110,4 @@
             Collections.sort(result.contents, mSortOrder);
         }
     }
-
-    private String getQuerySortOrder() {
-        if (mSortOrder instanceof Document.DateComparator) {
-            return DocumentColumns.LAST_MODIFIED + " DESC";
-        } else if (mSortOrder instanceof Document.NameComparator) {
-            return DocumentColumns.DISPLAY_NAME + " ASC";
-        } else if (mSortOrder instanceof Document.SizeComparator) {
-            return DocumentColumns.SIZE + " DESC";
-        } else {
-            return null;
-        }
-    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
index 72afd9e..0ce5968 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
@@ -21,12 +21,11 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.provider.DocumentsContract.DocumentRoot;
 import android.util.Log;
 
-import com.android.documentsui.model.Root;
-
 /**
- * Handles {@link Root} changes which invalidate cached data.
+ * Handles {@link DocumentRoot} changes which invalidate cached data.
  */
 public class DocumentChangedReceiver extends BroadcastReceiver {
     @Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 22e3b98..73ca8fa 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -22,7 +22,7 @@
 import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_OPEN;
 import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
 import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DATE;
+import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
 
 import android.app.ActionBar;
 import android.app.ActionBar.OnNavigationListener;
@@ -32,6 +32,7 @@
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
 import android.content.ComponentName;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Intent;
@@ -41,7 +42,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.DocumentRoot;
 import android.support.v4.app.ActionBarDrawerToggle;
 import android.support.v4.view.GravityCompat;
 import android.support.v4.widget.DrawerLayout;
@@ -61,7 +62,6 @@
 
 import com.android.documentsui.model.Document;
 import com.android.documentsui.model.DocumentStack;
-import com.android.documentsui.model.Root;
 
 import java.io.FileNotFoundException;
 import java.util.Arrays;
@@ -101,7 +101,7 @@
             mAction = ACTION_CREATE;
         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
             mAction = ACTION_GET_CONTENT;
-        } else if (Intent.ACTION_MANAGE_DOCUMENT.equals(action)) {
+        } else if (DocumentsContract.ACTION_MANAGE_DOCUMENTS.equals(action)) {
             mAction = ACTION_MANAGE;
         }
 
@@ -143,7 +143,7 @@
         }
 
         if (mAction == ACTION_MANAGE) {
-            mDisplayState.sortOrder = SORT_ORDER_DATE;
+            mDisplayState.sortOrder = SORT_ORDER_LAST_MODIFIED;
         }
 
         mRootsContainer = findViewById(R.id.container_roots);
@@ -160,10 +160,7 @@
             mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
 
             final Uri rootUri = intent.getData();
-            final String authority = rootUri.getAuthority();
-            final String rootId = DocumentsContract.getRootId(rootUri);
-
-            final Root root = mRoots.findRoot(authority, rootId);
+            final DocumentRoot root = mRoots.findRoot(rootUri);
             if (root != null) {
                 onRootPicked(root, true);
             } else {
@@ -255,10 +252,10 @@
             mDrawerToggle.setDrawerIndicatorEnabled(true);
 
         } else {
-            final Root root = getCurrentRoot();
-            actionBar.setIcon(root != null ? root.icon : null);
+            final DocumentRoot root = getCurrentRoot();
+            actionBar.setIcon(root != null ? root.loadIcon(this) : null);
 
-            if (root.isRecents) {
+            if (mRoots.isRecentsRoot(root)) {
                 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
                 actionBar.setTitle(root.title);
             } else {
@@ -441,9 +438,8 @@
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
 
-            final Document cwd = getCurrentDirectory();
-            if (cwd != null) {
-                title.setText(cwd.displayName);
+            if (mStack.size() > 0) {
+                title.setText(mStack.getTitle(mRoots));
             } else {
                 // No directory means recents
                 title.setText(R.string.root_recent);
@@ -477,10 +473,9 @@
         }
     };
 
-    public Root getCurrentRoot() {
-        final Document cwd = getCurrentDirectory();
-        if (cwd != null) {
-            return mRoots.findRoot(cwd);
+    public DocumentRoot getCurrentRoot() {
+        if (mStack.size() > 0) {
+            return mStack.getRoot(mRoots);
         } else {
             return mRoots.getRecentsRoot();
         }
@@ -538,13 +533,14 @@
         onCurrentDirectoryChanged();
     }
 
-    public void onRootPicked(Root root, boolean closeDrawer) {
+    public void onRootPicked(DocumentRoot root, boolean closeDrawer) {
         // Clear entire backstack and start in new root
         mStack.clear();
 
-        if (!root.isRecents) {
+        if (!mRoots.isRecentsRoot(root)) {
             try {
-                onDocumentPicked(Document.fromRoot(getContentResolver(), root));
+                final Uri uri = DocumentsContract.buildDocumentUri(root.authority, root.docId);
+                onDocumentPicked(Document.fromUri(getContentResolver(), uri));
             } catch (FileNotFoundException e) {
             }
         } else {
@@ -611,16 +607,21 @@
     }
 
     public void onSaveRequested(String mimeType, String displayName) {
-        final ContentValues values = new ContentValues();
-        values.put(DocumentColumns.MIME_TYPE, mimeType);
-        values.put(DocumentColumns.DISPLAY_NAME, displayName);
-
         final Document cwd = getCurrentDirectory();
-        final Uri childUri = getContentResolver().insert(cwd.uri, values);
-        if (childUri != null) {
+        final String authority = cwd.uri.getAuthority();
+
+        final ContentProviderClient client = getContentResolver()
+                .acquireUnstableContentProviderClient(authority);
+        try {
+            final String docId = DocumentsContract.createDocument(client,
+                    DocumentsContract.getDocId(cwd.uri), mimeType, displayName);
+
+            final Uri childUri = DocumentsContract.buildDocumentUri(authority, docId);
             onFinished(childUri);
-        } else {
+        } catch (Exception e) {
             Toast.makeText(this, R.string.save_error, Toast.LENGTH_SHORT).show();
+        } finally {
+            ContentProviderClient.closeQuietly(client);
         }
     }
 
@@ -680,7 +681,7 @@
         public int action;
         public int mode = MODE_LIST;
         public String[] acceptMimes;
-        public int sortOrder = SORT_ORDER_NAME;
+        public int sortOrder = SORT_ORDER_DISPLAY_NAME;
         public boolean allowMultiple = false;
         public boolean showSize = false;
         public boolean localOnly = false;
@@ -693,8 +694,8 @@
         public static final int MODE_LIST = 0;
         public static final int MODE_GRID = 1;
 
-        public static final int SORT_ORDER_NAME = 0;
-        public static final int SORT_ORDER_DATE = 1;
+        public static final int SORT_ORDER_DISPLAY_NAME = 0;
+        public static final int SORT_ORDER_LAST_MODIFIED = 1;
         public static final int SORT_ORDER_SIZE = 2;
     }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index 5466dbf..3447a51 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -29,6 +29,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
+import android.provider.DocumentsContract.DocumentRoot;
 import android.text.TextUtils.TruncateAt;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -42,7 +43,6 @@
 import android.widget.TextView;
 
 import com.android.documentsui.model.DocumentStack;
-import com.android.documentsui.model.Root;
 import com.google.android.collect.Lists;
 
 import libcore.io.IoUtils;
@@ -181,8 +181,8 @@
             final View summaryList = convertView.findViewById(R.id.summary_list);
 
             final DocumentStack stack = getItem(position);
-            final Root root = roots.findRoot(stack.peek());
-            icon.setImageDrawable(root != null ? root.icon : null);
+            final DocumentRoot root = stack.getRoot(roots);
+            icon.setImageDrawable(root.loadIcon(context));
 
             final StringBuilder builder = new StringBuilder();
             for (int i = stack.size() - 1; i >= 0; i--) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index c3b498e..aa21457 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -18,30 +18,24 @@
 
 import static com.android.documentsui.DocumentsActivity.TAG;
 
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 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.provider.DocumentsContract.DocumentRoot;
 import android.provider.DocumentsContract.Documents;
 import android.util.Log;
-import android.util.Pair;
 
-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.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Objects;
 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;
 
 /**
@@ -54,14 +48,9 @@
 
     private final Context mContext;
 
-    /** Map from authority to cached info */
-    private HashMap<String, DocumentsProviderInfo> mProviders = Maps.newHashMap();
-    /** Map from (authority+rootId) to cached info */
-    private HashMap<Pair<String, String>, Root> mRoots = Maps.newHashMap();
+    public List<DocumentRoot> mRoots = Lists.newArrayList();
 
-    public ArrayList<Root> mRootsList = Lists.newArrayList();
-
-    private Root mRecentsRoot;
+    private DocumentRoot mRecentsRoot;
 
     public RootsCache(Context context) {
         mContext = context;
@@ -73,95 +62,78 @@
      */
     @GuardedBy("ActivityThread")
     public void update() {
-        mProviders.clear();
         mRoots.clear();
-        mRootsList.clear();
 
         {
             // Create special root for recents
-            final Root root = Root.buildRecents(mContext);
-            mRootsList.add(root);
+            final DocumentRoot root = new DocumentRoot();
+            root.rootType = DocumentRoot.ROOT_TYPE_SHORTCUT;
+            root.docId = null;
+            root.icon = R.drawable.ic_dir;
+            root.title = mContext.getString(R.string.root_recent);
+            root.summary = null;
+            root.availableBytes = -1;
+
+            mRoots.add(root);
             mRecentsRoot = root;
         }
 
         // Query for other storage backends
+        final ContentResolver resolver = mContext.getContentResolver();
         final PackageManager pm = mContext.getPackageManager();
         final List<ProviderInfo> providers = pm.queryContentProviders(
                 null, -1, PackageManager.GET_META_DATA);
-        for (ProviderInfo providerInfo : providers) {
-            if (providerInfo.metaData != null && providerInfo.metaData.containsKey(
+        for (ProviderInfo info : providers) {
+            if (info.metaData != null && info.metaData.containsKey(
                     DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
-                final DocumentsProviderInfo info = DocumentsProviderInfo.parseInfo(
-                        mContext, providerInfo);
-                if (info == null) {
-                    Log.w(TAG, "Missing info for " + providerInfo);
-                    continue;
-                }
 
-                mProviders.put(info.providerInfo.authority, info);
-
+                // TODO: remove deprecated customRoots flag
+                // TODO: populate roots on background thread, and cache results
+                final ContentProviderClient client = resolver
+                        .acquireUnstableContentProviderClient(info.authority);
                 try {
-                    // TODO: remove deprecated customRoots flag
-                    // TODO: populate roots on background thread, and cache results
-                    final Uri uri = DocumentsContract.buildRootsUri(providerInfo.authority);
-                    final Cursor cursor = mContext.getContentResolver()
-                            .query(uri, null, null, null, null);
-                    try {
-                        while (cursor.moveToNext()) {
-                            final Root root = Root.fromCursor(mContext, info, cursor);
-                            mRoots.put(Pair.create(info.providerInfo.authority, root.rootId), root);
-                            mRootsList.add(root);
-                        }
-                    } finally {
-                        cursor.close();
+                    final List<DocumentRoot> roots = DocumentsContract.getDocumentRoots(client);
+                    for (DocumentRoot root : roots) {
+                        root.authority = info.authority;
                     }
+                    mRoots.addAll(roots);
                 } catch (Exception e) {
-                    Log.w(TAG, "Failed to load some roots from " + info.providerInfo.authority
-                            + ": " + e);
+                    Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e);
+                } finally {
+                    ContentProviderClient.closeQuietly(client);
                 }
             }
         }
     }
 
-    @GuardedBy("ActivityThread")
-    public DocumentsProviderInfo findProvider(String authority) {
-        return mProviders.get(authority);
+    public DocumentRoot findRoot(Uri uri) {
+        final String authority = uri.getAuthority();
+        final String docId = DocumentsContract.getDocId(uri);
+        for (DocumentRoot root : mRoots) {
+            if (Objects.equal(root.authority, authority) && Objects.equal(root.docId, docId)) {
+                return root;
+            }
+        }
+        return null;
     }
 
     @GuardedBy("ActivityThread")
-    public Root findRoot(String authority, String rootId) {
-        return mRoots.get(Pair.create(authority, rootId));
-    }
-
-    @GuardedBy("ActivityThread")
-    public Root findRoot(Document doc) {
-        final String authority = doc.uri.getAuthority();
-        final String rootId = DocumentsContract.getRootId(doc.uri);
-        return findRoot(authority, rootId);
-    }
-
-    @GuardedBy("ActivityThread")
-    public Root getRecentsRoot() {
+    public DocumentRoot getRecentsRoot() {
         return mRecentsRoot;
     }
 
     @GuardedBy("ActivityThread")
-    public Collection<Root> getRoots() {
-        return mRootsList;
+    public boolean isRecentsRoot(DocumentRoot root) {
+        return mRecentsRoot == root;
     }
 
     @GuardedBy("ActivityThread")
-    public Drawable resolveDocumentIcon(Context context, String authority, String mimeType) {
-        // Custom icons take precedence
-        final DocumentsProviderInfo info = mProviders.get(authority);
-        if (info != null) {
-            for (Icon icon : info.customIcons) {
-                if (MimePredicate.mimeMatches(icon.mimeType, mimeType)) {
-                    return icon.icon;
-                }
-            }
-        }
+    public List<DocumentRoot> getRoots() {
+        return mRoots;
+    }
 
+    @GuardedBy("ActivityThread")
+    public static Drawable resolveDocumentIcon(Context context, String mimeType) {
         if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
             return context.getResources().getDrawable(R.drawable.ic_dir);
         } else {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index 8a48e2a..2cfa841 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -26,7 +26,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
-import android.provider.DocumentsContract.Roots;
+import android.provider.DocumentsContract.DocumentRoot;
 import android.text.format.Formatter;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -40,10 +40,9 @@
 import android.widget.TextView;
 
 import com.android.documentsui.SectionedListAdapter.SectionAdapter;
-import com.android.documentsui.model.Root;
-import com.android.documentsui.model.Root.RootComparator;
+import com.android.documentsui.model.Document;
 
-import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
 
 /**
@@ -102,8 +101,8 @@
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             final DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
             final Object item = mAdapter.getItem(position);
-            if (item instanceof Root) {
-                activity.onRootPicked((Root) item, true);
+            if (item instanceof DocumentRoot) {
+                activity.onRootPicked((DocumentRoot) item, true);
             } else if (item instanceof ResolveInfo) {
                 activity.onAppPicked((ResolveInfo) item);
             } else {
@@ -112,7 +111,7 @@
         }
     };
 
-    private static class RootsAdapter extends ArrayAdapter<Root> implements SectionAdapter {
+    private static class RootsAdapter extends ArrayAdapter<DocumentRoot> implements SectionAdapter {
         private int mHeaderId;
 
         public RootsAdapter(Context context, int headerId) {
@@ -132,14 +131,14 @@
             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);
+            final DocumentRoot root = getItem(position);
+            icon.setImageDrawable(root.loadIcon(context));
             title.setText(root.title);
 
             // Device summary is always available space
             final String summaryText;
-            if ((root.rootType == Roots.ROOT_TYPE_DEVICE
-                    || root.rootType == Roots.ROOT_TYPE_DEVICE_ADVANCED)
+            if ((root.rootType == DocumentRoot.ROOT_TYPE_DEVICE
+                    || root.rootType == DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED)
                     && root.availableBytes >= 0) {
                 summaryText = context.getString(R.string.root_available_bytes,
                         Formatter.formatFileSize(context, root.availableBytes));
@@ -216,27 +215,27 @@
         private final RootsAdapter mDevicesAdvanced;
         private final AppsAdapter mApps;
 
-        public SectionedRootsAdapter(Context context, Collection<Root> roots, Intent includeApps) {
+        public SectionedRootsAdapter(Context context, List<DocumentRoot> roots, Intent includeApps) {
             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);
             mApps = new AppsAdapter(context);
 
-            for (Root root : roots) {
+            for (DocumentRoot root : roots) {
                 Log.d(TAG, "Found rootType=" + root.rootType);
                 switch (root.rootType) {
-                    case Roots.ROOT_TYPE_SERVICE:
+                    case DocumentRoot.ROOT_TYPE_SERVICE:
                         mServices.add(root);
                         break;
-                    case Roots.ROOT_TYPE_SHORTCUT:
+                    case DocumentRoot.ROOT_TYPE_SHORTCUT:
                         mShortcuts.add(root);
                         break;
-                    case Roots.ROOT_TYPE_DEVICE:
+                    case DocumentRoot.ROOT_TYPE_DEVICE:
                         mDevices.add(root);
                         mDevicesAdvanced.add(root);
                         break;
-                    case Roots.ROOT_TYPE_DEVICE_ADVANCED:
+                    case DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED:
                         mDevicesAdvanced.add(root);
                         break;
                 }
@@ -281,4 +280,16 @@
             }
         }
     }
+
+    public static class RootComparator implements Comparator<DocumentRoot> {
+        @Override
+        public int compare(DocumentRoot lhs, DocumentRoot rhs) {
+            final int score = Document.compareToIgnoreCaseNullable(lhs.title, rhs.title);
+            if (score != 0) {
+                return score;
+            } else {
+                return Document.compareToIgnoreCaseNullable(lhs.summary, rhs.summary);
+            }
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index 8eb81b8..7e1a297 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -73,8 +73,8 @@
         final View view = inflater.inflate(R.layout.fragment_save, container, false);
 
         final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
-        icon.setImageDrawable(roots.resolveDocumentIcon(
-                context, null, getArguments().getString(EXTRA_MIME_TYPE)));
+        icon.setImageDrawable(
+                RootsCache.resolveDocumentIcon(context, getArguments().getString(EXTRA_MIME_TYPE)));
 
         mDisplayName = (EditText) view.findViewById(android.R.id.title);
         mDisplayName.addTextChangedListener(mDisplayNameWatcher);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
index c0f21cb..692d171 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/Document.java
@@ -53,17 +53,11 @@
         this.size = size;
     }
 
-    public static Document fromRoot(ContentResolver resolver, Root root)
-            throws FileNotFoundException {
-        return fromUri(resolver, root.uri);
-    }
-
     public static Document fromDirectoryCursor(Uri parent, Cursor cursor) {
         final String authority = parent.getAuthority();
-        final String rootId = DocumentsContract.getRootId(parent);
         final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
 
-        final Uri uri = DocumentsContract.buildDocumentUri(authority, rootId, docId);
+        final Uri uri = DocumentsContract.buildDocumentUri(authority, docId);
         final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
         final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
         final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
@@ -74,6 +68,7 @@
         return new Document(uri, mimeType, displayName, lastModified, flags, summary, size);
     }
 
+    @Deprecated
     public static Document fromRecentOpenCursor(ContentResolver resolver, Cursor recentCursor)
             throws FileNotFoundException {
         final Uri uri = Uri.parse(getCursorString(recentCursor, RecentsProvider.COL_URI));
@@ -176,7 +171,7 @@
         return (index != -1) ? cursor.getInt(index) : 0;
     }
 
-    public static class NameComparator implements Comparator<Document> {
+    public static class DisplayNameComparator implements Comparator<Document> {
         @Override
         public int compare(Document lhs, Document rhs) {
             final boolean leftDir = lhs.isDirectory();
@@ -185,12 +180,12 @@
             if (leftDir != rightDir) {
                 return leftDir ? -1 : 1;
             } else {
-                return Root.compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
+                return compareToIgnoreCaseNullable(lhs.displayName, rhs.displayName);
             }
         }
     }
 
-    public static class DateComparator implements Comparator<Document> {
+    public static class LastModifiedComparator implements Comparator<Document> {
         @Override
         public int compare(Document lhs, Document rhs) {
             return Long.compare(rhs.lastModified, lhs.lastModified);
@@ -213,4 +208,10 @@
         fnfe.initCause(t);
         throw fnfe;
     }
+
+    public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
+        if (lhs == null) return -1;
+        if (rhs == null) return 1;
+        return lhs.compareToIgnoreCase(rhs);
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
index d6c852e..81f75d2 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
@@ -21,8 +21,11 @@
 
 import android.content.ContentResolver;
 import android.net.Uri;
+import android.provider.DocumentsContract.DocumentRoot;
 import android.util.Log;
 
+import com.android.documentsui.RootsCache;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 
@@ -62,4 +65,18 @@
         // TODO: handle roots that have gone missing
         return stack;
     }
+
+    public DocumentRoot getRoot(RootsCache roots) {
+        return roots.findRoot(getLast().uri);
+    }
+
+    public String getTitle(RootsCache roots) {
+        if (size() == 1) {
+            return getRoot(roots).title;
+        } else if (size() > 1) {
+            return peek().displayName;
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java
deleted file mode 100644
index 96eb58e..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.model;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ProviderInfo;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.graphics.drawable.Drawable;
-import android.provider.DocumentsContract;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.Xml;
-
-import com.android.documentsui.DocumentsActivity;
-import com.google.android.collect.Lists;
-
-import libcore.io.IoUtils;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Representation of a storage backend.
- */
-public class DocumentsProviderInfo {
-    private static final String TAG = DocumentsActivity.TAG;
-
-    public ProviderInfo providerInfo;
-    public boolean customRoots;
-    public List<Icon> customIcons;
-
-    public static class Icon {
-        public String mimeType;
-        public Drawable icon;
-    }
-
-    private static final String TAG_DOCUMENTS_PROVIDER = "documents-provider";
-    private static final String TAG_ICON = "icon";
-
-    public static DocumentsProviderInfo buildRecents(Context context, ProviderInfo providerInfo) {
-        final DocumentsProviderInfo info = new DocumentsProviderInfo();
-        info.providerInfo = providerInfo;
-        info.customRoots = false;
-        return info;
-    }
-
-    public static DocumentsProviderInfo parseInfo(Context context, ProviderInfo providerInfo) {
-        final DocumentsProviderInfo info = new DocumentsProviderInfo();
-        info.providerInfo = providerInfo;
-        info.customIcons = Lists.newArrayList();
-
-        final PackageManager pm = context.getPackageManager();
-        final Resources res;
-        try {
-            res = pm.getResourcesForApplication(providerInfo.applicationInfo);
-        } catch (NameNotFoundException e) {
-            Log.w(TAG, "Failed to find resources for " + providerInfo, e);
-            return null;
-        }
-
-        XmlResourceParser parser = null;
-        try {
-            parser = providerInfo.loadXmlMetaData(
-                    pm, DocumentsContract.META_DATA_DOCUMENT_PROVIDER);
-            AttributeSet attrs = Xml.asAttributeSet(parser);
-
-            int type = 0;
-            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
-                final String tag = parser.getName();
-                if (type == XmlPullParser.START_TAG && TAG_DOCUMENTS_PROVIDER.equals(tag)) {
-                    final TypedArray a = res.obtainAttributes(
-                            attrs, com.android.internal.R.styleable.DocumentsProviderInfo);
-                    info.customRoots = a.getBoolean(
-                            com.android.internal.R.styleable.DocumentsProviderInfo_customRoots,
-                            false);
-                    a.recycle();
-
-                } else if (type == XmlPullParser.START_TAG && TAG_ICON.equals(tag)) {
-                    final TypedArray a = res.obtainAttributes(
-                            attrs, com.android.internal.R.styleable.Icon);
-                    final Icon icon = new Icon();
-                    icon.mimeType = a.getString(com.android.internal.R.styleable.Icon_mimeType);
-                    icon.icon = a.getDrawable(com.android.internal.R.styleable.Icon_icon);
-                    info.customIcons.add(icon);
-                    a.recycle();
-                }
-            }
-        } catch (IOException e) {
-            Log.w(TAG, "Failed to parse metadata", e);
-            return null;
-        } catch (XmlPullParserException e) {
-            Log.w(TAG, "Failed to parse metadata", e);
-            return null;
-        } finally {
-            IoUtils.closeQuietly(parser);
-        }
-
-        return info;
-    }
-}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java b/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
deleted file mode 100644
index 23d16df..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/model/Root.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.model;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources.NotFoundException;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.Documents;
-import android.provider.DocumentsContract.RootColumns;
-import android.provider.DocumentsContract.Roots;
-
-import com.android.documentsui.R;
-
-import java.util.Comparator;
-
-/**
- * Representation of a root under a storage backend.
- */
-public class Root {
-    public String rootId;
-    public int rootType;
-    public Uri uri;
-    public Drawable icon;
-    public String title;
-    public String summary;
-    public long availableBytes = -1;
-    public boolean isRecents;
-
-    public static Root buildRecents(Context context) {
-        final PackageManager pm = context.getPackageManager();
-        final Root root = new Root();
-        root.rootId = null;
-        root.rootType = Roots.ROOT_TYPE_SHORTCUT;
-        root.uri = null;
-        root.icon = context.getResources().getDrawable(R.drawable.ic_dir);
-        root.title = context.getString(R.string.root_recent);
-        root.summary = null;
-        root.availableBytes = -1;
-        root.isRecents = true;
-        return root;
-    }
-
-    public static Root fromCursor(
-            Context context, DocumentsProviderInfo info, Cursor cursor) {
-        final PackageManager pm = context.getPackageManager();
-
-        final Root root = new Root();
-        root.rootId = cursor.getString(cursor.getColumnIndex(RootColumns.ROOT_ID));
-        root.rootType = cursor.getInt(cursor.getColumnIndex(RootColumns.ROOT_TYPE));
-        root.uri = DocumentsContract.buildDocumentUri(
-                info.providerInfo.authority, root.rootId, Documents.DOC_ID_ROOT);
-        root.icon = info.providerInfo.loadIcon(pm);
-        root.title = info.providerInfo.loadLabel(pm).toString();
-        root.availableBytes = cursor.getLong(cursor.getColumnIndex(RootColumns.AVAILABLE_BYTES));
-        root.summary = null;
-
-        final int icon = cursor.getInt(cursor.getColumnIndex(RootColumns.ICON));
-        if (icon != 0) {
-            try {
-                root.icon = pm.getResourcesForApplication(info.providerInfo.applicationInfo)
-                        .getDrawable(icon);
-            } catch (NotFoundException e) {
-                throw new RuntimeException(e);
-            } catch (NameNotFoundException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        final String title = cursor.getString(cursor.getColumnIndex(RootColumns.TITLE));
-        if (title != null) {
-            root.title = title;
-        }
-
-        root.summary = cursor.getString(cursor.getColumnIndex(RootColumns.SUMMARY));
-        root.isRecents = false;
-
-        return root;
-    }
-
-    /**
-     * Return string most suited to showing in a directory listing.
-     */
-    public String getDirectoryString() {
-        return (summary != null) ? summary : title;
-    }
-
-    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);
-    }
-}
diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml
index 8bd2a6d..5272166 100644
--- a/packages/ExternalStorageProvider/AndroidManifest.xml
+++ b/packages/ExternalStorageProvider/AndroidManifest.xml
@@ -15,18 +15,5 @@
                 android:name="android.content.DOCUMENT_PROVIDER"
                 android:resource="@xml/document_provider" />
         </provider>
-
-        <!-- TODO: remove when we have real providers -->
-        <provider
-            android:name=".CloudTestDocumentsProvider"
-            android:authorities="com.android.externalstorage.cloudtest"
-            android:grantUriPermissions="true"
-            android:exported="true"
-            android:enabled="false"
-            android:permission="android.permission.MANAGE_DOCUMENTS">
-            <meta-data
-                android:name="android.content.DOCUMENT_PROVIDER"
-                android:resource="@xml/document_provider" />
-        </provider>
     </application>
 </manifest>
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java
deleted file mode 100644
index 119d92e..0000000
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * 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.externalstorage;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MatrixCursor.RowBuilder;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.DocumentColumns;
-import android.provider.DocumentsContract.Documents;
-import android.provider.DocumentsContract.RootColumns;
-import android.provider.DocumentsContract.Roots;
-import android.util.Log;
-
-import com.google.android.collect.Lists;
-
-import libcore.io.IoUtils;
-
-import java.io.FileNotFoundException;
-import java.util.List;
-
-public class CloudTestDocumentsProvider extends ContentProvider {
-    private static final String TAG = "CloudTest";
-
-    private static final String AUTHORITY = "com.android.externalstorage.cloudtest";
-
-    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
-    private static final int URI_ROOTS = 1;
-    private static final int URI_ROOTS_ID = 2;
-    private static final int URI_DOCS_ID = 3;
-    private static final int URI_DOCS_ID_CONTENTS = 4;
-    private static final int URI_DOCS_ID_SEARCH = 5;
-
-    static {
-        sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
-        sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
-        sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
-        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
-        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
-    }
-
-    private static final String[] ALL_ROOTS_COLUMNS = new String[] {
-            RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
-            RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
-    };
-
-    private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
-            DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
-            DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
-    };
-
-    private List<String> mKnownDocs = Lists.newArrayList("meow.png", "kittens.pdf");
-
-    private int mPage;
-
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        switch (sMatcher.match(uri)) {
-            case URI_ROOTS: {
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_ROOTS_COLUMNS);
-                includeDefaultRoot(result);
-                return result;
-            }
-            case URI_ROOTS_ID: {
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_ROOTS_COLUMNS);
-                includeDefaultRoot(result);
-                return result;
-            }
-            case URI_DOCS_ID: {
-                final String docId = DocumentsContract.getDocId(uri);
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
-                includeDoc(result, docId);
-                return result;
-            }
-            case URI_DOCS_ID_CONTENTS: {
-                final CloudCursor result = new CloudCursor(
-                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS, uri);
-                for (String docId : mKnownDocs) {
-                    includeDoc(result, docId);
-                }
-                if (mPage < 3) {
-                    result.setHasMore();
-                }
-                result.setNotificationUri(getContext().getContentResolver(), uri);
-                return result;
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    private void includeDefaultRoot(MatrixCursor result) {
-        final RowBuilder row = result.newRow();
-        row.offer(RootColumns.ROOT_ID, "testroot");
-        row.offer(RootColumns.ROOT_TYPE, Roots.ROOT_TYPE_SERVICE);
-        row.offer(RootColumns.TITLE, "_TestTitle");
-        row.offer(RootColumns.SUMMARY, "_TestSummary");
-    }
-
-    private void includeDoc(MatrixCursor result, String docId) {
-        int flags = 0;
-
-        final String mimeType;
-        if (Documents.DOC_ID_ROOT.equals(docId)) {
-            mimeType = Documents.MIME_TYPE_DIR;
-        } else {
-            mimeType = "application/octet-stream";
-        }
-
-        final RowBuilder row = result.newRow();
-        row.offer(DocumentColumns.DOC_ID, docId);
-        row.offer(DocumentColumns.DISPLAY_NAME, docId);
-        row.offer(DocumentColumns.MIME_TYPE, mimeType);
-        row.offer(DocumentColumns.LAST_MODIFIED, System.currentTimeMillis());
-        row.offer(DocumentColumns.FLAGS, flags);
-    }
-
-    private class CloudCursor extends MatrixCursor {
-        private final Uri mUri;
-        private Bundle mExtras = new Bundle();
-
-        public CloudCursor(String[] columnNames, Uri uri) {
-            super(columnNames);
-            mUri = uri;
-        }
-
-        public void setHasMore() {
-            mExtras.putBoolean(DocumentsContract.EXTRA_HAS_MORE, true);
-        }
-
-        @Override
-        public Bundle getExtras() {
-            Log.d(TAG, "getExtras() " + mExtras);
-            return mExtras;
-        }
-
-        @Override
-        public Bundle respond(Bundle extras) {
-            extras.size();
-            Log.d(TAG, "respond() " + extras);
-            if (extras.getBoolean(DocumentsContract.EXTRA_REQUEST_MORE, false)) {
-                new CloudTask().execute(mUri);
-            }
-            return Bundle.EMPTY;
-        }
-    }
-
-    private class CloudTask extends AsyncTask<Uri, Void, Void> {
-        @Override
-        protected Void doInBackground(Uri... uris) {
-            final Uri uri = uris[0];
-
-            SystemClock.sleep(1000);
-
-            // Grab some files from the cloud
-            for (int i = 0; i < 5; i++) {
-                mKnownDocs.add("cloud-page" + mPage + "-file" + i);
-            }
-            mPage++;
-
-            Log.d(TAG, "Loaded more; notifying " + uri);
-            getContext().getContentResolver().notifyChange(uri, null, false);
-            return null;
-        }
-    }
-
-    private interface TypeQuery {
-        final String[] PROJECTION = {
-                DocumentColumns.MIME_TYPE };
-
-        final int MIME_TYPE = 0;
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        switch (sMatcher.match(uri)) {
-            case URI_ROOTS: {
-                return Roots.MIME_TYPE_DIR;
-            }
-            case URI_ROOTS_ID: {
-                return Roots.MIME_TYPE_ITEM;
-            }
-            case URI_DOCS_ID: {
-                final Cursor cursor = query(uri, TypeQuery.PROJECTION, null, null, null);
-                try {
-                    if (cursor.moveToFirst()) {
-                        return cursor.getString(TypeQuery.MIME_TYPE);
-                    } else {
-                        return null;
-                    }
-                } finally {
-                    IoUtils.closeQuietly(cursor);
-                }
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        throw new UnsupportedOperationException("Unsupported Uri " + uri);
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        throw new UnsupportedOperationException("Unsupported Uri " + uri);
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException("Unsupported Uri " + uri);
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException("Unsupported Uri " + uri);
-    }
-}
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 8843e19..583ecc9 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -16,205 +16,130 @@
 
 package com.android.externalstorage;
 
-import android.content.ContentProvider;
 import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.UriMatcher;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
+import android.graphics.Point;
 import android.media.ExifInterface;
-import android.net.Uri;
-import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
-import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.DocumentColumns;
+import android.provider.DocumentsContract.DocumentRoot;
 import android.provider.DocumentsContract.Documents;
-import android.provider.DocumentsContract.RootColumns;
-import android.provider.DocumentsContract.Roots;
-import android.util.Log;
+import android.provider.DocumentsProvider;
 import android.webkit.MimeTypeMap;
 
+import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
 
-public class ExternalStorageProvider extends ContentProvider {
+public class ExternalStorageProvider extends DocumentsProvider {
     private static final String TAG = "ExternalStorage";
 
-    private static final String AUTHORITY = "com.android.externalstorage.documents";
+    // docId format: root:path/to/file
 
-    // TODO: support multiple storage devices
-
-    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
-    private static final int URI_ROOTS = 1;
-    private static final int URI_ROOTS_ID = 2;
-    private static final int URI_DOCS_ID = 3;
-    private static final int URI_DOCS_ID_CONTENTS = 4;
-    private static final int URI_DOCS_ID_SEARCH = 5;
-
-    static {
-        sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS);
-        sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID);
-        sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID);
-        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS);
-        sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH);
-    }
-
-    private HashMap<String, Root> mRoots = Maps.newHashMap();
-
-    private static class Root {
-        public int rootType;
-        public String name;
-        public int icon = 0;
-        public String title = null;
-        public String summary = null;
-        public File path;
-    }
-
-    private static final String[] ALL_ROOTS_COLUMNS = new String[] {
-            RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, RootColumns.TITLE,
-            RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES
-    };
-
-    private static final String[] ALL_DOCUMENTS_COLUMNS = new String[] {
+    private static final String[] SUPPORTED_COLUMNS = new String[] {
             DocumentColumns.DOC_ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE,
             DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, DocumentColumns.FLAGS
     };
 
+    private ArrayList<DocumentRoot> mRoots;
+    private HashMap<String, DocumentRoot> mTagToRoot;
+    private HashMap<String, File> mTagToPath;
+
     @Override
     public boolean onCreate() {
-        mRoots.clear();
+        mRoots = Lists.newArrayList();
+        mTagToRoot = Maps.newHashMap();
+        mTagToPath = Maps.newHashMap();
 
-        final Root root = new Root();
-        root.rootType = Roots.ROOT_TYPE_DEVICE_ADVANCED;
-        root.name = "primary";
-        root.title = getContext().getString(R.string.root_internal_storage);
-        root.path = Environment.getExternalStorageDirectory();
-        mRoots.put(root.name, root);
+        // TODO: support multiple storage devices
+
+        try {
+            final String tag = "primary";
+            final File path = Environment.getExternalStorageDirectory();
+            mTagToPath.put(tag, path);
+
+            final DocumentRoot root = new DocumentRoot();
+            root.docId = getDocIdForFile(path);
+            root.rootType = DocumentRoot.ROOT_TYPE_DEVICE_ADVANCED;
+            root.title = getContext().getString(R.string.root_internal_storage);
+            root.icon = R.drawable.ic_pdf;
+            root.flags = DocumentRoot.FLAG_LOCAL_ONLY;
+            mRoots.add(root);
+            mTagToRoot.put(tag, root);
+        } catch (FileNotFoundException e) {
+            throw new IllegalStateException(e);
+        }
 
         return true;
     }
 
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        switch (sMatcher.match(uri)) {
-            case URI_ROOTS: {
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_ROOTS_COLUMNS);
-                for (Root root : mRoots.values()) {
-                    includeRoot(result, root);
-                }
-                return result;
-            }
-            case URI_ROOTS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+    private String getDocIdForFile(File file) throws FileNotFoundException {
+        String path = file.getAbsolutePath();
 
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_ROOTS_COLUMNS);
-                includeRoot(result, root);
-                return result;
-            }
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
-                final File file = docIdToFile(root, docId);
-                includeFile(result, root, file);
-                return result;
-            }
-            case URI_DOCS_ID_CONTENTS: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
-                final File parent = docIdToFile(root, docId);
-
-                for (File file : parent.listFiles()) {
-                    includeFile(result, root, file);
-                }
-
-                return result;
-            }
-            case URI_DOCS_ID_SEARCH: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-                final String query = DocumentsContract.getSearchQuery(uri).toLowerCase();
-
-                final MatrixCursor result = new MatrixCursor(
-                        projection != null ? projection : ALL_DOCUMENTS_COLUMNS);
-                final File parent = docIdToFile(root, docId);
-
-                final LinkedList<File> pending = new LinkedList<File>();
-                pending.add(parent);
-                while (!pending.isEmpty() && result.getCount() < 20) {
-                    final File file = pending.removeFirst();
-                    if (file.isDirectory()) {
-                        for (File child : file.listFiles()) {
-                            pending.add(child);
-                        }
-                    } else {
-                        if (file.getName().toLowerCase().contains(query)) {
-                            includeFile(result, root, file);
-                        }
-                    }
-                }
-
-                return result;
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+        // Find the most-specific root path
+        Map.Entry<String, File> mostSpecific = null;
+        for (Map.Entry<String, File> root : mTagToPath.entrySet()) {
+            final String rootPath = root.getValue().getPath();
+            if (path.startsWith(rootPath) && (mostSpecific == null
+                    || rootPath.length() > mostSpecific.getValue().getPath().length())) {
+                mostSpecific = root;
             }
         }
-    }
 
-    private String fileToDocId(Root root, File file) {
-        String rootPath = root.path.getAbsolutePath();
-        final String path = file.getAbsolutePath();
-        if (path.equals(rootPath)) {
-            return Documents.DOC_ID_ROOT;
+        if (mostSpecific == null) {
+            throw new FileNotFoundException("Failed to find root that contains " + path);
         }
 
-        if (!rootPath.endsWith("/")) {
-            rootPath += "/";
-        }
-        if (!path.startsWith(rootPath)) {
-            throw new IllegalArgumentException("File " + path + " outside root " + root.path);
+        // Start at first char of path under root
+        final String rootPath = mostSpecific.getValue().getPath();
+        if (rootPath.equals(path)) {
+            path = "";
+        } else if (rootPath.endsWith("/")) {
+            path = path.substring(rootPath.length());
         } else {
-            return path.substring(rootPath.length());
+            path = path.substring(rootPath.length() + 1);
         }
+
+        return mostSpecific.getKey() + ':' + path;
     }
 
-    private File docIdToFile(Root root, String docId) {
-        if (Documents.DOC_ID_ROOT.equals(docId)) {
-            return root.path;
+    private File getFileForDocId(String docId) throws FileNotFoundException {
+        final int splitIndex = docId.indexOf(':', 1);
+        final String tag = docId.substring(0, splitIndex);
+        final String path = docId.substring(splitIndex + 1);
+
+        File target = mTagToPath.get(tag);
+        if (target == null) {
+            throw new FileNotFoundException("No root for " + tag);
+        }
+        target = new File(target, path);
+        if (!target.exists()) {
+            throw new FileNotFoundException("Missing file for " + docId + " at " + target);
+        }
+        return target;
+    }
+
+    private void includeFile(MatrixCursor result, String docId, File file)
+            throws FileNotFoundException {
+        if (docId == null) {
+            docId = getDocIdForFile(file);
         } else {
-            return new File(root.path, docId);
+            file = getFileForDocId(docId);
         }
-    }
 
-    private void includeRoot(MatrixCursor result, Root root) {
-        final RowBuilder row = result.newRow();
-        row.offer(RootColumns.ROOT_ID, root.name);
-        row.offer(RootColumns.ROOT_TYPE, root.rootType);
-        row.offer(RootColumns.ICON, root.icon);
-        row.offer(RootColumns.TITLE, root.title);
-        row.offer(RootColumns.SUMMARY, root.summary);
-        row.offer(RootColumns.AVAILABLE_BYTES, root.path.getFreeSpace());
-    }
-
-    private void includeFile(MatrixCursor result, Root root, File file) {
         int flags = 0;
 
         if (file.isDirectory()) {
@@ -229,19 +154,12 @@
             flags |= Documents.FLAG_SUPPORTS_DELETE;
         }
 
+        final String displayName = file.getName();
         final String mimeType = getTypeForFile(file);
         if (mimeType.startsWith("image/")) {
             flags |= Documents.FLAG_SUPPORTS_THUMBNAIL;
         }
 
-        final String docId = fileToDocId(root, file);
-        final String displayName;
-        if (Documents.DOC_ID_ROOT.equals(docId)) {
-            displayName = root.title;
-        } else {
-            displayName = file.getName();
-        }
-
         final RowBuilder row = result.newRow();
         row.offer(DocumentColumns.DOC_ID, docId);
         row.offer(DocumentColumns.DISPLAY_NAME, displayName);
@@ -252,26 +170,129 @@
     }
 
     @Override
-    public String getType(Uri uri) {
-        switch (sMatcher.match(uri)) {
-            case URI_ROOTS: {
-                return Roots.MIME_TYPE_DIR;
+    public List<DocumentRoot> getDocumentRoots() {
+        // Update free space
+        for (String tag : mTagToRoot.keySet()) {
+            final DocumentRoot root = mTagToRoot.get(tag);
+            final File path = mTagToPath.get(tag);
+            root.availableBytes = path.getFreeSpace();
+        }
+        return mRoots;
+    }
+
+    @Override
+    public String createDocument(String docId, String mimeType, String displayName)
+            throws FileNotFoundException {
+        final File parent = getFileForDocId(docId);
+        displayName = validateDisplayName(mimeType, displayName);
+
+        final File file = new File(parent, displayName);
+        if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
+            if (!file.mkdir()) {
+                throw new IllegalStateException("Failed to mkdir " + file);
             }
-            case URI_ROOTS_ID: {
-                return Roots.MIME_TYPE_ITEM;
+        } else {
+            try {
+                if (!file.createNewFile()) {
+                    throw new IllegalStateException("Failed to touch " + file);
+                }
+            } catch (IOException e) {
+                throw new IllegalStateException("Failed to touch " + file + ": " + e);
             }
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-                return getTypeForFile(docIdToFile(root, docId));
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
+        }
+        return getDocIdForFile(file);
+    }
+
+    @Override
+    public void renameDocument(String docId, String displayName) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        final File newFile = new File(file.getParentFile(), displayName);
+        if (!file.renameTo(newFile)) {
+            throw new IllegalStateException("Failed to rename " + docId);
+        }
+        // TODO: update any outstanding grants
+    }
+
+    @Override
+    public void deleteDocument(String docId) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        if (!file.delete()) {
+            throw new IllegalStateException("Failed to delete " + file);
         }
     }
 
-    private String getTypeForFile(File file) {
+    @Override
+    public Cursor queryDocument(String docId) throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
+        includeFile(result, docId, null);
+        return result;
+    }
+
+    @Override
+    public Cursor queryDocumentChildren(String docId) throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
+        final File parent = getFileForDocId(docId);
+        for (File file : parent.listFiles()) {
+            includeFile(result, null, file);
+        }
+        return result;
+    }
+
+    @Override
+    public Cursor querySearch(String docId, String query) throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(SUPPORTED_COLUMNS);
+        final File parent = getFileForDocId(docId);
+
+        final LinkedList<File> pending = new LinkedList<File>();
+        pending.add(parent);
+        while (!pending.isEmpty() && result.getCount() < 20) {
+            final File file = pending.removeFirst();
+            if (file.isDirectory()) {
+                for (File child : file.listFiles()) {
+                    pending.add(child);
+                }
+            } else {
+                if (file.getName().toLowerCase().contains(query)) {
+                    includeFile(result, null, file);
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String getType(String docId) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        return getTypeForFile(file);
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(null, mode));
+    }
+
+    @Override
+    public AssetFileDescriptor openDocumentThumbnail(
+            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
+                file, ParcelFileDescriptor.MODE_READ_ONLY);
+
+        try {
+            final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
+            final long[] thumb = exif.getThumbnailRange();
+            if (thumb != null) {
+                return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
+            }
+        } catch (IOException e) {
+        }
+
+        return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
+    }
+
+    private static String getTypeForFile(File file) {
         if (file.isDirectory()) {
             return Documents.MIME_TYPE_DIR;
         } else {
@@ -279,7 +300,7 @@
         }
     }
 
-    private String getTypeForName(String name) {
+    private static String getTypeForName(String name) {
         final int lastDot = name.lastIndexOf('.');
         if (lastDot >= 0) {
             final String extension = name.substring(lastDot + 1);
@@ -292,129 +313,7 @@
         return "application/octet-stream";
     }
 
-    @Override
-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        switch (sMatcher.match(uri)) {
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final File file = docIdToFile(root, docId);
-                return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    @Override
-    public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
-            throws FileNotFoundException {
-        if (opts == null || !opts.containsKey(DocumentsContract.EXTRA_THUMBNAIL_SIZE)) {
-            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
-        }
-
-        switch (sMatcher.match(uri)) {
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final File file = docIdToFile(root, docId);
-                final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
-                        file, ParcelFileDescriptor.MODE_READ_ONLY);
-
-                try {
-                    final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
-                    final long[] thumb = exif.getThumbnailRange();
-                    if (thumb != null) {
-                        return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
-                    }
-                } catch (IOException e) {
-                }
-
-                return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        switch (sMatcher.match(uri)) {
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final File parent = docIdToFile(root, docId);
-
-                final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE);
-                final String name = validateDisplayName(
-                        values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType);
-
-                final File file = new File(parent, name);
-                if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
-                    if (!file.mkdir()) {
-                        return null;
-                    }
-
-                } else {
-                    try {
-                        if (!file.createNewFile()) {
-                            return null;
-                        }
-                    } catch (IOException e) {
-                        Log.w(TAG, "Failed to create file", e);
-                        return null;
-                    }
-                }
-
-                final String newDocId = fileToDocId(root, file);
-                return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        switch (sMatcher.match(uri)) {
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final File file = docIdToFile(root, docId);
-                final File newFile = new File(
-                        file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME));
-                return file.renameTo(newFile) ? 1 : 0;
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        switch (sMatcher.match(uri)) {
-            case URI_DOCS_ID: {
-                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
-                final String docId = DocumentsContract.getDocId(uri);
-
-                final File file = docIdToFile(root, docId);
-                return file.delete() ? 1 : 0;
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
-    }
-
-    private String validateDisplayName(String displayName, String mimeType) {
+    private static String validateDisplayName(String mimeType, String displayName) {
         if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
             return displayName;
         } else {