Loading core/java/android/provider/DocumentsContract.java +92 −18 Original line number Diff line number Diff line Loading @@ -19,6 +19,10 @@ package android.provider; import static android.net.TrafficStats.KB_IN_BYTES; import static android.system.OsConstants.SEEK_SET; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull; import static com.android.internal.util.Preconditions.checkCollectionNotEmpty; import android.annotation.Nullable; import android.content.ContentProviderClient; import android.content.ContentResolver; Loading Loading @@ -55,6 +59,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; import java.util.Objects; /** * Defines the contract between a documents provider and the platform. Loading Loading @@ -1311,21 +1316,26 @@ public final class DocumentsContract { } /** * Finds the canonical path to the root. Document id should be unique across * roots. * Finds the canonical path to the top of the tree. The return value starts * from the top of the tree or the root document to the requested document, * both inclusive. * * Document id should be unique across roots. * * @param documentUri uri of the document which path is requested. * @return the path to the root of the document, or {@code null} if failed. * @see DocumentsProvider#findPath(String) * @param treeUri treeUri of the document which path is requested. * @return a list of documents ID starting from the top of the tree to the * requested document, or {@code null} if failed. * @see DocumentsProvider#findPath(String, String) * * {@hide} */ public static Path findPath(ContentResolver resolver, Uri documentUri) throws RemoteException { public static List<String> findPath(ContentResolver resolver, Uri treeUri) { checkArgument(isTreeUri(treeUri), treeUri + " is not a tree uri."); final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( documentUri.getAuthority()); treeUri.getAuthority()); try { return findPath(client, documentUri); return findPath(client, treeUri).getPath(); } catch (Exception e) { Log.w(TAG, "Failed to find path", e); return null; Loading @@ -1334,11 +1344,24 @@ public final class DocumentsContract { } } /** {@hide} */ public static Path findPath(ContentProviderClient client, Uri documentUri) throws RemoteException { /** * Finds the canonical path. If uri is a document uri returns path to a root and * its associated root id. If uri is a tree uri returns the path to the top of * the tree. The {@link Path#getPath()} in the return value starts from the top of * the tree or the root document to the requested document, both inclusive. * * Document id should be unique across roots. * * @param uri uri of the document which path is requested. It can be either a * plain document uri or a tree uri. * @return the path of the document. * @see DocumentsProvider#findPath(String, String) * * {@hide} */ public static Path findPath(ContentProviderClient client, Uri uri) throws RemoteException { final Bundle in = new Bundle(); in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); in.putParcelable(DocumentsContract.EXTRA_URI, uri); final Bundle out = client.call(METHOD_FIND_PATH, null, in); Loading Loading @@ -1392,20 +1415,71 @@ public final class DocumentsContract { */ public static final class Path implements Parcelable { public final String mRootId; public final List<String> mPath; private final @Nullable String mRootId; private final List<String> mPath; /** * Creates a Path. * @param rootId the id of the root * @param path the list of document ids from the root document * at position 0 to the target document * * @param rootId the ID of the root. May be null. * @param path the list of document ids from the parent document at * position 0 to the child document. */ public Path(String rootId, List<String> path) { checkCollectionNotEmpty(path, "path"); checkCollectionElementsNotNull(path, "path"); mRootId = rootId; mPath = path; } /** * Returns the root id or null if the calling package doesn't have * permission to access root information. */ public @Nullable String getRootId() { return mRootId; } /** * Returns the path. The path is trimmed to the top of tree if * calling package doesn't have permission to access those * documents. */ public List<String> getPath() { return mPath; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || !(o instanceof Path)) { return false; } Path path = (Path) o; return Objects.equals(mRootId, path.mRootId) && Objects.equals(mPath, path.mPath); } @Override public int hashCode() { return Objects.hash(mRootId, mPath); } @Override public String toString() { return new StringBuilder() .append("DocumentsContract.Path{") .append("rootId=") .append(mRootId) .append(", path=") .append(mPath) .append("}") .toString(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mRootId); Loading core/java/android/provider/DocumentsProvider.java +60 −24 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import static android.provider.DocumentsContract.isTreeUri; import android.Manifest; import android.annotation.CallSuper; import android.annotation.Nullable; import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentResolver; Loading @@ -54,8 +55,8 @@ import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.util.Log; import libcore.io.IoUtils; Loading Loading @@ -154,17 +155,7 @@ public abstract class DocumentsProvider extends ContentProvider { */ @Override public void attachInfo(Context context, ProviderInfo info) { mAuthority = info.authority; mMatcher = new UriMatcher(UriMatcher.NO_MATCH); mMatcher.addURI(mAuthority, "root", MATCH_ROOTS); mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT); mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE); mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE); registerAuthority(info.authority); // Sanity check our setup if (!info.exported) { Loading @@ -181,6 +172,28 @@ public abstract class DocumentsProvider extends ContentProvider { 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 Loading Loading @@ -326,23 +339,28 @@ public abstract class DocumentsProvider extends ContentProvider { } /** * Finds the canonical path to the root for the requested document. If there are * more than one path to this document, return the most typical one. * 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 * <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 documentId the document which path is requested. * @return the path of the requested document to the root, or null if * such operation is not supported. * @param childDocumentId the document which path is requested. * @param parentDocumentId the document with which path starts if not null, or * null to indicate path to root 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. * * @hide */ public Path findPath(String documentId) public Path findPath(String childDocumentId, @Nullable String parentDocumentId) throws FileNotFoundException { Log.w(TAG, "findPath is called on an unsupported provider."); return null; throw new UnsupportedOperationException("findPath not supported."); } /** Loading Loading @@ -897,9 +915,27 @@ public abstract class DocumentsProvider extends ContentProvider { // It's responsibility of the provider to revoke any grants, as the document may be // still attached to another parents. } else if (METHOD_FIND_PATH.equals(method)) { final boolean isTreeUri = isTreeUri(documentUri); if (isTreeUri) { enforceReadPermissionInner(documentUri, getCallingPackage(), null); } else { getContext().enforceCallingPermission(Manifest.permission.MANAGE_DOCUMENTS, null); } final String parentDocumentId = isTreeUri ? DocumentsContract.getTreeDocumentId(documentUri) : null; final Path path = findPath(documentId); final Path path = findPath(documentId, parentDocumentId); // Ensure provider doesn't leak information to unprivileged callers. if (isTreeUri && (path.getRootId() != null || !Objects.equals(path.getPath().get(0), parentDocumentId))) { throw new IllegalStateException( "Provider returns an invalid result for findPath."); } out.putParcelable(DocumentsContract.EXTRA_RESULT, path); } else { Loading core/tests/coretests/src/android/provider/DocumentsProviderTest.java 0 → 100644 +114 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 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 android.content.ContentProviderClient; import android.content.ContentResolver; import android.net.Uri; import android.provider.DocumentsContract.Path; import android.support.test.filters.SmallTest; import android.test.ProviderTestCase2; import java.util.Arrays; import java.util.List; /** * Unit tests for {@link DocumentsProvider}. */ @SmallTest public class DocumentsProviderTest extends ProviderTestCase2<TestDocumentsProvider> { private static final String ROOT_ID = "rootId"; private static final String DOCUMENT_ID = "docId"; private static final String PARENT_DOCUMENT_ID = "parentDocId"; private static final String ANCESTOR_DOCUMENT_ID = "ancestorDocId"; private TestDocumentsProvider mProvider; private ContentResolver mResolver; public DocumentsProviderTest() { super(TestDocumentsProvider.class, TestDocumentsProvider.AUTHORITY); } public void setUp() throws Exception { super.setUp(); mProvider = getProvider(); mResolver = getMockContentResolver(); } public void testFindPath_docUri() throws Exception { final Path expected = new Path(ROOT_ID, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); mProvider.nextPath = expected; final Uri docUri = DocumentsContract.buildDocumentUri(TestDocumentsProvider.AUTHORITY, DOCUMENT_ID); try (ContentProviderClient client = mResolver.acquireUnstableContentProviderClient(docUri)) { final Path actual = DocumentsContract.findPath(client, docUri); assertEquals(expected, actual); } } public void testFindPath_treeUri() throws Exception { mProvider.nextIsChildDocument = true; final Path expected = new Path(null, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); mProvider.nextPath = expected; final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); final List<String> actual = DocumentsContract.findPath(mResolver, docUri); assertEquals(expected.getPath(), actual); } public void testFindPath_treeUri_throwsOnNonChildDocument() throws Exception { mProvider.nextPath = new Path(null, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); assertNull(DocumentsContract.findPath(mResolver, docUri)); } public void testFindPath_treeUri_throwsOnNonNullRootId() throws Exception { mProvider.nextIsChildDocument = true; mProvider.nextPath = new Path(ROOT_ID, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); assertNull(DocumentsContract.findPath(mResolver, docUri)); } public void testFindPath_treeUri_throwsOnDifferentParentDocId() throws Exception { mProvider.nextIsChildDocument = true; mProvider.nextPath = new Path( null, Arrays.asList(ANCESTOR_DOCUMENT_ID, PARENT_DOCUMENT_ID, DOCUMENT_ID)); final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); assertNull(DocumentsContract.findPath(mResolver, docUri)); } private static Uri buildTreeDocumentUri(String authority, String parentDocId, String docId) { final Uri treeUri = DocumentsContract.buildTreeDocumentUri(authority, parentDocId); return DocumentsContract.buildDocumentUriUsingTree(treeUri, docId); } } core/tests/coretests/src/android/provider/TestDocumentsProvider.java 0 → 100644 +125 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 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 android.annotation.Nullable; import android.app.AppOpsManager; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Path; import org.mockito.Mockito; import java.io.FileNotFoundException; /** * Provides a test double of {@link DocumentsProvider}. */ public class TestDocumentsProvider extends DocumentsProvider { public static final String AUTHORITY = "android.provider.TestDocumentsProvider"; public Path nextPath; public boolean nextIsChildDocument; public String lastDocumentId; public String lastParentDocumentId; @Override public void attachInfoForTesting(Context context, ProviderInfo info) { context = new TestContext(context); super.attachInfoForTesting(context, info); } @Override public boolean onCreate() { return true; } @Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { return null; } @Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { return null; } @Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { return null; } @Override public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { return null; } @Override public boolean isChildDocument(String parentDocumentId, String documentId) { return nextIsChildDocument; } @Override public Path findPath(String documentId, @Nullable String parentDocumentId) { lastDocumentId = documentId; lastParentDocumentId = parentDocumentId; return nextPath; } @Override protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken) { return AppOpsManager.MODE_ALLOWED; } @Override protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken) { return AppOpsManager.MODE_ALLOWED; } private static class TestContext extends ContextWrapper { private TestContext(Context context) { super(context); } @Override public void enforceCallingPermission(String permission, String message) { // Always granted } @Override public Object getSystemService(String name) { if (Context.APP_OPS_SERVICE.equals(name)) { return Mockito.mock(AppOpsManager.class); } return super.getSystemService(name); } } } packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +36 −14 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.externalstorage; import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; Loading @@ -40,8 +41,8 @@ import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.provider.MediaStore; import android.provider.Settings; Loading Loading @@ -325,14 +326,19 @@ public class ExternalStorageProvider extends DocumentsProvider { } private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { return resolveDocId(docId, visible).second; RootInfo root = getRootFromDocId(docId); return buildFile(root, docId, visible); } private Pair<RootInfo, File> resolveDocId(String docId, boolean visible) throws FileNotFoundException { RootInfo root = getRootFromDocId(docId); return Pair.create(root, buildFile(root, docId, visible)); } private RootInfo getRootFromDocId(String docId) throws FileNotFoundException { final int splitIndex = docId.indexOf(':', 1); final String tag = docId.substring(0, splitIndex); final String path = docId.substring(splitIndex + 1); RootInfo root; synchronized (mRootsLock) { Loading @@ -342,6 +348,14 @@ public class ExternalStorageProvider extends DocumentsProvider { throw new FileNotFoundException("No root for " + tag); } return root; } private File buildFile(RootInfo root, String docId, boolean visible) throws FileNotFoundException { final int splitIndex = docId.indexOf(':', 1); final String path = docId.substring(splitIndex + 1); File target = visible ? root.visiblePath : root.path; if (target == null) { return null; Loading @@ -353,7 +367,7 @@ public class ExternalStorageProvider extends DocumentsProvider { if (!target.exists()) { throw new FileNotFoundException("Missing file for " + docId + " at " + target); } return Pair.create(root, target); return target; } private void includeFile(MatrixCursor result, String docId, File file) Loading Loading @@ -430,25 +444,33 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override public Path findPath(String documentId) public Path findPath(String childDocId, @Nullable String parentDocId) throws FileNotFoundException { LinkedList<String> path = new LinkedList<>(); final Pair<RootInfo, File> resolvedDocId = resolveDocId(documentId, false); RootInfo root = resolvedDocId.first; File file = resolvedDocId.second; final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false); final RootInfo root = resolvedDocId.first; File child = resolvedDocId.second; final File parent = TextUtils.isEmpty(parentDocId) ? root.path : getFileForDocId(parentDocId); if (!child.exists()) { throw new FileNotFoundException(childDocId + " is not found."); } if (!file.exists()) { throw new FileNotFoundException(); if (!child.getAbsolutePath().startsWith(parent.getAbsolutePath())) { throw new FileNotFoundException(childDocId + " is not found under " + parentDocId); } while (file != null && file.getAbsolutePath().startsWith(root.path.getAbsolutePath())) { path.addFirst(getDocIdForFile(file)); while (child != null && child.getAbsolutePath().startsWith(parent.getAbsolutePath())) { path.addFirst(getDocIdForFile(child)); file = file.getParentFile(); child = child.getParentFile(); } return new Path(root.rootId, path); return new Path(parentDocId == null ? root.rootId : null, path); } @Override Loading Loading
core/java/android/provider/DocumentsContract.java +92 −18 Original line number Diff line number Diff line Loading @@ -19,6 +19,10 @@ package android.provider; import static android.net.TrafficStats.KB_IN_BYTES; import static android.system.OsConstants.SEEK_SET; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkCollectionElementsNotNull; import static com.android.internal.util.Preconditions.checkCollectionNotEmpty; import android.annotation.Nullable; import android.content.ContentProviderClient; import android.content.ContentResolver; Loading Loading @@ -55,6 +59,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; import java.util.Objects; /** * Defines the contract between a documents provider and the platform. Loading Loading @@ -1311,21 +1316,26 @@ public final class DocumentsContract { } /** * Finds the canonical path to the root. Document id should be unique across * roots. * Finds the canonical path to the top of the tree. The return value starts * from the top of the tree or the root document to the requested document, * both inclusive. * * Document id should be unique across roots. * * @param documentUri uri of the document which path is requested. * @return the path to the root of the document, or {@code null} if failed. * @see DocumentsProvider#findPath(String) * @param treeUri treeUri of the document which path is requested. * @return a list of documents ID starting from the top of the tree to the * requested document, or {@code null} if failed. * @see DocumentsProvider#findPath(String, String) * * {@hide} */ public static Path findPath(ContentResolver resolver, Uri documentUri) throws RemoteException { public static List<String> findPath(ContentResolver resolver, Uri treeUri) { checkArgument(isTreeUri(treeUri), treeUri + " is not a tree uri."); final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( documentUri.getAuthority()); treeUri.getAuthority()); try { return findPath(client, documentUri); return findPath(client, treeUri).getPath(); } catch (Exception e) { Log.w(TAG, "Failed to find path", e); return null; Loading @@ -1334,11 +1344,24 @@ public final class DocumentsContract { } } /** {@hide} */ public static Path findPath(ContentProviderClient client, Uri documentUri) throws RemoteException { /** * Finds the canonical path. If uri is a document uri returns path to a root and * its associated root id. If uri is a tree uri returns the path to the top of * the tree. The {@link Path#getPath()} in the return value starts from the top of * the tree or the root document to the requested document, both inclusive. * * Document id should be unique across roots. * * @param uri uri of the document which path is requested. It can be either a * plain document uri or a tree uri. * @return the path of the document. * @see DocumentsProvider#findPath(String, String) * * {@hide} */ public static Path findPath(ContentProviderClient client, Uri uri) throws RemoteException { final Bundle in = new Bundle(); in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); in.putParcelable(DocumentsContract.EXTRA_URI, uri); final Bundle out = client.call(METHOD_FIND_PATH, null, in); Loading Loading @@ -1392,20 +1415,71 @@ public final class DocumentsContract { */ public static final class Path implements Parcelable { public final String mRootId; public final List<String> mPath; private final @Nullable String mRootId; private final List<String> mPath; /** * Creates a Path. * @param rootId the id of the root * @param path the list of document ids from the root document * at position 0 to the target document * * @param rootId the ID of the root. May be null. * @param path the list of document ids from the parent document at * position 0 to the child document. */ public Path(String rootId, List<String> path) { checkCollectionNotEmpty(path, "path"); checkCollectionElementsNotNull(path, "path"); mRootId = rootId; mPath = path; } /** * Returns the root id or null if the calling package doesn't have * permission to access root information. */ public @Nullable String getRootId() { return mRootId; } /** * Returns the path. The path is trimmed to the top of tree if * calling package doesn't have permission to access those * documents. */ public List<String> getPath() { return mPath; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || !(o instanceof Path)) { return false; } Path path = (Path) o; return Objects.equals(mRootId, path.mRootId) && Objects.equals(mPath, path.mPath); } @Override public int hashCode() { return Objects.hash(mRootId, mPath); } @Override public String toString() { return new StringBuilder() .append("DocumentsContract.Path{") .append("rootId=") .append(mRootId) .append(", path=") .append(mPath) .append("}") .toString(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mRootId); Loading
core/java/android/provider/DocumentsProvider.java +60 −24 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import static android.provider.DocumentsContract.isTreeUri; import android.Manifest; import android.annotation.CallSuper; import android.annotation.Nullable; import android.content.ClipDescription; import android.content.ContentProvider; import android.content.ContentResolver; Loading @@ -54,8 +55,8 @@ import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.util.Log; import libcore.io.IoUtils; Loading Loading @@ -154,17 +155,7 @@ public abstract class DocumentsProvider extends ContentProvider { */ @Override public void attachInfo(Context context, ProviderInfo info) { mAuthority = info.authority; mMatcher = new UriMatcher(UriMatcher.NO_MATCH); mMatcher.addURI(mAuthority, "root", MATCH_ROOTS); mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT); mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT); mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH); mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT); mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN); mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE); mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE); registerAuthority(info.authority); // Sanity check our setup if (!info.exported) { Loading @@ -181,6 +172,28 @@ public abstract class DocumentsProvider extends ContentProvider { 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 Loading Loading @@ -326,23 +339,28 @@ public abstract class DocumentsProvider extends ContentProvider { } /** * Finds the canonical path to the root for the requested document. If there are * more than one path to this document, return the most typical one. * 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 * <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 documentId the document which path is requested. * @return the path of the requested document to the root, or null if * such operation is not supported. * @param childDocumentId the document which path is requested. * @param parentDocumentId the document with which path starts if not null, or * null to indicate path to root 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. * * @hide */ public Path findPath(String documentId) public Path findPath(String childDocumentId, @Nullable String parentDocumentId) throws FileNotFoundException { Log.w(TAG, "findPath is called on an unsupported provider."); return null; throw new UnsupportedOperationException("findPath not supported."); } /** Loading Loading @@ -897,9 +915,27 @@ public abstract class DocumentsProvider extends ContentProvider { // It's responsibility of the provider to revoke any grants, as the document may be // still attached to another parents. } else if (METHOD_FIND_PATH.equals(method)) { final boolean isTreeUri = isTreeUri(documentUri); if (isTreeUri) { enforceReadPermissionInner(documentUri, getCallingPackage(), null); } else { getContext().enforceCallingPermission(Manifest.permission.MANAGE_DOCUMENTS, null); } final String parentDocumentId = isTreeUri ? DocumentsContract.getTreeDocumentId(documentUri) : null; final Path path = findPath(documentId); final Path path = findPath(documentId, parentDocumentId); // Ensure provider doesn't leak information to unprivileged callers. if (isTreeUri && (path.getRootId() != null || !Objects.equals(path.getPath().get(0), parentDocumentId))) { throw new IllegalStateException( "Provider returns an invalid result for findPath."); } out.putParcelable(DocumentsContract.EXTRA_RESULT, path); } else { Loading
core/tests/coretests/src/android/provider/DocumentsProviderTest.java 0 → 100644 +114 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 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 android.content.ContentProviderClient; import android.content.ContentResolver; import android.net.Uri; import android.provider.DocumentsContract.Path; import android.support.test.filters.SmallTest; import android.test.ProviderTestCase2; import java.util.Arrays; import java.util.List; /** * Unit tests for {@link DocumentsProvider}. */ @SmallTest public class DocumentsProviderTest extends ProviderTestCase2<TestDocumentsProvider> { private static final String ROOT_ID = "rootId"; private static final String DOCUMENT_ID = "docId"; private static final String PARENT_DOCUMENT_ID = "parentDocId"; private static final String ANCESTOR_DOCUMENT_ID = "ancestorDocId"; private TestDocumentsProvider mProvider; private ContentResolver mResolver; public DocumentsProviderTest() { super(TestDocumentsProvider.class, TestDocumentsProvider.AUTHORITY); } public void setUp() throws Exception { super.setUp(); mProvider = getProvider(); mResolver = getMockContentResolver(); } public void testFindPath_docUri() throws Exception { final Path expected = new Path(ROOT_ID, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); mProvider.nextPath = expected; final Uri docUri = DocumentsContract.buildDocumentUri(TestDocumentsProvider.AUTHORITY, DOCUMENT_ID); try (ContentProviderClient client = mResolver.acquireUnstableContentProviderClient(docUri)) { final Path actual = DocumentsContract.findPath(client, docUri); assertEquals(expected, actual); } } public void testFindPath_treeUri() throws Exception { mProvider.nextIsChildDocument = true; final Path expected = new Path(null, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); mProvider.nextPath = expected; final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); final List<String> actual = DocumentsContract.findPath(mResolver, docUri); assertEquals(expected.getPath(), actual); } public void testFindPath_treeUri_throwsOnNonChildDocument() throws Exception { mProvider.nextPath = new Path(null, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); assertNull(DocumentsContract.findPath(mResolver, docUri)); } public void testFindPath_treeUri_throwsOnNonNullRootId() throws Exception { mProvider.nextIsChildDocument = true; mProvider.nextPath = new Path(ROOT_ID, Arrays.asList(PARENT_DOCUMENT_ID, DOCUMENT_ID)); final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); assertNull(DocumentsContract.findPath(mResolver, docUri)); } public void testFindPath_treeUri_throwsOnDifferentParentDocId() throws Exception { mProvider.nextIsChildDocument = true; mProvider.nextPath = new Path( null, Arrays.asList(ANCESTOR_DOCUMENT_ID, PARENT_DOCUMENT_ID, DOCUMENT_ID)); final Uri docUri = buildTreeDocumentUri( TestDocumentsProvider.AUTHORITY, PARENT_DOCUMENT_ID, DOCUMENT_ID); assertNull(DocumentsContract.findPath(mResolver, docUri)); } private static Uri buildTreeDocumentUri(String authority, String parentDocId, String docId) { final Uri treeUri = DocumentsContract.buildTreeDocumentUri(authority, parentDocId); return DocumentsContract.buildDocumentUriUsingTree(treeUri, docId); } }
core/tests/coretests/src/android/provider/TestDocumentsProvider.java 0 → 100644 +125 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 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 android.annotation.Nullable; import android.app.AppOpsManager; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.net.Uri; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Path; import org.mockito.Mockito; import java.io.FileNotFoundException; /** * Provides a test double of {@link DocumentsProvider}. */ public class TestDocumentsProvider extends DocumentsProvider { public static final String AUTHORITY = "android.provider.TestDocumentsProvider"; public Path nextPath; public boolean nextIsChildDocument; public String lastDocumentId; public String lastParentDocumentId; @Override public void attachInfoForTesting(Context context, ProviderInfo info) { context = new TestContext(context); super.attachInfoForTesting(context, info); } @Override public boolean onCreate() { return true; } @Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { return null; } @Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { return null; } @Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { return null; } @Override public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { return null; } @Override public boolean isChildDocument(String parentDocumentId, String documentId) { return nextIsChildDocument; } @Override public Path findPath(String documentId, @Nullable String parentDocumentId) { lastDocumentId = documentId; lastParentDocumentId = parentDocumentId; return nextPath; } @Override protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken) { return AppOpsManager.MODE_ALLOWED; } @Override protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken) { return AppOpsManager.MODE_ALLOWED; } private static class TestContext extends ContextWrapper { private TestContext(Context context) { super(context); } @Override public void enforceCallingPermission(String permission, String message) { // Always granted } @Override public Object getSystemService(String name) { if (Context.APP_OPS_SERVICE.equals(name)) { return Mockito.mock(AppOpsManager.class); } return super.getSystemService(name); } } }
packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +36 −14 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.externalstorage; import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; Loading @@ -40,8 +41,8 @@ import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.provider.MediaStore; import android.provider.Settings; Loading Loading @@ -325,14 +326,19 @@ public class ExternalStorageProvider extends DocumentsProvider { } private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { return resolveDocId(docId, visible).second; RootInfo root = getRootFromDocId(docId); return buildFile(root, docId, visible); } private Pair<RootInfo, File> resolveDocId(String docId, boolean visible) throws FileNotFoundException { RootInfo root = getRootFromDocId(docId); return Pair.create(root, buildFile(root, docId, visible)); } private RootInfo getRootFromDocId(String docId) throws FileNotFoundException { final int splitIndex = docId.indexOf(':', 1); final String tag = docId.substring(0, splitIndex); final String path = docId.substring(splitIndex + 1); RootInfo root; synchronized (mRootsLock) { Loading @@ -342,6 +348,14 @@ public class ExternalStorageProvider extends DocumentsProvider { throw new FileNotFoundException("No root for " + tag); } return root; } private File buildFile(RootInfo root, String docId, boolean visible) throws FileNotFoundException { final int splitIndex = docId.indexOf(':', 1); final String path = docId.substring(splitIndex + 1); File target = visible ? root.visiblePath : root.path; if (target == null) { return null; Loading @@ -353,7 +367,7 @@ public class ExternalStorageProvider extends DocumentsProvider { if (!target.exists()) { throw new FileNotFoundException("Missing file for " + docId + " at " + target); } return Pair.create(root, target); return target; } private void includeFile(MatrixCursor result, String docId, File file) Loading Loading @@ -430,25 +444,33 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override public Path findPath(String documentId) public Path findPath(String childDocId, @Nullable String parentDocId) throws FileNotFoundException { LinkedList<String> path = new LinkedList<>(); final Pair<RootInfo, File> resolvedDocId = resolveDocId(documentId, false); RootInfo root = resolvedDocId.first; File file = resolvedDocId.second; final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false); final RootInfo root = resolvedDocId.first; File child = resolvedDocId.second; final File parent = TextUtils.isEmpty(parentDocId) ? root.path : getFileForDocId(parentDocId); if (!child.exists()) { throw new FileNotFoundException(childDocId + " is not found."); } if (!file.exists()) { throw new FileNotFoundException(); if (!child.getAbsolutePath().startsWith(parent.getAbsolutePath())) { throw new FileNotFoundException(childDocId + " is not found under " + parentDocId); } while (file != null && file.getAbsolutePath().startsWith(root.path.getAbsolutePath())) { path.addFirst(getDocIdForFile(file)); while (child != null && child.getAbsolutePath().startsWith(parent.getAbsolutePath())) { path.addFirst(getDocIdForFile(child)); file = file.getParentFile(); child = child.getParentFile(); } return new Path(root.rootId, path); return new Path(parentDocId == null ? root.rootId : null, path); } @Override Loading