diff options
Diffstat (limited to 'core/java/android/provider/DocumentsProvider.java')
| -rw-r--r-- | core/java/android/provider/DocumentsProvider.java | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java new file mode 100644 index 000000000000..c9efb53a959d --- /dev/null +++ b/core/java/android/provider/DocumentsProvider.java @@ -0,0 +1,592 @@ +/* + * 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.EXTRA_THUMBNAIL_SIZE; +import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; +import static android.provider.DocumentsContract.getDocumentId; +import static android.provider.DocumentsContract.getRootId; +import static android.provider.DocumentsContract.getSearchDocumentsQuery; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.UriMatcher; +import android.content.pm.PackageManager; +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.Document; +import android.provider.DocumentsContract.Root; +import android.util.Log; + +import libcore.io.IoUtils; + +import java.io.FileNotFoundException; + +/** + * Base class for a document provider. A document provider offers read and write + * access to durable files, such as files stored on a local disk, or files in a + * cloud storage service. To create a document provider, extend this class, + * implement the abstract methods, and add it to your manifest like this: + * + * <pre class="prettyprint"><manifest> + * ... + * <application> + * ... + * <provider + * android:name="com.example.MyCloudProvider" + * android:authorities="com.example.mycloudprovider" + * android:exported="true" + * android:grantUriPermissions="true" + * android:permission="android.permission.MANAGE_DOCUMENTS"> + * <intent-filter> + * <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> + * </intent-filter> + * </provider> + * ... + * </application> + *</manifest></pre> + * <p> + * When defining your provider, you must protect it with + * {@link android.Manifest.permission#MANAGE_DOCUMENTS}, which is a permission + * only the system can obtain. Applications cannot use a documents provider + * directly; they must go through {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_CREATE_DOCUMENT} which requires a user to actively + * navigate and select documents. When a user selects documents through that + * UI, the system issues narrow URI permission grants to the requesting + * application. + * </p> + * <h3>Documents</h3> + * <p> + * A document can be either an openable stream (with a specific MIME type), or a + * directory containing additional documents (with the + * {@link Document#MIME_TYPE_DIR} MIME type). Each directory represents the top + * of a subtree containing zero or more documents, which can recursively contain + * even more documents and directories. + * </p> + * <p> + * Each document can have different capabilities, as described by + * {@link Document#COLUMN_FLAGS}. For example, if a document can be represented + * as a thumbnail, a provider can set {@link Document#FLAG_SUPPORTS_THUMBNAIL} + * and implement + * {@link #openDocumentThumbnail(String, Point, CancellationSignal)} to return + * that thumbnail. + * </p> + * <p> + * Each document under a provider is uniquely referenced by its + * {@link Document#COLUMN_DOCUMENT_ID}, which must not change once returned. A + * single document can be included in multiple directories when responding to + * {@link #queryChildDocuments(String, String[], String)}. For example, a + * provider might surface a single photo in multiple locations: once in a + * directory of locations, and again in a directory of dates. + * </p> + * <h3>Roots</h3> + * <p> + * All documents are surfaced through one or more "roots." Each root represents + * the top of a document tree that a user can navigate. For example, a root + * could represent an account or a physical storage device. Similar to + * documents, each root can have capabilities expressed through + * {@link Root#COLUMN_FLAGS}. + * </p> + * + * @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_ROOTS = 1; + private static final int MATCH_ROOT = 2; + private static final int MATCH_RECENT = 3; + private static final int MATCH_SEARCH = 4; + private static final int MATCH_DOCUMENT = 5; + private static final int MATCH_CHILDREN = 6; + + private String mAuthority; + + private UriMatcher mMatcher; + + /** + * Implementation is provided by the parent class. + */ + @Override + public void attachInfo(Context context, ProviderInfo info) { + mAuthority = info.authority; + + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(mAuthority, "root", MATCH_ROOTS); + mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT); + mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); + mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); + mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); + mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); + + // 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); + } + + /** + * Create a new document and return its newly generated + * {@link Document#COLUMN_DOCUMENT_ID}. A provider must allocate a new + * {@link Document#COLUMN_DOCUMENT_ID} to represent the document, which must + * not change once returned. + * + * @param parentDocumentId the parent directory to create the new document + * under. + * @param mimeType the concrete MIME type associated with the new document. + * If the MIME type is not supported, the provider must throw. + * @param displayName the display name of the new document. The provider may + * alter this name to meet any internal constraints, such as + * conflicting names. + */ + @SuppressWarnings("unused") + public String createDocument(String parentDocumentId, String mimeType, String displayName) + throws FileNotFoundException { + throw new UnsupportedOperationException("Create not supported"); + } + + /** + * Delete the requested document. Upon returning, any URI permission grants + * for the requested document will be revoked. If additional documents were + * deleted as a side effect of this call, such as documents inside a + * directory, the implementor is responsible for revoking those permissions. + * + * @param documentId the document to delete. + */ + @SuppressWarnings("unused") + public void deleteDocument(String documentId) throws FileNotFoundException { + throw new UnsupportedOperationException("Delete not supported"); + } + + /** + * Return all roots currently provided. A provider must define at least one + * root to display to users, and it should avoid making network requests to + * keep this request fast. + * <p> + * Each root is defined by the metadata columns described in {@link Root}, + * including {@link Root#COLUMN_DOCUMENT_ID} which points to a directory + * representing a tree of documents to display under that root. + * <p> + * If this set of roots changes, you must call {@link ContentResolver#notifyChange(Uri, + * android.database.ContentObserver)} to notify the system. + * + * @param projection list of {@link Root} columns to put into the cursor. If + * {@code null} all supported columns should be included. + */ + public abstract Cursor queryRoots(String[] projection) throws FileNotFoundException; + + /** + * Return recently modified documents under the requested root. This will + * only be called for roots that advertise + * {@link Root#FLAG_SUPPORTS_RECENTS}. The returned documents should be + * sorted by {@link Document#COLUMN_LAST_MODIFIED} in descending order, and + * limited to only return the 64 most recently modified documents. + * + * @param projection list of {@link Document} columns to put into the + * cursor. If {@code null} all supported columns should be + * included. + * @see DocumentsContract#EXTRA_LOADING + */ + @SuppressWarnings("unused") + public Cursor queryRecentDocuments(String rootId, String[] projection) + throws FileNotFoundException { + throw new UnsupportedOperationException("Recent not supported"); + } + + /** + * Return metadata for the single requested document. A provider should + * avoid making network requests to keep this request fast. + * + * @param documentId the document to return. + * @param projection list of {@link Document} columns to put into the + * cursor. If {@code null} all supported columns should be + * included. + */ + public abstract Cursor queryDocument(String documentId, String[] projection) + throws FileNotFoundException; + + /** + * Return the children documents contained in the requested directory. This + * must only return immediate descendants, as additional queries will be + * issued to recursively explore the tree. + * <p> + * If your provider is cloud-based, and you have some data cached or pinned + * locally, you may return the local data immediately, setting + * {@link DocumentsContract#EXTRA_LOADING} on the Cursor to indicate that + * your provider is still fetching additional data. Then, when the network + * data is available, you can call {@link ContentResolver#notifyChange(Uri, + * android.database.ContentObserver)} to trigger a requery and return the + * complete contents. + * + * @param parentDocumentId the directory to return children for. + * @param projection list of {@link Document} columns to put into the + * cursor. If {@code null} all supported columns should be + * included. + * @param sortOrder how to order the rows, formatted as an SQL + * {@code ORDER BY} clause (excluding the ORDER BY itself). + * Passing {@code null} will use the default sort order, which + * may be unordered. This ordering is a hint that can be used to + * prioritize how data is fetched from the network, but UI may + * always enforce a specific ordering. + * @see DocumentsContract#EXTRA_LOADING + * @see DocumentsContract#EXTRA_INFO + * @see DocumentsContract#EXTRA_ERROR + */ + public abstract Cursor queryChildDocuments( + String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException; + + /** {@hide} */ + @SuppressWarnings("unused") + public Cursor queryChildDocumentsForManage( + String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException { + throw new UnsupportedOperationException("Manage not supported"); + } + + /** + * Return documents that that match the given query under the requested + * root. The returned documents should be sorted by relevance in descending + * order. How documents are matched against the query string is an + * implementation detail left to each provider, but it's suggested that at + * least {@link Document#COLUMN_DISPLAY_NAME} be matched in a + * case-insensitive fashion. + * <p> + * Only documents may be returned; directories are not supported in search + * results. + * + * @param rootId the root to search under. + * @param query string to match documents against. + * @param projection list of {@link Document} columns to put into the + * cursor. If {@code null} all supported columns should be + * included. + * @see DocumentsContract#EXTRA_LOADING + * @see DocumentsContract#EXTRA_INFO + * @see DocumentsContract#EXTRA_ERROR + */ + @SuppressWarnings("unused") + public Cursor querySearchDocuments(String rootId, String query, String[] projection) + throws FileNotFoundException { + throw new UnsupportedOperationException("Search not supported"); + } + + /** + * Return concrete MIME type of the requested document. Must match the value + * of {@link Document#COLUMN_MIME_TYPE} for this document. The default + * implementation queries {@link #queryDocument(String, String[])}, so + * providers may choose to override this as an optimization. + */ + public String getDocumentType(String documentId) throws FileNotFoundException { + final Cursor cursor = queryDocument(documentId, null); + try { + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); + } else { + return null; + } + } finally { + IoUtils.closeQuietly(cursor); + } + } + + /** + * Open and return the requested document. + * <p> + * 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. + * <p> + * If a provider blocks while downloading content, it should periodically + * check {@link CancellationSignal#isCanceled()} to abort abandoned open + * requests. + * + * @param documentId 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() + * @see ParcelFileDescriptor#parseMode(String) + */ + public abstract ParcelFileDescriptor openDocument( + String documentId, String mode, CancellationSignal signal) throws FileNotFoundException; + + /** + * Open and return a thumbnail of the requested document. + * <p> + * 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. + * <p> + * 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 documentId 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 Document#FLAG_SUPPORTS_THUMBNAIL + */ + @SuppressWarnings("unused") + public AssetFileDescriptor openDocumentThumbnail( + String documentId, Point sizeHint, CancellationSignal signal) + throws FileNotFoundException { + throw new UnsupportedOperationException("Thumbnails not supported"); + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #queryRoots(String[]) + * @see #queryRecentDocuments(String, String[]) + * @see #queryDocument(String, String[]) + * @see #queryChildDocuments(String, String[], String) + * @see #querySearchDocuments(String, String, String[]) + */ + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + try { + switch (mMatcher.match(uri)) { + case MATCH_ROOTS: + return queryRoots(projection); + case MATCH_RECENT: + return queryRecentDocuments(getRootId(uri), projection); + case MATCH_SEARCH: + return querySearchDocuments( + getRootId(uri), getSearchDocumentsQuery(uri), projection); + case MATCH_DOCUMENT: + return queryDocument(getDocumentId(uri), projection); + case MATCH_CHILDREN: + if (DocumentsContract.isManageMode(uri)) { + return queryChildDocumentsForManage( + getDocumentId(uri), projection, sortOrder); + } else { + return queryChildDocuments(getDocumentId(uri), projection, sortOrder); + } + default: + throw new UnsupportedOperationException("Unsupported Uri " + uri); + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed during query", e); + return null; + } + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #getDocumentType(String) + */ + @Override + public final String getType(Uri uri) { + try { + switch (mMatcher.match(uri)) { + case MATCH_ROOT: + return DocumentsContract.Root.MIME_TYPE_ITEM; + case MATCH_DOCUMENT: + return getDocumentType(getDocumentId(uri)); + default: + return null; + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed during getType", e); + return null; + } + } + + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + * + * @see #createDocument(String, String, String) + */ + @Override + public final Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("Insert not supported"); + } + + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + * + * @see #deleteDocument(String) + */ + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Delete not supported"); + } + + /** + * Implementation is provided by the parent class. Throws by default, and + * cannot be overriden. + */ + @Override + public final int update( + Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Update not supported"); + } + + /** + * Implementation is provided by the parent class. Can be overridden to + * provide additional functionality, but subclasses <em>must</em> always + * call the superclass. If the superclass returns {@code null}, the subclass + * may implement custom behavior. + * + * @see #openDocument(String, String, CancellationSignal) + * @see #deleteDocument(String) + */ + @Override + public Bundle call(String method, String arg, Bundle extras) { + final Context context = getContext(); + + if (!method.startsWith("android:")) { + // Let non-platform methods pass through + return super.call(method, arg, extras); + } + + final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID); + final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId); + + // Require that caller can manage requested document + final boolean callerHasManage = + context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS) + == PackageManager.PERMISSION_GRANTED; + if (!callerHasManage) { + getContext().enforceCallingOrSelfUriPermission( + documentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, method); + } + + final Bundle out = new Bundle(); + try { + if (METHOD_CREATE_DOCUMENT.equals(method)) { + final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); + final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); + + final String newDocumentId = createDocument(documentId, mimeType, displayName); + out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId); + + // Extend permission grant towards caller if needed + if (!callerHasManage) { + final Uri newDocumentUri = DocumentsContract.buildDocumentUri( + mAuthority, newDocumentId); + context.grantUriPermission(getCallingPackage(), newDocumentUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + } + + } else if (METHOD_DELETE_DOCUMENT.equals(method)) { + deleteDocument(documentId); + + // Document no longer exists, clean up any grants + context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + + } else { + throw new UnsupportedOperationException("Method not supported " + method); + } + } catch (FileNotFoundException e) { + throw new IllegalStateException("Failed call " + method, e); + } + return out; + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #openDocument(String, String, CancellationSignal) + */ + @Override + public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return openDocument(getDocumentId(uri), mode, null); + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #openDocument(String, String, CancellationSignal) + */ + @Override + public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) + throws FileNotFoundException { + return openDocument(getDocumentId(uri), mode, signal); + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #openDocumentThumbnail(String, Point, CancellationSignal) + */ + @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(getDocumentId(uri), sizeHint, null); + } else { + return super.openTypedAssetFile(uri, mimeTypeFilter, opts); + } + } + + /** + * Implementation is provided by the parent class. Cannot be overriden. + * + * @see #openDocumentThumbnail(String, Point, CancellationSignal) + */ + @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(getDocumentId(uri), sizeHint, signal); + } else { + return super.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); + } + } +} |
