Iteration on findPath API.
* Extend this API to take tree URI * Add toString(), equals() and hashCode() to Path * Address Jeff's comments in ag/1513538 * Add unit tests for findPath Bug: 30948740 Change-Id: Iaf852d0e40fae37623e9bb9ffa1c6fbe334c1b21 (cherry picked from commit d4ab7ade7171a4382ef4f61f2a5f078a17800e83)
This commit is contained in:
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
*
|
||||
* @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)
|
||||
* Document id should be unique across roots.
|
||||
*
|
||||
* @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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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)) {
|
||||
getContext().enforceCallingPermission(Manifest.permission.MANAGE_DOCUMENTS, null);
|
||||
final boolean isTreeUri = isTreeUri(documentUri);
|
||||
|
||||
final Path path = findPath(documentId);
|
||||
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, 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 {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.externalstorage;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException();
|
||||
final File parent = TextUtils.isEmpty(parentDocId)
|
||||
? root.path
|
||||
: getFileForDocId(parentDocId);
|
||||
|
||||
if (!child.exists()) {
|
||||
throw new FileNotFoundException(childDocId + " is not found.");
|
||||
}
|
||||
|
||||
while (file != null && file.getAbsolutePath().startsWith(root.path.getAbsolutePath())) {
|
||||
path.addFirst(getDocIdForFile(file));
|
||||
|
||||
file = file.getParentFile();
|
||||
if (!child.getAbsolutePath().startsWith(parent.getAbsolutePath())) {
|
||||
throw new FileNotFoundException(childDocId + " is not found under " + parentDocId);
|
||||
}
|
||||
|
||||
return new Path(root.rootId, path);
|
||||
while (child != null && child.getAbsolutePath().startsWith(parent.getAbsolutePath())) {
|
||||
path.addFirst(getDocIdForFile(child));
|
||||
|
||||
child = child.getParentFile();
|
||||
}
|
||||
|
||||
return new Path(parentDocId == null ? root.rootId : null, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user