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