Refactor directory API to "opening document tree."
Cleans up API so it consistently refers to opening or working with a
subtree of documents. Also separates isChildDocument() support from
the concept of directory tree selection.
Bug: 15429194
Change-Id: Ice66a751ff4bd0cc4d34c44c5da13a0dc4186dc9
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index fad3851..cd0de12 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2731,6 +2731,7 @@
* returned in {@link #getClipData()}.
*
* @see DocumentsContract
+ * @see #ACTION_OPEN_DOCUMENT_TREE
* @see #ACTION_CREATE_DOCUMENT
* @see #FLAG_GRANT_PERSISTABLE_URI_PERMISSION
*/
@@ -2765,28 +2766,30 @@
*
* @see DocumentsContract
* @see #ACTION_OPEN_DOCUMENT
+ * @see #ACTION_OPEN_DOCUMENT_TREE
* @see #FLAG_GRANT_PERSISTABLE_URI_PERMISSION
*/
@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.
+ * Activity Action: Allow the user to pick a directory subtree. 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.
+ * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)} and
+ * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)}
+ * with the returned URI.
* <p>
- * Output: The URI representing the selected directory.
+ * Output: The URI representing the selected directory tree.
*
* @see DocumentsContract
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_PICK_DIRECTORY = "android.intent.action.PICK_DIRECTORY";
+ public static final String
+ ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE";
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
@@ -3365,8 +3368,8 @@
*
* @see #ACTION_GET_CONTENT
* @see #ACTION_OPEN_DOCUMENT
+ * @see #ACTION_OPEN_DOCUMENT_TREE
* @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/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 6b8e2de..327fe4a 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -60,7 +60,8 @@
* <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}.
+ * {@link Intent#ACTION_OPEN_DOCUMENT}, {@link Intent#ACTION_CREATE_DOCUMENT},
+ * or {@link Intent#ACTION_OPEN_DOCUMENT_TREE}.
*
* @see DocumentsProvider
*/
@@ -73,8 +74,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/
+ // content://com.example/tree/12/document/24/
+ // content://com.example/tree/12/document/24/children/
private DocumentsContract() {
}
@@ -441,12 +442,13 @@
public static final int FLAG_SUPPORTS_SEARCH = 1 << 3;
/**
- * Flag indicating that this root supports directory selection.
+ * Flag indicating that this root supports testing parent child
+ * relationships.
*
* @see #COLUMN_FLAGS
* @see DocumentsProvider#isChildDocument(String, String)
*/
- public static final int FLAG_SUPPORTS_DIR_SELECTION = 1 << 4;
+ public static final int FLAG_SUPPORTS_IS_CHILD = 1 << 4;
/**
* Flag indicating that this root is currently empty. This may be used
@@ -518,7 +520,7 @@
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 PATH_TREE = "tree";
private static final String PARAM_QUERY = "query";
private static final String PARAM_MANAGE = "manage";
@@ -564,17 +566,17 @@
* Build URI representing access to descendant documents of the given
* {@link Document#COLUMN_DOCUMENT_ID}.
*
- * @see #getViaDocumentId(Uri)
+ * @see #getTreeDocumentId(Uri)
*/
- public static Uri buildViaUri(String authority, String documentId) {
+ public static Uri buildTreeDocumentUri(String authority, String documentId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority)
- .appendPath(PATH_VIA).appendPath(documentId).build();
+ .appendPath(PATH_TREE).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}.
+ * Build URI representing the target {@link Document#COLUMN_DOCUMENT_ID} in
+ * a document provider. When queried, a provider will return a single row
+ * with columns defined by {@link Document}.
*
* @see DocumentsProvider#queryDocument(String, String[])
* @see #getDocumentId(Uri)
@@ -585,42 +587,46 @@
}
/**
- * 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.
+ * Build URI representing the target {@link Document#COLUMN_DOCUMENT_ID} in
+ * a document provider. When queried, a provider will return a single row
+ * with columns defined by {@link Document}.
+ * <p>
+ * However, instead of directly accessing the target document, the returned
+ * URI will leverage access granted through a subtree URI, typically
+ * returned by {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. The target document
+ * must be a descendant (child, grandchild, etc) of the subtree.
* <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.
+ * directory tree, 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 treeUri the subtree to leverage to gain access to the target
+ * document. The target directory must be a descendant of this
+ * subtree.
* @param documentId the target document, which the caller may not have
* direct access to.
- * @see Intent#ACTION_PICK_DIRECTORY
+ * @see Intent#ACTION_OPEN_DOCUMENT_TREE
* @see DocumentsProvider#isChildDocument(String, String)
* @see #buildDocumentUri(String, String)
*/
- public static Uri buildDocumentViaUri(Uri viaUri, String documentId) {
+ public static Uri buildDocumentUriUsingTree(Uri treeUri, String documentId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
- .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+ .authority(treeUri.getAuthority()).appendPath(PATH_TREE)
+ .appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)
.appendPath(documentId).build();
}
/** {@hide} */
- public static Uri buildDocumentMaybeViaUri(Uri baseUri, String documentId) {
- if (isViaUri(baseUri)) {
- return buildDocumentViaUri(baseUri, documentId);
+ public static Uri buildDocumentUriMaybeUsingTree(Uri baseUri, String documentId) {
+ if (isTreeUri(baseUri)) {
+ return buildDocumentUriUsingTree(baseUri, documentId);
} else {
return buildDocumentUri(baseUri.getAuthority(), documentId);
}
}
/**
- * Build URI representing the children of the given directory in a document
+ * Build URI representing the children of the target directory in a document
* provider. When queried, a provider will return zero or more rows with
* columns defined by {@link Document}.
*
@@ -637,28 +643,33 @@
}
/**
- * 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.
+ * Build URI representing the children of the target directory in a document
+ * provider. When queried, a provider will return zero or more rows with
+ * columns defined by {@link Document}.
+ * <p>
+ * However, instead of directly accessing the target directory, the returned
+ * URI will leverage access granted through a subtree URI, typically
+ * returned by {@link Intent#ACTION_OPEN_DOCUMENT_TREE}. The target
+ * directory must be a descendant (child, grandchild, etc) of the subtree.
* <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.
+ * directory tree, 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
+ * @param treeUri the subtree to leverage to gain access to the target
+ * document. The target directory must be a descendant of this
+ * subtree.
+ * @param parentDocumentId the document to return children for, which the
+ * caller may not have direct access to, and which must be a
+ * directory with MIME type of {@link Document#MIME_TYPE_DIR}.
+ * @see Intent#ACTION_OPEN_DOCUMENT_TREE
* @see DocumentsProvider#isChildDocument(String, String)
* @see #buildChildDocumentsUri(String, String)
*/
- public static Uri buildChildDocumentsViaUri(Uri viaUri, String parentDocumentId) {
+ public static Uri buildChildDocumentsUriUsingTree(Uri treeUri, String parentDocumentId) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(viaUri.getAuthority()).appendPath(PATH_VIA)
- .appendPath(getViaDocumentId(viaUri)).appendPath(PATH_DOCUMENT)
+ .authority(treeUri.getAuthority()).appendPath(PATH_TREE)
+ .appendPath(getTreeDocumentId(treeUri)).appendPath(PATH_DOCUMENT)
.appendPath(parentDocumentId).appendPath(PATH_CHILDREN).build();
}
@@ -683,21 +694,24 @@
* {@link DocumentsProvider}.
*
* @see #buildDocumentUri(String, String)
- * @see #buildDocumentViaUri(Uri, String)
+ * @see #buildDocumentUriUsingTree(Uri, String)
*/
public static boolean isDocumentUri(Context context, Uri uri) {
final List<String> paths = uri.getPathSegments();
- if (paths.size() >= 2
- && (PATH_DOCUMENT.equals(paths.get(0)) || PATH_VIA.equals(paths.get(0)))) {
+ if (paths.size() == 2 && PATH_DOCUMENT.equals(paths.get(0))) {
+ return isDocumentsProvider(context, uri.getAuthority());
+ }
+ if (paths.size() == 4 && PATH_TREE.equals(paths.get(0))
+ && PATH_DOCUMENT.equals(paths.get(2))) {
return isDocumentsProvider(context, uri.getAuthority());
}
return false;
}
/** {@hide} */
- public static boolean isViaUri(Uri uri) {
+ public static boolean isTreeUri(Uri uri) {
final List<String> paths = uri.getPathSegments();
- return (paths.size() >= 2 && PATH_VIA.equals(paths.get(0)));
+ return (paths.size() >= 2 && PATH_TREE.equals(paths.get(0)));
}
private static boolean isDocumentsProvider(Context context, String authority) {
@@ -733,7 +747,7 @@
if (paths.size() >= 2 && PATH_DOCUMENT.equals(paths.get(0))) {
return paths.get(1);
}
- if (paths.size() >= 4 && PATH_VIA.equals(paths.get(0))
+ if (paths.size() >= 4 && PATH_TREE.equals(paths.get(0))
&& PATH_DOCUMENT.equals(paths.get(2))) {
return paths.get(3);
}
@@ -742,12 +756,10 @@
/**
* Extract the via {@link Document#COLUMN_DOCUMENT_ID} from the given URI.
- *
- * @see #isViaUri(Uri)
*/
- public static String getViaDocumentId(Uri documentUri) {
+ public static String getTreeDocumentId(Uri documentUri) {
final List<String> paths = documentUri.getPathSegments();
- if (paths.size() >= 2 && PATH_VIA.equals(paths.get(0))) {
+ if (paths.size() >= 2 && PATH_TREE.equals(paths.get(0))) {
return paths.get(1);
}
throw new IllegalArgumentException("Invalid URI: " + documentUri);
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 066b4aa..021fff4 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -20,10 +20,14 @@
import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_DELETE_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.isViaUri;
+import static android.provider.DocumentsContract.getTreeDocumentId;
+import static android.provider.DocumentsContract.isTreeUri;
import android.content.ContentProvider;
import android.content.ContentResolver;
@@ -117,6 +121,7 @@
* </p>
*
* @see Intent#ACTION_OPEN_DOCUMENT
+ * @see Intent#ACTION_OPEN_DOCUMENT_TREE
* @see Intent#ACTION_CREATE_DOCUMENT
*/
public abstract class DocumentsProvider extends ContentProvider {
@@ -128,8 +133,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 static final int MATCH_DOCUMENT_TREE = 7;
+ private static final int MATCH_CHILDREN_TREE = 8;
private String mAuthority;
@@ -149,8 +154,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);
+ mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE);
+ mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE);
// Sanity check our setup
if (!info.exported) {
@@ -169,23 +174,24 @@
/**
* 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.
+ * 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_DIR_SELECTION
+ * @see DocumentsContract.Root#FLAG_SUPPORTS_IS_CHILD
*/
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);
+ private void enforceTree(Uri documentUri) {
+ if (isTreeUri(documentUri)) {
+ final String parent = getTreeDocumentId(documentUri);
+ final String child = getDocumentId(documentUri);
if (Objects.equals(parent, child)) {
return;
}
@@ -479,12 +485,12 @@
return querySearchDocuments(
getRootId(uri), getSearchDocumentsQuery(uri), projection);
case MATCH_DOCUMENT:
- case MATCH_DOCUMENT_VIA:
- enforceVia(uri);
+ case MATCH_DOCUMENT_TREE:
+ enforceTree(uri);
return queryDocument(getDocumentId(uri), projection);
case MATCH_CHILDREN:
- case MATCH_CHILDREN_VIA:
- enforceVia(uri);
+ case MATCH_CHILDREN_TREE:
+ enforceTree(uri);
if (DocumentsContract.isManageMode(uri)) {
return queryChildDocumentsForManage(
getDocumentId(uri), projection, sortOrder);
@@ -512,8 +518,8 @@
case MATCH_ROOT:
return DocumentsContract.Root.MIME_TYPE_ITEM;
case MATCH_DOCUMENT:
- case MATCH_DOCUMENT_VIA:
- enforceVia(uri);
+ case MATCH_DOCUMENT_TREE:
+ enforceTree(uri);
return getDocumentType(getDocumentId(uri));
default:
return null;
@@ -530,21 +536,20 @@
* 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
+ * 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#buildDocumentViaUri(Uri, String)
+ * @see DocumentsContract#buildDocumentUriUsingTree(Uri, String)
*/
@Override
public Uri canonicalize(Uri uri) {
final Context context = getContext();
switch (mMatcher.match(uri)) {
- case MATCH_DOCUMENT_VIA:
- enforceVia(uri);
+ case MATCH_DOCUMENT_TREE:
+ enforceTree(uri);
- final Uri narrowUri = DocumentsContract.buildDocumentUri(uri.getAuthority(),
- DocumentsContract.getDocumentId(uri));
+ final Uri narrowUri = buildDocumentUri(uri.getAuthority(), getDocumentId(uri));
// Caller may only have prefix grant, so extend them a grant to
// the narrow URI.
@@ -628,7 +633,7 @@
throw new SecurityException(
"Requested authority " + authority + " doesn't match provider " + mAuthority);
}
- enforceVia(documentUri);
+ enforceTree(documentUri);
final Bundle out = new Bundle();
try {
@@ -641,8 +646,8 @@
// 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,
+ // tree style URI if that's how they called us.
+ final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri,
newDocumentId);
out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri);
@@ -653,12 +658,12 @@
final String newDocumentId = renameDocument(documentId, displayName);
if (newDocumentId != null) {
- final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri(
- documentUri, newDocumentId);
+ 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 (!isViaUri(newDocumentUri)) {
+ if (!isTreeUri(newDocumentUri)) {
final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context,
documentUri);
context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags);
@@ -694,8 +699,8 @@
*/
public final void revokeDocumentPermission(String documentId) {
final Context context = getContext();
- context.revokeUriPermission(DocumentsContract.buildDocumentUri(mAuthority, documentId), ~0);
- context.revokeUriPermission(DocumentsContract.buildViaUri(mAuthority, documentId), ~0);
+ context.revokeUriPermission(buildDocumentUri(mAuthority, documentId), ~0);
+ context.revokeUriPermission(buildTreeDocumentUri(mAuthority, documentId), ~0);
}
/**
@@ -705,7 +710,7 @@
*/
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- enforceVia(uri);
+ enforceTree(uri);
return openDocument(getDocumentId(uri), mode, null);
}
@@ -717,7 +722,7 @@
@Override
public final ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
throws FileNotFoundException {
- enforceVia(uri);
+ enforceTree(uri);
return openDocument(getDocumentId(uri), mode, signal);
}
@@ -730,7 +735,7 @@
@SuppressWarnings("resource")
public final AssetFileDescriptor openAssetFile(Uri uri, String mode)
throws FileNotFoundException {
- enforceVia(uri);
+ enforceTree(uri);
final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, null);
return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
}
@@ -744,7 +749,7 @@
@SuppressWarnings("resource")
public final AssetFileDescriptor openAssetFile(Uri uri, String mode, CancellationSignal signal)
throws FileNotFoundException {
- enforceVia(uri);
+ enforceTree(uri);
final ParcelFileDescriptor fd = openDocument(getDocumentId(uri), mode, signal);
return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null;
}
@@ -757,7 +762,7 @@
@Override
public final AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
- enforceVia(uri);
+ enforceTree(uri);
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocumentId(uri), sizeHint, null);
@@ -775,7 +780,7 @@
public final AssetFileDescriptor openTypedAssetFile(
Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
throws FileNotFoundException {
- enforceVia(uri);
+ enforceTree(uri);
if (opts != null && opts.containsKey(EXTRA_THUMBNAIL_SIZE)) {
final Point sizeHint = opts.getParcelable(EXTRA_THUMBNAIL_SIZE);
return openDocumentThumbnail(getDocumentId(uri), sizeHint, signal);