diff options
author | Jeff Sharkey <jsharkey@android.com> | 2014-05-21 22:22:03 -0700 |
---|---|---|
committer | Jeff Sharkey <jsharkey@android.com> | 2014-05-21 22:24:45 -0700 |
commit | b7e1255d5c8d9e4fa8dd389afb9f5aab35434df3 (patch) | |
tree | 9dc25a4e712ca3bcef16a555484d2e17ae37bb00 | |
parent | 05ad48206a082057e17723d32493c153faa6881e (diff) | |
download | frameworks_base-b7e1255d5c8d9e4fa8dd389afb9f5aab35434df3.zip frameworks_base-b7e1255d5c8d9e4fa8dd389afb9f5aab35434df3.tar.gz frameworks_base-b7e1255d5c8d9e4fa8dd389afb9f5aab35434df3.tar.bz2 |
Support for renaming documents.
DocumentsProviders can mark documents as supporting rename, and they
have the opportunity to change the DOCUMENT_ID as a side effect of
the rename. This supports providers that embed the display name
into DOCUMENT_ID. Issues a URI permission grant to the new document,
if any.
Adds renaming support to platform ExternalStorageProvider. Also
adds directory deletion support.
Bug: 12350110
Change-Id: Ica4b1ae6769ee994f70f6b6b2402213eebd064e0
4 files changed, 155 insertions, 22 deletions
diff --git a/api/current.txt b/api/current.txt index 5f8b90c..a7fb425 100644 --- a/api/current.txt +++ b/api/current.txt @@ -23315,6 +23315,7 @@ package android.provider { method public static java.lang.String getSearchDocumentsQuery(android.net.Uri); method public static java.lang.String getViaDocumentId(android.net.Uri); method public static boolean isDocumentUri(android.content.Context, android.net.Uri); + method public static android.net.Uri renameDocument(android.content.ContentResolver, android.net.Uri, java.lang.String); field public static final java.lang.String EXTRA_ERROR = "error"; field public static final java.lang.String EXTRA_INFO = "info"; field public static final java.lang.String EXTRA_LOADING = "loading"; @@ -23334,6 +23335,7 @@ package android.provider { field public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 32; // 0x20 field public static final int FLAG_DIR_SUPPORTS_CREATE = 8; // 0x8 field public static final int FLAG_SUPPORTS_DELETE = 4; // 0x4 + field public static final int FLAG_SUPPORTS_RENAME = 64; // 0x40 field public static final int FLAG_SUPPORTS_THUMBNAIL = 1; // 0x1 field public static final int FLAG_SUPPORTS_WRITE = 2; // 0x2 field public static final java.lang.String MIME_TYPE_DIR = "vnd.android.document/directory"; @@ -23378,6 +23380,7 @@ package android.provider { method public android.database.Cursor queryRecentDocuments(java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; method public abstract android.database.Cursor queryRoots(java.lang.String[]) throws java.io.FileNotFoundException; method public android.database.Cursor querySearchDocuments(java.lang.String, java.lang.String, java.lang.String[]) throws java.io.FileNotFoundException; + method public java.lang.String renameDocument(java.lang.String, java.lang.String) throws java.io.FileNotFoundException; method public final void revokeDocumentPermission(java.lang.String); method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]); } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index b907375..6b8e2de 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -287,6 +287,16 @@ public final class DocumentsContract { public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 5; /** + * Flag indicating that a document can be renamed. + * + * @see #COLUMN_FLAGS + * @see DocumentsContract#renameDocument(ContentProviderClient, Uri, + * String) + * @see DocumentsProvider#renameDocument(String, String) + */ + public static final int FLAG_SUPPORTS_RENAME = 1 << 6; + + /** * Flag indicating that document titles should be hidden when viewing * this directory in a larger format grid. For example, a directory * containing only images may want the image thumbnails to speak for @@ -494,6 +504,8 @@ public final class DocumentsContract { /** {@hide} */ public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; /** {@hide} */ + public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument"; + /** {@hide} */ public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; /** {@hide} */ @@ -898,6 +910,45 @@ public final class DocumentsContract { } /** + * Change the display name of an existing document. + * <p> + * If the underlying provider needs to create a new + * {@link Document#COLUMN_DOCUMENT_ID} to represent the updated display + * name, that new document is returned and the original document is no + * longer valid. Otherwise, the original document is returned. + * + * @param documentUri document with {@link Document#FLAG_SUPPORTS_RENAME} + * @param displayName updated name for document + * @return the existing or new document after the rename, or {@code null} if + * failed. + */ + public static Uri renameDocument(ContentResolver resolver, Uri documentUri, + String displayName) { + final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( + documentUri.getAuthority()); + try { + return renameDocument(client, documentUri, displayName); + } catch (Exception e) { + Log.w(TAG, "Failed to rename document", e); + return null; + } finally { + ContentProviderClient.releaseQuietly(client); + } + } + + /** {@hide} */ + public static Uri renameDocument(ContentProviderClient client, Uri documentUri, + String displayName) throws RemoteException { + final Bundle in = new Bundle(); + in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); + in.putString(Document.COLUMN_DISPLAY_NAME, displayName); + + final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in); + final Uri outUri = out.getParcelable(DocumentsContract.EXTRA_URI); + return (outUri != null) ? outUri : documentUri; + } + + /** * Delete the given document. * * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE} diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 1a7a00f2..066b4aa 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -19,9 +19,11 @@ package android.provider; import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE; import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; import static android.provider.DocumentsContract.getDocumentId; import static android.provider.DocumentsContract.getRootId; import static android.provider.DocumentsContract.getSearchDocumentsQuery; +import static android.provider.DocumentsContract.isViaUri; import android.content.ContentProvider; import android.content.ContentResolver; @@ -206,7 +208,7 @@ public abstract class DocumentsProvider extends ContentProvider { * If the MIME type is not supported, the provider must throw. * @param displayName the display name of the new document. The provider may * alter this name to meet any internal constraints, such as - * conflicting names. + * avoiding conflicting names. */ @SuppressWarnings("unused") public String createDocument(String parentDocumentId, String mimeType, String displayName) @@ -215,11 +217,33 @@ public abstract class DocumentsProvider extends ContentProvider { } /** - * Delete the requested document. Upon returning, any URI permission grants - * for the given document will be revoked. If additional documents were - * deleted as a side effect of this call (such as documents inside a - * directory) the implementor is responsible for revoking those permissions - * using {@link #revokeDocumentPermission(String)}. + * Rename an existing document. + * <p> + * If a different {@link Document#COLUMN_DOCUMENT_ID} must be used to + * represent the renamed document, generate and return it. Any outstanding + * URI permission grants will be updated to point at the new document. If + * the original {@link Document#COLUMN_DOCUMENT_ID} is still valid after the + * rename, return {@code null}. + * + * @param documentId the document to rename. + * @param displayName the updated display name of the document. The provider + * may alter this name to meet any internal constraints, such as + * avoiding conflicting names. + */ + @SuppressWarnings("unused") + public String renameDocument(String documentId, String displayName) + throws FileNotFoundException { + throw new UnsupportedOperationException("Rename not supported"); + } + + /** + * Delete the requested document. + * <p> + * Upon returning, any URI permission grants for the given document will be + * revoked. If additional documents were deleted as a side effect of this + * call (such as documents inside a directory) the implementor is + * responsible for revoking those permissions using + * {@link #revokeDocumentPermission(String)}. * * @param documentId the document to delete. */ @@ -523,26 +547,33 @@ public abstract class DocumentsProvider extends ContentProvider { DocumentsContract.getDocumentId(uri)); // Caller may only have prefix grant, so extend them a grant to - // the narrow Uri. Caller already holds read grant to get here, - // so check for any other modes we should extend. - int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; - if (context.checkCallingOrSelfUriPermission(uri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - == PackageManager.PERMISSION_GRANTED) { - modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - } - if (context.checkCallingOrSelfUriPermission(uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - == PackageManager.PERMISSION_GRANTED) { - modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; - } + // the narrow URI. + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, uri); context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags); return narrowUri; } return null; } + private static int getCallingOrSelfUriPermissionModeFlags(Context context, Uri uri) { + // TODO: move this to a direct AMS call + int modeFlags = 0; + if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + modeFlags |= Intent.FLAG_GRANT_READ_URI_PERMISSION; + } + if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + } + if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + modeFlags |= Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; + } + return modeFlags; + } + /** * Implementation is provided by the parent class. Throws by default, and * cannot be overriden. @@ -588,6 +619,7 @@ public abstract class DocumentsProvider extends ContentProvider { return super.call(method, arg, extras); } + final Context context = getContext(); final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); final String authority = documentUri.getAuthority(); final String documentId = DocumentsContract.getDocumentId(documentUri); @@ -605,7 +637,6 @@ public abstract class DocumentsProvider extends ContentProvider { final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); - final String newDocumentId = createDocument(documentId, mimeType, displayName); // No need to issue new grants here, since caller either has @@ -615,6 +646,30 @@ public abstract class DocumentsProvider extends ContentProvider { newDocumentId); out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + } else if (METHOD_RENAME_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri); + + final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); + final String newDocumentId = renameDocument(documentId, displayName); + + if (newDocumentId != null) { + final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri( + documentUri, newDocumentId); + + // If caller came in with a narrow grant, issue them a + // narrow grant for the newly renamed document. + if (!isViaUri(newDocumentUri)) { + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, + documentUri); + context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); + } + + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + + // Original document no longer exists, clean up any grants + revokeDocumentPermission(documentId); + } + } else if (METHOD_DELETE_DOCUMENT.equals(method)) { enforceWritePermissionInner(documentUri); deleteDocument(documentId); diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index 16fc3e5..d388ab7 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -35,6 +35,7 @@ import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; +import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; @@ -239,9 +240,12 @@ public class ExternalStorageProvider extends DocumentsProvider { if (file.canWrite()) { if (file.isDirectory()) { flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + flags |= Document.FLAG_SUPPORTS_DELETE; + flags |= Document.FLAG_SUPPORTS_RENAME; } else { flags |= Document.FLAG_SUPPORTS_WRITE; flags |= Document.FLAG_SUPPORTS_DELETE; + flags |= Document.FLAG_SUPPORTS_RENAME; } } @@ -332,9 +336,29 @@ public class ExternalStorageProvider extends DocumentsProvider { } @Override + public String renameDocument(String docId, String displayName) throws FileNotFoundException { + final File before = getFileForDocId(docId); + final File after = new File(before.getParentFile(), displayName); + if (after.exists()) { + throw new IllegalStateException("Already exists " + after); + } + if (!before.renameTo(after)) { + throw new IllegalStateException("Failed to rename to " + after); + } + final String afterDocId = getDocIdForFile(after); + if (!TextUtils.equals(docId, afterDocId)) { + return afterDocId; + } else { + return null; + } + } + + @Override public void deleteDocument(String docId) throws FileNotFoundException { - // TODO: extend to delete directories final File file = getFileForDocId(docId); + if (file.isDirectory()) { + FileUtils.deleteContents(file); + } if (!file.delete()) { throw new IllegalStateException("Failed to delete " + file); } |