summaryrefslogtreecommitdiffstats
path: root/core/java/android/provider
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/provider')
-rw-r--r--core/java/android/provider/DocumentsContract.java554
-rw-r--r--core/java/android/provider/DocumentsProvider.java384
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);
+ }
+}