/* * 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.METHOD_COPY_DOCUMENT; import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_CREATE_WEB_LINK_INTENT; import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_EJECT_ROOT; import static android.provider.DocumentsContract.METHOD_FIND_DOCUMENT_PATH; import static android.provider.DocumentsContract.METHOD_GET_DOCUMENT_METADATA; import static android.provider.DocumentsContract.METHOD_IS_CHILD_DOCUMENT; import static android.provider.DocumentsContract.METHOD_MOVE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_REMOVE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; import static android.provider.DocumentsContract.buildDocumentUri; import static android.provider.DocumentsContract.buildDocumentUriMaybeUsingTree; import static android.provider.DocumentsContract.buildTreeDocumentUri; import static android.provider.DocumentsContract.getDocumentId; import static android.provider.DocumentsContract.getRootId; import static android.provider.DocumentsContract.getSearchDocumentsQuery; import static android.provider.DocumentsContract.getTreeDocumentId; import static android.provider.DocumentsContract.isTreeUri; import android.Manifest; import android.annotation.CallSuper; import android.annotation.Nullable; import android.app.AuthenticationRequiredException; import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentSender; 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.ParcelableException; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.util.Log; import libcore.io.IoUtils; import java.io.FileNotFoundException; import java.util.LinkedList; import java.util.Objects; /** * 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"
 *            android:enabled="@bool/isAtLeastKitKat">
 *            <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, your 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 geographic 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_OPEN_DOCUMENT_TREE * @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 static final int MATCH_DOCUMENT_TREE = 7; private static final int MATCH_CHILDREN_TREE = 8; private String mAuthority; private UriMatcher mMatcher; /** * Implementation is provided by the parent class. */ @Override public void attachInfo(Context context, ProviderInfo info) { registerAuthority(info.authority); // 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); } /** {@hide} */ @Override public void attachInfoForTesting(Context context, ProviderInfo info) { registerAuthority(info.authority); super.attachInfoForTesting(context, info); } private void registerAuthority(String authority) { mAuthority = 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); mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE); mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE); } /** * Test if a document is descendant (child, grandchild, etc) from the given * parent. For example, providers must implement this to support * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. You should avoid making network * requests to keep this request fast. * * @param parentDocumentId parent to verify against. * @param documentId child to verify. * @return if given document is a descendant of the given parent. * @see DocumentsContract.Root#FLAG_SUPPORTS_IS_CHILD */ public boolean isChildDocument(String parentDocumentId, String documentId) { return false; } /** {@hide} */ private void enforceTree(Uri documentUri) { if (isTreeUri(documentUri)) { final String parent = getTreeDocumentId(documentUri); final String child = getDocumentId(documentUri); if (Objects.equals(parent, child)) { return; } if (!isChildDocument(parent, child)) { throw new SecurityException( "Document " + child + " is not a descendant of " + parent); } } } /** * Create a new document and return its newly generated * {@link Document#COLUMN_DOCUMENT_ID}. You 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 * avoiding conflicting names. * @throws AuthenticationRequiredException If authentication is required from the user (such as * login credentials), but it is not guaranteed that the client will handle this * properly. */ @SuppressWarnings("unused") public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { throw new UnsupportedOperationException("Create not supported"); } /** * Rename an existing document. *

* If a different {@link Document#COLUMN_DOCUMENT_ID} must be used to * represent the renamed document, generate and return it. Any outstanding * URI permission grants will be updated to point at the new document. If * the original {@link Document#COLUMN_DOCUMENT_ID} is still valid after the * rename, return {@code null}. * * @param documentId the document to rename. * @param displayName the updated display name of the document. The provider * may alter this name to meet any internal constraints, such as * avoiding conflicting names. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ @SuppressWarnings("unused") public String renameDocument(String documentId, String displayName) throws FileNotFoundException { throw new UnsupportedOperationException("Rename not supported"); } /** * Delete the requested document. *

* Upon returning, any URI permission grants for the given 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 using * {@link #revokeDocumentPermission(String)}. * * @param documentId the document to delete. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ @SuppressWarnings("unused") public void deleteDocument(String documentId) throws FileNotFoundException { throw new UnsupportedOperationException("Delete not supported"); } /** * Copy the requested document or a document tree. *

* Copies a document including all child documents to another location within * the same document provider. Upon completion returns the document id of * the copied document at the target destination. {@code null} must never * be returned. * * @param sourceDocumentId the document to copy. * @param targetParentDocumentId the target document to be copied into as a child. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ @SuppressWarnings("unused") public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException { throw new UnsupportedOperationException("Copy not supported"); } /** * Move the requested document or a document tree. * *

Moves a document including all child documents to another location within * the same document provider. Upon completion returns the document id of * the copied document at the target destination. {@code null} must never * be returned. * *

It's the responsibility of the provider to revoke grants if the document * is no longer accessible using sourceDocumentId. * * @param sourceDocumentId the document to move. * @param sourceParentDocumentId the parent of the document to move. * @param targetParentDocumentId the target document to be a new parent of the * source document. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ @SuppressWarnings("unused") public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException { throw new UnsupportedOperationException("Move not supported"); } /** * Removes the requested document or a document tree. * *

In contrast to {@link #deleteDocument} it requires specifying the parent. * This method is especially useful if the document can be in multiple parents. * *

It's the responsibility of the provider to revoke grants if the document is * removed from the last parent, and effectively the document is deleted. * * @param documentId the document to remove. * @param parentDocumentId the parent of the document to move. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ @SuppressWarnings("unused") public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException { throw new UnsupportedOperationException("Remove not supported"); } /** * Finds the canonical path for the requested document. The path must start * from the parent document if parentDocumentId is not null or the root document * if parentDocumentId is null. If there are more than one path to this document, * return the most typical one. Include both the parent document or root document * and the requested document in the returned path. * *

This API assumes that document ID has enough info to infer the root. * Different roots should use different document ID to refer to the same * document. * * * @param parentDocumentId the document from which the path starts if not null, * or null to indicate a path from the root is requested. * @param childDocumentId the document which path is requested. * @return the path of the requested document. If parentDocumentId is null * returned root ID must not be null. If parentDocumentId is not null * returned root ID must be null. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ public Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId) throws FileNotFoundException { throw new UnsupportedOperationException("findDocumentPath not supported."); } /** * Creates an intent sender for a web link, if the document is web linkable. *

* {@link AuthenticationRequiredException} can be thrown if user does not have * sufficient permission for the linked document. Before any new permissions * are granted for the linked document, a visible UI must be shown, so the * user can explicitly confirm whether the permission grants are expected. * The user must be able to cancel the operation. *

* Options passed as an argument may include a list of recipients, such * as email addresses. The provider should reflect these options if possible, * but it's acceptable to ignore them. In either case, confirmation UI must * be shown before any new permission grants are granted. *

* It is all right to generate a web link without granting new permissions, * if opening the link would result in a page for requesting permission * access. If it's impossible then the operation must fail by throwing an exception. * * @param documentId the document to create a web link intent for. * @param options additional information, such as list of recipients. Optional. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * * @see DocumentsContract.Document#FLAG_WEB_LINKABLE * @see android.app.PendingIntent#getIntentSender */ public IntentSender createWebLinkIntent(String documentId, @Nullable Bundle options) throws FileNotFoundException { throw new UnsupportedOperationException("createWebLink is not supported."); } /** * Return all roots currently provided. To display to users, you must define * at least one root. You 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, boolean)} with * {@link DocumentsContract#buildRootsUri(String)} 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. *

* Recent documents do not support change notifications. * * @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. You 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. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ 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. *

* Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher * should override {@link #queryChildDocuments(String, String[], Bundle)}. *

* 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 * you are still fetching additional data. Then, when the network data is * available, you can send a change notification to trigger a requery and * return the complete contents. To return a Cursor with extras, you need to * extend and override {@link Cursor#getExtras()}. *

* To support change notifications, you must * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant * Uri, such as * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then * you can call {@link ContentResolver#notifyChange(Uri, * android.database.ContentObserver, boolean)} with that Uri to send change * notifications. * * @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. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * @see DocumentsContract#EXTRA_LOADING * @see DocumentsContract#EXTRA_INFO * @see DocumentsContract#EXTRA_ERROR */ public abstract Cursor queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException; /** * Override this method to return the children documents contained * in the requested directory. This must return immediate descendants only. * *

If your provider is cloud-based, and you have data cached * locally, you may return the local data immediately, setting * {@link DocumentsContract#EXTRA_LOADING} on Cursor extras to indicate that * you are still fetching additional data. Then, when the network data is * available, you can send a change notification to trigger a requery and * return the complete contents. To return a Cursor with extras, you need to * extend and override {@link Cursor#getExtras()}. * *

To support change notifications, you must * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant * Uri, such as * {@link DocumentsContract#buildChildDocumentsUri(String, String)}. Then * you can call {@link ContentResolver#notifyChange(Uri, * android.database.ContentObserver, boolean)} with that Uri to send change * notifications. * * @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 queryArgs Bundle containing sorting information or other * argument useful to the provider. If no sorting * information is available, default sorting * will be used, which may be unordered. See * {@link ContentResolver#QUERY_ARG_SORT_COLUMNS} for * details. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * * @see DocumentsContract#EXTRA_LOADING * @see DocumentsContract#EXTRA_INFO * @see DocumentsContract#EXTRA_ERROR */ public Cursor queryChildDocuments( String parentDocumentId, @Nullable String[] projection, @Nullable Bundle queryArgs) throws FileNotFoundException { return queryChildDocuments( parentDocumentId, projection, getSortClause(queryArgs)); } /** {@hide} */ @SuppressWarnings("unused") public Cursor queryChildDocumentsForManage( String parentDocumentId, @Nullable String[] projection, @Nullable String sortOrder) throws FileNotFoundException { throw new UnsupportedOperationException("Manage not supported"); } /** * Return documents 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. *

* 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 * you are still fetching additional data. Then, when the network data is * available, you can send a change notification to trigger a requery and * return the complete contents. *

* To support change notifications, you must * {@link Cursor#setNotificationUri(ContentResolver, Uri)} with a relevant * Uri, such as {@link DocumentsContract#buildSearchDocumentsUri(String, * String, String)}. Then you can call {@link ContentResolver#notifyChange(Uri, * android.database.ContentObserver, boolean)} with that Uri to send change * notifications. * * @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. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * * @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"); } /** * Ejects the root. Throws {@link IllegalStateException} if ejection failed. * * @param rootId the root to be ejected. * @see Root#FLAG_SUPPORTS_EJECT */ @SuppressWarnings("unused") public void ejectRoot(String rootId) { throw new UnsupportedOperationException("Eject not supported"); } /** {@hide} */ public @Nullable Bundle getDocumentMetadata(String documentId, @Nullable String[] tags) throws FileNotFoundException { throw new UnsupportedOperationException("Metadata 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. *

* @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. */ 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. *

* Your provider should return a reliable {@link ParcelFileDescriptor} to * detect when the remote caller has finished reading or writing the * document. *

* Mode "r" should always be supported. Provider should throw * {@link UnsupportedOperationException} if the passing mode is not supported. * You may return a pipe or socket pair if the mode is exclusively "r" or * "w", but complex modes like "rw" imply a normal file on disk that * supports seeking. *

* If you block while downloading content, you 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. May be null. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * @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 you perform expensive operations to download or generate a thumbnail, * you 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. May be null. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * @see Document#FLAG_SUPPORTS_THUMBNAIL */ @SuppressWarnings("unused") public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported"); } /** * Open and return the document in a format matching the specified MIME * type filter. *

* A provider may perform a conversion if the documents's MIME type is not * matching the specified MIME type filter. *

* Virtual documents must have at least one streamable format. * * @param documentId the document to return. * @param mimeTypeFilter the MIME type filter for the requested format. May * be *\/*, which matches any MIME type. * @param opts extra options from the client. Specific to the content * provider. * @param signal used by the caller to signal if the request should be * cancelled. May be null. * @throws AuthenticationRequiredException If authentication is required from * the user (such as login credentials), but it is not guaranteed * that the client will handle this properly. * @see #getDocumentStreamTypes(String, String) */ @SuppressWarnings("unused") public AssetFileDescriptor openTypedDocument( String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) throws FileNotFoundException { throw new FileNotFoundException("The requested MIME type is not supported."); } @Override public final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary // transport method. We override that, and don't ever delegate to this method. throw new UnsupportedOperationException("Pre-Android-O query format not supported."); } /** * WARNING: Sub-classes should not override this method. This method is non-final * solely for the purposes of backwards compatibility. * * @see #queryChildDocuments(String, String[], Bundle), * {@link #queryDocument(String, String[])}, * {@link #queryRecentDocuments(String, String[])}, * {@link #queryRoots(String[])}, and * {@link #querySearchDocuments(String, String, String[])}. */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { // As of Android-O, ContentProvider#query (w/ bundle arg) is the primary // transport method. We override that, and don't ever delegate to this metohd. throw new UnsupportedOperationException("Pre-Android-O query format 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, Bundle queryArgs, CancellationSignal cancellationSignal) { 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: case MATCH_DOCUMENT_TREE: enforceTree(uri); return queryDocument(getDocumentId(uri), projection); case MATCH_CHILDREN: case MATCH_CHILDREN_TREE: enforceTree(uri); if (DocumentsContract.isManageMode(uri)) { // TODO: Update "ForManage" variant to support query args. return queryChildDocumentsForManage( getDocumentId(uri), projection, getSortClause(queryArgs)); } else { return queryChildDocuments(getDocumentId(uri), projection, queryArgs); } default: throw new UnsupportedOperationException("Unsupported Uri " + uri); } } catch (FileNotFoundException e) { Log.w(TAG, "Failed during query", e); return null; } } private static @Nullable String getSortClause(@Nullable Bundle queryArgs) { queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY; String sortClause = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER); if (sortClause == null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { sortClause = ContentResolver.createSqlSortClause(queryArgs); } return sortClause; } /** * 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: case MATCH_DOCUMENT_TREE: enforceTree(uri); 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. 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. *

* This is typically used to resolve a subtree URI into a concrete document * reference, issuing a narrower single-document URI permission grant along * the way. * * @see DocumentsContract#buildDocumentUriUsingTree(Uri, String) */ @CallSuper @Override public Uri canonicalize(Uri uri) { final Context context = getContext(); switch (mMatcher.match(uri)) { case MATCH_DOCUMENT_TREE: enforceTree(uri); final Uri narrowUri = buildDocumentUri(uri.getAuthority(), getDocumentId(uri)); // Caller may only have prefix grant, so extend them a grant to // the narrow URI. final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, uri); context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags); return narrowUri; } return null; } private static int getCallingOrSelfUriPermissionModeFlags(Context context, Uri uri) { // TODO: move this to a direct AMS call int modeFlags = 0; if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED) { modeFlags |= Intent.FLAG_GRANT_READ_URI_PERMISSION; } if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED) { modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) == PackageManager.PERMISSION_GRANTED) { modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; } return modeFlags; } /** * 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. */ @CallSuper @Override public Bundle call(String method, String arg, Bundle extras) { if (!method.startsWith("android:")) { // Ignore non-platform methods return super.call(method, arg, extras); } try { return callUnchecked(method, arg, extras); } catch (FileNotFoundException e) { throw new ParcelableException(e); } } private Bundle callUnchecked(String method, String arg, Bundle extras) throws FileNotFoundException { final Context context = getContext(); final Bundle out = new Bundle(); if (METHOD_EJECT_ROOT.equals(method)) { // Given that certain system apps can hold MOUNT_UNMOUNT permission, but only apps // signed with platform signature can hold MANAGE_DOCUMENTS, we are going to check for // MANAGE_DOCUMENTS or associated URI permission here instead final Uri rootUri = extras.getParcelable(DocumentsContract.EXTRA_URI); enforceWritePermissionInner(rootUri, getCallingPackage(), null); final String rootId = DocumentsContract.getRootId(rootUri); ejectRoot(rootId); return out; } final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); final String authority = documentUri.getAuthority(); final String documentId = DocumentsContract.getDocumentId(documentUri); if (!mAuthority.equals(authority)) { throw new SecurityException( "Requested authority " + authority + " doesn't match provider " + mAuthority); } // If the URI is a tree URI performs some validation. enforceTree(documentUri); if (METHOD_IS_CHILD_DOCUMENT.equals(method)) { enforceReadPermissionInner(documentUri, getCallingPackage(), null); final Uri childUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); final String childAuthority = childUri.getAuthority(); final String childId = DocumentsContract.getDocumentId(childUri); out.putBoolean( DocumentsContract.EXTRA_RESULT, mAuthority.equals(childAuthority) && isChildDocument(documentId, childId)); } else if (METHOD_CREATE_DOCUMENT.equals(method)) { enforceWritePermissionInner(documentUri, getCallingPackage(), null); 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); // No need to issue new grants here, since caller either has // manage permission or a prefix grant. We might generate a // tree style URI if that's how they called us. final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, newDocumentId); out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); } else if (METHOD_CREATE_WEB_LINK_INTENT.equals(method)) { enforceWritePermissionInner(documentUri, getCallingPackage(), null); final Bundle options = extras.getBundle(DocumentsContract.EXTRA_OPTIONS); final IntentSender intentSender = createWebLinkIntent(documentId, options); out.putParcelable(DocumentsContract.EXTRA_RESULT, intentSender); } else if (METHOD_RENAME_DOCUMENT.equals(method)) { enforceWritePermissionInner(documentUri, getCallingPackage(), null); final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); final String newDocumentId = renameDocument(documentId, displayName); if (newDocumentId != null) { final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, newDocumentId); // If caller came in with a narrow grant, issue them a // narrow grant for the newly renamed document. if (!isTreeUri(newDocumentUri)) { final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, documentUri); context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); } out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); // Original document no longer exists, clean up any grants. revokeDocumentPermission(documentId); } } else if (METHOD_DELETE_DOCUMENT.equals(method)) { enforceWritePermissionInner(documentUri, getCallingPackage(), null); deleteDocument(documentId); // Document no longer exists, clean up any grants. revokeDocumentPermission(documentId); } else if (METHOD_COPY_DOCUMENT.equals(method)) { final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); final String targetId = DocumentsContract.getDocumentId(targetUri); enforceReadPermissionInner(documentUri, getCallingPackage(), null); enforceWritePermissionInner(targetUri, getCallingPackage(), null); final String newDocumentId = copyDocument(documentId, targetId); if (newDocumentId != null) { final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, newDocumentId); if (!isTreeUri(newDocumentUri)) { final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, documentUri); context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); } out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); } } else if (METHOD_MOVE_DOCUMENT.equals(method)) { final Uri parentSourceUri = extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI); final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri); final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); final String targetId = DocumentsContract.getDocumentId(targetUri); enforceWritePermissionInner(documentUri, getCallingPackage(), null); enforceReadPermissionInner(parentSourceUri, getCallingPackage(), null); enforceWritePermissionInner(targetUri, getCallingPackage(), null); final String newDocumentId = moveDocument(documentId, parentSourceId, targetId); if (newDocumentId != null) { final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, newDocumentId); if (!isTreeUri(newDocumentUri)) { final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, documentUri); context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); } out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); } } else if (METHOD_REMOVE_DOCUMENT.equals(method)) { final Uri parentSourceUri = extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI); final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri); enforceReadPermissionInner(parentSourceUri, getCallingPackage(), null); enforceWritePermissionInner(documentUri, getCallingPackage(), null); removeDocument(documentId, parentSourceId); // It's responsibility of the provider to revoke any grants, as the document may be // still attached to another parents. } else if (METHOD_FIND_DOCUMENT_PATH.equals(method)) { final boolean isTreeUri = isTreeUri(documentUri); if (isTreeUri) { enforceReadPermissionInner(documentUri, getCallingPackage(), null); } else { getContext().enforceCallingPermission(Manifest.permission.MANAGE_DOCUMENTS, null); } final String parentDocumentId = isTreeUri ? DocumentsContract.getTreeDocumentId(documentUri) : null; Path path = findDocumentPath(parentDocumentId, documentId); // Ensure provider doesn't leak information to unprivileged callers. if (isTreeUri) { if (!Objects.equals(path.getPath().get(0), parentDocumentId)) { Log.wtf(TAG, "Provider doesn't return path from the tree root. Expected: " + parentDocumentId + " found: " + path.getPath().get(0)); LinkedList docs = new LinkedList<>(path.getPath()); while (docs.size() > 1 && !Objects.equals(docs.getFirst(), parentDocumentId)) { docs.removeFirst(); } path = new Path(null, docs); } if (path.getRootId() != null) { Log.wtf(TAG, "Provider returns root id :" + path.getRootId() + " unexpectedly. Erase root id."); path = new Path(null, path.getPath()); } } out.putParcelable(DocumentsContract.EXTRA_RESULT, path); } else if (METHOD_GET_DOCUMENT_METADATA.equals(method)) { return getDocumentMetadata( documentId, extras.getStringArray(DocumentsContract.EXTRA_METADATA_TAGS)); } else { throw new UnsupportedOperationException("Method not supported " + method); } return out; } /** * Revoke any active permission grants for the given * {@link Document#COLUMN_DOCUMENT_ID}, usually called when a document * becomes invalid. Follows the same semantics as * {@link Context#revokeUriPermission(Uri, int)}. */ public final void revokeDocumentPermission(String documentId) { final Context context = getContext(); context.revokeUriPermission(buildDocumentUri(mAuthority, documentId), ~0); context.revokeUriPermission(buildTreeDocumentUri(mAuthority, documentId), ~0); } /** * 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 { enforceTree(uri); 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 { enforceTree(uri); return openDocument(getDocumentId(uri), mode, signal); } /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocument(String, String, CancellationSignal) */ @Override @SuppressWarnings("resource") public final AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { enforceTree(uri); final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null); return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; } /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocument(String, String, CancellationSignal) */ @Override @SuppressWarnings("resource") public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException { enforceTree(uri); final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal); return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; } /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocumentThumbnail(String, Point, CancellationSignal) * @see #openTypedDocument(String, String, Bundle, CancellationSignal) * @see #getDocumentStreamTypes(String, String) */ @Override public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) throws FileNotFoundException { return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, null); } /** * Implementation is provided by the parent class. Cannot be overriden. * * @see #openDocumentThumbnail(String, Point, CancellationSignal) * @see #openTypedDocument(String, String, Bundle, CancellationSignal) * @see #getDocumentStreamTypes(String, String) */ @Override public final AssetFileDescriptor openTypedAssetFile( Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) throws FileNotFoundException { return openTypedAssetFileImpl(uri, mimeTypeFilter, opts, signal); } /** * Return a list of streamable MIME types matching the filter, which can be passed to * {@link #openTypedDocument(String, String, Bundle, CancellationSignal)}. * *

The default implementation returns a MIME type provided by * {@link #queryDocument(String, String[])} as long as it matches the filter and the document * does not have the {@link Document#FLAG_VIRTUAL_DOCUMENT} flag set. * *

Virtual documents must have at least one streamable format. * * @see #getStreamTypes(Uri, String) * @see #openTypedDocument(String, String, Bundle, CancellationSignal) */ public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) { Cursor cursor = null; try { cursor = queryDocument(documentId, null); if (cursor.moveToFirst()) { final String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); final long flags = cursor.getLong(cursor.getColumnIndexOrThrow(Document.COLUMN_FLAGS)); if ((flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0 && mimeType != null && mimeTypeMatches(mimeTypeFilter, mimeType)) { return new String[] { mimeType }; } } } catch (FileNotFoundException e) { return null; } finally { IoUtils.closeQuietly(cursor); } // No streamable MIME types. return null; } /** * Called by a client to determine the types of data streams that this content provider * support for the given URI. * *

Overriding this method is deprecated. Override {@link #openTypedDocument} instead. * * @see #getDocumentStreamTypes(String, String) */ @Override public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { enforceTree(uri); return getDocumentStreamTypes(getDocumentId(uri), mimeTypeFilter); } /** * @hide */ private final AssetFileDescriptor openTypedAssetFileImpl( Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal) throws FileNotFoundException { enforceTree(uri); final String documentId = getDocumentId(uri); if (opts != null && opts.containsKey(ContentResolver.EXTRA_SIZE)) { final Point sizeHint = opts.getParcelable(ContentResolver.EXTRA_SIZE); return openDocumentThumbnail(documentId, sizeHint, signal); } if ("*/*".equals(mimeTypeFilter)) { // If they can take anything, the untyped open call is good enough. return openAssetFile(uri, "r"); } final String baseType = getType(uri); if (baseType != null && ClipDescription.compareMimeTypes(baseType, mimeTypeFilter)) { // Use old untyped open call if this provider has a type for this // URI and it matches the request. return openAssetFile(uri, "r"); } // For any other yet unhandled case, let the provider subclass handle it. return openTypedDocument(documentId, mimeTypeFilter, opts, signal); } /** * @hide */ public static boolean mimeTypeMatches(String filter, String test) { if (test == null) { return false; } else if (filter == null || "*/*".equals(filter)) { return true; } else if (filter.equals(test)) { return true; } else if (filter.endsWith("/*")) { return filter.regionMatches(0, test, 0, filter.indexOf('/')); } else { return false; } } }