Support sorting in storage UI.

Extract mode information into DisplayState which is now consistent
across directory traversal.  Use grid mode by default when working
with images.  Dialog to switch sort order.

Add testing UI to exercise until we have real clients.

Change-Id: Ic423584d4559732fb3d2aea9e0406b57d43f6e6d
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 69ee466..ae11a8c 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -12,11 +12,21 @@
             <intent-filter android:priority="100">
                 <action android:name="android.intent.action.OPEN_DOCUMENT" />
                 <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="*/*" />
             </intent-filter>
             <intent-filter android:priority="100">
                 <action android:name="android.intent.action.CREATE_DOCUMENT" />
-                <data android:mimeType="*/*"/>
                 <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="*/*" />
+            </intent-filter>
+        </activity>
+
+        <!-- TODO: remove when we have real clients -->
+        <activity android:name=".TestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
diff --git a/packages/DocumentsUI/res/menu/directory.xml b/packages/DocumentsUI/res/menu/directory.xml
index 52c8f5c..c1fa228 100644
--- a/packages/DocumentsUI/res/menu/directory.xml
+++ b/packages/DocumentsUI/res/menu/directory.xml
@@ -18,13 +18,16 @@
     <item
         android:id="@+id/menu_grid"
         android:title="@string/menu_grid"
-        android:icon="@drawable/ic_menu_grid" />
+        android:icon="@drawable/ic_menu_grid"
+        android:showAsAction="ifRoom" />
     <item
         android:id="@+id/menu_list"
         android:title="@string/menu_list"
-        android:icon="@drawable/ic_menu_list" />
+        android:icon="@drawable/ic_menu_list"
+        android:showAsAction="ifRoom" />
     <item
         android:id="@+id/menu_sort"
         android:title="@string/menu_sort"
-        android:icon="@drawable/ic_menu_sort" />
+        android:icon="@drawable/ic_menu_sort"
+        android:showAsAction="ifRoom" />
 </menu>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 141ba80..6ae2d12 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -30,4 +30,7 @@
 
     <string name="mode_selected_count"><xliff:g id="count" example="3">%1$d</xliff:g> selected</string>
 
+    <string name="sort_name">Name</string>
+    <string name="sort_date">Date modified</string>
+
 </resources>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index bae42d5..531eaf3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -16,12 +16,17 @@
 
 package com.android.documentsui;
 
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.Context;
 import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
 import android.content.Loader;
 import android.database.Cursor;
 import android.net.Uri;
@@ -47,6 +52,7 @@
 import android.widget.ListView;
 import android.widget.TextView;
 
+import com.android.documentsui.DocumentsActivity.DisplayState;
 import com.android.documentsui.DocumentsActivity.Document;
 import com.google.android.collect.Lists;
 
@@ -58,7 +64,8 @@
 public class DirectoryFragment extends Fragment {
 
     // TODO: show storage backend in item views when requested
-    // TODO: implement sorting dialog
+
+    private static final String TAG_SORT = "sort";
 
     private ListView mListView;
     private GridView mGridView;
@@ -71,20 +78,12 @@
     private int mFlags;
 
     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, boolean allowMultiple) {
+    public static void show(FragmentManager fm, Uri uri, String displayName) {
         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);
@@ -127,8 +126,18 @@
         mCallbacks = new LoaderCallbacks<Cursor>() {
             @Override
             public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+                final DisplayState state = getDisplayState(DirectoryFragment.this);
+                final String sortOrder;
+                if (state.sortBy == DisplayState.SORT_BY_NAME) {
+                    sortOrder = DocumentColumns.DISPLAY_NAME + " ASC";
+                } else if (state.sortBy == DisplayState.SORT_BY_DATE) {
+                    sortOrder = DocumentColumns.LAST_MODIFIED + " DESC";
+                } else {
+                    sortOrder = null;
+                }
+
                 final Uri contentsUri = DocumentsContract.buildContentsUri(uri);
-                return new CursorLoader(context, contentsUri, null, null, null, null);
+                return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
             }
 
             @Override
@@ -170,21 +179,27 @@
     @Override
     public void onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
-        final int mode = getMode();
-        menu.findItem(R.id.menu_grid).setVisible(mode != MODE_GRID);
-        menu.findItem(R.id.menu_list).setVisible(mode != MODE_LIST);
+        final DisplayState state = getDisplayState(this);
+        menu.findItem(R.id.menu_grid).setVisible(state.mode != DisplayState.MODE_GRID);
+        menu.findItem(R.id.menu_list).setVisible(state.mode != DisplayState.MODE_LIST);
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
+        final DisplayState state = getDisplayState(this);
         final int id = item.getItemId();
         if (id == R.id.menu_grid) {
-            getArguments().putInt(EXTRA_MODE, MODE_GRID);
+            state.mode = DisplayState.MODE_GRID;
             updateMode();
+            getFragmentManager().invalidateOptionsMenu();
             return true;
         } else if (id == R.id.menu_list) {
-            getArguments().putInt(EXTRA_MODE, MODE_LIST);
+            state.mode = DisplayState.MODE_LIST;
             updateMode();
+            getFragmentManager().invalidateOptionsMenu();
+            return true;
+        } else if (id == R.id.menu_sort) {
+            SortFragment.show(this);
             return true;
         } else {
             return super.onOptionsItemSelected(item);
@@ -192,19 +207,19 @@
     }
 
     private void updateMode() {
-        final int mode = getMode();
+        final DisplayState state = getDisplayState(this);
 
-        mListView.setVisibility(mode == MODE_LIST ? View.VISIBLE : View.GONE);
-        mGridView.setVisibility(mode == MODE_GRID ? View.VISIBLE : View.GONE);
+        mListView.setVisibility(state.mode == DisplayState.MODE_LIST ? View.VISIBLE : View.GONE);
+        mGridView.setVisibility(state.mode == DisplayState.MODE_GRID ? View.VISIBLE : View.GONE);
 
         final int choiceMode;
-        if (getArguments().getBoolean(EXTRA_ALLOW_MULTIPLE)) {
+        if (state.allowMultiple) {
             choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
         } else {
             choiceMode = ListView.CHOICE_MODE_NONE;
         }
 
-        if (mode == MODE_GRID) {
+        if (state.mode == DisplayState.MODE_GRID) {
             mListView.setAdapter(null);
             mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
             mGridView.setAdapter(mAdapter);
@@ -212,15 +227,21 @@
             mGridView.setNumColumns(GridView.AUTO_FIT);
             mGridView.setChoiceMode(choiceMode);
             mCurrentView = mGridView;
-        } else {
+        } else if (state.mode == DisplayState.MODE_LIST) {
             mGridView.setAdapter(null);
             mGridView.setChoiceMode(ListView.CHOICE_MODE_NONE);
             mListView.setAdapter(mAdapter);
             mListView.setChoiceMode(choiceMode);
             mCurrentView = mListView;
+        } else {
+            throw new IllegalStateException();
         }
     }
 
+    private void updateSortBy() {
+        getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
+    }
+
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
@@ -288,12 +309,8 @@
         }
     };
 
-    private boolean getSupportsCreate() {
-        return (mFlags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
-    }
-
-    private int getMode() {
-        return getArguments().getInt(EXTRA_MODE, MODE_LIST);
+    private static DisplayState getDisplayState(Fragment fragment) {
+        return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
     }
 
     private class DocumentsAdapter extends CursorAdapter {
@@ -304,10 +321,10 @@
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             final LayoutInflater inflater = LayoutInflater.from(context);
-            final int mode = getMode();
-            if (mode == MODE_LIST) {
+            final DisplayState state = getDisplayState(DirectoryFragment.this);
+            if (state.mode == DisplayState.MODE_LIST) {
                 return inflater.inflate(R.layout.item_doc_list, parent, false);
-            } else if (mode == MODE_GRID) {
+            } else if (state.mode == DisplayState.MODE_GRID) {
                 return inflater.inflate(R.layout.item_doc_grid, parent, false);
             } else {
                 throw new IllegalStateException();
@@ -341,6 +358,38 @@
         }
     }
 
+    public static class SortFragment extends DialogFragment {
+        public static void show(DirectoryFragment parent) {
+            if (!parent.isAdded()) return;
+
+            final SortFragment dialog = new SortFragment();
+            dialog.setTargetFragment(parent, 0);
+            dialog.show(parent.getFragmentManager(), TAG_SORT);
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            final Context context = getActivity();
+            final DisplayState state = getDisplayState(this);
+
+            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+            builder.setTitle(R.string.menu_sort);
+            builder.setSingleChoiceItems(new CharSequence[] {
+                    getText(R.string.sort_name),
+                    getText(R.string.sort_date),
+            }, state.sortBy, new OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    state.sortBy = which;
+                    ((DirectoryFragment) getTargetFragment()).updateSortBy();
+                    dismiss();
+                }
+            });
+
+            return builder.create();
+        }
+    }
+
     private static int getDocumentFlags(Context context, Uri uri) {
         final Cursor cursor = context.getContentResolver().query(uri, new String[] {
                 DocumentColumns.FLAGS }, null, null, null);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index ca4ea82..c45d2b4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -55,13 +55,14 @@
     // TODO: fragment to show recently opened documents
     // TODO: pull actionbar icon from current backend
 
-    private static final int MODE_OPEN = 1;
-    private static final int MODE_CREATE = 2;
+    private static final int ACTION_OPEN = 1;
+    private static final int ACTION_CREATE = 2;
 
-    private int mMode;
-    private boolean mAllowMultiple;
+    private int mAction;
     private String[] mAcceptMimes;
 
+    private final DisplayState mDisplayState = new DisplayState();
+
     private boolean mIgnoreNextNavigation;
 
     private Uri mCurrentDir;
@@ -74,11 +75,11 @@
         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);
+            mAction = ACTION_OPEN;
+            mDisplayState.allowMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
         } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
-            mMode = MODE_CREATE;
-            mAllowMultiple = false;
+            mAction = ACTION_CREATE;
+            mDisplayState.allowMultiple = false;
         }
 
         if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
@@ -87,6 +88,12 @@
             mAcceptMimes = new String[] { intent.getType() };
         }
 
+        if (mimeMatches("image/*", mAcceptMimes)) {
+            mDisplayState.mode = DisplayState.MODE_GRID;
+        } else {
+            mDisplayState.mode = DisplayState.MODE_LIST;
+        }
+
         setResult(Activity.RESULT_CANCELED);
         setContentView(R.layout.activity);
 
@@ -95,7 +102,7 @@
 
         updateActionBar();
 
-        if (mMode == MODE_CREATE) {
+        if (mAction == ACTION_CREATE) {
             final String mimeType = getIntent().getType();
             final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE);
             SaveFragment.show(getFragmentManager(), mimeType, title);
@@ -120,9 +127,9 @@
             actionBar.setDisplayShowHomeEnabled(false);
             actionBar.setDisplayHomeAsUpEnabled(false);
 
-            if (mMode == MODE_OPEN) {
+            if (mAction == ACTION_OPEN) {
                 actionBar.setTitle(R.string.title_open);
-            } else if (mMode == MODE_CREATE) {
+            } else if (mAction == ACTION_CREATE) {
                 actionBar.setTitle(R.string.title_save);
             }
         }
@@ -140,7 +147,7 @@
         super.onPrepareOptionsMenu(menu);
 
         final MenuItem createDir = menu.findItem(R.id.menu_create_dir);
-        createDir.setVisible(mMode == MODE_CREATE);
+        createDir.setVisible(mAction == ACTION_CREATE);
         createDir.setEnabled(mCurrentSupportsCreate);
 
         return true;
@@ -209,11 +216,15 @@
         }
     };
 
+    public DisplayState getDisplayState() {
+        return mDisplayState;
+    }
+
     public void onDirectoryChanged(Uri uri, int flags) {
         mCurrentDir = uri;
         mCurrentSupportsCreate = (flags & DocumentsContract.FLAG_SUPPORTS_CREATE) != 0;
 
-        if (mMode == MODE_CREATE) {
+        if (mAction == ACTION_CREATE) {
             final FragmentManager fm = getFragmentManager();
             SaveFragment.get(fm).setSaveEnabled(mCurrentSupportsCreate);
         }
@@ -225,18 +236,18 @@
         final Uri uri = DocumentsContract.buildDocumentUri(
                 info.authority, DocumentsContract.ROOT_GUID);
         final CharSequence displayName = info.loadLabel(getPackageManager());
-        DirectoryFragment.show(getFragmentManager(), uri, displayName.toString(), mAllowMultiple);
+        DirectoryFragment.show(getFragmentManager(), uri, displayName.toString());
     }
 
     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, mAllowMultiple);
-        } else if (mMode == MODE_OPEN) {
+            DirectoryFragment.show(fm, doc.uri, doc.displayName);
+        } else if (mAction == ACTION_OPEN) {
             // Explicit file picked, return
             onFinished(doc.uri);
-        } else if (mMode == MODE_CREATE) {
+        } else if (mAction == ACTION_CREATE) {
             // Overwrite current filename
             SaveFragment.get(fm).setDisplayName(doc.displayName);
         }
@@ -274,7 +285,7 @@
 
         intent.addFlags(
                 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_PERSIST_GRANT_URI_PERMISSION);
-        if (mMode == MODE_CREATE) {
+        if (mAction == ACTION_CREATE) {
             intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
         }
 
@@ -282,6 +293,18 @@
         finish();
     }
 
+    public static class DisplayState {
+        public int mode;
+        public int sortBy;
+        public boolean allowMultiple;
+
+        public static final int MODE_LIST = 0;
+        public static final int MODE_GRID = 1;
+
+        public static final int SORT_BY_NAME = 0;
+        public static final int SORT_BY_DATE = 1;
+    }
+
     public static class Document {
         public Uri uri;
         public String mimeType;
@@ -297,6 +320,27 @@
         }
     }
 
+    public static boolean mimeMatches(String filter, String[] tests) {
+        for (String test : tests) {
+            if (mimeMatches(filter, test)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean mimeMatches(String filter, String test) {
+        if (filter.equals(test)) {
+            return true;
+        } else if ("*/*".equals(filter)) {
+            return true;
+        } else if (filter.endsWith("/*")) {
+            return filter.regionMatches(0, test, 0, filter.indexOf('/'));
+        } else {
+            return false;
+        }
+    }
+
     public static Drawable resolveDocumentIcon(Context context, String mimeType) {
         // TODO: allow backends to provide custom MIME icons
         if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
new file mode 100644
index 0000000..b15d123
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/TestActivity.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+package com.android.documentsui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class TestActivity extends Activity {
+    private TextView mResult;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        final Context context = this;
+
+        final LinearLayout view = new LinearLayout(context);
+        view.setOrientation(LinearLayout.VERTICAL);
+
+        final CheckBox multiple = new CheckBox(context);
+        multiple.setText("ALLOW_MULTIPLE");
+        view.addView(multiple);
+
+        Button button;
+        button = new Button(context);
+        button.setText("OPEN_DOC */*");
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.setType("*/*");
+                if (multiple.isChecked()) {
+                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+                }
+                startActivityForResult(intent, 42);
+            }
+        });
+        view.addView(button);
+
+        button = new Button(context);
+        button.setText("OPEN_DOC image/*");
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.setType("image/*");
+                if (multiple.isChecked()) {
+                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+                }
+                startActivityForResult(intent, 42);
+            }
+        });
+        view.addView(button);
+
+        button = new Button(context);
+        button.setText("OPEN_DOC text/plain, application/msword");
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+                intent.setType("*/*");
+                intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
+                        "text/plain", "application/msword" });
+                if (multiple.isChecked()) {
+                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+                }
+                startActivityForResult(intent, 42);
+            }
+        });
+        view.addView(button);
+
+        button = new Button(context);
+        button.setText("CREATE_DOC text/plain");
+        button.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+                intent.setType("text/plain");
+                intent.putExtra(Intent.EXTRA_TITLE, "foobar.txt");
+                startActivityForResult(intent, 42);
+            }
+        });
+        view.addView(button);
+
+        mResult = new TextView(context);
+        view.addView(mResult);
+
+        setContentView(view);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        mResult.setText("resultCode=" + resultCode + ", data=" + String.valueOf(data));
+    }
+}