summaryrefslogtreecommitdiffstats
path: root/core/java/android/provider/DocumentsProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/provider/DocumentsProvider.java')
-rw-r--r--core/java/android/provider/DocumentsProvider.java384
1 files changed, 384 insertions, 0 deletions
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);
+ }
+}