Docsui-level work for implementing Eject on Roots list.

1. Added Eject Icon for Roots that support eject
2. Added Context Menu for RootsFragment (Settings and Eject)

Bug: 29584653
Change-Id: I97f582de05763e3f0327bc0d2dc6d4e2222e047c
(cherry picked from commit d96661f8b0f613b40f2bdfc178bbe06022b5f76c)
diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java
index ac8f375..6117ce4 100644
--- a/core/java/android/provider/DocumentsProvider.java
+++ b/core/java/android/provider/DocumentsProvider.java
@@ -730,6 +730,23 @@
             throws FileNotFoundException {
 
         final Context context = getContext();
+        final Bundle out = new Bundle();
+
+        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);
+
+            return out;
+        }
+
         final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
         final String authority = documentUri.getAuthority();
         final String documentId = DocumentsContract.getDocumentId(documentUri);
@@ -739,8 +756,6 @@
                     "Requested authority " + authority + " doesn't match provider " + mAuthority);
         }
 
-        final Bundle out = new Bundle();
-
         // If the URI is a tree URI performs some validation.
         enforceTree(documentUri);
 
@@ -858,17 +873,6 @@
 
             // 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/DocumentsUI/res/color/item_eject_icon.xml b/packages/DocumentsUI/res/color/item_eject_icon.xml
new file mode 100644
index 0000000..15e7e8e
--- /dev/null
+++ b/packages/DocumentsUI/res/color/item_eject_icon.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false" android:alpha="@*android:dimen/disabled_alpha_material_light" android:color="@*android:color/primary_text_default_material_light" />
+    <item android:color="@*android:color/primary_text_default_material_light" />
+</selector>
diff --git a/packages/DocumentsUI/res/drawable/ic_eject.xml b/packages/DocumentsUI/res/drawable/ic_eject.xml
new file mode 100644
index 0000000..cbcd755
--- /dev/null
+++ b/packages/DocumentsUI/res/drawable/ic_eject.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF737373"
+        android:pathData="M5 17h14v2H5zm7-12L5.33 15h13.34z"/>
+</vector>
diff --git a/packages/DocumentsUI/res/layout/item_root.xml b/packages/DocumentsUI/res/layout/item_root.xml
index 3e447c9..9671813 100644
--- a/packages/DocumentsUI/res/layout/item_root.xml
+++ b/packages/DocumentsUI/res/layout/item_root.xml
@@ -46,7 +46,8 @@
         android:layout_height="wrap_content"
         android:paddingTop="8dp"
         android:paddingBottom="8dp"
-        android:orientation="vertical">
+        android:orientation="vertical"
+        android:layout_weight="1">
 
         <TextView
             android:id="@android:id/title"
@@ -70,4 +71,19 @@
 
     </LinearLayout>
 
+     <FrameLayout
+        android:layout_width="@dimen/icon_size"
+        android:layout_height="@dimen/icon_size"
+        android:duplicateParentState="true">
+
+        <ImageView
+            android:id="@+id/unmount_icon"
+            android:layout_width="@dimen/root_icon_size"
+            android:layout_height="match_parent"
+            android:scaleType="centerInside"
+            android:contentDescription="@string/menu_eject_root"
+            android:visibility="gone" />
+
+    </FrameLayout>
+
 </com.android.documentsui.RootItemView>
diff --git a/packages/DocumentsUI/res/menu/root_context_menu.xml b/packages/DocumentsUI/res/menu/root_context_menu.xml
new file mode 100644
index 0000000..0cf00a4
--- /dev/null
+++ b/packages/DocumentsUI/res/menu/root_context_menu.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_eject_root"
+        android:title="@string/menu_eject_root" />
+    <item
+        android:id="@+id/menu_settings"
+        android:title="@string/menu_settings" />
+</menu>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index bf34461..28f5f88 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -199,6 +199,8 @@
     <string name="menu_rename">Rename</string>
     <!-- Toast shown when renaming document failed with an error [CHAR LIMIT=48] -->
     <string name="rename_error">Failed to rename document</string>
+    <!-- Context Menu item that ejects the root selected [CHAR LIMIT=24] -->
+    <string name="menu_eject_root">Eject</string>
     <!-- First line for notifications saying that some files were converted to a different format
          during a copy. [CHAR LIMIT=48] -->
     <string name="notification_copy_files_converted_title">Some files were converted</string>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 3d1a176..f40e771 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -103,6 +103,7 @@
 
     abstract void onTaskFinished(Uri... uris);
     abstract void refreshDirectory(int anim);
+    abstract void openRootSettings(RootInfo root);
     /** Allows sub-classes to include information in a newly created State instance. */
     abstract void includeState(State initialState);
 
@@ -289,15 +290,6 @@
                 setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
                 return true;
 
-            case R.id.menu_settings:
-                Metrics.logUserAction(this, Metrics.USER_ACTION_SETTINGS);
-
-                final RootInfo root = getCurrentRoot();
-                final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
-                intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
-                startActivity(intent);
-                return true;
-
             default:
                 return super.onOptionsItemSelected(item);
         }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CheckedTask.java b/packages/DocumentsUI/src/com/android/documentsui/CheckedTask.java
new file mode 100644
index 0000000..ae15902
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/CheckedTask.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import android.os.AsyncTask;
+
+/**
+ * An {@link AsyncTask} that guards work with checks that a paired {@link Check}
+ * has not yet given signals to stop progress.
+ *
+ * <p>Use this type of task for greater safety when executing tasks that might complete
+ * after the owner of the task has explicitly given a signal to stop progress.
+ *
+ * <p>Also useful as tasks can be static, limiting scope, but still have access to
+ * signal from the owning class.
+ *
+ * @template Input input type
+ * @template Output output type
+ */
+abstract class CheckedTask<Input, Output>
+        extends AsyncTask<Input, Void, Output> {
+
+    private Check mCheck ;
+
+    public CheckedTask(Check check) {
+        mCheck = check;
+    }
+
+    /** Called prior to run being executed. Analogous to {@link AsyncTask#onPreExecute} */
+    void prepare() {}
+
+    /** Analogous to {@link AsyncTask#doInBackground} */
+    abstract Output run(Input... input);
+
+    /** Analogous to {@link AsyncTask#onPostExecute} */
+    abstract void finish(Output output);
+
+    @Override
+    final protected void onPreExecute() {
+        if (mCheck.stop()) {
+            return;
+        }
+        prepare();
+    }
+
+    @Override
+    final protected Output doInBackground(Input... input) {
+        if (mCheck.stop()) {
+            return null;
+        }
+        return run(input);
+    }
+
+    @Override
+    final protected void onPostExecute(Output result) {
+        if (mCheck.stop()) {
+            return;
+        }
+        finish(result);
+    }
+
+    interface Check {
+        boolean stop();
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 0a518cd..8041a1b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -233,6 +233,11 @@
     }
 
     @Override
+    void openRootSettings(RootInfo root) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     void refreshDirectory(int anim) {
         final FragmentManager fm = getFragmentManager();
         final RootInfo root = getCurrentRoot();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/EjectRootTask.java b/packages/DocumentsUI/src/com/android/documentsui/EjectRootTask.java
new file mode 100644
index 0000000..fcee472
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/EjectRootTask.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.util.Log;
+
+import java.util.function.Consumer;
+
+final class EjectRootTask
+        extends CheckedTask<Void, Boolean> {
+    private final String mAuthority;
+    private final String mRootId;
+    private final Consumer<Boolean> mListener;
+    private Context mContext;
+
+    public EjectRootTask(Check check,
+            String authority,
+            String rootId,
+            Context context,
+            Consumer<Boolean> listener) {
+        super(check);
+        mAuthority = authority;
+        mRootId = rootId;
+        mContext = context;
+        mListener = listener;
+    }
+
+    @Override
+    protected Boolean run(Void... params) {
+        final ContentResolver resolver = mContext.getContentResolver();
+
+        Uri rootUri = DocumentsContract.buildRootUri(mAuthority, mRootId);
+        ContentProviderClient client = null;
+        try {
+            client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                    resolver, mAuthority);
+            return DocumentsContract.ejectRoot(client, rootUri);
+        } catch (Exception e) {
+            Log.w(Shared.TAG, "Failed to eject root", e);
+        } finally {
+            ContentProviderClient.releaseQuietly(client);
+        }
+
+        return false;
+    }
+
+    @Override
+    protected void finish(Boolean ejected) {
+        mListener.accept(ejected);
+    }
+}
\ No newline at end of file
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index 1edfffe..c7c53ba 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -230,12 +230,24 @@
                     dir.pasteFromClipboard();
                 }
                 break;
+            case R.id.menu_settings:
+                final RootInfo root = getCurrentRoot();
+                openRootSettings(root);
+                break;
             default:
                 return super.onOptionsItemSelected(item);
         }
         return true;
     }
 
+    @Override
+    void openRootSettings(RootInfo root) {
+        Metrics.logUserAction(this, Metrics.USER_ACTION_SETTINGS);
+        final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
+        intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
+        startActivity(intent);
+    }
+
     private void createNewWindow() {
         Metrics.logUserAction(this, Metrics.USER_ACTION_NEW_WINDOW);
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesMenuManager.java b/packages/DocumentsUI/src/com/android/documentsui/FilesMenuManager.java
index 78d95f6..e1da944 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesMenuManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesMenuManager.java
@@ -16,16 +16,11 @@
 
 package com.android.documentsui;
 
-import static com.android.documentsui.State.ACTION_CREATE;
-import static com.android.documentsui.State.ACTION_GET_CONTENT;
-import static com.android.documentsui.State.ACTION_OPEN;
-import static com.android.documentsui.State.ACTION_OPEN_TREE;
-import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
-
+import android.provider.DocumentsContract.Root;
 import android.view.Menu;
 import android.view.MenuItem;
 
-import com.android.documentsui.MenuManager.DirectoryDetails;
+import com.android.documentsui.model.RootInfo;
 
 final class FilesMenuManager extends MenuManager {
 
@@ -42,6 +37,18 @@
     }
 
     @Override
+    void updateSettings(MenuItem settings, RootInfo root) {
+        settings.setVisible(true);
+        settings.setEnabled(root.hasSettings());
+    }
+
+    @Override
+    void updateEject(MenuItem eject, RootInfo root) {
+        eject.setVisible(true);
+        eject.setEnabled(((root.flags & Root.FLAG_SUPPORTS_EJECT) > 0) && !root.ejecting);
+    }
+
+    @Override
     void updateSettings(MenuItem settings, DirectoryDetails directoryDetails) {
         settings.setVisible(directoryDetails.hasRootSettings());
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/MenuManager.java b/packages/DocumentsUI/src/com/android/documentsui/MenuManager.java
index 6e960be..a23203b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/MenuManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/MenuManager.java
@@ -17,9 +17,12 @@
 package com.android.documentsui;
 
 import android.annotation.Nullable;
+import android.provider.DocumentsContract.Root;
 import android.view.Menu;
 import android.view.MenuItem;
 
+import com.android.documentsui.model.RootInfo;
+
 public abstract class MenuManager {
 
     final State mState;
@@ -94,6 +97,14 @@
         delete.setVisible(true);
     }
 
+    public void updateRootContextMenu(Menu menu, RootInfo root) {
+        MenuItem settings = menu.findItem(R.id.menu_settings);
+        MenuItem eject = menu.findItem(R.id.menu_eject_root);
+
+        updateSettings(settings, root);
+        updateEject(eject, root);
+    }
+
     void updateModePicker(MenuItem grid, MenuItem list, DirectoryDetails directoryDetails) {
         grid.setVisible(mState.derivedMode != State.MODE_GRID);
         list.setVisible(mState.derivedMode != State.MODE_LIST);
@@ -122,6 +133,14 @@
         settings.setVisible(false);
     }
 
+    void updateSettings(MenuItem settings, RootInfo root) {
+        settings.setVisible(false);
+    }
+
+    void updateEject(MenuItem eject, RootInfo root) {
+        eject.setVisible(false);
+    }
+
     void updateNewWindow(MenuItem newWindow, DirectoryDetails directoryDetails) {
         newWindow.setVisible(false);
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java b/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java
index b74acb8..7d2da0b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/PairedTask.java
@@ -20,58 +20,20 @@
 import android.os.AsyncTask;
 
 /**
- * An {@link AsyncTask} that guards work with checks that a paired {@link Activity}
+ * An {@link CheckedTask} that guards work with checks that a paired {@link Activity}
  * is still alive. Instances of this class make no progress.
  *
- * <p>Use this type of task for greater safety when executing tasks that might complete
- * after an Activity is destroyed.
- *
- * <p>Also useful as tasks can be static, limiting scope, but still have access to
- * the owning class (by way the A template and the mActivity field).
- *
  * @template Owner Activity type.
  * @template Input input type
  * @template Output output type
  */
 abstract class PairedTask<Owner extends Activity, Input, Output>
-        extends AsyncTask<Input, Void, Output> {
+        extends CheckedTask<Input, Output> {
 
     protected final Owner mOwner;
 
     public PairedTask(Owner owner) {
+        super(owner::isDestroyed);
         mOwner = owner;
     }
-
-    /** Called prior to run being executed. Analogous to {@link AsyncTask#onPreExecute} */
-    void prepare() {}
-
-    /** Analogous to {@link AsyncTask#doInBackground} */
-    abstract Output run(Input... input);
-
-    /** Analogous to {@link AsyncTask#onPostExecute} */
-    abstract void finish(Output output);
-
-    @Override
-    final protected void onPreExecute() {
-        if (mOwner.isDestroyed()) {
-            return;
-        }
-        prepare();
-    }
-
-    @Override
-    final protected Output doInBackground(Input... input) {
-        if (mOwner.isDestroyed()) {
-            return null;
-        }
-        return run(input);
-    }
-
-    @Override
-    final protected void onPostExecute(Output result) {
-        if (mOwner.isDestroyed()) {
-            return;
-        }
-        finish(result);
-    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index b333379..a33b35b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -33,16 +33,26 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Looper;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract;
 import android.provider.Settings;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.text.format.Formatter;
 import android.util.Log;
+import android.view.ContextMenu;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.View.OnDragListener;
+import android.view.View.OnGenericMotionListener;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.AdapterView.OnItemLongClickListener;
 import android.widget.ArrayAdapter;
@@ -60,6 +70,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
 
 /**
  * Display list of known storage backend roots.
@@ -96,6 +108,22 @@
         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
         mList = (ListView) view.findViewById(R.id.roots_list);
         mList.setOnItemClickListener(mItemListener);
+        // For right-clicks, we want to trap the click and not pass it to OnClickListener
+        // For all other clicks, we will pass the events down
+        mList.setOnGenericMotionListener(
+                new OnGenericMotionListener() {
+            @Override
+            public boolean onGenericMotion(View v, MotionEvent event) {
+                if (Events.isMouseEvent(event)
+                        && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
+                    registerForContextMenu(v);
+                    v.showContextMenu(event.getX(), event.getY());
+                    unregisterForContextMenu(v);
+                    return true;
+                }
+                return false;
+            }
+        });
         mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
         return view;
     }
@@ -190,6 +218,10 @@
         startActivity(intent);
     }
 
+    private BaseActivity getBaseActivity() {
+        return (BaseActivity) getActivity();
+    }
+
     @Override
     public void runOnUiThread(Runnable runnable) {
         getActivity().runOnUiThread(runnable);
@@ -218,6 +250,69 @@
         itemView.setHighlight(highlight);
     }
 
+    @Override
+    public void onCreateContextMenu(
+            ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, v, menuInfo);
+        AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
+        final Item item = mAdapter.getItem(adapterMenuInfo.position);
+        if (item instanceof RootItem) {
+            RootItem rootItem = (RootItem) item;
+            MenuInflater inflater = getActivity().getMenuInflater();
+            inflater.inflate(R.menu.root_context_menu, menu);
+            (getBaseActivity()).getMenuManager().updateRootContextMenu(menu, rootItem.root);
+        }
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo();
+        final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position);
+        switch(item.getItemId()) {
+            case R.id.menu_eject_root:
+                final View unmountIcon = adapterMenuInfo.targetView.findViewById(R.id.unmount_icon);
+                ejectClicked(unmountIcon, rootItem.root);
+                return true;
+            case R.id.menu_settings:
+                final RootInfo root = rootItem.root;
+                getBaseActivity().openRootSettings(root);
+                return true;
+            default:
+                if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
+                return false;
+        }
+    }
+
+    private static void ejectClicked(View ejectIcon, RootInfo root) {
+        assert(ejectIcon != null);
+        assert(ejectIcon.getContext() instanceof BaseActivity);
+        ejectIcon.setEnabled(false);
+        root.ejecting = true;
+        ejectRoot(
+                ejectIcon,
+                root.authority,
+                root.rootId,
+                new Consumer<Boolean>() {
+                    @Override
+                    public void accept(Boolean ejected) {
+                        ejectIcon.setEnabled(!ejected);
+                        root.ejecting = false;
+                    }
+                });
+    }
+
+    static void ejectRoot(
+            View ejectIcon, String authority, String rootId, Consumer<Boolean> listener) {
+        BooleanSupplier predicate = () -> {
+            return !(ejectIcon.getVisibility() == View.VISIBLE);
+        };
+        new EjectRootTask(predicate::getAsBoolean,
+                authority,
+                rootId,
+                ejectIcon.getContext(),
+                listener).executeOnExecutor(ProviderExecutor.forAuthority(authority));
+    }
+
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
@@ -290,11 +385,25 @@
             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
             final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
+            final ImageView unmountIcon = (ImageView) convertView.findViewById(R.id.unmount_icon);
 
             final Context context = convertView.getContext();
             icon.setImageDrawable(root.loadDrawerIcon(context));
             title.setText(root.title);
 
+            if (root.supportsEject()) {
+                unmountIcon.setVisibility(View.VISIBLE);
+                unmountIcon.setImageDrawable(root.loadEjectIcon(context));
+                unmountIcon.setOnClickListener(new OnClickListener() {
+                    @Override
+                    public void onClick(View unmountIcon) {
+                        RootsFragment.ejectClicked(unmountIcon, root);
+                    }
+                });
+            } else {
+                unmountIcon.setVisibility(View.GONE);
+                unmountIcon.setOnClickListener(null);
+            }
             // Show available space if no summary
             String summaryText = root.summary;
             if (TextUtils.isEmpty(summaryText) && root.availableBytes >= 0) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java
index e175331..5201089 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/FragmentTuner.java
@@ -16,7 +16,6 @@
 
 package com.android.documentsui.dirlist;
 
-import static com.android.documentsui.State.ACTION_BROWSE;
 import static com.android.documentsui.State.ACTION_CREATE;
 import static com.android.documentsui.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.State.ACTION_OPEN;
@@ -25,13 +24,9 @@
 
 import android.content.Context;
 import android.provider.DocumentsContract.Document;
-import android.view.Menu;
-import android.view.MenuItem;
 
 import com.android.documentsui.BaseActivity;
-import com.android.documentsui.Menus;
 import com.android.documentsui.MimePredicate;
-import com.android.documentsui.R;
 import com.android.documentsui.State;
 import com.android.documentsui.dirlist.DirectoryFragment.ResultType;
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
index 649dde0..92eea5e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
@@ -94,6 +94,9 @@
     public String[] derivedMimeTypes;
     public int derivedIcon;
     public @RootType int derivedType;
+    // Currently, we are not persisting this and we should be asking Provider whether a Root
+    // is in the process of eject. Provider does not have this available yet.
+    public transient boolean ejecting;
 
     public RootInfo() {
         reset();
@@ -110,6 +113,7 @@
         documentId = null;
         availableBytes = -1;
         mimeTypes = null;
+        ejecting = false;
 
         derivedMimeTypes = null;
         derivedIcon = 0;
@@ -298,6 +302,10 @@
         return (flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
     }
 
+    public boolean supportsEject() {
+        return (flags & Root.FLAG_SUPPORTS_EJECT) != 0;
+    }
+
     public boolean isAdvanced() {
         return (flags & Root.FLAG_ADVANCED) != 0;
     }
@@ -334,6 +342,10 @@
         }
     }
 
+    public Drawable loadEjectIcon(Context context) {
+        return IconUtils.applyTintColor(context, R.drawable.ic_eject, R.color.item_eject_icon);
+    }
+
     @Override
     public boolean equals(Object o) {
         if (o == null) {
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
index b6540d5..b23dd7a 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
@@ -21,9 +21,11 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import android.provider.DocumentsContract.Root;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.documentsui.model.RootInfo;
 import com.android.documentsui.testing.TestDirectoryDetails;
 import com.android.documentsui.testing.TestMenu;
 import com.android.documentsui.testing.TestMenuItem;
@@ -54,11 +56,14 @@
     private TestMenuItem sort;
     private TestMenuItem sortSize;
     private TestMenuItem advanced;
+    private TestMenuItem settings;
+    private TestMenuItem eject;
 
     private TestSelectionDetails selectionDetails;
     private TestDirectoryDetails directoryDetails;
     private TestSearchViewManager testSearchManager;
     private State state = new State();
+    private RootInfo testRootInfo;
 
     @Before
     public void setUp() {
@@ -78,10 +83,13 @@
         sort = testMenu.findItem(R.id.menu_sort);
         sortSize = testMenu.findItem(R.id.menu_sort_size);
         advanced = testMenu.findItem(R.id.menu_advanced);
+        settings = testMenu.findItem(R.id.menu_settings);
+        eject = testMenu.findItem(R.id.menu_eject_root);
 
         selectionDetails = new TestSelectionDetails();
         directoryDetails = new TestDirectoryDetails();
         testSearchManager = new TestSearchViewManager();
+        testRootInfo = new RootInfo();
         state.action = ACTION_CREATE;
         state.allowMultiple = true;
     }
@@ -208,4 +216,34 @@
         createDir.assertVisible();
         delete.assertVisible();
     }
+
+    @Test
+    public void testRootContextMenu() {
+        DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
+        mgr.updateRootContextMenu(testMenu, testRootInfo);
+
+        eject.assertVisible();
+        eject.assertDisabled();
+
+        settings.assertVisible();
+        settings.assertDisabled();
+    }
+
+    @Test
+    public void testRootContextMenu_hasRootSettings() {
+        testRootInfo.flags = Root.FLAG_HAS_SETTINGS;
+        DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
+        mgr.updateRootContextMenu(testMenu, testRootInfo);
+
+        settings.assertInvisible();
+    }
+
+    @Test
+    public void testRootContextMenu_canEject() {
+        testRootInfo.flags = Root.FLAG_SUPPORTS_EJECT;
+        DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
+        mgr.updateRootContextMenu(testMenu, testRootInfo);
+
+        eject.assertInvisible();
+    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesMenuManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesMenuManagerTest.java
index 00b9fd2..76ca2f3 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesMenuManagerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesMenuManagerTest.java
@@ -18,9 +18,11 @@
 
 import static org.junit.Assert.assertTrue;
 
+import android.provider.DocumentsContract.Root;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.documentsui.model.RootInfo;
 import com.android.documentsui.testing.TestDirectoryDetails;
 import com.android.documentsui.testing.TestMenu;
 import com.android.documentsui.testing.TestMenuItem;
@@ -52,9 +54,11 @@
     private TestMenuItem sort;
     private TestMenuItem sortSize;
     private TestMenuItem advanced;
+    private TestMenuItem eject;
     private TestSelectionDetails selectionDetails;
     private TestDirectoryDetails directoryDetails;
     private TestSearchViewManager testSearchManager;
+    private RootInfo testRootInfo;
     private State state = new State();
 
     @Before
@@ -75,6 +79,7 @@
         sort = testMenu.findItem(R.id.menu_sort);
         sortSize = testMenu.findItem(R.id.menu_sort_size);
         advanced = testMenu.findItem(R.id.menu_advanced);
+        eject = testMenu.findItem(R.id.menu_eject_root);
 
         // These items by default are visible
         testMenu.findItem(R.id.menu_select_all).setVisible(true);
@@ -84,6 +89,7 @@
         selectionDetails = new TestSelectionDetails();
         directoryDetails = new TestDirectoryDetails();
         testSearchManager = new TestSearchViewManager();
+        testRootInfo = new RootInfo();
     }
 
     @Test
@@ -237,4 +243,34 @@
         createDir.assertVisible();
         delete.assertVisible();
     }
+
+    @Test
+    public void testRootContextMenu() {
+        FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
+        mgr.updateRootContextMenu(testMenu, testRootInfo);
+
+        eject.assertVisible();
+        eject.assertDisabled();
+
+        settings.assertVisible();
+        settings.assertDisabled();
+    }
+
+    @Test
+    public void testRootContextMenu_hasRootSettings() {
+        testRootInfo.flags = Root.FLAG_HAS_SETTINGS;
+        FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
+        mgr.updateRootContextMenu(testMenu, testRootInfo);
+
+        settings.assertEnabled();
+    }
+
+    @Test
+    public void testRootContextMenu_canEject() {
+        testRootInfo.flags = Root.FLAG_SUPPORTS_EJECT;
+        FilesMenuManager mgr = new FilesMenuManager(testSearchManager, state);
+        mgr.updateRootContextMenu(testMenu, testRootInfo);
+
+        eject.assertEnabled();
+    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestMenu.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestMenu.java
index 7e35266..a8699b9 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestMenu.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestMenu.java
@@ -54,7 +54,8 @@
                 R.id.menu_list,
                 R.id.menu_sort,
                 R.id.menu_sort_size,
-                R.id.menu_advanced);
+                R.id.menu_advanced,
+                R.id.menu_eject_root);
     }
 
     public static TestMenu create(int... ids) {