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);