1469 lines
65 KiB
Java
1469 lines
65 KiB
Java
/*
|
|
* 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.getTreeDocumentId;
|
|
import static android.provider.DocumentsContract.isTreeUri;
|
|
|
|
import android.Manifest;
|
|
import android.annotation.CallSuper;
|
|
import android.annotation.NonNull;
|
|
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.MimeTypeFilter;
|
|
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 com.android.internal.util.Preconditions;
|
|
|
|
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:
|
|
*
|
|
* <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"
|
|
* android:enabled="@bool/isAtLeastKitKat">
|
|
* <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, your 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 geographic 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_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);
|
|
|
|
// Validity 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 enforceTreeForExtraUris(Bundle extras) {
|
|
enforceTree(extras.getParcelable(DocumentsContract.EXTRA_URI, android.net.Uri.class));
|
|
enforceTree(extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI, android.net.Uri.class));
|
|
enforceTree(extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI, android.net.Uri.class));
|
|
}
|
|
|
|
/** {@hide} */
|
|
private void enforceTree(@Nullable Uri documentUri) {
|
|
if (documentUri != null && 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
private Uri validateIncomingNullableUri(@Nullable Uri uri) {
|
|
return uri == null ? null : validateIncomingUri(uri);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>It's the responsibility of the provider to revoke grants if the document
|
|
* is no longer accessible using <code>sourceDocumentId</code>.
|
|
*
|
|
* @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.
|
|
*
|
|
* <p>In contrast to {@link #deleteDocument} it requires specifying the parent.
|
|
* This method is especially useful if the document can be in multiple parents.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
* <p>
|
|
* {@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.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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.
|
|
* <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, boolean)} with
|
|
* {@link DocumentsContract#buildRootsUri(String)} to notify the system.
|
|
* <p>
|
|
*
|
|
* @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.
|
|
* <p>
|
|
* 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 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 of
|
|
* the most recently modified documents.
|
|
* <p>
|
|
* If this method is overriden by the concrete DocumentsProvider and
|
|
* {@link ContentResolver#QUERY_ARG_LIMIT} is specified with a nonnegative
|
|
* int under queryArgs, the result will be limited by that number and
|
|
* {@link ContentResolver#QUERY_ARG_LIMIT} will be specified under
|
|
* {@link ContentResolver#EXTRA_HONORED_ARGS}. Otherwise, a default 64 limit
|
|
* will be used and no QUERY_ARG* will be specified under
|
|
* {@link ContentResolver#EXTRA_HONORED_ARGS}.
|
|
* <p>
|
|
* 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.
|
|
* @param queryArgs the extra query arguments.
|
|
* @param signal used by the caller to signal if the request should be
|
|
* cancelled. May be null.
|
|
* @see DocumentsContract#EXTRA_LOADING
|
|
*/
|
|
@SuppressWarnings("unused")
|
|
@Nullable
|
|
public Cursor queryRecentDocuments(
|
|
@NonNull String rootId, @Nullable String[] projection, @Nullable Bundle queryArgs,
|
|
@Nullable CancellationSignal signal) throws FileNotFoundException {
|
|
Preconditions.checkNotNull(rootId, "rootId can not be null");
|
|
|
|
Cursor c = queryRecentDocuments(rootId, projection);
|
|
Bundle extras = new Bundle();
|
|
c.setExtras(extras);
|
|
extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[0]);
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
* Apps targeting {@link android.os.Build.VERSION_CODES#O} or higher
|
|
* should override {@link #queryChildDocuments(String, String[], Bundle)}.
|
|
* <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
|
|
* 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()}.
|
|
* <p>
|
|
* 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.
|
|
*
|
|
* <p>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()}.
|
|
*
|
|
* <p>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.
|
|
* <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
|
|
* 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.
|
|
* <p>
|
|
* 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");
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <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
|
|
* 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.
|
|
* <p>
|
|
* 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 projection list of {@link Document} columns to put into the
|
|
* cursor. If {@code null} all supported columns should be
|
|
* included.
|
|
* @param queryArgs the query arguments.
|
|
* {@link DocumentsContract#QUERY_ARG_EXCLUDE_MEDIA},
|
|
* {@link DocumentsContract#QUERY_ARG_DISPLAY_NAME},
|
|
* {@link DocumentsContract#QUERY_ARG_MIME_TYPES},
|
|
* {@link DocumentsContract#QUERY_ARG_FILE_SIZE_OVER},
|
|
* {@link DocumentsContract#QUERY_ARG_LAST_MODIFIED_AFTER}.
|
|
* @return cursor containing search result. Include
|
|
* {@link ContentResolver#EXTRA_HONORED_ARGS} in {@link Cursor}
|
|
* extras {@link Bundle} when any QUERY_ARG_* value was honored
|
|
* during the preparation of the results.
|
|
*
|
|
* @see Root#COLUMN_QUERY_ARGS
|
|
* @see ContentResolver#EXTRA_HONORED_ARGS
|
|
* @see DocumentsContract#EXTRA_LOADING
|
|
* @see DocumentsContract#EXTRA_INFO
|
|
* @see DocumentsContract#EXTRA_ERROR
|
|
*/
|
|
@SuppressWarnings("unused")
|
|
@Nullable
|
|
public Cursor querySearchDocuments(@NonNull String rootId,
|
|
@Nullable String[] projection, @NonNull Bundle queryArgs) throws FileNotFoundException {
|
|
Preconditions.checkNotNull(rootId, "rootId can not be null");
|
|
Preconditions.checkNotNull(queryArgs, "queryArgs can not be null");
|
|
return querySearchDocuments(rootId, DocumentsContract.getSearchDocumentsQuery(queryArgs),
|
|
projection);
|
|
}
|
|
|
|
/**
|
|
* 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");
|
|
}
|
|
|
|
/**
|
|
* Returns metadata associated with the document. The type of metadata returned
|
|
* is specific to the document type. For example the data returned for an image
|
|
* file will likely consist primarily or solely of EXIF metadata.
|
|
*
|
|
* <p>The returned {@link Bundle} will contain zero or more entries depending
|
|
* on the type of data supported by the document provider.
|
|
*
|
|
* <ol>
|
|
* <li>A {@link DocumentsContract#METADATA_TYPES} containing a {@code String[]} value.
|
|
* The string array identifies the type or types of metadata returned. Each
|
|
* value in the can be used to access a {@link Bundle} of data
|
|
* containing that type of data.
|
|
* <li>An entry each for each type of returned metadata. Each set of metadata is
|
|
* itself represented as a bundle and accessible via a string key naming
|
|
* the type of data.
|
|
* </ol>
|
|
*
|
|
* @param documentId get the metadata of the document
|
|
* @return a Bundle of Bundles.
|
|
* @see DocumentsContract#getDocumentMetadata(ContentResolver, Uri)
|
|
*/
|
|
public @Nullable Bundle getDocumentMetadata(@NonNull String documentId)
|
|
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.
|
|
* <p>
|
|
* @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.
|
|
* <p>
|
|
* Your provider should return a reliable {@link ParcelFileDescriptor} to
|
|
* detect when the remote caller has finished reading or writing the
|
|
* document.
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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,
|
|
@Nullable 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 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.
|
|
* <p>
|
|
* A provider may perform a conversion if the documents's MIME type is not
|
|
* matching the specified MIME type filter.
|
|
* <p>
|
|
* 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[], Bundle)}.
|
|
*/
|
|
@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 overridden.
|
|
*
|
|
* @see #queryRoots(String[])
|
|
* @see #queryRecentDocuments(String, String[], Bundle, CancellationSignal)
|
|
* @see #queryDocument(String, String[])
|
|
* @see #queryChildDocuments(String, String[], String)
|
|
* @see #querySearchDocuments(String, String[], Bundle)
|
|
*/
|
|
@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, queryArgs, cancellationSignal);
|
|
case MATCH_SEARCH:
|
|
return querySearchDocuments(getRootId(uri), projection, queryArgs);
|
|
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 overridden.
|
|
*
|
|
* @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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An unrestricted version of getType, which does not reveal sensitive information
|
|
*/
|
|
@Override
|
|
public final @Nullable String getTypeAnonymous(@NonNull Uri uri) {
|
|
switch (mMatcher.match(uri)) {
|
|
case MATCH_ROOT:
|
|
return DocumentsContract.Root.MIME_TYPE_ITEM;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
* 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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*/
|
|
@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.
|
|
*/
|
|
@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 the URI is a tree URI performs some validation.
|
|
enforceTreeForExtraUris(extras);
|
|
|
|
final Uri extraUri = validateIncomingNullableUri(
|
|
extras.getParcelable(DocumentsContract.EXTRA_URI, android.net.Uri.class));
|
|
final Uri extraTargetUri = validateIncomingNullableUri(
|
|
extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI, android.net.Uri.class));
|
|
final Uri extraParentUri = validateIncomingNullableUri(
|
|
extras.getParcelable(DocumentsContract.EXTRA_PARENT_URI, android.net.Uri.class));
|
|
|
|
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 = extraUri;
|
|
enforceWritePermissionInner(rootUri, getCallingAttributionSource());
|
|
|
|
final String rootId = DocumentsContract.getRootId(rootUri);
|
|
ejectRoot(rootId);
|
|
|
|
return out;
|
|
}
|
|
|
|
final Uri documentUri = extraUri;
|
|
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 (METHOD_IS_CHILD_DOCUMENT.equals(method)) {
|
|
enforceReadPermissionInner(documentUri, getCallingAttributionSource());
|
|
|
|
final Uri childUri = extraTargetUri;
|
|
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, getCallingAttributionSource());
|
|
|
|
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, getCallingAttributionSource());
|
|
|
|
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, getCallingAttributionSource());
|
|
|
|
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, getCallingAttributionSource());
|
|
deleteDocument(documentId);
|
|
|
|
// Document no longer exists, clean up any grants.
|
|
revokeDocumentPermission(documentId);
|
|
|
|
} else if (METHOD_COPY_DOCUMENT.equals(method)) {
|
|
final Uri targetUri = extraTargetUri;
|
|
final String targetId = DocumentsContract.getDocumentId(targetUri);
|
|
|
|
enforceReadPermissionInner(documentUri, getCallingAttributionSource());
|
|
enforceWritePermissionInner(targetUri, getCallingAttributionSource());
|
|
|
|
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 = extraParentUri;
|
|
final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri);
|
|
final Uri targetUri = extraTargetUri;
|
|
final String targetId = DocumentsContract.getDocumentId(targetUri);
|
|
|
|
enforceWritePermissionInner(documentUri, getCallingAttributionSource());
|
|
enforceReadPermissionInner(parentSourceUri, getCallingAttributionSource());
|
|
enforceWritePermissionInner(targetUri, getCallingAttributionSource());
|
|
|
|
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 = extraParentUri;
|
|
final String parentSourceId = DocumentsContract.getDocumentId(parentSourceUri);
|
|
|
|
enforceReadPermissionInner(parentSourceUri, getCallingAttributionSource());
|
|
enforceWritePermissionInner(documentUri, getCallingAttributionSource());
|
|
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, getCallingAttributionSource());
|
|
} 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<String> 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);
|
|
} 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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*
|
|
* @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 overridden.
|
|
*
|
|
* @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)}.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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 &&
|
|
MimeTypeFilter.matches(mimeType, mimeTypeFilter)) {
|
|
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.
|
|
*
|
|
* <p>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, android.graphics.Point.class);
|
|
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);
|
|
}
|
|
}
|