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