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/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index cb92d76..b2be11b 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/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;
         }
     }
 }