[multi-part] Enable bidrectional sorting

* Wire new sort model to existing sorting logic
* Add sort by menu item to sort controller
* Enable sorting in Recents
* Add tests for SortModel

Bug: 22823056
Change-Id: I864e79e711bf18f3018abd9db90bcf267f0ed390
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 5c15228..1a171bd 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -17,9 +17,6 @@
 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;
 
@@ -38,6 +35,8 @@
 import com.android.documentsui.Shared;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -59,10 +58,10 @@
     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}
+     * by {@link #mSortModel}
      */
     private String mIds[] = new String[0];
-    private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
+    private SortModel mSortModel;
 
     @Nullable String info;
     @Nullable String error;
@@ -104,7 +103,7 @@
 
         mCursor = result.cursor;
         mCursorCount = mCursor.getCount();
-        mSortOrder = result.sortOrder;
+        mSortModel = result.sortModel;
         doc = result.doc;
 
         updateModelData();
@@ -135,12 +134,13 @@
         String[] displayNames = null;
         long[] longValues = null;
 
-        switch (mSortOrder) {
-            case SORT_ORDER_DISPLAY_NAME:
+        final int id = mSortModel.getSortedDimensionId();
+        switch (id) {
+            case SortModel.SORT_DIMENSION_ID_TITLE:
                 displayNames = new String[mCursorCount];
                 break;
-            case SORT_ORDER_LAST_MODIFIED:
-            case SORT_ORDER_SIZE:
+            case SortModel.SORT_DIMENSION_ID_DATE:
+            case SortModel.SORT_DIMENSION_ID_SIZE:
                 longValues = new long[mCursorCount];
                 break;
         }
@@ -169,28 +169,29 @@
             mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
             isDirs[pos] = Document.MIME_TYPE_DIR.equals(mimeType);
 
-            switch(mSortOrder) {
-                case SORT_ORDER_DISPLAY_NAME:
+            switch(id) {
+                case SortModel.SORT_DIMENSION_ID_TITLE:
                     final String displayName = getCursorString(
                             mCursor, Document.COLUMN_DISPLAY_NAME);
                     displayNames[pos] = displayName;
                     break;
-                case SORT_ORDER_LAST_MODIFIED:
+                case SortModel.SORT_DIMENSION_ID_DATE:
                     longValues[pos] = getLastModified(mCursor);
                     break;
-                case SORT_ORDER_SIZE:
+                case SortModel.SORT_DIMENSION_ID_SIZE:
                     longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
                     break;
             }
         }
 
-        switch (mSortOrder) {
-            case SORT_ORDER_DISPLAY_NAME:
-                binarySort(displayNames, isDirs, positions, mIds);
+        final SortDimension dimension = mSortModel.getDimensionById(id);
+        switch (id) {
+            case SortModel.SORT_DIMENSION_ID_TITLE:
+                binarySort(displayNames, isDirs, positions, mIds, dimension.getSortDirection());
                 break;
-            case SORT_ORDER_LAST_MODIFIED:
-            case SORT_ORDER_SIZE:
-                binarySort(longValues, isDirs, positions, mIds);
+            case SortModel.SORT_DIMENSION_ID_DATE:
+            case SortModel.SORT_DIMENSION_ID_SIZE:
+                binarySort(longValues, isDirs, positions, mIds, dimension.getSortDirection());
                 break;
         }
 
@@ -211,7 +212,12 @@
      * @param positions Cursor positions to be sorted.
      * @param ids Model IDs to be sorted.
      */
-    private static void binarySort(String[] sortKey, boolean[] isDirs, int[] positions, String[] ids) {
+    private static void binarySort(
+            String[] sortKey,
+            boolean[] isDirs,
+            int[] positions,
+            String[] ids,
+            @SortDimension.SortDirection int direction) {
         final int count = positions.length;
         for (int start = 1; start < count; start++) {
             final int pivotPosition = positions[start];
@@ -235,7 +241,17 @@
                 } else {
                     final String lhs = pivotValue;
                     final String rhs = sortKey[mid];
-                    compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
+                    switch (direction) {
+                        case SortDimension.SORT_DIRECTION_ASCENDING:
+                            compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
+                            break;
+                        case SortDimension.SORT_DIRECTION_DESCENDING:
+                            compare = -Shared.compareToIgnoreCaseNullable(lhs, rhs);
+                            break;
+                        default:
+                            throw new IllegalArgumentException(
+                                    "Unknown sorting direction: " + direction);
+                    }
                 }
 
                 if (compare < 0) {
@@ -284,7 +300,11 @@
      * @param ids Model IDs to be sorted.
      */
     private static void binarySort(
-            long[] sortKey, boolean[] isDirs, int[] positions, String[] ids) {
+            long[] sortKey,
+            boolean[] isDirs,
+            int[] positions,
+            String[] ids,
+            @SortDimension.SortDirection int direction) {
         final int count = positions.length;
         for (int start = 1; start < count; start++) {
             final int pivotPosition = positions[start];
@@ -308,9 +328,17 @@
                 } else {
                     final long lhs = pivotValue;
                     final long rhs = sortKey[mid];
-                    // Sort in descending numerical order. This matches legacy behaviour, which
-                    // yields largest or most recent items on top.
-                    compare = -Long.compare(lhs, rhs);
+                    switch (direction) {
+                        case SortDimension.SORT_DIRECTION_ASCENDING:
+                            compare = Long.compare(lhs, rhs);
+                            break;
+                        case SortDimension.SORT_DIRECTION_DESCENDING:
+                            compare = -Long.compare(lhs, rhs);
+                            break;
+                        default:
+                            throw new IllegalArgumentException(
+                                    "Unknown sorting direction: " + direction);
+                    }
                 }
 
                 // If numerical comparison yields a tie, use document ID as a tie breaker.  This
@@ -358,7 +386,7 @@
     /**
      * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
      * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
-     * when sorting by date.
+     * when sorting descending by date.
      */
     long getLastModified(Cursor cursor) {
         long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
@@ -410,6 +438,14 @@
         return DocumentInfo.getUri(cursor);
     }
 
+    /**
+     * @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 String[] getModelIds() {
+        return mIds;
+    }
+
     void addUpdateListener(UpdateListener listener) {
         mUpdateListeners.add(listener);
     }
@@ -418,7 +454,7 @@
         mUpdateListeners.remove(listener);
     }
 
-    static interface UpdateListener {
+    interface UpdateListener {
         /**
          * Called when a successful update has occurred.
          */
@@ -429,12 +465,4 @@
          */
         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 String[] getModelIds() {
-        return mIds;
-    }
 }