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/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;
+    }
 }