diff options
Diffstat (limited to 'core/java/android/provider')
-rw-r--r-- | core/java/android/provider/DocumentsContract.java | 554 | ||||
-rw-r--r-- | core/java/android/provider/DocumentsProvider.java | 384 |
2 files changed, 678 insertions, 260 deletions
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); + } +} |