diff options
author | Jeff Sharkey <jsharkey@android.com> | 2013-08-27 18:26:48 -0700 |
---|---|---|
committer | Jeff Sharkey <jsharkey@android.com> | 2013-08-28 20:49:42 -0700 |
commit | aeb16e2435f9975b9fa1fc4b747796647a21292e (patch) | |
tree | 2e228b0ca25799bf59472d5df08d71e068498b2b /core/java | |
parent | 61f0f9ada51a6718075acfe7fafbb9954f38e980 (diff) | |
download | frameworks_base-aeb16e2435f9975b9fa1fc4b747796647a21292e.zip frameworks_base-aeb16e2435f9975b9fa1fc4b747796647a21292e.tar.gz frameworks_base-aeb16e2435f9975b9fa1fc4b747796647a21292e.tar.bz2 |
Stronger DocumentsProvider contract.
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
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/android/content/ContentProviderClient.java | 7 | ||||
-rw-r--r-- | core/java/android/content/Intent.java | 4 | ||||
-rw-r--r-- | core/java/android/os/Bundle.java | 8 | ||||
-rw-r--r-- | core/java/android/provider/DocumentsContract.java | 554 | ||||
-rw-r--r-- | core/java/android/provider/DocumentsProvider.java | 384 |
5 files changed, 693 insertions, 264 deletions
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 024a521..4e8dd82 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -316,4 +316,11 @@ public class ContentProviderClient { public ContentProvider getLocalContentProvider() { return ContentProvider.coerceToLocalContentProvider(mContentProvider); } + + /** {@hide} */ + public static void closeQuietly(ContentProviderClient client) { + if (client != null) { + client.release(); + } + } } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index c99f09c..d7ca915 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2687,10 +2687,6 @@ 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()). diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index f474504..32b1b60 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -22,6 +22,7 @@ import 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 diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 65c9220..ebb7eb8 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -19,9 +19,8 @@ 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 index 0000000..eeb8c41 --- /dev/null +++ b/core/java/android/provider/DocumentsProvider.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.provider; + +import static android.provider.DocumentsContract.ACTION_DOCUMENT_ROOT_CHANGED; +import static android.provider.DocumentsContract.EXTRA_AUTHORITY; +import static android.provider.DocumentsContract.EXTRA_ROOTS; +import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE; +import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_GET_ROOTS; +import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; +import static android.provider.DocumentsContract.getDocId; +import static android.provider.DocumentsContract.getSearchQuery; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.UriMatcher; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.graphics.Point; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.ParcelFileDescriptor.OnCloseListener; +import android.provider.DocumentsContract.DocumentColumns; +import android.provider.DocumentsContract.DocumentRoot; +import android.provider.DocumentsContract.Documents; +import android.util.Log; + +import libcore.io.IoUtils; + +import java.io.FileNotFoundException; +import java.util.List; + +/** + * Base class for a document provider. A document provider should extend this + * class and implement the abstract methods. + * <p> + * Each document provider expresses one or more "roots" which each serve as the + * top-level of a tree. For example, a root could represent an account, or a + * physical storage device. Under each root, documents are referenced by + * {@link DocumentColumns#DOC_ID}, which must not change once returned. + * <p> + * Documents can be either an openable file (with a specific MIME type), or a + * directory containing additional documents (with the + * {@link Documents#MIME_TYPE_DIR} MIME type). Each document can have different + * capabilities, as described by {@link DocumentColumns#FLAGS}. The same + * {@link DocumentColumns#DOC_ID} can be included in multiple directories. + * <p> + * Document providers must be protected with the + * {@link android.Manifest.permission#MANAGE_DOCUMENTS} permission, which can + * only be requested by the system. The system-provided UI then issues narrow + * Uri permission grants for individual documents when the user explicitly picks + * documents. + * + * @see Intent#ACTION_OPEN_DOCUMENT + * @see Intent#ACTION_CREATE_DOCUMENT + */ +public abstract class DocumentsProvider extends ContentProvider { + private static final String TAG = "DocumentsProvider"; + + private static final int MATCH_DOCUMENT = 1; + private static final int MATCH_CHILDREN = 2; + private static final int MATCH_SEARCH = 3; + + private String mAuthority; + + private UriMatcher mMatcher; + + @Override + public void attachInfo(Context context, ProviderInfo info) { + mAuthority = info.authority; + + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(mAuthority, "docs/*", MATCH_DOCUMENT); + mMatcher.addURI(mAuthority, "docs/*/children", MATCH_CHILDREN); + mMatcher.addURI(mAuthority, "docs/*/search", MATCH_SEARCH); + + // Sanity check our setup + if (!info.exported) { + throw new SecurityException("Provider must be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("Provider must grantUriPermissions"); + } + if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission) + || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) { + throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS"); + } + + super.attachInfo(context, info); + } + + /** + * Return list of all document roots provided by this document provider. + * When this list changes, a provider must call + * {@link #notifyDocumentRootsChanged()}. + */ + public abstract List<DocumentRoot> getDocumentRoots(); + + /** + * Create and return a new document. A provider must allocate a new + * {@link DocumentColumns#DOC_ID} to represent the document, which must not + * change once returned. + * + * @param docId the parent directory to create the new document under. + * @param mimeType the MIME type associated with the new document. + * @param displayName the display name of the new document. + */ + @SuppressWarnings("unused") + public String createDocument(String docId, String mimeType, String displayName) + throws FileNotFoundException { + throw new UnsupportedOperationException("Create not supported"); + } + + /** + * Rename the given document. + * + * @param docId the document to rename. + * @param displayName the new display name. + */ + @SuppressWarnings("unused") + public void renameDocument(String docId, String displayName) throws FileNotFoundException { + throw new UnsupportedOperationException("Rename not supported"); + } + + /** + * Delete the given document. + * + * @param docId the document to delete. + */ + @SuppressWarnings("unused") + public void deleteDocument(String docId) throws FileNotFoundException { + throw new UnsupportedOperationException("Delete not supported"); + } + + /** + * Return metadata for the given document. A provider should avoid making + * network requests to keep this request fast. + * + * @param docId the document to return. + */ + public abstract Cursor queryDocument(String docId) throws FileNotFoundException; + + /** + * Return the children of the given document which is a directory. + * + * @param docId the directory to return children for. + */ + public abstract Cursor queryDocumentChildren(String docId) throws FileNotFoundException; + + /** + * Return documents that that match the given query, starting the search at + * the given directory. + * + * @param docId the directory to start search at. + */ + @SuppressWarnings("unused") + public Cursor querySearch(String docId, String query) throws FileNotFoundException { + throw new UnsupportedOperationException("Search not supported"); + } + + /** + * Return MIME type for the given document. Must match the value of + * {@link DocumentColumns#MIME_TYPE} for this document. + */ + public String getType(String docId) throws FileNotFoundException { + final Cursor cursor = queryDocument(docId); + try { + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndexOrThrow(DocumentColumns.MIME_TYPE)); + } else { + return null; + } + } finally { + IoUtils.closeQuietly(cursor); + } + } + + /** + * Open and return the requested document. A provider should return a + * reliable {@link ParcelFileDescriptor} to detect when the remote caller + * has finished reading or writing the document. A provider may return a + * pipe or socket pair if the mode is exclusively + * {@link ParcelFileDescriptor#MODE_READ_ONLY} or + * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, but complex modes like + * {@link ParcelFileDescriptor#MODE_READ_WRITE} require a normal file on + * disk. If a provider blocks while downloading content, it should + * periodically check {@link CancellationSignal#isCanceled()} to abort + * abandoned open requests. + * + * @param docId the document to return. + * @param mode the mode to open with, such as 'r', 'w', or 'rw'. + * @param signal used by the caller to signal if the request should be + * cancelled. + * @see ParcelFileDescriptor#open(java.io.File, int, android.os.Handler, + * OnCloseListener) + * @see ParcelFileDescriptor#createReliablePipe() + * @see ParcelFileDescriptor#createReliableSocketPair() + */ + public abstract ParcelFileDescriptor openDocument( + String docId, String mode, CancellationSignal signal) throws FileNotFoundException; + + /** + * Open and return a thumbnail of the requested document. A provider should + * return a thumbnail closely matching the hinted size, attempting to serve + * from a local cache if possible. A provider should never return images + * more than double the hinted size. If a provider performs expensive + * operations to download or generate a thumbnail, it should periodically + * check {@link CancellationSignal#isCanceled()} to abort abandoned + * thumbnail requests. + * + * @param docId the document to return. + * @param sizeHint hint of the optimal thumbnail dimensions. + * @param signal used by the caller to signal if the request should be + * cancelled. + * @see Documents#FLAG_SUPPORTS_THUMBNAIL + */ + @SuppressWarnings("unused") + public AssetFileDescriptor openDocumentThumbnail( + String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { + throw new UnsupportedOperationException("Thumbnails not supported"); + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + try { + switch (mMatcher.match(uri)) { + case MATCH_DOCUMENT: + return queryDocument(getDocId(uri)); + case MATCH_CHILDREN: + return queryDocumentChildren(getDocId(uri)); + case MATCH_SEARCH: + return querySearch(getDocId(uri), getSearchQuery(uri)); + default: + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed during query", e); + return null; + } + } + + @Override + public final String getType(Uri uri) { + try { + switch (mMatcher.match(uri)) { + case MATCH_DOCUMENT: + return getType(getDocId(uri)); + default: + return null; + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed during getType", e); + return null; + } + } + + @Override + public final Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("Insert not supported"); + } + + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Delete not supported"); + } + + @Override + public final int update( + Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Update not supported"); + } + + @Override + public final Bundle callFromPackage( + String callingPackage, String method, String arg, Bundle extras) { + if (!method.startsWith("android:")) { + // Let non-platform methods pass through + return super.callFromPackage(callingPackage, method, arg, extras); + } + + // Platform operations require the caller explicitly hold manage + // permission; Uri permissions don't extend management operations. + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_DOCUMENTS, "Document management"); + + final Bundle out = new Bundle(); + try { + if (METHOD_GET_ROOTS.equals(method)) { + final List<DocumentRoot> roots = getDocumentRoots(); + out.putParcelableList(EXTRA_ROOTS, roots); + + } else if (METHOD_CREATE_DOCUMENT.equals(method)) { + final String docId = extras.getString(DocumentColumns.DOC_ID); + final String mimeType = extras.getString(DocumentColumns.MIME_TYPE); + final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME); + + // TODO: issue Uri grant towards caller + final String newDocId = createDocument(docId, mimeType, displayName); + out.putString(DocumentColumns.DOC_ID, newDocId); + + } else if (METHOD_RENAME_DOCUMENT.equals(method)) { + final String docId = extras.getString(DocumentColumns.DOC_ID); + final String displayName = extras.getString(DocumentColumns.DISPLAY_NAME); + renameDocument(docId, displayName); + + } else if (METHOD_DELETE_DOCUMENT.equals(method)) { + final String docId = extras.getString(DocumentColumns.DOC_ID); + deleteDocument(docId); + + } else { + throw new UnsupportedOperationException("Method not supported " + method); + } + } catch (FileNotFoundException e) { + throw new IllegalStateException("Failed call " + method, e); + } + return out; + } + + @Override + public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return openDocument(getDocId(uri), mode, null); + } + + @Override + public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) + throws FileNotFoundException { + return openDocument(getDocId(uri), mode, signal); + } + + @Override + public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) + throws FileNotFoundException { + if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { + final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); + return openDocumentThumbnail(getDocId(uri), sizeHint, null); + } else { + return super.openTypedAssetFile(uri, mimeTypeFilter, opts); + } + } + + @Override + public final AssetFileDescriptor openTypedAssetFile( + Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) + throws FileNotFoundException { + if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) { + final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE); + return openDocumentThumbnail(getDocId(uri), sizeHint, signal); + } else { + return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); + } + } + + /** + * Notify system that {@link #getDocumentRoots()} has changed, usually due to an + * account or device change. + */ + public void notifyDocumentRootsChanged() { + final Intent intent = new Intent(ACTION_DOCUMENT_ROOT_CHANGED); + intent.putExtra(EXTRA_AUTHORITY, mAuthority); + getContext().sendBroadcast(intent); + } +} |