Precompute cursor indexes in DocumentsUI and improve perf by 2.7%.

Relands after a revert, with a tiny fix.

Bug: 27286016
Change-Id: Ifb6cea46a86e8158b05db5dbf87878892c6cc702
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index c5ee592..179cb9b 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -20,8 +20,6 @@
 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.Cursor;
 import android.os.Bundle;
@@ -48,6 +46,7 @@
 @VisibleForTesting
 public class Model {
     private static final String TAG = "Model";
+    private static final String EMPTY = "";
 
     private boolean mIsLoading;
     private List<UpdateListener> mUpdateListeners = new ArrayList<>();
@@ -62,6 +61,13 @@
     private String mIds[] = new String[0];
     private int mSortOrder = SORT_ORDER_DISPLAY_NAME;
 
+    private int mAuthorityIndex = -1;
+    private int mDocIdIndex = -1;
+    private int mMimeTypeIndex = -1;
+    private int mDisplayNameIndex = -1;
+    private int mSizeIndex = -1;
+    private int mLastModifiedIndex = -1;
+
     @Nullable String info;
     @Nullable String error;
     @Nullable DocumentInfo doc;
@@ -72,20 +78,6 @@
      *
      * @param c A cursor that refers to a document.
      */
-    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 createModelId(
-                getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY),
-                getCursorString(c, Document.COLUMN_DOCUMENT_ID));
-    }
-
-    /**
-     * 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.
-     */
     static String createModelId(String authority, String docId) {
         return authority + "|" + docId;
     }
@@ -127,6 +119,14 @@
         mCursor = result.cursor;
         mCursorCount = mCursor.getCount();
         mSortOrder = result.sortOrder;
+        mAuthorityIndex = mCursor.getColumnIndex(RootCursorWrapper.COLUMN_AUTHORITY);
+        assert(mAuthorityIndex != -1);
+        mDocIdIndex = mCursor.getColumnIndex(Document.COLUMN_DOCUMENT_ID);
+        mMimeTypeIndex = mCursor.getColumnIndex(Document.COLUMN_MIME_TYPE);
+        mDisplayNameIndex = mCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME);
+        mLastModifiedIndex = mCursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED);
+        mSizeIndex = mCursor.getColumnIndex(Document.COLUMN_SIZE);
+
         doc = result.doc;
 
         updateModelData();
@@ -173,22 +173,24 @@
         for (int pos = 0; pos < mCursorCount; ++pos) {
             mCursor.moveToNext();
             positions[pos] = pos;
-            mIds[pos] = createModelId(mCursor);
 
-            mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
+            // 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.
+            mIds[pos] = createModelId(
+                    getStringOrEmpty(mAuthorityIndex), getStringOrEmpty(mDocIdIndex));
+
+            mimeType = getStringOrEmpty(mMimeTypeIndex);
             isDirs[pos] = Document.MIME_TYPE_DIR.equals(mimeType);
 
-            switch(mSortOrder) {
+            switch (mSortOrder) {
                 case SORT_ORDER_DISPLAY_NAME:
-                    final String displayName = getCursorString(
-                            mCursor, Document.COLUMN_DISPLAY_NAME);
-                    displayNames[pos] = displayName;
+                    displayNames[pos] = getStringOrEmpty(mDisplayNameIndex);
                     break;
                 case SORT_ORDER_LAST_MODIFIED:
-                    longValues[pos] = getLastModified(mCursor);
+                    longValues[pos] = getLastModified();
                     break;
                 case SORT_ORDER_SIZE:
-                    longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
+                    longValues[pos] = getDocSize();
                     break;
             }
         }
@@ -244,7 +246,7 @@
                 } else {
                     final String lhs = pivotValue;
                     final String rhs = sortKey[mid];
-                    compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
+                    compare = Shared.compareToIgnoreCase(lhs, rhs);
                 }
 
                 if (compare < 0) {
@@ -365,13 +367,42 @@
     }
 
     /**
-     * @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.
+     * @return Value of the string column, or an empty string if no value, or empty value.
      */
-    long getLastModified(Cursor cursor) {
-        long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
-        return (l == -1) ? Long.MAX_VALUE : l;
+    private String getStringOrEmpty(int columnIndex) {
+        if (columnIndex == -1)
+            return EMPTY;
+        final String result = mCursor.getString(columnIndex);
+        return result != null ? result : EMPTY;
+    }
+
+    /**
+     * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
+     * or missing timestamp - these will be replaced with MAX_LONG so that such files get sorted to
+     * the top when sorting by date.
+     */
+    private long getLastModified() {
+        if (mLastModifiedIndex == -1)
+            return Long.MAX_VALUE;
+        try {
+            final long result = mCursor.getLong(mLastModifiedIndex);
+            return result > 0 ? result : Long.MAX_VALUE;
+        } catch (NumberFormatException e) {
+            return Long.MAX_VALUE;
+        }
+    }
+
+    /**
+     * @return Size for the given document. If the size is unknown or invalid, returns 0.
+     */
+    private long getDocSize() {
+        if (mSizeIndex == -1)
+            return 0;
+        try {
+            return mCursor.getLong(mSizeIndex);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
     }
 
     public @Nullable Cursor getItem(String modelId) {