More recents work; filtering and sorting.

Update DirectoryFragment to render List<Document>, making it more
general purpose.  Feed it documents either from a backend Cursor or
after resolving fields from a recents Cursor.  Start in recents when
no persisted stack available.  Synthesize a root for recents.

Local directory filtering and sorting using predicates and
comparators, all performed on background thread.  Introduce
UriDerivativeLoader which handles ContentObserver updates while
producing a derivative work of a Cursor.

Split data model classes into separate files.

Change-Id: Idb88b4ee22c58c8e508328e678877f7e4c978533
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 2740e53..f6f3f9c 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -21,13 +21,10 @@
 import android.app.FragmentTransaction;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.Context;
-import android.content.CursorLoader;
 import android.content.Loader;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.DocumentColumns;
 import android.text.format.DateUtils;
 import android.util.SparseBooleanArray;
 import android.view.ActionMode;
@@ -41,17 +38,20 @@
 import android.widget.AbsListView.MultiChoiceModeListener;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
-import android.widget.CursorAdapter;
+import android.widget.BaseAdapter;
 import android.widget.GridView;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 
 import com.android.documentsui.DocumentsActivity.DisplayState;
-import com.android.documentsui.DocumentsActivity.Document;
+import com.android.documentsui.model.Document;
+import com.android.internal.util.Predicate;
 import com.google.android.collect.Lists;
 
 import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
 
 /**
  * Display the documents inside a single directory.
@@ -59,23 +59,21 @@
 public class DirectoryFragment extends Fragment {
 
     // TODO: show storage backend in item views when requested
-    // TODO: apply sort order locally
-    // TODO: apply MIME filtering locally
 
     private ListView mListView;
     private GridView mGridView;
 
     private AbsListView mCurrentView;
 
-    private static final int TYPE_NORMAL = 1;
-    private static final int TYPE_SEARCH = 2;
-    private static final int TYPE_RECENT_OPEN = 3;
-    private static final int TYPE_RECENT_CREATE = 4;
+    public static final int TYPE_NORMAL = 1;
+    public static final int TYPE_SEARCH = 2;
+    public static final int TYPE_RECENT_OPEN = 3;
+    public static final int TYPE_RECENT_CREATE = 4;
 
     private int mType = TYPE_NORMAL;
 
     private DocumentsAdapter mAdapter;
-    private LoaderCallbacks<Cursor> mCallbacks;
+    private LoaderCallbacks<List<Document>> mCallbacks;
 
     private static final String EXTRA_URI = "uri";
 
@@ -93,6 +91,11 @@
         ft.commitAllowingStateLoss();
     }
 
+    public static DirectoryFragment get(FragmentManager fm) {
+        // TODO: deal with multiple directories shown at once
+        return (DirectoryFragment) fm.findFragmentById(R.id.directory);
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -114,7 +117,7 @@
         mGridView.setOnItemClickListener(mItemListener);
         mGridView.setMultiChoiceModeListener(mMultiListener);
 
-        mAdapter = new DocumentsAdapter(context);
+        mAdapter = new DocumentsAdapter();
         updateMode();
 
         final Uri uri = getArguments().getParcelable(EXTRA_URI);
@@ -129,18 +132,10 @@
             mType = TYPE_NORMAL;
         }
 
-        mCallbacks = new LoaderCallbacks<Cursor>() {
+        mCallbacks = new LoaderCallbacks<List<Document>>() {
             @Override
-            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            public Loader<List<Document>> 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;
                 if (mType == TYPE_NORMAL) {
@@ -149,17 +144,29 @@
                     contentsUri = uri;
                 }
 
-                return new CursorLoader(context, contentsUri, null, null, null, sortOrder);
+                final Predicate<Document> filter = new MimePredicate(state.acceptMimes);
+
+                final Comparator<Document> sortOrder;
+                if (state.sortOrder == DisplayState.SORT_ORDER_DATE || mType == TYPE_RECENT_OPEN
+                        || mType == TYPE_RECENT_CREATE) {
+                    sortOrder = new Document.DateComparator();
+                } else if (state.sortOrder == DisplayState.SORT_ORDER_NAME) {
+                    sortOrder = new Document.NameComparator();
+                } else {
+                    throw new IllegalArgumentException("Unknown sort order " + state.sortOrder);
+                }
+
+                return new DirectoryLoader(context, contentsUri, mType, filter, sortOrder);
             }
 
             @Override
-            public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-                mAdapter.swapCursor(data);
+            public void onLoadFinished(Loader<List<Document>> loader, List<Document> data) {
+                mAdapter.swapDocuments(data);
             }
 
             @Override
-            public void onLoaderReset(Loader<Cursor> loader) {
-                mAdapter.swapCursor(null);
+            public void onLoaderReset(Loader<List<Document>> loader) {
+                mAdapter.swapDocuments(null);
             }
         };
 
@@ -243,16 +250,16 @@
         }
     }
 
-    public void updateSortBy() {
+    public void updateSortOrder() {
         getLoaderManager().restartLoader(LOADER_DOCUMENTS, getArguments(), mCallbacks);
+        mListView.smoothScrollToPosition(0);
+        mGridView.smoothScrollToPosition(0);
     }
 
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @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, cursor);
+            final Document doc = mAdapter.getItem(position);
             ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
         }
     };
@@ -279,8 +286,8 @@
                 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, cursor));
+                        final Document doc = mAdapter.getItem(checked.keyAt(i));
+                        docs.add(doc);
                     }
                 }
 
@@ -300,11 +307,9 @@
         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)) {
+                final Document doc = mAdapter.getItem(position);
+                if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(doc.mimeType)) {
                     mCurrentView.setItemChecked(position, false);
                 }
             }
@@ -318,61 +323,68 @@
         return ((DocumentsActivity) fragment.getActivity()).getDisplayState();
     }
 
-    private class DocumentsAdapter extends CursorAdapter {
-        public DocumentsAdapter(Context context) {
-            super(context, null, false);
+    private class DocumentsAdapter extends BaseAdapter {
+        private List<Document> mDocuments;
+
+        public DocumentsAdapter() {
+        }
+
+        public void swapDocuments(List<Document> documents) {
+            mDocuments = documents;
+            notifyDataSetChanged();
         }
 
         @Override
-        public View newView(Context context, Cursor cursor, ViewGroup parent) {
-            final LayoutInflater inflater = LayoutInflater.from(context);
-            final DisplayState state = getDisplayState(DirectoryFragment.this);
-            if (state.mode == DisplayState.MODE_LIST) {
-                return inflater.inflate(R.layout.item_doc_list, parent, false);
-            } else if (state.mode == DisplayState.MODE_GRID) {
-                return inflater.inflate(R.layout.item_doc_grid, parent, false);
-            } else {
-                throw new IllegalStateException();
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final Context context = parent.getContext();
+
+            if (convertView == null) {
+                final LayoutInflater inflater = LayoutInflater.from(context);
+                final DisplayState state = getDisplayState(DirectoryFragment.this);
+                if (state.mode == DisplayState.MODE_LIST) {
+                    convertView = inflater.inflate(R.layout.item_doc_list, parent, false);
+                } else if (state.mode == DisplayState.MODE_GRID) {
+                    convertView = inflater.inflate(R.layout.item_doc_grid, parent, false);
+                } else {
+                    throw new IllegalStateException();
+                }
             }
-        }
 
-        @Override
-        public void bindView(View view, Context context, Cursor cursor) {
-            final TextView title = (TextView) view.findViewById(android.R.id.title);
-            final TextView summary = (TextView) view.findViewById(android.R.id.summary);
-            final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
+            final Document doc = getItem(position);
 
-            final String docId = getCursorString(cursor, DocumentColumns.DOC_ID);
-            final String displayName = getCursorString(cursor, DocumentColumns.DISPLAY_NAME);
-            final String mimeType = getCursorString(cursor, DocumentColumns.MIME_TYPE);
-            final long lastModified = getCursorLong(cursor, DocumentColumns.LAST_MODIFIED);
-            final int flags = getCursorInt(cursor, DocumentColumns.FLAGS);
+            final TextView title = (TextView) convertView.findViewById(android.R.id.title);
+            final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
+            final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
 
-            final Uri uri = getArguments().getParcelable(EXTRA_URI);
-            if ((flags & DocumentsContract.FLAG_SUPPORTS_THUMBNAIL) != 0) {
-                final Uri childUri = DocumentsContract.buildDocumentUri(uri, docId);
-                icon.setImageURI(childUri);
+            if (doc.isThumbnailSupported()) {
+                // TODO: load thumbnails async
+                icon.setImageURI(doc.uri);
             } else {
                 icon.setImageDrawable(DocumentsActivity.resolveDocumentIcon(
-                        context, uri.getAuthority(), mimeType));
+                        context, doc.uri.getAuthority(), doc.mimeType));
             }
 
-            title.setText(displayName);
+            title.setText(doc.displayName);
             if (summary != null) {
-                summary.setText(DateUtils.getRelativeTimeSpanString(lastModified));
+                summary.setText(DateUtils.getRelativeTimeSpanString(doc.lastModified));
             }
+
+            return convertView;
         }
-    }
 
-    public static String getCursorString(Cursor cursor, String columnName) {
-        return cursor.getString(cursor.getColumnIndex(columnName));
-    }
+        @Override
+        public int getCount() {
+            return mDocuments != null ? mDocuments.size() : 0;
+        }
 
-    public static long getCursorLong(Cursor cursor, String columnName) {
-        return cursor.getLong(cursor.getColumnIndex(columnName));
-    }
+        @Override
+        public Document getItem(int position) {
+            return mDocuments.get(position);
+        }
 
-    public static int getCursorInt(Cursor cursor, String columnName) {
-        return cursor.getInt(cursor.getColumnIndex(columnName));
+        @Override
+        public long getItemId(int position) {
+            return getItem(position).uri.hashCode();
+        }
     }
 }