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
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 @@
     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 @@
     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 @@
     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 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 @@
     /** {@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 @@
     }
 
     /**
+     * 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 @@
 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 @@
      *            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 @@
     }
 
     /**
-     * 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 @@
                         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 @@
             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 @@
 
                 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 @@
                         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.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 @@
         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 @@
     }
 
     @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);
         }