Support multi-select in storage UI.
When caller has specified that multiple documents are okay, enable
multi-select action mode. Currently only allows document selection,
not directories. Returns multiple documents through ClipData.
Fix bug where GridView was stuck with 2 columns on tablets.
Change-Id: Id49b29a86330639b56fa116d37e7f0d874980c5b
diff --git a/packages/DocumentsUI/res/drawable/item_background.xml b/packages/DocumentsUI/res/drawable/item_background.xml
new file mode 100644
index 0000000..4fb32fc
--- /dev/null
+++ b/packages/DocumentsUI/res/drawable/item_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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_activated="true" android:state_pressed="true" android:drawable="@*android:drawable/list_activated_holo" />
+ <item android:state_activated="true" android:drawable="@*android:drawable/list_activated_holo" />
+ <item android:state_focused="true" android:state_enabled="false" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
+ <item android:state_focused="true" android:state_enabled="false" android:drawable="@*android:drawable/list_selector_disabled_holo_light" />
+ <item android:state_focused="true" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_background_transition_holo_light" />
+ <item android:state_focused="false" android:state_pressed="true" android:drawable="@*android:drawable/list_selector_background_transition_holo_light" />
+ <item android:state_focused="true" android:drawable="@*android:drawable/list_focused_holo" />
+ <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/packages/DocumentsUI/res/layout/fragment_directory.xml b/packages/DocumentsUI/res/layout/fragment_directory.xml
index 8085fa8..638ac92 100644
--- a/packages/DocumentsUI/res/layout/fragment_directory.xml
+++ b/packages/DocumentsUI/res/layout/fragment_directory.xml
@@ -21,7 +21,8 @@
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent"
+ android:listSelector="@android:color/transparent" />
<GridView
android:id="@+id/grid"
diff --git a/packages/DocumentsUI/res/layout/fragment_save.xml b/packages/DocumentsUI/res/layout/fragment_save.xml
index 5a4ce94..85c48b1 100644
--- a/packages/DocumentsUI/res/layout/fragment_save.xml
+++ b/packages/DocumentsUI/res/layout/fragment_save.xml
@@ -43,7 +43,7 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
- android:text="@string/btn_save"
+ android:text="@string/menu_save"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:padding="8dp" />
diff --git a/packages/DocumentsUI/res/layout/item_doc_grid.xml b/packages/DocumentsUI/res/layout/item_doc_grid.xml
index 93666ab..caa9db6 100644
--- a/packages/DocumentsUI/res/layout/item_doc_grid.xml
+++ b/packages/DocumentsUI/res/layout/item_doc_grid.xml
@@ -24,7 +24,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/chip"
- android:foreground="?android:attr/selectableItemBackground"
+ android:foreground="@drawable/item_background"
android:duplicateParentState="true">
<GridLayout
diff --git a/packages/DocumentsUI/res/layout/item_doc_list.xml b/packages/DocumentsUI/res/layout/item_doc_list.xml
index 85fca79..39e55be 100644
--- a/packages/DocumentsUI/res/layout/item_doc_list.xml
+++ b/packages/DocumentsUI/res/layout/item_doc_list.xml
@@ -17,6 +17,7 @@
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:background="@drawable/item_background"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
diff --git a/packages/DocumentsUI/res/menu/activity.xml b/packages/DocumentsUI/res/menu/activity.xml
index c675fb8..bf7c161 100644
--- a/packages/DocumentsUI/res/menu/activity.xml
+++ b/packages/DocumentsUI/res/menu/activity.xml
@@ -18,5 +18,6 @@
<item
android:id="@+id/menu_create_dir"
android:title="@string/menu_create_dir"
- android:icon="@drawable/ic_menu_create_dir" />
+ android:icon="@drawable/ic_menu_create_dir"
+ android:showAsAction="always" />
</menu>
diff --git a/packages/DocumentsUI/res/menu/mode_directory.xml b/packages/DocumentsUI/res/menu/mode_directory.xml
new file mode 100644
index 0000000..6b6d7e91
--- /dev/null
+++ b/packages/DocumentsUI/res/menu/mode_directory.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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_open"
+ android:title="@string/menu_open"
+ android:showAsAction="always" />
+</menu>
diff --git a/packages/DocumentsUI/res/values/dimens.xml b/packages/DocumentsUI/res/values/dimens.xml
new file mode 100644
index 0000000..e5c4138
--- /dev/null
+++ b/packages/DocumentsUI/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<resources>
+ <dimen name="grid_width">180dp</dimen>
+</resources>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 65f8da8..141ba80 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_label">Documents</string>
<string name="title_open">Open</string>
@@ -25,6 +25,9 @@
<string name="menu_list">List view</string>
<string name="menu_sort">Sort by</string>
- <string name="btn_save">Save</string>
+ <string name="menu_open">Open</string>
+ <string name="menu_save">Save</string>
+
+ <string name="mode_selected_count"><xliff:g id="count" example="3">%1$d</xliff:g> selected</string>
</resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 5ba1930..bae42d5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -29,12 +29,16 @@
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.DocumentColumns;
import android.text.format.DateUtils;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.CursorAdapter;
@@ -44,6 +48,9 @@
import android.widget.TextView;
import com.android.documentsui.DocumentsActivity.Document;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
/**
* Display the documents inside a single directory.
@@ -52,11 +59,12 @@
// TODO: show storage backend in item views when requested
// TODO: implement sorting dialog
- // TODO: support multiple selection with actionmode
private ListView mListView;
private GridView mGridView;
+ private AbsListView mCurrentView;
+
private DocumentsAdapter mAdapter;
private LoaderCallbacks<Cursor> mCallbacks;
@@ -64,16 +72,19 @@
private static final String EXTRA_URI = "uri";
private static final String EXTRA_MODE = "display_mode";
+ private static final String EXTRA_ALLOW_MULTIPLE = "allow_multiple";
private static final int MODE_LIST = 1;
private static final int MODE_GRID = 2;
private static final int LOADER_DOCUMENTS = 2;
- public static void show(FragmentManager fm, Uri uri, String displayName) {
+ public static void show(
+ FragmentManager fm, Uri uri, String displayName, boolean allowMultiple) {
final Bundle args = new Bundle();
args.putParcelable(EXTRA_URI, uri);
args.putInt(EXTRA_MODE, MODE_LIST);
+ args.putBoolean(EXTRA_ALLOW_MULTIPLE, allowMultiple);
final DirectoryFragment fragment = new DirectoryFragment();
fragment.setArguments(args);
@@ -100,9 +111,11 @@
mListView = (ListView) view.findViewById(R.id.list);
mListView.setOnItemClickListener(mItemListener);
+ mListView.setMultiChoiceModeListener(mMultiListener);
mGridView = (GridView) view.findViewById(R.id.grid);
mGridView.setOnItemClickListener(mItemListener);
+ mGridView.setMultiChoiceModeListener(mMultiListener);
mAdapter = new DocumentsAdapter(context);
updateMode();
@@ -184,13 +197,27 @@
mListView.setVisibility(mode == MODE_LIST ? View.VISIBLE : View.GONE);
mGridView.setVisibility(mode == MODE_GRID ? View.VISIBLE : View.GONE);
+ final int choiceMode;
+ if (getArguments().getBoolean(EXTRA_ALLOW_MULTIPLE)) {
+ choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
+ } else {
+ choiceMode = ListView.CHOICE_MODE_NONE;
+ }
+
if (mode == MODE_GRID) {
mListView.setAdapter(null);
+ mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
mGridView.setAdapter(mAdapter);
+ mGridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.grid_width));
mGridView.setNumColumns(GridView.AUTO_FIT);
+ mGridView.setChoiceMode(choiceMode);
+ mCurrentView = mGridView;
} else {
mGridView.setAdapter(null);
+ mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
mListView.setAdapter(mAdapter);
+ mListView.setChoiceMode(choiceMode);
+ mCurrentView = mListView;
}
}
@@ -198,13 +225,69 @@
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Cursor cursor = (Cursor) mAdapter.getItem(position);
-
final Uri uri = getArguments().getParcelable(EXTRA_URI);
final Document doc = Document.fromCursor(uri.getAuthority(), cursor);
((DocumentsActivity) getActivity()).onDocumentPicked(doc);
}
};
+ private MultiChoiceModeListener mMultiListener = new MultiChoiceModeListener() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (item.getItemId() == R.id.menu_open) {
+ final Uri uri = getArguments().getParcelable(EXTRA_URI);
+ final SparseBooleanArray checked = mCurrentView.getCheckedItemPositions();
+ final ArrayList<Document> docs = Lists.newArrayList();
+
+ final int size = checked.size();
+ for (int i = 0; i < size; i++) {
+ if (checked.valueAt(i)) {
+ final Cursor cursor = (Cursor) mAdapter.getItem(checked.keyAt(i));
+ docs.add(Document.fromCursor(uri.getAuthority(), cursor));
+ }
+ }
+
+ ((DocumentsActivity) getActivity()).onDocumentsPicked(docs);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ // ignored
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(
+ ActionMode mode, int position, long id, boolean checked) {
+ if (checked) {
+ final Cursor cursor = (Cursor) mAdapter.getItem(position);
+ final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
+
+ // Directories cannot be checked
+ if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
+ mCurrentView.setItemChecked(position, false);
+ }
+ }
+
+ mode.setTitle(getResources()
+ .getString(R.string.mode_selected_count, mCurrentView.getCheckedItemCount()));
+ }
+ };
+
private boolean getSupportsCreate() {
return (mFlags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index c83edc4..ca4ea82 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -24,6 +24,8 @@
import android.app.FragmentManager;
import android.app.FragmentManager.BackStackEntry;
import android.app.FragmentManager.OnBackStackChangedListener;
+import android.content.ClipData;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -44,6 +46,9 @@
import android.widget.BaseAdapter;
import android.widget.TextView;
+import java.util.Arrays;
+import java.util.List;
+
public class DocumentsActivity extends Activity {
private static final String TAG = "Documents";
@@ -54,6 +59,9 @@
private static final int MODE_CREATE = 2;
private int mMode;
+ private boolean mAllowMultiple;
+ private String[] mAcceptMimes;
+
private boolean mIgnoreNextNavigation;
private Uri mCurrentDir;
@@ -63,11 +71,20 @@
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
- final String action = getIntent().getAction();
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) {
mMode = MODE_OPEN;
+ mAllowMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
} else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
mMode = MODE_CREATE;
+ mAllowMultiple = false;
+ }
+
+ if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
+ mAcceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES);
+ } else {
+ mAcceptMimes = new String[] { intent.getType() };
}
setResult(Activity.RESULT_CANCELED);
@@ -208,14 +225,14 @@
final Uri uri = DocumentsContract.buildDocumentUri(
info.authority, DocumentsContract.ROOT_GUID);
final CharSequence displayName = info.loadLabel(getPackageManager());
- DirectoryFragment.show(getFragmentManager(), uri, displayName.toString());
+ DirectoryFragment.show(getFragmentManager(), uri, displayName.toString(), mAllowMultiple);
}
public void onDocumentPicked(Document doc) {
final FragmentManager fm = getFragmentManager();
if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
// Nested directory picked, recurse using new fragment
- DirectoryFragment.show(fm, doc.uri, doc.displayName);
+ DirectoryFragment.show(fm, doc.uri, doc.displayName, mAllowMultiple);
} else if (mMode == MODE_OPEN) {
// Explicit file picked, return
onFinished(doc.uri);
@@ -225,16 +242,35 @@
}
}
- public void onSaveRequested(String mimeType, String displayName) {
- // TODO: create file, confirming before overwriting
- onFinished(null);
+ public void onDocumentsPicked(List<Document> docs) {
+ final int size = docs.size();
+ final Uri[] uris = new Uri[size];
+ for (int i = 0; i < size; i++) {
+ uris[i] = docs.get(i).uri;
+ }
+ onFinished(uris);
}
- private void onFinished(Uri uri) {
- Log.d(TAG, "onFinished() " + uri);
+ public void onSaveRequested(String mimeType, String displayName) {
+ // TODO: create file, confirming before overwriting
+ final Uri uri = null;
+ onFinished(uri);
+ }
+
+ private void onFinished(Uri... uris) {
+ Log.d(TAG, "onFinished() " + Arrays.toString(uris));
final Intent intent = new Intent();
- intent.setData(uri);
+ if (uris.length == 1) {
+ intent.setData(uris[0]);
+ } else if (uris.length > 1) {
+ final ContentResolver resolver = getContentResolver();
+ final ClipData clipData = new ClipData(null, mAcceptMimes, new ClipData.Item(uris[0]));
+ for (int i = 1; i < uris.length; i++) {
+ clipData.addItem(new ClipData.Item(uris[i]));
+ }
+ intent.setClipData(clipData);
+ }
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);