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