Use Cursors directly when binding documents.

Instead of creating a DocumentInfo for every list item, bind the
adapter against Cursor directly.

Create new SortingCursorWrapper which performs sorting at query time
and keeps a O(1) mapping from sorted to unsorted positions in the
underlying Cursor.

Suppress extra loader passes that had been kicked off.  Use unstable
provider when querying to guard against broken providers.

Bug: 10567506, 10510851
Change-Id: I535814da6b17c38de04a1175e0afcc78c6b966ce
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 79d20a4..79f846a 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -20,9 +20,9 @@
 import static com.android.documentsui.DocumentsActivity.DisplayState.ACTION_MANAGE;
 import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_GRID;
 import static com.android.documentsui.DocumentsActivity.DisplayState.MODE_LIST;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_DISPLAY_NAME;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_LAST_MODIFIED;
-import static com.android.documentsui.DocumentsActivity.DisplayState.SORT_ORDER_SIZE;
+import static com.android.documentsui.model.DocumentInfo.getCursorInt;
+import static com.android.documentsui.model.DocumentInfo.getCursorLong;
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
 
 import android.app.Fragment;
 import android.app.FragmentManager;
@@ -32,12 +32,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.Loader;
+import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
 import android.text.format.DateUtils;
 import android.text.format.Formatter;
 import android.text.format.Time;
@@ -66,7 +68,6 @@
 import com.google.android.collect.Lists;
 
 import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -85,7 +86,6 @@
 
     public static final int TYPE_NORMAL = 1;
     public static final int TYPE_SEARCH = 2;
-    public static final int TYPE_RECENT_OPEN = 3;
 
     private int mType = TYPE_NORMAL;
 
@@ -99,6 +99,8 @@
 
     private static AtomicInteger sLoaderId = new AtomicInteger(4000);
 
+    private int mLastSortOrder = -1;
+
     private final int mLoaderId = sLoaderId.incrementAndGet();
 
     public static void showNormal(FragmentManager fm, Uri uri) {
@@ -111,8 +113,9 @@
         show(fm, TYPE_SEARCH, searchUri);
     }
 
+    @Deprecated
     public static void showRecentsOpen(FragmentManager fm) {
-        show(fm, TYPE_RECENT_OPEN, null);
+        // TODO: new recents behavior
     }
 
     private static void show(FragmentManager fm, int type, Uri uri) {
@@ -137,7 +140,6 @@
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         final Context context = inflater.getContext();
-
         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
 
         mEmptyView = view.findViewById(android.R.id.empty);
@@ -150,80 +152,65 @@
         mGridView.setOnItemClickListener(mItemListener);
         mGridView.setMultiChoiceModeListener(mMultiListener);
 
-        mAdapter = new DocumentsAdapter();
+        return view;
+    }
 
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        final Context context = getActivity();
         final Uri uri = getArguments().getParcelable(EXTRA_URI);
+
+        mAdapter = new DocumentsAdapter(uri.getAuthority());
         mType = getArguments().getInt(EXTRA_TYPE);
 
         mCallbacks = new LoaderCallbacks<DirectoryResult>() {
             @Override
             public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
                 final DisplayState state = getDisplayState(DirectoryFragment.this);
-                mFilter = new MimePredicate(state.acceptMimes);
 
                 Uri contentsUri;
                 if (mType == TYPE_NORMAL) {
                     contentsUri = DocumentsContract.buildChildDocumentsUri(
                             uri.getAuthority(), DocumentsContract.getDocumentId(uri));
-                } else if (mType == TYPE_RECENT_OPEN) {
-                    contentsUri = RecentsProvider.buildRecentOpen();
                 } else {
                     contentsUri = uri;
                 }
 
-                final Comparator<DocumentInfo> sortOrder;
-                if (state.sortOrder == SORT_ORDER_LAST_MODIFIED || mType == TYPE_RECENT_OPEN) {
-                    sortOrder = new DocumentInfo.LastModifiedComparator();
-                } else if (state.sortOrder == SORT_ORDER_DISPLAY_NAME) {
-                    sortOrder = new DocumentInfo.DisplayNameComparator();
-                } else if (state.sortOrder == SORT_ORDER_SIZE) {
-                    sortOrder = new DocumentInfo.SizeComparator();
-                } else {
-                    throw new IllegalArgumentException("Unknown sort order " + state.sortOrder);
-                }
-
-                return new DirectoryLoader(context, contentsUri, mType, null, sortOrder);
+                return new DirectoryLoader(context, contentsUri, state.sortOrder);
             }
 
             @Override
             public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
-                mAdapter.swapDocuments(result.contents);
+                mAdapter.swapCursor(result.cursor);
             }
 
             @Override
             public void onLoaderReset(Loader<DirectoryResult> loader) {
-                mAdapter.swapDocuments(null);
+                mAdapter.swapCursor(null);
             }
         };
 
         updateDisplayState();
-
-        return view;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks);
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        getLoaderManager().destroyLoader(mLoaderId);
     }
 
     public void updateDisplayState() {
         final DisplayState state = getDisplayState(this);
 
-        // TODO: avoid kicking loader when nothing changed
-        getLoaderManager().restartLoader(mLoaderId, getArguments(), mCallbacks);
+        if (mLastSortOrder != state.sortOrder) {
+            getLoaderManager().restartLoader(mLoaderId, null, mCallbacks);
+            mLastSortOrder = state.sortOrder;
+        }
+
         mListView.smoothScrollToPosition(0);
         mGridView.smoothScrollToPosition(0);
 
         mListView.setVisibility(state.mode == MODE_LIST ? View.VISIBLE : View.GONE);
         mGridView.setVisibility(state.mode == MODE_GRID ? View.VISIBLE : View.GONE);
 
+        mFilter = new MimePredicate(state.acceptMimes);
+
         final int choiceMode;
         if (state.allowMultiple) {
             choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL;
@@ -258,7 +245,9 @@
     private OnItemClickListener mItemListener = new OnItemClickListener() {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            final DocumentInfo doc = mAdapter.getItem(position);
+            final Cursor cursor = mAdapter.getItem(position);
+            final Uri uri = getArguments().getParcelable(EXTRA_URI);
+            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
             if (mFilter.apply(doc)) {
                 ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
             }
@@ -295,7 +284,9 @@
             final int size = checked.size();
             for (int i = 0; i < size; i++) {
                 if (checked.valueAt(i)) {
-                    final DocumentInfo doc = mAdapter.getItem(checked.keyAt(i));
+                    final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
+                    final Uri uri = getArguments().getParcelable(EXTRA_URI);
+                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
                     docs.add(doc);
                 }
             }
@@ -328,8 +319,9 @@
                 ActionMode mode, int position, long id, boolean checked) {
             if (checked) {
                 // Directories cannot be checked
-                final DocumentInfo doc = mAdapter.getItem(position);
-                if (doc.isDirectory()) {
+                final Cursor cursor = mAdapter.getItem(position);
+                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+                if (Document.MIME_TYPE_DIR.equals(docMimeType)) {
                     mCurrentView.setItemChecked(position, false);
                 }
             }
@@ -396,15 +388,18 @@
     }
 
     private class DocumentsAdapter extends BaseAdapter {
-        private List<DocumentInfo> mDocuments;
+        private final String mAuthority;
 
-        public DocumentsAdapter() {
+        private Cursor mCursor;
+
+        public DocumentsAdapter(String authority) {
+            mAuthority = authority;
         }
 
-        public void swapDocuments(List<DocumentInfo> documents) {
-            mDocuments = documents;
+        public void swapCursor(Cursor cursor) {
+            mCursor = cursor;
 
-            if (mDocuments != null && mDocuments.isEmpty()) {
+            if (isEmpty()) {
                 mEmptyView.setVisibility(View.VISIBLE);
             } else {
                 mEmptyView.setVisibility(View.GONE);
@@ -433,7 +428,16 @@
                 }
             }
 
-            final DocumentInfo doc = getItem(position);
+            final Cursor cursor = getItem(position);
+
+            final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
+            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+            final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+            final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
+            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+            final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
+            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
 
             final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
             final TextView title = (TextView) convertView.findViewById(android.R.id.title);
@@ -448,32 +452,31 @@
                 oldTask.cancel(false);
             }
 
-            if (doc.isThumbnailSupported()) {
-                final Bitmap cachedResult = thumbs.get(doc.uri);
+            if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) {
+                final Uri uri = DocumentsContract.buildDocumentUri(mAuthority, docId);
+                final Bitmap cachedResult = thumbs.get(uri);
                 if (cachedResult != null) {
                     icon.setImageBitmap(cachedResult);
                 } else {
                     final ThumbnailAsyncTask task = new ThumbnailAsyncTask(icon, mThumbSize);
                     icon.setImageBitmap(null);
                     icon.setTag(task);
-                    task.execute(doc.uri);
+                    task.execute(uri);
                 }
+            } else if (docIcon != 0) {
+                icon.setImageDrawable(DocumentInfo.loadIcon(context, mAuthority, docIcon));
             } else {
-                icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, doc.mimeType));
+                icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType));
             }
 
-            title.setText(doc.displayName);
+            title.setText(docDisplayName);
 
-            if (mType == TYPE_NORMAL || mType == TYPE_SEARCH) {
-                icon1.setVisibility(View.GONE);
-                if (doc.summary != null) {
-                    summary.setText(doc.summary);
-                    summary.setVisibility(View.VISIBLE);
-                } else {
-                    summary.setVisibility(View.INVISIBLE);
-                }
-            } else if (mType == TYPE_RECENT_OPEN) {
-                // TODO: resolve storage root
+            icon1.setVisibility(View.GONE);
+            if (docSummary != null) {
+                summary.setText(docSummary);
+                summary.setVisibility(View.VISIBLE);
+            } else {
+                summary.setVisibility(View.INVISIBLE);
             }
 
             if (summaryGrid != null) {
@@ -481,18 +484,18 @@
                         (summary.getVisibility() == View.VISIBLE) ? View.VISIBLE : View.GONE);
             }
 
-            if (doc.lastModified == -1) {
+            if (docLastModified == -1) {
                 date.setText(null);
             } else {
-                date.setText(formatTime(context, doc.lastModified));
+                date.setText(formatTime(context, docLastModified));
             }
 
             if (state.showSize) {
                 size.setVisibility(View.VISIBLE);
-                if (doc.isDirectory() || doc.size == -1) {
+                if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
                     size.setText(null);
                 } else {
-                    size.setText(Formatter.formatFileSize(context, doc.size));
+                    size.setText(Formatter.formatFileSize(context, docSize));
                 }
             } else {
                 size.setVisibility(View.GONE);
@@ -503,17 +506,20 @@
 
         @Override
         public int getCount() {
-            return mDocuments != null ? mDocuments.size() : 0;
+            return mCursor != null ? mCursor.getCount() : 0;
         }
 
         @Override
-        public DocumentInfo getItem(int position) {
-            return mDocuments.get(position);
+        public Cursor getItem(int position) {
+            if (mCursor != null) {
+                mCursor.moveToPosition(position);
+            }
+            return mCursor;
         }
 
         @Override
         public long getItemId(int position) {
-            return getItem(position).uri.hashCode();
+            return position;
         }
     }