DO NOT MERGE ANYWHERE: Add findPath API to SAF.
Implement it in ExternalStorageProvider.
Bug: 30948740
Change-Id: I03241cdfa561ef2fc0a0b829c9a59ad845e8f844
(cherry picked from commit 51efc73f3f341393cf93f71604be791205021b69)
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 6ddaf3b..98371f4 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -36,8 +36,10 @@
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
+import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.storage.StorageVolume;
import android.system.ErrnoException;
@@ -644,6 +646,8 @@
public static final String METHOD_REMOVE_DOCUMENT = "android:removeDocument";
/** {@hide} */
public static final String METHOD_EJECT_ROOT = "android:ejectRoot";
+ /** {@hide} */
+ public static final String METHOD_FIND_PATH = "android:findPath";
/** {@hide} */
public static final String EXTRA_PARENT_URI = "parentUri";
@@ -1307,6 +1311,41 @@
}
/**
+ * Finds the canonical path to the root. 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)
+ *
+ * {@hide}
+ */
+ public static Path findPath(ContentResolver resolver, Uri documentUri)
+ throws RemoteException {
+ final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+ documentUri.getAuthority());
+ try {
+ return findPath(client, documentUri);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to find path", e);
+ return null;
+ } finally {
+ ContentProviderClient.releaseQuietly(client);
+ }
+ }
+
+ /** {@hide} */
+ public static Path findPath(ContentProviderClient client, Uri documentUri)
+ throws RemoteException {
+ final Bundle in = new Bundle();
+ in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);
+
+ final Bundle out = client.call(METHOD_FIND_PATH, null, in);
+
+ return out.getParcelable(DocumentsContract.EXTRA_RESULT);
+ }
+
+ /**
* Open the given image for thumbnail purposes, using any embedded EXIF
* thumbnail if available, and providing orientation hints from the parent
* image.
@@ -1345,4 +1384,51 @@
return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, extras);
}
+
+ /**
+ * Holds a path from a root to a particular document under it.
+ *
+ * @hide
+ */
+ public static final class Path implements Parcelable {
+
+ public final String mRootId;
+ public 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
+ */
+ public Path(String rootId, List<String> path) {
+ mRootId = rootId;
+ mPath = path;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mRootId);
+ dest.writeStringList(mPath);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<Path> CREATOR = new Creator<Path>() {
+ @Override
+ public Path createFromParcel(Parcel in) {
+ final String rootId = in.readString();
+ final List<String> path = in.createStringArrayList();
+ return new Path(rootId, path);
+ }
+
+ @Override
+ public Path[] newArray(int size) {
+ return new Path[size];
+ }
+ };
+ }
}
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 6117ce4..6234f6a 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -20,6 +20,7 @@
import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_EJECT_ROOT;
+import static android.provider.DocumentsContract.METHOD_FIND_PATH;
import static android.provider.DocumentsContract.METHOD_IS_CHILD_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_MOVE_DOCUMENT;
import static android.provider.DocumentsContract.METHOD_REMOVE_DOCUMENT;
@@ -33,6 +34,7 @@
import static android.provider.DocumentsContract.getTreeDocumentId;
import static android.provider.DocumentsContract.isTreeUri;
+import android.Manifest;
import android.annotation.CallSuper;
import android.content.ClipDescription;
import android.content.ContentProvider;
@@ -53,6 +55,7 @@
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Path;
import android.util.Log;
import libcore.io.IoUtils;
@@ -323,6 +326,26 @@
}
/**
+ * 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.
+ *
+ * <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.
+ *
+ * @hide
+ */
+ public Path findPath(String documentId)
+ throws FileNotFoundException {
+ Log.w(TAG, "findPath is called on an unsupported provider.");
+ return null;
+ }
+
+ /**
* Return all roots currently provided. To display to users, you must define
* at least one root. You should avoid making network requests to keep this
* request fast.
@@ -873,6 +896,12 @@
// 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 Path path = findPath(documentId);
+
+ out.putParcelable(DocumentsContract.EXTRA_RESULT, path);
} else {
throw new UnsupportedOperationException("Method not supported " + method);
}
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 33d6b9a..3b575a8 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -41,6 +41,7 @@
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Path;
import android.provider.DocumentsProvider;
import android.provider.MediaStore;
import android.provider.Settings;
@@ -48,6 +49,7 @@
import android.util.ArrayMap;
import android.util.DebugUtils;
import android.util.Log;
+import android.util.Pair;
import android.webkit.MimeTypeMap;
import com.android.internal.annotations.GuardedBy;
@@ -183,7 +185,8 @@
root.rootId = rootId;
root.volumeId = volume.id;
root.flags = Root.FLAG_LOCAL_ONLY
- | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
+ | Root.FLAG_SUPPORTS_SEARCH
+ | Root.FLAG_SUPPORTS_IS_CHILD;
final DiskInfo disk = volume.getDisk();
if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk);
@@ -270,7 +273,6 @@
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
}
-
private String getDocIdForFile(File file) throws FileNotFoundException {
return getDocIdForFileMaybeCreate(file, false);
}
@@ -323,6 +325,11 @@
}
private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
+ return resolveDocId(docId, visible).second;
+ }
+
+ private Pair<RootInfo, File> resolveDocId(String docId, boolean visible)
+ throws FileNotFoundException {
final int splitIndex = docId.indexOf(':', 1);
final String tag = docId.substring(0, splitIndex);
final String path = docId.substring(splitIndex + 1);
@@ -346,7 +353,7 @@
if (!target.exists()) {
throw new FileNotFoundException("Missing file for " + docId + " at " + target);
}
- return target;
+ return Pair.create(root, target);
}
private void includeFile(MatrixCursor result, String docId, File file)
@@ -423,6 +430,28 @@
}
@Override
+ public Path findPath(String documentId)
+ throws FileNotFoundException {
+ LinkedList<String> path = new LinkedList<>();
+
+ final Pair<RootInfo, File> resolvedDocId = resolveDocId(documentId, false);
+ RootInfo root = resolvedDocId.first;
+ File file = resolvedDocId.second;
+
+ if (!file.exists()) {
+ throw new FileNotFoundException();
+ }
+
+ while (file != null && file.getAbsolutePath().startsWith(root.path.getAbsolutePath())) {
+ path.addFirst(getDocIdForFile(file));
+
+ file = file.getParentFile();
+ }
+
+ return new Path(root.rootId, path);
+ }
+
+ @Override
public String createDocument(String docId, String mimeType, String displayName)
throws FileNotFoundException {
displayName = FileUtils.buildValidFatFilename(displayName);