Add findPath API to SAF.
Implement it in ExternalStorageProvider. Bug: 30948740 Change-Id: I1b7717a794ae3892cd1be5ed90ca155adf9a64f4 (cherry picked from commit 51efc73f3f341393cf93f71604be791205021b69)
This commit is contained in:
@@ -36,8 +36,10 @@ import android.net.Uri;
|
||||
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 final class DocumentsContract {
|
||||
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";
|
||||
@@ -1306,6 +1310,41 @@ public final class DocumentsContract {
|
||||
return out.getBoolean(DocumentsContract.EXTRA_RESULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1345,4 +1384,51 @@ public final class DocumentsContract {
|
||||
|
||||
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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import static android.provider.DocumentsContract.METHOD_COPY_DOCUMENT;
|
||||
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.getSearchDocumentsQuery;
|
||||
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;
|
||||
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;
|
||||
@@ -322,6 +325,26 @@ public abstract class DocumentsProvider extends ContentProvider {
|
||||
throw new UnsupportedOperationException("Remove not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -873,6 +896,12 @@ 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 Path path = findPath(documentId);
|
||||
|
||||
out.putParcelable(DocumentsContract.EXTRA_RESULT, path);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Method not supported " + method);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ 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.DocumentsProvider;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
@@ -48,6 +49,7 @@ import android.text.TextUtils;
|
||||
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 @@ public class ExternalStorageProvider extends DocumentsProvider {
|
||||
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 @@ public class ExternalStorageProvider extends DocumentsProvider {
|
||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||
}
|
||||
|
||||
|
||||
private String getDocIdForFile(File file) throws FileNotFoundException {
|
||||
return getDocIdForFileMaybeCreate(file, false);
|
||||
}
|
||||
@@ -323,6 +325,11 @@ public class ExternalStorageProvider extends DocumentsProvider {
|
||||
}
|
||||
|
||||
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 @@ public class ExternalStorageProvider extends DocumentsProvider {
|
||||
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)
|
||||
@@ -422,6 +429,28 @@ public class ExternalStorageProvider extends DocumentsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
Reference in New Issue
Block a user