Provider-level changes for implementing direct eject of a root in Files app.

Several changes at different levels:
1. Introduction of ejectRoot(String) for DocumentsProvider
2. Introduction of ejectRoot(ContentResolver, Uri, String) for
DocumentsContract
4. Additional permission for MOUNT_UNMOUNT for ExternalStorageProvider
5. Implementation of ejectRoot(String) for External StorageProvider

Bug: 29584653
Change-Id: I28557af63259548784cf24d5b051eb06ad5193ca
(cherry picked from commit 2ccc18357d6741dde56edc4d5a2608f15f4b9078)
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 1158776..b0ea99c 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -593,6 +593,9 @@
          * @hide
          */
         public static final int FLAG_REMOVABLE_USB = 1 << 20;
+
+        /** {@hide} */
+        public static final int FLAG_SUPPORTS_EJECT = 1 << 21;
     }
 
     /**
@@ -643,6 +646,8 @@
     public static final String METHOD_IS_CHILD_DOCUMENT = "android:isChildDocument";
     /** {@hide} */
     public static final String METHOD_REMOVE_DOCUMENT = "android:removeDocument";
+    /** {@hide} */
+    public static final String METHOD_EJECT_ROOT = "android:ejectRoot";
 
     /** {@hide} */
     public static final String EXTRA_PARENT_URI = "parentUri";
@@ -1274,6 +1279,37 @@
         client.call(METHOD_REMOVE_DOCUMENT, null, in);
     }
 
+    /** {@hide} */
+    public static boolean ejectRoot(ContentResolver resolver, Uri rootUri) {
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                rootUri.getAuthority());
+        try {
+            return ejectRoot(client, rootUri);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to eject root", e);
+            return false;
+        } finally {
+            ContentProviderClient.releaseQuietly(client);
+        }
+    }
+
+    /** {@hide} */
+    public static boolean ejectRoot(ContentProviderClient client, Uri rootUri)
+            throws RemoteException {
+        final Bundle in = new Bundle();
+        in.putParcelable(DocumentsContract.EXTRA_URI, rootUri);
+
+        final Bundle out = client.call(METHOD_EJECT_ROOT, null, in);
+
+        if (out == null) {
+            throw new RemoteException("Failed to get a reponse from ejectRoot.");
+        }
+        if (!out.containsKey(DocumentsContract.EXTRA_RESULT)) {
+            throw new RemoteException("Response did not include result field..");
+        }
+        return out.getBoolean(DocumentsContract.EXTRA_RESULT);
+    }
+
     /**
      * Open the given image for thumbnail purposes, using any embedded EXIF
      * thumbnail if available, and providing orientation hints from the parent
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index 515f975..ac8f375 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -19,6 +19,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_IS_CHILD_DOCUMENT;
 import static android.provider.DocumentsContract.METHOD_MOVE_DOCUMENT;
 import static android.provider.DocumentsContract.METHOD_REMOVE_DOCUMENT;
@@ -458,6 +459,12 @@
         throw new UnsupportedOperationException("Search not supported");
     }
 
+    /** {@hide} */
+    @SuppressWarnings("unused")
+    public boolean ejectRoot(String rootId) {
+        throw new UnsupportedOperationException("Eject not supported");
+    }
+
     /**
      * Return concrete MIME type of the requested document. Must match the value
      * of {@link Document#COLUMN_MIME_TYPE} for this document. The default
@@ -851,7 +858,17 @@
 
             // It's responsibility of the provider to revoke any grants, as the document may be
             // still attached to another parents.
+        } else if (METHOD_EJECT_ROOT.equals(method)) {
+            // Given that certain system apps can hold MOUNT_UNMOUNT permission, but only apps
+            // signed with platform signature can hold MANAGE_DOCUMENTS, we are going to check for
+            // MANAGE_DOCUMENTS here instead
+            getContext().enforceCallingPermission(
+                    android.Manifest.permission.MANAGE_DOCUMENTS, null);
+            final Uri rootUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+            final String rootId = DocumentsContract.getRootId(rootUri);
+            final boolean ejected = ejectRoot(rootId);
 
+            out.putBoolean(DocumentsContract.EXTRA_RESULT, ejected);
         } else {
             throw new UnsupportedOperationException("Method not supported " + method);
         }
diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml
index 3185917..0b290ce 100644
--- a/packages/ExternalStorageProvider/AndroidManifest.xml
+++ b/packages/ExternalStorageProvider/AndroidManifest.xml
@@ -4,6 +4,7 @@
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
 
     <application android:label="@string/app_label">
         <provider
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 62f33bf..0643e9be 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -25,6 +25,7 @@
 import android.database.MatrixCursor.RowBuilder;
 import android.graphics.Point;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Environment;
@@ -86,6 +87,7 @@
 
     private static class RootInfo {
         public String rootId;
+        public String volumeId;
         public int flags;
         public String title;
         public String docId;
@@ -182,6 +184,7 @@
             mRoots.put(rootId, root);
 
             root.rootId = rootId;
+            root.volumeId = volume.id;
             root.flags = Root.FLAG_LOCAL_ONLY
                     | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
 
@@ -193,6 +196,10 @@
                 root.flags |= Root.FLAG_REMOVABLE_USB;
             }
 
+            if (!VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
+                root.flags |= Root.FLAG_SUPPORTS_EJECT;
+            }
+
             if (volume.isPrimary()) {
                 // save off the primary volume for subsequent "Home" dir initialization.
                 primaryVolume = volume;
@@ -584,6 +591,24 @@
     }
 
     @Override
+    public boolean ejectRoot(String rootId) {
+        final long token = Binder.clearCallingIdentity();
+        boolean ejected = false;
+        RootInfo root = mRoots.get(rootId);
+        if (root != null) {
+            try {
+                mStorageManager.unmount(root.volumeId);
+                ejected = true;
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Root '" + root.title + "' could not be ejected");
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+        return ejected;
+    }
+
+    @Override
     public String getDocumentType(String documentId) throws FileNotFoundException {
         if (mArchiveHelper.isArchivedDocument(documentId)) {
             return mArchiveHelper.getDocumentType(documentId);