Re-enable sorting in the DirectoryFragment.

- Move sorting from the back-end (using SortingCursorWrapper) to the the
  front-end (in DocumentsAdapter).  This makes it such that re-sorting
  the directory contents doesn't necessitate a reload.
- Update DirectoryLoaders to just return unsorted results, and rely on
  the UI to sort them.
- Remove the (now-unused) SortingCursorWrapper.
- Update Model tests to test sorting.

BUG=26024369

Change-Id: I871cc0e496267d381ae546e0309125d04649415a
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index edf384e..5cb3b0d 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -85,7 +85,7 @@
     private int mLayoutId;
     private DirectoryContainerView mDirectoryContainer;
 
-    public abstract void onDocumentPicked(DocumentInfo doc, @Nullable DocumentContext siblings);
+    public abstract void onDocumentPicked(DocumentInfo doc, @Nullable SiblingProvider siblings);
     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
 
     abstract void onTaskFinished(Uri... uris);
@@ -826,7 +826,7 @@
      * Interface providing access to current view of documents
      * even when all documents are not homed to the same parent.
      */
-    public interface DocumentContext {
+    public interface SiblingProvider {
         /**
          * Returns the cursor for the selected document. The cursor can be used to retrieve
          * details about a document and its siblings.
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index b0bbec3..4eacee5 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -156,9 +156,6 @@
             if (mType == DirectoryFragment.TYPE_SEARCH) {
                 // Filter directories out of search results, for now
                 cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
-            } else {
-                // Normal directories should have sorting applied
-                cursor = new SortingCursorWrapper(cursor, result.sortOrder);
             }
 
             result.client = client;
diff --git a/src/com/android/documentsui/DocumentsActivity.java b/src/com/android/documentsui/DocumentsActivity.java
index 8754f68..313d303 100644
--- a/src/com/android/documentsui/DocumentsActivity.java
+++ b/src/com/android/documentsui/DocumentsActivity.java
@@ -19,8 +19,8 @@
 import static com.android.documentsui.State.ACTION_CREATE;
 import static com.android.documentsui.State.ACTION_GET_CONTENT;
 import static com.android.documentsui.State.ACTION_OPEN;
-import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
 import static com.android.documentsui.State.ACTION_OPEN_TREE;
+import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
 import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE;
 
 import android.app.Activity;
@@ -31,7 +31,6 @@
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
@@ -419,7 +418,7 @@
     }
 
     @Override
-    public void onDocumentPicked(DocumentInfo doc, DocumentContext context) {
+    public void onDocumentPicked(DocumentInfo doc, SiblingProvider siblings) {
         final FragmentManager fm = getFragmentManager();
         if (doc.isContainer()) {
             openContainerDocument(doc);
diff --git a/src/com/android/documentsui/DownloadsActivity.java b/src/com/android/documentsui/DownloadsActivity.java
index cccbbc8..b806ced 100644
--- a/src/com/android/documentsui/DownloadsActivity.java
+++ b/src/com/android/documentsui/DownloadsActivity.java
@@ -183,7 +183,7 @@
     }
 
     @Override
-    public void onDocumentPicked(DocumentInfo doc, DocumentContext context) {
+    public void onDocumentPicked(DocumentInfo doc, SiblingProvider siblings) {
         final FragmentManager fm = getFragmentManager();
         if (doc.isContainer()) {
             openContainerDocument(doc);
diff --git a/src/com/android/documentsui/FilesActivity.java b/src/com/android/documentsui/FilesActivity.java
index 0b6a09f..dd8ccf9 100644
--- a/src/com/android/documentsui/FilesActivity.java
+++ b/src/com/android/documentsui/FilesActivity.java
@@ -323,7 +323,7 @@
     }
 
     @Override
-    public void onDocumentPicked(DocumentInfo doc, @Nullable DocumentContext siblings) {
+    public void onDocumentPicked(DocumentInfo doc, @Nullable SiblingProvider siblings) {
         if (doc.isContainer()) {
             openContainerDocument(doc);
         } else {
@@ -334,7 +334,7 @@
     /**
      * Launches an intent to view the specified document.
      */
-    private void openDocument(DocumentInfo doc, @Nullable DocumentContext siblings) {
+    private void openDocument(DocumentInfo doc, @Nullable SiblingProvider siblings) {
         Intent intent = null;
         if (siblings != null) {
             QuickViewIntentBuilder builder = new QuickViewIntentBuilder(
diff --git a/src/com/android/documentsui/QuickViewIntentBuilder.java b/src/com/android/documentsui/QuickViewIntentBuilder.java
index 605c530..5a80c39 100644
--- a/src/com/android/documentsui/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/QuickViewIntentBuilder.java
@@ -34,7 +34,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.documentsui.BaseActivity.DocumentContext;
+import com.android.documentsui.BaseActivity.SiblingProvider;
 import com.android.documentsui.model.DocumentInfo;
 
 /**
@@ -43,7 +43,7 @@
 final class QuickViewIntentBuilder {
 
     private final DocumentInfo mDocument;
-    private final DocumentContext mContext;
+    private final SiblingProvider mSiblings;
 
     private final PackageManager mPkgManager;
     private final Resources mResources;
@@ -55,12 +55,12 @@
             PackageManager pkgManager,
             Resources resources,
             DocumentInfo doc,
-            DocumentContext context) {
+            SiblingProvider siblings) {
 
         mPkgManager = pkgManager;
         mResources = resources;
         mDocument = doc;
-        mContext = context;
+        mSiblings = siblings;
     }
 
     /**
@@ -84,7 +84,7 @@
             intent.setPackage(trustedPkg);
             if (hasRegisteredHandler(intent)) {
                 // We have a trusted handler. Load all of the docs into the intent.
-                Cursor cursor = mContext.getCursor();
+                Cursor cursor = mSiblings.getCursor();
                 for (int i = 0; i < cursor.getCount(); i++) {
                     onNextItem(i, cursor);
                 }
diff --git a/src/com/android/documentsui/RecentLoader.java b/src/com/android/documentsui/RecentLoader.java
index 4bd6ae6..0ee54e6 100644
--- a/src/com/android/documentsui/RecentLoader.java
+++ b/src/com/android/documentsui/RecentLoader.java
@@ -233,12 +233,6 @@
         final DirectoryResult result = new DirectoryResult();
         result.sortOrder = SORT_ORDER_LAST_MODIFIED;
 
-        // Hint to UI if we're still loading
-        final Bundle extras = new Bundle();
-        if (!allDone) {
-            extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
-        }
-
         final Cursor merged;
         if (cursors.size() > 0) {
             merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
@@ -247,14 +241,13 @@
             merged = new MatrixCursor(new String[0]);
         }
 
-        final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder) {
-            @Override
-            public Bundle getExtras() {
-                return extras;
-            }
-        };
+        // Tell the UI if this is an in-progress result. When loading is complete, another update is
+        // sent with EXTRA_LOADING set to false.
+        Bundle extras = new Bundle();
+        extras.putBoolean(DocumentsContract.EXTRA_LOADING, !allDone);
+        merged.setExtras(extras);
 
-        result.cursor = sorted;
+        result.cursor = merged;
 
         return result;
     }
diff --git a/src/com/android/documentsui/SortingCursorWrapper.java b/src/com/android/documentsui/SortingCursorWrapper.java
deleted file mode 100644
index 6698ff1..0000000
--- a/src/com/android/documentsui/SortingCursorWrapper.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * 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 static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME;
-import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
-import static com.android.documentsui.State.SORT_ORDER_SIZE;
-import static com.android.documentsui.model.DocumentInfo.getCursorLong;
-import static com.android.documentsui.model.DocumentInfo.getCursorString;
-
-import android.database.AbstractCursor;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.provider.DocumentsContract.Document;
-
-import com.android.documentsui.model.DocumentInfo;
-
-/**
- * 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 SORT_ORDER_DISPLAY_NAME:
-                mValueString = new String[count];
-                mValueLong = null;
-                break;
-            case SORT_ORDER_LAST_MODIFIED:
-            case SORT_ORDER_SIZE:
-                mValueString = null;
-                mValueLong = new long[count];
-                break;
-            default:
-                throw new IllegalArgumentException();
-        }
-
-        cursor.moveToPosition(-1);
-        for (int i = 0; i < count; i++) {
-            cursor.moveToNext();
-            mPosition[i] = i;
-
-            switch (sortOrder) {
-                case SORT_ORDER_DISPLAY_NAME:
-                    final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-                    final String displayName = getCursorString(
-                            cursor, Document.COLUMN_DISPLAY_NAME);
-                    if (Document.MIME_TYPE_DIR.equals(mimeType)) {
-                        mValueString[i] = DocumentInfo.DIR_PREFIX + displayName;
-                    } else {
-                        mValueString[i] = displayName;
-                    }
-                    break;
-                case SORT_ORDER_LAST_MODIFIED:
-                    mValueLong[i] = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-                    break;
-                case SORT_ORDER_SIZE:
-                    mValueLong[i] = getCursorLong(cursor, Document.COLUMN_SIZE);
-                    break;
-            }
-        }
-
-        switch (sortOrder) {
-            case SORT_ORDER_DISPLAY_NAME:
-                synchronized (SortingCursorWrapper.class) {
-
-                    binarySort(mPosition, mValueString);
-                }
-                break;
-            case SORT_ORDER_LAST_MODIFIED:
-            case SORT_ORDER_SIZE:
-                binarySort(mPosition, mValueLong);
-                break;
-        }
-    }
-
-    @Override
-    public Bundle getExtras() {
-        return mCursor.getExtras();
-    }
-
-    @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 = DocumentInfo.compareToIgnoreCaseNullable(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;
-        }
-    }
-
-    /**
-     * 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/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index b66a124..ef71693 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -86,7 +86,6 @@
 import android.widget.TextView;
 
 import com.android.documentsui.BaseActivity;
-import com.android.documentsui.BaseActivity.DocumentContext;
 import com.android.documentsui.CopyService;
 import com.android.documentsui.DirectoryLoader;
 import com.android.documentsui.DirectoryResult;
@@ -107,7 +106,6 @@
 import com.android.documentsui.RootCursorWrapper;
 import com.android.documentsui.RootsCache;
 import com.android.documentsui.Shared;
-import com.android.documentsui.Shared;
 import com.android.documentsui.Snackbars;
 import com.android.documentsui.State;
 import com.android.documentsui.ThumbnailCache;
@@ -436,6 +434,9 @@
                 if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) {
                     getView().restoreHierarchyState(container);
                 } else if (mLastSortOrder != state.derivedSortOrder) {
+                    // The derived sort order takes the user sort order into account, but applies
+                    // directory-specific defaults when the user doesn't explicitly set the sort
+                    // order. Scroll to the top if the sort order actually changed.
                     mRecView.smoothScrollToPosition(0);
                 }
 
@@ -996,16 +997,14 @@
     }
 
     final class DocumentsAdapter
-        extends RecyclerView.Adapter<DocumentHolder>
-        implements Model.UpdateListener {
+            extends RecyclerView.Adapter<DocumentHolder>
+            implements Model.UpdateListener {
 
         static private final String TAG = "DocumentsAdapter";
         private final Context mContext;
         /**
          * Map of model IDs to adapter positions. This is the data structure that determines what
-         * shows up in the UI, and where. Note that item positions can change if items are
-         * removed/added, so this list should only be used in onBindViewHolder. See
-         * {@link RecyclerView.Adapter.onCreateViewHolder} for details.
+         * shows up in the UI, and where.
          */
         // TODO(stable-id): need to keep this up-to-date when items are added/removed
         private List<String> mModelIds = new ArrayList<>();
@@ -1063,8 +1062,8 @@
             final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
                     context, mThumbSize);
 
-            final String modelId = mModelIds.get(position);
-            final Cursor cursor = mModel.getItem(modelId);
+            holder.modelId = mModelIds.get(position);
+            final Cursor cursor = mModel.getItem(holder.modelId);
             checkNotNull(cursor, "Cursor cannot be null.");
 
             final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
@@ -1078,10 +1077,9 @@
             final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
             final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
 
-            holder.modelId = Model.createId(cursor);
             final View itemView = holder.itemView;
 
-            holder.setSelected(isSelected(modelId));
+            holder.setSelected(isSelected(holder.modelId));
 
             final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
             final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
@@ -1222,15 +1220,13 @@
 
         @Override
         public int getItemCount() {
-            if (DEBUG) Log.d(TAG, "getItemCount called: " + mModelIds.size());
             return mModelIds.size();
         }
 
         @Override
         public void onModelUpdate(Model model) {
             // TODO(stable-id): Sort model IDs, categorize by dir/file, etc
-            if (DEBUG) Log.d(TAG, "onModelUpdate called");
-            mModelIds = Lists.newArrayList(model.getIds());
+            mModelIds = Lists.newArrayList(model.getModelIds());
         }
 
         @Override
@@ -1285,6 +1281,16 @@
                 notifyItemInserted(pos);
             }
         }
+
+        /**
+         * Returns a list of model IDs of items currently in the adapter. Excludes items that are
+         * currently hidden (see {@link #hide(String...)}).
+         *
+         * @return A list of Model IDs.
+         */
+        public List<String> getModelIds() {
+            return mModelIds;
+        }
     }
 
     private static String formatTime(Context context, long when) {
@@ -1440,7 +1446,7 @@
                 Snackbars.makeSnackbar(activity,
                         activity.getResources().getQuantityString(
                                 R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
-                                Snackbar.LENGTH_SHORT).show();
+                        Snackbar.LENGTH_SHORT).show();
             }
         }.execute(selection);
     }
@@ -1476,7 +1482,8 @@
     }
 
     public void selectAllFiles() {
-        boolean changed = mSelectionManager.setItemsSelected(mModel.getIds(), true);
+        // Only select things currently visible in the adapter.
+        boolean changed = mSelectionManager.setItemsSelected(mAdapter.getModelIds(), true);
         if (changed) {
             updateDisplayState();
         }
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index fce16c6..0d4d37e 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -17,6 +17,10 @@
 package com.android.documentsui.dirlist;
 
 import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.State.SORT_ORDER_DISPLAY_NAME;
+import static com.android.documentsui.State.SORT_ORDER_LAST_MODIFIED;
+import static com.android.documentsui.State.SORT_ORDER_SIZE;
+import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 import static com.android.internal.util.Preconditions.checkNotNull;
 
@@ -34,7 +38,7 @@
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 
-import com.android.documentsui.BaseActivity.DocumentContext;
+import com.android.documentsui.BaseActivity.SiblingProvider;
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.RootCursorWrapper;
@@ -44,46 +48,49 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Set;
+import java.util.Map;
 
 /**
  * The data model for the current loaded directory.
  */
 @VisibleForTesting
-public class Model implements DocumentContext {
+public class Model implements SiblingProvider {
     private static final String TAG = "Model";
+
     private Context mContext;
-    private int mCursorCount;
     private boolean mIsLoading;
     private List<UpdateListener> mUpdateListeners = new ArrayList<>();
     @Nullable private Cursor mCursor;
+    private int mCursorCount;
+    /** Maps Model ID to cursor positions, for looking up items by Model ID. */
+    private Map<String, Integer> mPositions = new HashMap<>();
+    /**
+     * A sorted array of model IDs for the files currently in the Model.  Sort order is determined
+     * by {@link #mSortOrder}
+     */
+    private List<String> mIds = new ArrayList<>();
+    private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
+
     @Nullable String info;
     @Nullable String error;
-    private HashMap<String, Integer> mPositions = new HashMap<>();
 
     Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
         mContext = context;
     }
 
     /**
-     * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
-     * unique string that can be used to identify the document referred to by the cursor.
+     * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
+     * string that can be used to identify the document referred to by the cursor.
      *
      * @param c A cursor that refers to a document.
      */
-    public static String createId(Cursor c) {
+    private static String createModelId(Cursor c) {
+        // TODO: Maybe more efficient to use just the document ID, in cases where there is only one
+        // authority (which should be the majority of cases).
         return getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY) +
                 "|" + getCursorString(c, Document.COLUMN_DOCUMENT_ID);
     }
 
-    /**
-     * @return Model IDs for all known items in the model. Note that this will include items
-     *         pending deletion.
-     */
-    public Set<String> getIds() {
-        return mPositions.keySet();
-    }
-
     private void notifyUpdateListeners() {
         for (UpdateListener listener: mUpdateListeners) {
             listener.onModelUpdate(this);
@@ -102,6 +109,8 @@
         if (result == null) {
             mCursor = null;
             mCursorCount = 0;
+            mIds.clear();
+            mPositions.clear();
             info = null;
             error = null;
             mIsLoading = false;
@@ -117,8 +126,9 @@
 
         mCursor = result.cursor;
         mCursorCount = mCursor.getCount();
+        mSortOrder = result.sortOrder;
 
-        updatePositions();
+        updateModelData();
 
         final Bundle extras = mCursor.getExtras();
         if (extras != null) {
@@ -136,14 +146,171 @@
     }
 
     /**
-     * Update the ModelId-position map.
+     * Scan over the incoming cursor data, generate Model IDs for each row, and sort the IDs
+     * according to the current sort order.
      */
-    private void updatePositions() {
-        mPositions.clear();
+    private void updateModelData() {
+        int[] positions = new int[mCursorCount];
+        mIds.clear();
+        String[] strings = null;
+        long[] longs = null;
+
+        switch (mSortOrder) {
+            case SORT_ORDER_DISPLAY_NAME:
+                strings = new String[mCursorCount];
+                break;
+            case SORT_ORDER_LAST_MODIFIED:
+            case SORT_ORDER_SIZE:
+                longs = new long[mCursorCount];
+                break;
+        }
+
         mCursor.moveToPosition(-1);
         for (int pos = 0; pos < mCursorCount; ++pos) {
             mCursor.moveToNext();
-            mPositions.put(Model.createId(mCursor), pos);
+            positions[pos] = pos;
+            mIds.add(createModelId(mCursor));
+
+            switch(mSortOrder) {
+                case SORT_ORDER_DISPLAY_NAME:
+                    final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
+                    final String displayName = getCursorString(
+                            mCursor, Document.COLUMN_DISPLAY_NAME);
+                    if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+                        strings[pos] = DocumentInfo.DIR_PREFIX + displayName;
+                    } else {
+                        strings[pos] = displayName;
+                    }
+                    break;
+                case SORT_ORDER_LAST_MODIFIED:
+                    longs[pos] = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
+                    break;
+                case SORT_ORDER_SIZE:
+                    longs[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
+                    break;
+            }
+        }
+
+        switch (mSortOrder) {
+            case SORT_ORDER_DISPLAY_NAME:
+                binarySort(positions, strings, mIds);
+                break;
+            case SORT_ORDER_LAST_MODIFIED:
+            case SORT_ORDER_SIZE:
+                binarySort(positions, longs, mIds);
+                break;
+        }
+
+        // Populate the positions.
+        mPositions.clear();
+        for (int i = 0; i < mCursorCount; ++i) {
+            mPositions.put(mIds.get(i), positions[i]);
+        }
+    }
+
+    /**
+     * Borrowed from TimSort.binarySort(), but modified to sort three-column data set.
+     */
+    private static void binarySort(int[] positions, String[] strings, List<String> ids) {
+        final int count = positions.length;
+        for (int start = 1; start < count; start++) {
+            final int pivotPosition = positions[start];
+            final String pivotValue = strings[start];
+            final String pivotId = ids.get(start);
+
+            int left = 0;
+            int right = start;
+
+            while (left < right) {
+                int mid = (left + right) >>> 1;
+
+                final String lhs = pivotValue;
+                final String rhs = strings[mid];
+                final int compare = DocumentInfo.compareToIgnoreCaseNullable(lhs, rhs);
+
+                if (compare < 0) {
+                    right = mid;
+                } else {
+                    left = mid + 1;
+                }
+            }
+
+            int n = start - left;
+            switch (n) {
+                case 2:
+                    positions[left + 2] = positions[left + 1];
+                    strings[left + 2] = strings[left + 1];
+                    ids.set(left + 2, ids.get(left + 1));
+                case 1:
+                    positions[left + 1] = positions[left];
+                    strings[left + 1] = strings[left];
+                    ids.set(left + 1, ids.get(left));
+                    break;
+                default:
+                    System.arraycopy(positions, left, positions, left + 1, n);
+                    System.arraycopy(strings, left, strings, left + 1, n);
+                    for (int i = n; i >= 1; --i) {
+                        ids.set(left + i, ids.get(left + i - 1));
+                    }
+            }
+
+            positions[left] = pivotPosition;
+            strings[left] = pivotValue;
+            ids.set(left, pivotId);
+        }
+    }
+
+    /**
+     * Borrowed from TimSort.binarySort(), but modified to sort three-column data set.
+     */
+   private static void binarySort(int[] positions, long[] longs, List<String> ids) {
+        final int count = positions.length;
+        for (int start = 1; start < count; start++) {
+            final int pivotPosition = positions[start];
+            final long pivotValue = longs[start];
+            final String pivotId = ids.get(start);
+
+            int left = 0;
+            int right = start;
+
+            while (left < right) {
+                int mid = (left + right) >>> 1;
+
+                final long lhs = pivotValue;
+                final long rhs = longs[mid];
+                // Sort in descending numerical order. This matches legacy behaviour, which yields
+                // largest or most recent items on top.
+                final int compare = -Long.compare(lhs, rhs);
+
+                if (compare < 0) {
+                    right = mid;
+                } else {
+                    left = mid + 1;
+                }
+            }
+
+            int n = start - left;
+            switch (n) {
+                case 2:
+                    positions[left + 2] = positions[left + 1];
+                    longs[left + 2] = longs[left + 1];
+                    ids.set(left + 2, ids.get(left + 1));
+                case 1:
+                    positions[left + 1] = positions[left];
+                    longs[left + 1] = longs[left];
+                    ids.set(left + 1, ids.get(left));
+                    break;
+                default:
+                    System.arraycopy(positions, left, positions, left + 1, n);
+                    System.arraycopy(longs, left, longs, left + 1, n);
+                    for (int i = n; i >= 1; --i) {
+                        ids.set(left + i, ids.get(left + i - 1));
+                    }
+            }
+
+            positions[left] = pivotPosition;
+            longs[left] = pivotValue;
+            ids.set(left, pivotId);
         }
     }
 
@@ -279,4 +446,12 @@
          */
         void onModelUpdateFailed(Exception e);
     }
+
+    /**
+     * @return An ordered array of model IDs representing the documents in the model. It is sorted
+     *         according to the current sort order, which was set by the last model update.
+     */
+    public List<String> getModelIds() {
+        return mIds;
+    }
 }
diff --git a/tests/src/com/android/documentsui/dirlist/ModelTest.java b/tests/src/com/android/documentsui/dirlist/ModelTest.java
index 96ca0f9..a2b6ad5 100644
--- a/tests/src/com/android/documentsui/dirlist/ModelTest.java
+++ b/tests/src/com/android/documentsui/dirlist/ModelTest.java
@@ -16,8 +16,6 @@
 
 package com.android.documentsui.dirlist;
 
-import static android.test.MoreAsserts.assertNotEqual;
-
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -36,11 +34,14 @@
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.RootCursorWrapper;
+import com.android.documentsui.State;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
 
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.List;
+import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 
 @SmallTest
@@ -51,23 +52,43 @@
     private static final String[] COLUMNS = new String[]{
         RootCursorWrapper.COLUMN_AUTHORITY,
         Document.COLUMN_DOCUMENT_ID,
-        Document.COLUMN_FLAGS
+        Document.COLUMN_FLAGS,
+        Document.COLUMN_DISPLAY_NAME,
+        Document.COLUMN_SIZE
     };
     private static Cursor cursor;
 
     private Context context;
     private Model model;
     private TestContentProvider provider;
+    private static final String[] NAMES = new String[] {
+        "4",
+        "foo",
+        "1",
+        "bar",
+        "*(Ljifl;a",
+        "0",
+        "baz",
+        "2",
+        "3",
+        "%$%VD"
+    };
 
     public void setUp() {
         setupTestContext();
 
+        Random rand = new Random();
+
         MatrixCursor c = new MatrixCursor(COLUMNS);
         for (int i = 0; i < ITEM_COUNT; ++i) {
             MatrixCursor.RowBuilder row = c.newRow();
             row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
             row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
             row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
+            // Generate random document names and sizes. This forces the model's internal sort code
+            // to actually do something.
+            row.add(Document.COLUMN_DISPLAY_NAME, NAMES[i]);
+            row.add(Document.COLUMN_SIZE, rand.nextInt());
         }
         cursor = c;
 
@@ -80,6 +101,15 @@
         model.update(r);
     }
 
+    // Tests that the model is properly emptied out after a null update.
+    public void testNullUpdate() {
+        model.update(null);
+
+        assertTrue(model.isEmpty());
+        assertEquals(0, model.getItemCount());
+        assertEquals(0, model.getModelIds().size());
+    }
+
     // Tests that the item count is correct.
     public void testItemCount() {
         assertEquals(ITEM_COUNT, model.getItemCount());
@@ -87,39 +117,108 @@
 
     // Tests multiple authorities with clashing document IDs.
     public void testModelIdIsUnique() {
-        MatrixCursor c0 = new MatrixCursor(COLUMNS);
-        MatrixCursor c1 = new MatrixCursor(COLUMNS);
-
+        MatrixCursor cIn = new MatrixCursor(COLUMNS);
 
         // Make two sets of items with the same IDs, under different authorities.
         final String AUTHORITY0 = "auth0";
         final String AUTHORITY1 = "auth1";
         for (int i = 0; i < ITEM_COUNT; ++i) {
-            MatrixCursor.RowBuilder row0 = c0.newRow();
+            MatrixCursor.RowBuilder row0 = cIn.newRow();
             row0.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY0);
             row0.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
 
-            MatrixCursor.RowBuilder row1 = c1.newRow();
+            MatrixCursor.RowBuilder row1 = cIn.newRow();
             row1.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY1);
             row1.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
         }
 
-        for (int i = 0; i < ITEM_COUNT; ++i) {
-            c0.moveToPosition(i);
-            c1.moveToPosition(i);
-            assertNotEqual(Model.createId(c0), Model.createId(c1));
+        // Update the model, then make sure it contains all the expected items.
+        DirectoryResult r = new DirectoryResult();
+        r.cursor = cIn;
+        model.update(r);
+
+        assertEquals(ITEM_COUNT * 2, model.getItemCount());
+        BitSet b0 = new BitSet(ITEM_COUNT);
+        BitSet b1 = new BitSet(ITEM_COUNT);
+
+        for (String id: model.getModelIds()) {
+            Cursor cOut = model.getItem(id);
+            String authority =
+                    DocumentInfo.getCursorString(cOut, RootCursorWrapper.COLUMN_AUTHORITY);
+            String docId = DocumentInfo.getCursorString(cOut, Document.COLUMN_DOCUMENT_ID);
+
+            switch (authority) {
+                case AUTHORITY0:
+                    b0.set(Integer.parseInt(docId));
+                    break;
+                case AUTHORITY1:
+                    b1.set(Integer.parseInt(docId));
+                    break;
+                default:
+                    fail("Unrecognized authority string");
+            }
         }
+
+        assertEquals(ITEM_COUNT, b0.cardinality());
+        assertEquals(ITEM_COUNT, b1.cardinality());
     }
 
     // Tests the base case for Model.getItem.
     public void testGetItem() {
+        List<String> ids = model.getModelIds();
+        assertEquals(ITEM_COUNT, ids.size());
         for (int i = 0; i < ITEM_COUNT; ++i) {
-            cursor.moveToPosition(i);
-            Cursor c = model.getItem(Model.createId(cursor));
+            Cursor c = model.getItem(ids.get(i));
             assertEquals(i, c.getPosition());
         }
     }
 
+    // Tests sorting by item name.
+    public void testSort_names() {
+        BitSet seen = new BitSet(ITEM_COUNT);
+        List<String> names = new ArrayList<>();
+
+        DirectoryResult r = new DirectoryResult();
+        r.cursor = cursor;
+        r.sortOrder = State.SORT_ORDER_DISPLAY_NAME;
+        model.update(r);
+
+        for (String id: model.getModelIds()) {
+            Cursor c = model.getItem(id);
+            seen.set(c.getPosition());
+            names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME));
+        }
+
+        assertEquals(ITEM_COUNT, seen.cardinality());
+        for (int i = 0; i < names.size()-1; ++i) {
+            assertTrue(DocumentInfo.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) <= 0);
+        }
+    }
+
+    // Tests sorting by item size.
+    public void testSort_sizes() {
+        BitSet seen = new BitSet(ITEM_COUNT);
+        List<Integer> sizes = new ArrayList<>();
+
+        DirectoryResult r = new DirectoryResult();
+        r.cursor = cursor;
+        r.sortOrder = State.SORT_ORDER_SIZE;
+        model.update(r);
+
+        for (String id: model.getModelIds()) {
+            Cursor c = model.getItem(id);
+            seen.set(c.getPosition());
+            sizes.add(DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE));
+        }
+
+        assertEquals(ITEM_COUNT, seen.cardinality());
+        for (int i = 0; i < sizes.size()-1; ++i) {
+            // Note: sizes are sorted descending.
+            assertTrue(sizes.get(i) >= sizes.get(i+1));
+        }
+    }
+
+
     // Tests that Model.delete works correctly.
     public void testDelete() throws Exception {
         // Simulate deleting 2 files.
@@ -143,11 +242,11 @@
     }
 
     private Selection positionToSelection(int... positions) {
+        List<String> ids = model.getModelIds();
         Selection s = new Selection();
         // Construct a selection of the given positions.
         for (int p: positions) {
-            cursor.moveToPosition(p);
-            s.add(Model.createId(cursor));
+            s.add(ids.get(p));
         }
         return s;
     }