/* * 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: * *
<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>
*

* 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. *

*

Documents

*

* 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. *

*

* 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. *

*

* 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. *

*

Roots

*

* 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}. *

* * @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. *

* 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. *

* 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. *

* 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. *

* 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. *

* 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 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. *

* 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 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 must 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); } } }