Add directory selection to DocumentsProvider.
Introduce new ACTION_PICK_DIRECTORY that allows users to grant access
to an entire document subtree. Instead of requiring grants for each
individual document, this leverages new prefix URI permission grants
by defining new "via"-style URIs:
content://com.example/via/12/document/24/
This references document 24 by using a prefix grant given for
document 12. Internally, we use isChildDocument() to enforce that
24 is actually a descendant (child, grandchild, etc) of 12. Since
this is an optional API, providers indicate support with
Root.FLAG_SUPPORTS_DIR_SELECTION.
Extend DocumentsUI to support picking directories. Expose
createDocument() API to work with returned directories.
Offer to canonicalize via-style URIs into direct URIs, generating
exact permission grants along the way. Override openAssetFile()
to pass through CancellationSignal. Move testing code into ApiDemos.
Bug: 10607375
Change-Id: Ifffc1cff878870f8152eb6ca0199c5d014b9cb07
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 67b6737..c0f04af 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2700,9 +2700,11 @@
* take the persistable permissions using
* {@link ContentResolver#takePersistableUriPermission(Uri, int)}.
* <p>
- * Callers can restrict document selection to a specific kind of data, such
- * as photos, by setting one or more MIME types in
- * {@link #EXTRA_MIME_TYPES}.
+ * Callers must indicate the acceptable document MIME types through
+ * {@link #setType(String)}. For example, to select photos, use
+ * {@code image/*}. If multiple disjoint MIME types are acceptable, define
+ * them in {@link #EXTRA_MIME_TYPES} and {@link #setType(String)} to
+ * {@literal *}/*.
* <p>
* If the caller can handle multiple returned items (the user performing
* multiple selection), then you can specify {@link #EXTRA_ALLOW_MULTIPLE}
@@ -2712,9 +2714,10 @@
* returned URIs can be opened with
* {@link ContentResolver#openFileDescriptor(Uri, String)}.
* <p>
- * Output: The URI of the item that was picked. This must be a
- * {@code content://} URI so that any receiver can access it. If multiple
- * documents were selected, they are returned in {@link #getClipData()}.
+ * Output: The URI of the item that was picked, returned in
+ * {@link #getData()}. This must be a {@code content://} URI so that any
+ * receiver can access it. If multiple documents were selected, they are
+ * returned in {@link #getClipData()}.
*
* @see DocumentsContract
* @see #ACTION_CREATE_DOCUMENT
@@ -2756,6 +2759,24 @@
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT";
+ /**
+ * Activity Action: Allow the user to pick a directory. When invoked, the
+ * system will display the various {@link DocumentsProvider} instances
+ * installed on the device, letting the user navigate through them. Apps can
+ * fully manage documents within the returned directory.
+ * <p>
+ * To gain access to descendant (child, grandchild, etc) documents, use
+ * {@link DocumentsContract#buildDocumentViaUri(Uri, String)} and
+ * {@link DocumentsContract#buildChildDocumentsViaUri(Uri, String)} using
+ * the returned directory URI.
+ * <p>
+ * Output: The URI representing the selected directory.
+ *
+ * @see DocumentsContract
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY";
+
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Standard intent categories (see addCategory()).
@@ -3334,6 +3355,7 @@
* @see #ACTION_GET_CONTENT
* @see #ACTION_OPEN_DOCUMENT
* @see #ACTION_CREATE_DOCUMENT
+ * @see #ACTION_PICK_DIRECTORY
*/
public static final String EXTRA_LOCAL_ONLY =
"android.intent.extra.LOCAL_ONLY";
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index dc18dee..1089f27 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -370,8 +370,8 @@
* attacks.
*/
public static boolean contains(File dir, File file) {
- String dirPath = dir.getPath();
- String filePath = file.getPath();
+ String dirPath = dir.getAbsolutePath();
+ String filePath = file.getAbsolutePath();
if (dirPath.equals(filePath)) {
return true;
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index f0520b5..9a768e0 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -57,6 +57,10 @@
* <p>
* To create a document provider, extend {@link DocumentsProvider}, which
* provides a foundational implementation of this contract.
+ * <p>
+ * All client apps must hold a valid URI permission grant to access documents,
+ * typically issued when a user makes a selection through
+ * {@link Intent#ACTION_OPEN_DOCUMENT} or {@link Intent#ACTION_CREATE_DOCUMENT}.
*
* @see DocumentsProvider
*/
@@ -69,6 +73,8 @@
// content://com.example/root/sdcard/search/?query=pony
// content://com.example/document/12/
// content://com.example/document/12/children/
+ // content://com.example/via/12/document/24/
+ // content://com.example/via/12/document/24/children/
private DocumentsContract() {
}
@@ -425,6 +431,14 @@
public static final int FLAG_SUPPORTS_SEARCH = 1 << 3;
/**
+ * Flag indicating that this root supports directory selection.
+ *
+ * @see #COLUMN_FLAGS
+ * @see DocumentsProvider#isChildDocument(String, String)
+ */
+ public static final int FLAG_SUPPORTS_DIR_SELECTION = 1 << 4;
+
+ /**
* Flag indicating that this root is currently empty. This may be used
* to hide the root when opening documents, but the root will still be
* shown when creating documents and {@link #FLAG_SUPPORTS_CREATE} is
@@ -484,12 +498,15 @@
/** {@hide} */
public static final String EXTRA_THUMBNAIL_SIZE = "thumbnail_size";
+ /** {@hide} */
+ public static final String EXTRA_URI = "uri";
private static final String PATH_ROOT = "root";
private static final String PATH_RECENT = "recent";
private static final String PATH_DOCUMENT = "document";
private static final String PATH_CHILDREN = "children";
private static final String PATH_SEARCH = "search";
+ private static final String PATH_VIA = "via";
private static final String PARAM_QUERY = "query";
private static final String PARAM_MANAGE = "manage";
@@ -532,6 +549,17 @@
}
/**
+ * Build URI representing access to descendant documents of the given
+ * {@link Document#COLUMN_DOCUMENT_ID}.
+ *
+ * @see #getViaDocumentId(Uri)
+ */
+ public static Uri buildViaUri(String authority, String documentId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
+ .appendPath(PATH_VIA).appendPath(documentId).build();
+ }
+
+ /**
* Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a
* document provider. When queried, a provider will return a single row with
* columns defined by {@link Document}.
@@ -545,6 +573,41 @@
}
/**
+ * Build URI representing the given {@link Document#COLUMN_DOCUMENT_ID} in a
+ * document provider. Instead of directly accessing the target document,
+ * gain access via another document. The target document must be a
+ * descendant (child, grandchild, etc) of the via document.
+ * <p>
+ * This is typically used to access documents under a user-selected
+ * directory, since it doesn't require the user to separately confirm each
+ * new document access.
+ *
+ * @param viaUri a related document (directory) that the caller is
+ * leveraging to gain access to the target document. The target
+ * document must be a descendant of this directory.
+ * @param documentId the target document, which the caller may not have
+ * direct access to.
+ * @see Intent#ACTION_PICK_DIRECTORY
+ * @see DocumentsProvider#isChildDocument(String, String)
+ * @see #buildDocumentUri(String, String)
+ */
+ public static Uri buildDocumentViaUri(Uri viaUri, String documentId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
+ .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+ .appendPath(documentId).build();
+ }
+
+ /** {@hide} */
+ public static Uri buildDocumentMaybeViaUri(Uri baseUri, String documentId) {
+ if (isViaUri(baseUri)) {
+ return buildDocumentViaUri(baseUri, documentId);
+ } else {
+ return buildDocumentUri(baseUri.getAuthority(), documentId);
+ }
+ }
+
+ /**
* Build URI representing the children of the given directory in a document
* provider. When queried, a provider will return zero or more rows with
* columns defined by {@link Document}.
@@ -562,6 +625,32 @@
}
/**
+ * Build URI representing the children of the given directory in a document
+ * provider. Instead of directly accessing the target document, gain access
+ * via another document. The target document must be a descendant (child,
+ * grandchild, etc) of the via document.
+ * <p>
+ * This is typically used to access documents under a user-selected
+ * directory, since it doesn't require the user to separately confirm each
+ * new document access.
+ *
+ * @param viaUri a related document (directory) that the caller is
+ * leveraging to gain access to the target document. The target
+ * document must be a descendant of this directory.
+ * @param parentDocumentId the target document, which the caller may not
+ * have direct access to.
+ * @see Intent#ACTION_PICK_DIRECTORY
+ * @see DocumentsProvider#isChildDocument(String, String)
+ * @see #buildChildDocumentsUri(String, String)
+ */
+ public static Uri buildChildDocumentsViaUri(Uri viaUri, String parentDocumentId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
+ .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+ .appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build();
+ }
+
+ /**
* Build URI representing a search for matching documents under a specific
* root in a document provider. When queried, a provider will return zero or
* more rows with columns defined by {@link Document}.
@@ -580,21 +669,31 @@
/**
* Test if the given URI represents a {@link Document} backed by a
* {@link DocumentsProvider}.
+ *
+ * @see #buildDocumentUri(String, String)
+ * @see #buildDocumentViaUri(Uri, String)
*/
public static boolean isDocumentUri(Context context, Uri uri) {
final List<String> paths = uri.getPathSegments();
- if (paths.size() < 2) {
- return false;
+ if (paths.size() >= 2
+ && (PATH_DOCUMENT.equals(paths.get(0)) || PATH_VIA.equals(paths.get(0)))) {
+ return isDocumentsProvider(context, uri.getAuthority());
}
- if (!PATH_DOCUMENT.equals(paths.get(0))) {
- return false;
- }
+ return false;
+ }
+ /** {@hide} */
+ public static boolean isViaUri(Uri uri) {
+ final List<String> paths = uri.getPathSegments();
+ return (paths.size() >= 2 && PATH_VIA.equals(paths.get(0)));
+ }
+
+ private static boolean isDocumentsProvider(Context context, String authority) {
final Intent intent = new Intent(PROVIDER_INTERFACE);
final List<ResolveInfo> infos = context.getPackageManager()
.queryIntentContentProviders(intent, 0);
for (ResolveInfo info : infos) {
- if (uri.getAuthority().equals(info.providerInfo.authority)) {
+ if (authority.equals(info.providerInfo.authority)) {
return true;
}
}
@@ -606,27 +705,40 @@
*/
public static String getRootId(Uri rootUri) {
final List<String> paths = rootUri.getPathSegments();
- if (paths.size() < 2) {
- throw new IllegalArgumentException("Not a root: " + rootUri);
+ if (paths.size() >= 2 && PATH_ROOT.equals(paths.get(0))) {
+ return paths.get(1);
}
- if (!PATH_ROOT.equals(paths.get(0))) {
- throw new IllegalArgumentException("Not a root: " + rootUri);
- }
- return paths.get(1);
+ throw new IllegalArgumentException("Invalid URI: " + rootUri);
}
/**
* Extract the {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
+ *
+ * @see #isDocumentUri(Context, Uri)
*/
public static String getDocumentId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
- if (paths.size() < 2) {
- throw new IllegalArgumentException("Not a document: " + documentUri);
+ if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {
+ return paths.get(1);
}
- if (!PATH_DOCUMENT.equals(paths.get(0))) {
- throw new IllegalArgumentException("Not a document: " + documentUri);
+ if (paths.size() >= 4 && PATH_VIA.equals(paths.get(0))
+ && PATH_DOCUMENT.equals(paths.get(2))) {
+ return paths.get(3);
}
- return paths.get(1);
+ throw new IllegalArgumentException("Invalid URI: " + documentUri);
+ }
+
+ /**
+ * Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
+ *
+ * @see #isViaUri(Uri)
+ */
+ public static String getViaDocumentId(Uri documentUri) {
+ final List<String> paths = documentUri.getPathSegments();
+ if (paths.size() >= 2 && PATH_VIA.equals(paths.get(0))) {
+ return paths.get(1);
+ }
+ throw new IllegalArgumentException("Invalid URI: " + documentUri);
}
/**
@@ -758,7 +870,6 @@
* @param mimeType MIME type of new document
* @param displayName name of new document
* @return newly created document, or {@code null} if failed
- * @hide
*/
public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri,
String mimeType, String displayName) {
@@ -778,13 +889,12 @@
public static Uri createDocument(ContentProviderClient client, Uri parentDocumentUri,
String mimeType, String displayName) throws RemoteException {
final Bundle in = new Bundle();
- in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri));
+ in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri);
in.putString(Document.COLUMN_MIME_TYPE, mimeType);
in.putString(Document.COLUMN_DISPLAY_NAME, displayName);
final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
- return buildDocumentUri(
- parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID));
+ return out.getParcelable(DocumentsContract.EXTRA_URI);
}
/**
@@ -811,7 +921,7 @@
public static void deleteDocument(ContentProviderClient client, Uri documentUri)
throws RemoteException {
final Bundle in = new Bundle();
- in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri));
+ in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);
client.call(METHOD_DELETE_DOCUMENT, null, in);
}
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 49816f8..1a7a00f2 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -46,6 +46,7 @@
import libcore.io.IoUtils;
import java.io.FileNotFoundException;
+import java.util.Objects;
/**
* Base class for a document provider. A document provider offers read and write
@@ -125,6 +126,8 @@
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_VIA = 7;
+ private static final int MATCH_CHILDREN_VIA = 8;
private String mAuthority;
@@ -144,6 +147,8 @@
mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
+ mMatcher.addURI(mAuthority, "via/*/document/*", MATCH_DOCUMENT_VIA);
+ mMatcher.addURI(mAuthority, "via/*/document/*/children", MATCH_CHILDREN_VIA);
// Sanity check our setup
if (!info.exported) {
@@ -161,6 +166,35 @@
}
/**
+ * Test if a document is descendant (child, grandchild, etc) from the given
+ * parent. Providers must override this to support directory selection. 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_DIR_SELECTION
+ */
+ public boolean isChildDocument(String parentDocumentId, String documentId) {
+ return false;
+ }
+
+ /** {@hide} */
+ private void enforceVia(Uri documentUri) {
+ if (DocumentsContract.isViaUri(documentUri)) {
+ final String parent = DocumentsContract.getViaDocumentId(documentUri);
+ final String child = DocumentsContract.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
@@ -182,9 +216,10 @@
/**
* Delete the requested document. Upon returning, any URI permission grants
- * for the requested document will be revoked. If additional documents were
- * deleted as a side effect of this call, such as documents inside a
- * directory, the implementor is responsible for revoking those permissions.
+ * 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.
*/
@@ -420,8 +455,12 @@
return querySearchDocuments(
getRootId(uri), getSearchDocumentsQuery(uri), projection);
case MATCH_DOCUMENT:
+ case MATCH_DOCUMENT_VIA:
+ enforceVia(uri);
return queryDocument(getDocumentId(uri), projection);
case MATCH_CHILDREN:
+ case MATCH_CHILDREN_VIA:
+ enforceVia(uri);
if (DocumentsContract.isManageMode(uri)) {
return queryChildDocumentsForManage(
getDocumentId(uri), projection, sortOrder);
@@ -449,6 +488,8 @@
case MATCH_ROOT:
return DocumentsContract.Root.MIME_TYPE_ITEM;
case MATCH_DOCUMENT:
+ case MATCH_DOCUMENT_VIA:
+ enforceVia(uri);
return getDocumentType(getDocumentId(uri));
default:
return null;
@@ -460,6 +501,49 @@
}
/**
+ * 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 "via" URI into a concrete document
+ * reference, issuing a narrower single-document URI permission grant along
+ * the way.
+ *
+ * @see DocumentsContract#buildDocumentViaUri(Uri, String)
+ */
+ @Override
+ public Uri canonicalize(Uri uri) {
+ final Context context = getContext();
+ switch (mMatcher.match(uri)) {
+ case MATCH_DOCUMENT_VIA:
+ enforceVia(uri);
+
+ final Uri narrowUri = DocumentsContract.buildDocumentUri(uri.getAuthority(),
+ DocumentsContract.getDocumentId(uri));
+
+ // Caller may only have prefix grant, so extend them a grant to
+ // the narrow Uri. Caller already holds read grant to get here,
+ // so check for any other modes we should extend.
+ int 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;
+ }
+ context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags);
+ return narrowUri;
+ }
+ return null;
+ }
+
+ /**
* Implementation is provided by the parent class. Throws by default, and
* cannot be overriden.
*
@@ -496,54 +580,47 @@
* provide additional functionality, but subclasses <em>must</em> always
* call the superclass. If the superclass returns {@code null}, the subclass
* may implement custom behavior.
- *
- * @see #openDocument(String, String, CancellationSignal)
- * @see #deleteDocument(String)
*/
@Override
public Bundle call(String method, String arg, Bundle extras) {
- final Context context = getContext();
-
if (!method.startsWith("android:")) {
- // Let non-platform methods pass through
+ // Ignore non-platform methods
return super.call(method, arg, extras);
}
- final String documentId = extras.getString(Document.COLUMN_DOCUMENT_ID);
- final Uri documentUri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
+ final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+ final String authority = documentUri.getAuthority();
+ final String documentId = DocumentsContract.getDocumentId(documentUri);
- // Require that caller can manage requested document
- final boolean callerHasManage =
- context.checkCallingOrSelfPermission(android.Manifest.permission.MANAGE_DOCUMENTS)
- == PackageManager.PERMISSION_GRANTED;
- enforceWritePermissionInner(documentUri);
+ if (!mAuthority.equals(authority)) {
+ throw new SecurityException(
+ "Requested authority " + authority + " doesn't match provider " + mAuthority);
+ }
+ enforceVia(documentUri);
final Bundle out = new Bundle();
try {
if (METHOD_CREATE_DOCUMENT.equals(method)) {
+ enforceWritePermissionInner(documentUri);
+
final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME);
final String newDocumentId = createDocument(documentId, mimeType, displayName);
- out.putString(Document.COLUMN_DOCUMENT_ID, newDocumentId);
- // Extend permission grant towards caller if needed
- if (!callerHasManage) {
- final Uri newDocumentUri = DocumentsContract.buildDocumentUri(
- mAuthority, newDocumentId);
- context.grantUriPermission(getCallingPackage(), newDocumentUri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
- }
+ // No need to issue new grants here, since caller either has
+ // manage permission or a prefix grant. We might generate a
+ // "via" style URI if that's how they called us.
+ final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri(documentUri,
+ newDocumentId);
+ out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
} else if (METHOD_DELETE_DOCUMENT.equals(method)) {
+ enforceWritePermissionInner(documentUri);
deleteDocument(documentId);
// Document no longer exists, clean up any grants
- context.revokeUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+ revokeDocumentPermission(documentId);
} else {
throw new UnsupportedOperationException("Method not supported " + method);
@@ -555,12 +632,25 @@
}
/**
+ * 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(DocumentsContract.buildDocumentUri(mAuthority, documentId), ~0);
+ context.revokeUriPermission(DocumentsContract.buildViaUri(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 {
+ enforceVia(uri);
return openDocument(getDocumentId(uri), mode, null);
}
@@ -572,17 +662,47 @@
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
throws FileNotFoundException {
+ enforceVia(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 {
+ enforceVia(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 {
+ enforceVia(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)
*/
@Override
public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
+ enforceVia(uri);
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
@@ -600,6 +720,7 @@
public final AssetFileDescriptor openTypedAssetFile(
Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
throws FileNotFoundException {
+ enforceVia(uri);
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);