OSDN Git Service

Stronger DocumentsProvider contract.
authorJeff Sharkey <jsharkey@android.com>
Wed, 28 Aug 2013 01:26:48 +0000 (18:26 -0700)
committerJeff Sharkey <jsharkey@android.com>
Thu, 29 Aug 2013 03:49:42 +0000 (20:49 -0700)
Using a contract class requires that a provider implement it exactly
with little help. This change introduces a DocumentsProvider abstract
class that provides a client-side implementation of the contract that
greatly reduces developer burden, and improves correctness.

This also moves to first-class DocumentRoot objects, and moves calls
with complex side effects to be ContentProvider.call() invocations,
offering more granular permission control over Uri operations that
shouldn't be available through Uri grants.

This new design also relaxes the requirement that root information be
burned into every Uri.  Migrate ExternalDocumentsProvider and
DocumentsUI to adopt new API.

Bug: 10497206
Change-Id: I6f2b3f519bfd62a9d693223ea5628a971ce2e743

23 files changed:
api/current.txt
core/java/android/content/ContentProviderClient.java
core/java/android/content/Intent.java
core/java/android/os/Bundle.java
core/java/android/provider/DocumentsContract.java
core/java/android/provider/DocumentsProvider.java [new file with mode: 0644]
packages/DocumentsUI/AndroidManifest.xml
packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
packages/DocumentsUI/src/com/android/documentsui/model/Document.java
packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
packages/DocumentsUI/src/com/android/documentsui/model/DocumentsProviderInfo.java [deleted file]
packages/DocumentsUI/src/com/android/documentsui/model/Root.java [deleted file]
packages/ExternalStorageProvider/AndroidManifest.xml
packages/ExternalStorageProvider/src/com/android/externalstorage/CloudTestDocumentsProvider.java [deleted file]
packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java

index d249280..8e2b623 100644 (file)
@@ -20781,40 +20781,46 @@ package android.provider {
   }
 
   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
@@ -20822,25 +20828,32 @@ package android.provider {
     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 {
index 024a521..4e8dd82 100644 (file)
@@ -316,4 +316,11 @@ public class ContentProviderClient {
     public ContentProvider getLocalContentProvider() {
         return ContentProvider.coerceToLocalContentProvider(mContentProvider);
     }
+
+    /** {@hide} */
+    public static void closeQuietly(ContentProviderClient client) {
+        if (client != null) {
+            client.release();
+        }
+    }
 }
index c99f09c..d7ca915 100644 (file)
@@ -2687,10 +2687,6 @@ public class Intent implements Parcelable, Cloneable {
     @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()).
index f474504..32b1b60 100644 (file)
@@ -22,6 +22,7 @@ import android.util.SparseArray;
 
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -545,6 +546,13 @@ public final class Bundle implements Parcelable, Cloneable {
         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
index 65c9220..ebb7eb8 100644 (file)
@@ -19,9 +19,8 @@ package android.provider;
 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.database.Cursor;
 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 @@ import java.util.List;
 /**
  * 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 @@ public final class DocumentsContract {
          * 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 @@ public final class DocumentsContract {
          * 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 final class DocumentsContract {
         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 @@ public final class DocumentsContract {
     }
 
     /**
-     * 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.
+     * Extra boolean flag included in a directory {@link Cursor#getExtras()}
+     * 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 ContentProvider#openTypedAssetFile(Uri, String, Bundle)
+     * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
+     *      boolean)
      */
-    public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+    public static final String EXTRA_LOADING = "loading";
 
     /**
-     * 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.
+     * 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_HAS_MORE = "has_more";
+    public static final String EXTRA_INFO = "info";
 
     /**
-     * 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.
-     *
-     * @see Cursor#respond(Bundle)
-     * @see ContentResolver#notifyChange(Uri, android.database.ContentObserver,
-     *      boolean)
+     * 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_REQUEST_MORE = "request_more";
+    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_ROOTS = "roots";
     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 final class DocumentsContract {
      */
     public static String getDocId(Uri documentUri) {
         final List<String> paths = documentUri.getPathSegments();
-        if (paths.size() < 4) {
-            throw new IllegalArgumentException("Not a document: " + documentUri);
-        }
-        if (!PATH_ROOTS.equals(paths.get(0))) {
+        if (paths.size() < 2) {
             throw new IllegalArgumentException("Not a document: " + documentUri);
         }
-        if (!PATH_DOCS.equals(paths.get(2))) {
+        if (!PATH_DOCS.equals(paths.get(0))) {
             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 @@ public final class DocumentsContract {
 
         /**
          * 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 final class DocumentsContract {
         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 @@ public final class DocumentsContract {
          * 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 @@ public final class DocumentsContract {
          * 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";
+        /**
+         * Flag indicating that at least one directory under this root supports
+         * creating content.
+         *
+         * @see #flags
+         */
+        public static final int FLAG_SUPPORTS_CREATE = 1;
 
         /**
-         * Storage root type, use for clustering. 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 Roots#ROOT_TYPE_SERVICE
-         * @see Roots#ROOT_TYPE_DEVICE
+         * @see #flags
          */
-        public static final String ROOT_TYPE = "root_type";
+        public static final int FLAG_LOCAL_ONLY = 1 << 1;
+
+        /** {@hide} */
+        public String authority;
 
         /**
-         * 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)
+         * Root type, use for clustering.
+         *
+         * @see #ROOT_TYPE_SERVICE
+         * @see #ROOT_TYPE_DEVICE
          */
-        public static final String ICON = "icon";
+        public int rootType;
 
         /**
-         * 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
+         * Flags for this root.
+         *
+         * @see #FLAG_LOCAL_ONLY
          */
-        public static final String TITLE = "title";
+        public int flags;
 
         /**
-         * Summary for this storage root, or {@code null} to omit. This field is
-         * read-only to document clients.
-         * <p>
-         * Type: STRING
+         * Icon resource ID for this root.
          */
-        public static final String SUMMARY = "summary";
+        public int icon;
 
         /**
-         * 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)
+         * 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 static final String AVAILABLE_BYTES = "available_bytes";
+        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 @@ public final class DocumentsContract {
      * {@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 @@ public final class DocumentsContract {
         }
     }
 
+    /** {@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;
+        }
+    }
+
     /**
-     * Create a new document under a specific parent document with the given
-     * display name and MIME type.
+     * Create a new document under the given parent document with MIME type and
+     * display name.
      *
-     * @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
+     * @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 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);
+    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;
+        }
     }
 
     /**
-     * Rename the document at the given URI. Given document must have
-     * {@link Documents#FLAG_SUPPORTS_RENAME} set.
+     * Rename the given document.
      *
-     * @return if rename was successful.
+     * @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 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 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;
+        }
     }
 
     /**
-     * Notify the system that roots have changed for the given storage provider.
-     * This signal is used to invalidate internal caches.
+     * Delete the given document.
+     *
+     * @param docId document with {@link Documents#FLAG_SUPPORTS_DELETE}
+     * @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 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 (file)
index 0000000..eeb8c41
--- /dev/null
@@ -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);
+    }
+}
index d79f5c6..6cc92e3 100644 (file)
@@ -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>
 
index 575947f..6bc554f 100644 (file)
@@ -20,14 +20,14 @@ import android.app.AlertDialog;
 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 android.widget.Toast;
 
 import com.android.documentsui.model.Document;
 
-import java.io.FileNotFoundException;
-
 /**
  * Dialog to create a new directory.
  */
@@ -58,7 +56,7 @@ public class CreateDirectoryFragment extends DialogFragment {
         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 class CreateDirectoryFragment extends DialogFragment {
             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);
                 }
             }
         });
index dd9aee5..783b6ff 100644 (file)
@@ -20,8 +20,8 @@ import static com.android.documentsui.DocumentsActivity.TAG;
 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.ContentResolver;
 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.AbsListView.MultiChoiceModeListener;
 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 android.widget.Toast;
 
 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 @@ public class DirectoryFragment extends Fragment {
     private View mEmptyView;
     private ListView mListView;
     private GridView mGridView;
-    private Button mMoreView;
 
     private AbsListView mCurrentView;
 
@@ -110,7 +106,8 @@ public class DirectoryFragment extends Fragment {
     }
 
     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 @@ public class DirectoryFragment extends Fragment {
         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 @@ public class DirectoryFragment extends Fragment {
 
                 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 @@ public class DirectoryFragment extends Fragment {
             @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 @@ public class DirectoryFragment extends Fragment {
                     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 @@ public class DirectoryFragment extends Fragment {
                     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) {
index 14d6fd5..4ce5ef8 100644 (file)
@@ -26,7 +26,6 @@ import android.content.Context;
 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 @@ public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> {
     }
 
     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 @@ public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> {
             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;
-        }
-    }
 }
index 72afd9e..0ce5968 100644 (file)
@@ -21,12 +21,11 @@ import static com.android.documentsui.DocumentsActivity.TAG;
 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
index 22e3b98..73ca8fa 100644 (file)
@@ -22,7 +22,7 @@ import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANA
 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.app.FragmentManager;
 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.graphics.drawable.ColorDrawable;
 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 android.widget.Toast;
 
 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 @@ public class DocumentsActivity extends Activity {
             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 @@ public class DocumentsActivity extends Activity {
         }
 
         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 @@ public class DocumentsActivity extends Activity {
             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 @@ public class DocumentsActivity extends Activity {
             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 @@ public class DocumentsActivity extends Activity {
             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 class DocumentsActivity extends Activity {
         }
     };
 
-    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 @@ public class DocumentsActivity extends Activity {
         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 class DocumentsActivity extends Activity {
     }
 
     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 class DocumentsActivity extends Activity {
         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 class DocumentsActivity extends Activity {
         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;
     }
 
index 5466dbf..3447a51 100644 (file)
@@ -29,6 +29,7 @@ import android.database.Cursor;
 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.ListView;
 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 @@ public class RecentsCreateFragment extends Fragment {
             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--) {
index c3b498e..aa21457 100644 (file)
@@ -18,30 +18,24 @@ package com.android.documentsui;
 
 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 @@ public class RootsCache {
 
     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 @@ public class RootsCache {
      */
     @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);
-    }
-
-    @GuardedBy("ActivityThread")
-    public Root findRoot(String authority, String rootId) {
-        return mRoots.get(Pair.create(authority, rootId));
+    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(Document doc) {
-        final String authority = doc.uri.getAuthority();
-        final String rootId = DocumentsContract.getRootId(doc.uri);
-        return findRoot(authority, rootId);
+    public DocumentRoot getRecentsRoot() {
+        return mRecentsRoot;
     }
 
     @GuardedBy("ActivityThread")
-    public Root getRecentsRoot() {
-        return mRecentsRoot;
+    public boolean isRecentsRoot(DocumentRoot root) {
+        return mRecentsRoot == root;
     }
 
     @GuardedBy("ActivityThread")
-    public Collection<Root> getRoots() {
-        return mRootsList;
+    public List<DocumentRoot> getRoots() {
+        return mRoots;
     }
 
     @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 static Drawable resolveDocumentIcon(Context context, String mimeType) {
         if (Documents.MIME_TYPE_DIR.equals(mimeType)) {
             return context.getResources().getDrawable(R.drawable.ic_dir);
         } else {
index 8a48e2a..2cfa841 100644 (file)
@@ -26,7 +26,7 @@ import android.content.Intent;
 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.ListView;
 import android.widget.TextView;
 
 import com.android.documentsui.SectionedListAdapter.SectionAdapter;
-import com.android.documentsui.model.Root;
-import com.android.documentsui.model.Root.RootComparator;
+import com.android.documentsui.model.Document;
 
-import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
 
 /**
@@ -102,8 +101,8 @@ public class RootsFragment extends Fragment {
         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 @@ public class RootsFragment extends Fragment {
         }
     };
 
-    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 @@ public class RootsFragment extends Fragment {
             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 @@ public class RootsFragment extends Fragment {
         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 class RootsFragment extends Fragment {
             }
         }
     }
+
+    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);
+            }
+        }
+    }
 }
index 8eb81b8..7e1a297 100644 (file)
@@ -73,8 +73,8 @@ public class SaveFragment extends Fragment {
         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);
index c0f21cb..692d171 100644 (file)
@@ -53,17 +53,11 @@ public class Document {
         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 @@ public class Document {
         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 @@ public class Document {
         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 @@ public class Document {
             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 @@ public class Document {
         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);
+    }
 }
index d6c852e..81f75d2 100644 (file)
@@ -21,8 +21,11 @@ import static com.android.documentsui.model.Document.asFileNotFoundException;
 
 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 @@ public class DocumentStack extends LinkedList<Document> {
         // 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 (file)
index 96eb58e..0000000
+++ /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 (file)
index 23d16df..0000000
+++ /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);
-    }
-}
index 8bd2a6d..5272166 100644 (file)
                 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 (file)
index 119d92e..0000000
+++ /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);
-    }
-}
index 8843e19..583ecc9 100644 (file)
 
 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();
-
-        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);
+        mRoots = Lists.newArrayList();
+        mTagToRoot = Maps.newHashMap();
+        mTagToPath = Maps.newHashMap();
+
+        // 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;
+        // 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;
             }
-            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);
-                        }
-                    }
-                }
+        if (mostSpecific == null) {
+            throw new FileNotFoundException("Failed to find root that contains " + path);
+        }
 
-                return result;
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
+        // 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 {
+            path = path.substring(rootPath.length() + 1);
         }
+
+        return mostSpecific.getKey() + ':' + path;
     }
 
-    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;
-        }
+    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);
 
-        if (!rootPath.endsWith("/")) {
-            rootPath += "/";
+        File target = mTagToPath.get(tag);
+        if (target == null) {
+            throw new FileNotFoundException("No root for " + tag);
         }
-        if (!path.startsWith(rootPath)) {
-            throw new IllegalArgumentException("File " + path + " outside root " + root.path);
-        } else {
-            return path.substring(rootPath.length());
+        target = new File(target, path);
+        if (!target.exists()) {
+            throw new FileNotFoundException("Missing file for " + docId + " at " + target);
         }
+        return target;
     }
 
-    private File docIdToFile(Root root, String docId) {
-        if (Documents.DOC_ID_ROOT.equals(docId)) {
-            return root.path;
+    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 @@ public class ExternalStorageProvider extends ContentProvider {
             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,169 +170,150 @@ public class ExternalStorageProvider extends ContentProvider {
     }
 
     @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 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);
-            }
+    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;
     }
 
-    private String getTypeForFile(File file) {
-        if (file.isDirectory()) {
-            return Documents.MIME_TYPE_DIR;
+    @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);
+            }
         } else {
-            return getTypeForName(file.getName());
+            try {
+                if (!file.createNewFile()) {
+                    throw new IllegalStateException("Failed to touch " + file);
+                }
+            } catch (IOException e) {
+                throw new IllegalStateException("Failed to touch " + file + ": " + e);
+            }
         }
+        return getDocIdForFile(file);
     }
 
-    private String getTypeForName(String name) {
-        final int lastDot = name.lastIndexOf('.');
-        if (lastDot >= 0) {
-            final String extension = name.substring(lastDot + 1);
-            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
-            if (mime != null) {
-                return mime;
-            }
+    @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);
         }
-
-        return "application/octet-stream";
+        // TODO: update any outstanding grants
     }
 
     @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);
-            }
+    public void deleteDocument(String docId) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        if (!file.delete()) {
+            throw new IllegalStateException("Failed to delete " + file);
         }
     }
 
     @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);
+    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;
+    }
 
-        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) {
+    @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 new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
             }
         }
+        return result;
     }
 
     @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;
-                    }
-                }
+    public String getType(String docId) throws FileNotFoundException {
+        final File file = getFileForDocId(docId);
+        return getTypeForFile(file);
+    }
 
-                final String newDocId = fileToDocId(root, file);
-                return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId);
-            }
-            default: {
-                throw new UnsupportedOperationException("Unsupported Uri " + uri);
-            }
-        }
+    @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 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);
+    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);
     }
 
-    @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 static String getTypeForFile(File file) {
+        if (file.isDirectory()) {
+            return Documents.MIME_TYPE_DIR;
+        } else {
+            return getTypeForName(file.getName());
+        }
+    }
+
+    private static String getTypeForName(String name) {
+        final int lastDot = name.lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = name.substring(lastDot + 1);
+            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+            if (mime != null) {
+                return mime;
             }
         }
+
+        return "application/octet-stream";
     }
 
-    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 {