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/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 79d20a4..79f846a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/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;
         }
     }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index cb92d76..b2be11b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -16,98 +16,147 @@
 
 package com.android.documentsui;
 
-import static com.android.documentsui.DirectoryFragment.TYPE_NORMAL;
-import static com.android.documentsui.DirectoryFragment.TYPE_RECENT_OPEN;
-import static com.android.documentsui.DirectoryFragment.TYPE_SEARCH;
-import static com.android.documentsui.DocumentsActivity.TAG;
-
-import android.content.ContentResolver;
+import android.content.AsyncTaskLoader;
+import android.content.ContentProviderClient;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.CancellationSignal;
-import android.util.Log;
+import android.os.OperationCanceledException;
+import android.provider.DocumentsContract.Document;
 
-import com.android.documentsui.model.DocumentInfo;
-import com.android.internal.util.Predicate;
-import com.google.android.collect.Lists;
+import com.android.documentsui.DocumentsActivity.DisplayState;
 
 import libcore.io.IoUtils;
 
-import java.io.FileNotFoundException;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
 class DirectoryResult implements AutoCloseable {
+    ContentProviderClient client;
     Cursor cursor;
-    List<DocumentInfo> contents = Lists.newArrayList();
-    Exception e;
+    Exception exception;
 
     @Override
-    public void close() throws Exception {
+    public void close() {
         IoUtils.closeQuietly(cursor);
+        ContentProviderClient.closeQuietly(client);
+        cursor = null;
+        client = null;
     }
 }
 
-public class DirectoryLoader extends UriDerivativeLoader<Uri, DirectoryResult> {
+public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
+    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
 
-    private final int mType;
-    private Predicate<DocumentInfo> mFilter;
-    private Comparator<DocumentInfo> mSortOrder;
+    private final Uri mUri;
+    private final int mSortOrder;
 
-    public DirectoryLoader(Context context, Uri uri, int type, Predicate<DocumentInfo> filter,
-            Comparator<DocumentInfo> sortOrder) {
-        super(context, uri);
-        mType = type;
-        mFilter = filter;
+    private CancellationSignal mSignal;
+    private DirectoryResult mResult;
+
+    public DirectoryLoader(Context context, Uri uri, int sortOrder) {
+        super(context);
+        mUri = uri;
         mSortOrder = sortOrder;
     }
 
     @Override
-    public DirectoryResult loadInBackground(Uri uri, CancellationSignal signal) {
+    public final DirectoryResult loadInBackground() {
+        synchronized (this) {
+            if (isLoadInBackgroundCanceled()) {
+                throw new OperationCanceledException();
+            }
+            mSignal = new CancellationSignal();
+        }
         final DirectoryResult result = new DirectoryResult();
         try {
-            loadInBackgroundInternal(result, uri, signal);
+            result.client = getContext()
+                    .getContentResolver().acquireUnstableContentProviderClient(mUri.getAuthority());
+            final Cursor cursor = result.client.query(
+                    mUri, null, null, null, getQuerySortOrder(), mSignal);
+            result.cursor = new SortingCursorWrapper(cursor, mSortOrder);
+            result.cursor.registerContentObserver(mObserver);
         } catch (Exception e) {
-            result.e = e;
+            result.exception = e;
+            ContentProviderClient.closeQuietly(result.client);
+        } finally {
+            synchronized (this) {
+                mSignal = null;
+            }
         }
         return result;
     }
 
-    private void loadInBackgroundInternal(
-            DirectoryResult result, Uri uri, CancellationSignal signal) throws RuntimeException {
-        // TODO: switch to using unstable CPC
-        final ContentResolver resolver = getContext().getContentResolver();
-        final Cursor cursor = resolver.query(uri, null, null, null, null, signal);
-        result.cursor = cursor;
-        result.cursor.registerContentObserver(mObserver);
+    @Override
+    public void cancelLoadInBackground() {
+        super.cancelLoadInBackground();
 
-        while (cursor.moveToNext()) {
-            DocumentInfo doc = null;
-            switch (mType) {
-                case TYPE_NORMAL:
-                case TYPE_SEARCH:
-                    doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
-                    break;
-                case TYPE_RECENT_OPEN:
-                    try {
-                        doc = DocumentInfo.fromRecentOpenCursor(resolver, cursor);
-                    } catch (FileNotFoundException e) {
-                        Log.w(TAG, "Failed to find recent: " + e);
-                    }
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unknown type");
-            }
-
-            if (doc != null && (mFilter == null || mFilter.apply(doc))) {
-                result.contents.add(doc);
+        synchronized (this) {
+            if (mSignal != null) {
+                mSignal.cancel();
             }
         }
+    }
 
-        if (mSortOrder != null) {
-            Collections.sort(result.contents, mSortOrder);
+    @Override
+    public void deliverResult(DirectoryResult result) {
+        if (isReset()) {
+            IoUtils.closeQuietly(result);
+            return;
+        }
+        DirectoryResult oldResult = mResult;
+        mResult = result;
+
+        if (isStarted()) {
+            super.deliverResult(result);
+        }
+
+        if (oldResult != null && oldResult != result) {
+            IoUtils.closeQuietly(oldResult);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mResult != null) {
+            deliverResult(mResult);
+        }
+        if (takeContentChanged() || mResult == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    public void onCanceled(DirectoryResult result) {
+        IoUtils.closeQuietly(result);
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        // Ensure the loader is stopped
+        onStopLoading();
+
+        IoUtils.closeQuietly(mResult);
+        mResult = null;
+
+        getContext().getContentResolver().unregisterContentObserver(mObserver);
+    }
+
+    private String getQuerySortOrder() {
+        switch (mSortOrder) {
+            case DisplayState.SORT_ORDER_DISPLAY_NAME:
+                return Document.COLUMN_DISPLAY_NAME + " ASC";
+            case DisplayState.SORT_ORDER_LAST_MODIFIED:
+                return Document.COLUMN_LAST_MODIFIED + " DESC";
+            case DisplayState.SORT_ORDER_SIZE:
+                return Document.COLUMN_SIZE + " DESC";
+            default:
+                return null;
         }
     }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
index dbcb039..0c87783 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsProvider.java
@@ -61,6 +61,7 @@
     public static final String COL_PACKAGE_NAME = "package_name";
     public static final String COL_TIMESTAMP = "timestamp";
 
+    @Deprecated
     public static Uri buildRecentOpen() {
         return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(AUTHORITY).appendPath("recent_open").build();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
new file mode 100644
index 0000000..2d73732
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/SortingCursorWrapper.java
@@ -0,0 +1,256 @@
+/*
+ * 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.database.AbstractCursor;
+import android.database.Cursor;
+import android.provider.DocumentsContract.Document;
+
+import com.android.documentsui.DocumentsActivity.DisplayState;
+
+/**
+ * Cursor wrapper that presents a sorted view of the underlying cursor. Handles
+ * common {@link Document} sorting modes, such as ordering directories first.
+ */
+public class SortingCursorWrapper extends AbstractCursor {
+    private final Cursor mCursor;
+
+    private final int[] mPosition;
+    private final String[] mValueString;
+    private final long[] mValueLong;
+
+    public SortingCursorWrapper(Cursor cursor, int sortOrder) {
+        mCursor = cursor;
+
+        final int count = cursor.getCount();
+        mPosition = new int[count];
+        switch (sortOrder) {
+            case DisplayState.SORT_ORDER_DISPLAY_NAME:
+                mValueString = new String[count];
+                mValueLong = null;
+                break;
+            case DisplayState.SORT_ORDER_LAST_MODIFIED:
+            case DisplayState.SORT_ORDER_SIZE:
+                mValueString = null;
+                mValueLong = new long[count];
+                break;
+            default:
+                throw new IllegalArgumentException();
+        }
+
+        final int mimeTypeIndex = cursor.getColumnIndex(Document.COLUMN_MIME_TYPE);
+        final int displayNameIndex = cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME);
+        final int lastModifiedIndex = cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED);
+        final int sizeIndex = cursor.getColumnIndex(Document.COLUMN_SIZE);
+
+        cursor.moveToPosition(-1);
+        for (int i = 0; i < count; i++) {
+            cursor.moveToNext();
+            mPosition[i] = i;
+
+            switch (sortOrder) {
+                case DisplayState.SORT_ORDER_DISPLAY_NAME:
+                    final String mimeType = cursor.getString(mimeTypeIndex);
+                    final String displayName = cursor.getString(displayNameIndex);
+                    if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+                        mValueString[i] = '\001' + displayName;
+                    } else {
+                        mValueString[i] = displayName;
+                    }
+                    break;
+                case DisplayState.SORT_ORDER_LAST_MODIFIED:
+                    mValueLong[i] = cursor.getLong(lastModifiedIndex);
+                    break;
+                case DisplayState.SORT_ORDER_SIZE:
+                    mValueLong[i] = cursor.getLong(sizeIndex);
+                    break;
+            }
+        }
+
+        switch (sortOrder) {
+            case DisplayState.SORT_ORDER_DISPLAY_NAME:
+                synchronized (SortingCursorWrapper.class) {
+
+                    binarySort(mPosition, mValueString);
+                }
+                break;
+            case DisplayState.SORT_ORDER_LAST_MODIFIED:
+            case DisplayState.SORT_ORDER_SIZE:
+                binarySort(mPosition, mValueLong);
+                break;
+        }
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        mCursor.close();
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        return mCursor.moveToPosition(mPosition[newPosition]);
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mCursor.getColumnNames();
+    }
+
+    @Override
+    public int getCount() {
+        return mCursor.getCount();
+    }
+
+    @Override
+    public double getDouble(int column) {
+        return mCursor.getDouble(column);
+    }
+
+    @Override
+    public float getFloat(int column) {
+        return mCursor.getFloat(column);
+    }
+
+    @Override
+    public int getInt(int column) {
+        return mCursor.getInt(column);
+    }
+
+    @Override
+    public long getLong(int column) {
+        return mCursor.getLong(column);
+    }
+
+    @Override
+    public short getShort(int column) {
+        return mCursor.getShort(column);
+    }
+
+    @Override
+    public String getString(int column) {
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public int getType(int column) {
+        return mCursor.getType(column);
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return mCursor.isNull(column);
+    }
+
+    /**
+     * Borrowed from TimSort.binarySort(), but modified to sort two column
+     * dataset.
+     */
+    private static void binarySort(int[] position, String[] value) {
+        final int count = position.length;
+        for (int start = 1; start < count; start++) {
+            final int pivotPosition = position[start];
+            final String pivotValue = value[start];
+
+            int left = 0;
+            int right = start;
+
+            while (left < right) {
+                int mid = (left + right) >>> 1;
+
+                final String lhs = pivotValue;
+                final String rhs = value[mid];
+                final int compare;
+                if (lhs == null) {
+                    compare = -1;
+                } else if (rhs == null) {
+                    compare = 1;
+                } else {
+                    compare = lhs.compareToIgnoreCase(rhs);
+                }
+
+                if (compare < 0) {
+                    right = mid;
+                } else {
+                    left = mid + 1;
+                }
+            }
+
+            int n = start - left;
+            switch (n) {
+                case 2:
+                    position[left + 2] = position[left + 1];
+                    value[left + 2] = value[left + 1];
+                case 1:
+                    position[left + 1] = position[left];
+                    value[left + 1] = value[left];
+                    break;
+                default:
+                    System.arraycopy(position, left, position, left + 1, n);
+                    System.arraycopy(value, left, value, left + 1, n);
+            }
+
+            position[left] = pivotPosition;
+            value[left] = pivotValue;
+        }
+    }
+
+    /**
+     * Borrowed from TimSort.binarySort(), but modified to sort two column
+     * dataset.
+     */
+    private static void binarySort(int[] position, long[] value) {
+        final int count = position.length;
+        for (int start = 1; start < count; start++) {
+            final int pivotPosition = position[start];
+            final long pivotValue = value[start];
+
+            int left = 0;
+            int right = start;
+
+            while (left < right) {
+                int mid = (left + right) >>> 1;
+
+                final long lhs = pivotValue;
+                final long rhs = value[mid];
+                final int compare = Long.compare(lhs, rhs);
+                if (compare > 0) {
+                    right = mid;
+                } else {
+                    left = mid + 1;
+                }
+            }
+
+            int n = start - left;
+            switch (n) {
+                case 2:
+                    position[left + 2] = position[left + 1];
+                    value[left + 2] = value[left + 1];
+                case 1:
+                    position[left + 1] = position[left];
+                    value[left + 1] = value[left];
+                    break;
+                default:
+                    System.arraycopy(position, left, position, left + 1, n);
+                    System.arraycopy(value, left, value, left + 1, n);
+            }
+
+            position[left] = pivotPosition;
+            value[left] = pivotValue;
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
index f6e46a8..d571971 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
@@ -17,7 +17,11 @@
 package com.android.documentsui.model;
 
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
 import android.database.Cursor;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
@@ -33,38 +37,28 @@
  * Representation of a {@link Document}.
  */
 public class DocumentInfo {
-    public final Uri uri;
-    public final String mimeType;
-    public final String displayName;
-    public final long lastModified;
-    public final int flags;
-    public final String summary;
-    public final long size;
-
-    private DocumentInfo(Uri uri, String mimeType, String displayName, long lastModified, int flags,
-            String summary, long size) {
-        this.uri = uri;
-        this.mimeType = mimeType;
-        this.displayName = displayName;
-        this.lastModified = lastModified;
-        this.flags = flags;
-        this.summary = summary;
-        this.size = size;
-    }
+    public Uri uri;
+    public String mimeType;
+    public String displayName;
+    public long lastModified;
+    public int flags;
+    public String summary;
+    public long size;
+    public int icon;
 
     public static DocumentInfo fromDirectoryCursor(Uri parent, Cursor cursor) {
+        final DocumentInfo doc = new DocumentInfo();
         final String authority = parent.getAuthority();
         final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
-
-        final Uri uri = DocumentsContract.buildDocumentUri(authority, docId);
-        final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-        final String displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-        final long lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-        final int flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-        final String summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
-        final long size = getCursorLong(cursor, Document.COLUMN_SIZE);
-
-        return new DocumentInfo(uri, mimeType, displayName, lastModified, flags, summary, size);
+        doc.uri = DocumentsContract.buildDocumentUri(authority, docId);
+        doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+        doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+        doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
+        doc.size = getCursorLong(cursor, Document.COLUMN_SIZE);
+        doc.icon = getCursorInt(cursor, Document.COLUMN_ICON);
+        return doc;
     }
 
     @Deprecated
@@ -79,14 +73,18 @@
             if (!cursor.moveToFirst()) {
                 throw new FileNotFoundException("Missing details for " + uri);
             }
-            final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-            final String displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-            final int flags = getCursorInt(cursor, Document.COLUMN_FLAGS)
-                    & Document.FLAG_SUPPORTS_THUMBNAIL;
-            final String summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
-            final long size = getCursorLong(cursor, Document.COLUMN_SIZE);
 
-            return new DocumentInfo(uri, mimeType, displayName, lastModified, flags, summary, size);
+            final DocumentInfo doc = new DocumentInfo();
+            doc.uri = uri;
+            doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+            doc.lastModified = lastModified;
+            doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS)
+                    & Document.FLAG_SUPPORTS_THUMBNAIL;
+            doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
+            doc.size = getCursorLong(cursor, Document.COLUMN_SIZE);
+            doc.icon = getCursorInt(cursor, Document.COLUMN_ICON);
+            return doc;
         } catch (Throwable t) {
             throw asFileNotFoundException(t);
         } finally {
@@ -101,14 +99,16 @@
             if (!cursor.moveToFirst()) {
                 throw new FileNotFoundException("Missing details for " + uri);
             }
-            final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-            final String displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-            final long lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-            final int flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-            final String summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
-            final long size = getCursorLong(cursor, Document.COLUMN_SIZE);
-
-            return new DocumentInfo(uri, mimeType, displayName, lastModified, flags, summary, size);
+            final DocumentInfo doc = new DocumentInfo();
+            doc.uri = uri;
+            doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            doc.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+            doc.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+            doc.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+            doc.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
+            doc.size = getCursorLong(cursor, Document.COLUMN_SIZE);
+            doc.icon = getCursorInt(cursor, Document.COLUMN_ICON);
+            return doc;
         } catch (Throwable t) {
             throw asFileNotFoundException(t);
         } finally {
@@ -145,6 +145,25 @@
         return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
     }
 
+    public Drawable loadIcon(Context context) {
+        return loadIcon(context, uri.getAuthority(), icon);
+    }
+
+    public static Drawable loadIcon(Context context, String authority, int icon) {
+        if (icon != 0) {
+            if (authority != null) {
+                final PackageManager pm = context.getPackageManager();
+                final ProviderInfo info = pm.resolveContentProvider(authority, 0);
+                if (info != null) {
+                    return pm.getDrawable(info.packageName, icon, info.applicationInfo);
+                }
+            } else {
+                return context.getResources().getDrawable(icon);
+            }
+        }
+        return null;
+    }
+
     public static String getCursorString(Cursor cursor, String columnName) {
         final int index = cursor.getColumnIndex(columnName);
         return (index != -1) ? cursor.getString(index) : null;
@@ -170,6 +189,7 @@
         return (index != -1) ? cursor.getInt(index) : 0;
     }
 
+    @Deprecated
     public static class DisplayNameComparator implements Comparator<DocumentInfo> {
         @Override
         public int compare(DocumentInfo lhs, DocumentInfo rhs) {
@@ -184,6 +204,7 @@
         }
     }
 
+    @Deprecated
     public static class LastModifiedComparator implements Comparator<DocumentInfo> {
         @Override
         public int compare(DocumentInfo lhs, DocumentInfo rhs) {
@@ -191,6 +212,7 @@
         }
     }
 
+    @Deprecated
     public static class SizeComparator implements Comparator<DocumentInfo> {
         @Override
         public int compare(DocumentInfo lhs, DocumentInfo rhs) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
index f7027f3..9728838 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
@@ -21,8 +21,6 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.provider.DocumentsContract.Root;
@@ -56,17 +54,6 @@
     }
 
     public Drawable loadIcon(Context context) {
-        if (icon != 0) {
-            if (authority != null) {
-                final PackageManager pm = context.getPackageManager();
-                final ProviderInfo info = pm.resolveContentProvider(authority, 0);
-                if (info != null) {
-                    return pm.getDrawable(info.packageName, icon, info.applicationInfo);
-                }
-            } else {
-                return context.getResources().getDrawable(icon);
-            }
-        }
-        return null;
+        return DocumentInfo.loadIcon(context, authority, icon);
     }
 }