New recents behavior to match spec.

Updated behavior queries each documents root for recently modified
documents.  It uses a new variant of DirectoryLoader which limits the
maximum number of parallel queries to relieve memory pressure.  When
first started, it waits up to 500ms for everyone to finish, then
publishes whatever results are ready, and then refreshes results as
each straggler finishes.

New RootCursorWrapper that always blends in authority and rootId
columns, which are used for binding root details from blended cursors.

Bug: 10593596, 10329994
Change-Id: Icc0d4a2f1b6166edc72f78a4c88f444eeba6f2f0
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 549e196..c24341e 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -64,6 +64,7 @@
 
 import com.android.documentsui.DocumentsActivity.State;
 import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.RootInfo;
 import com.android.internal.util.Predicate;
 import com.google.android.collect.Lists;
 
@@ -86,6 +87,7 @@
 
     public static final int TYPE_NORMAL = 1;
     public static final int TYPE_SEARCH = 2;
+    public static final int TYPE_RECENT_OPEN = 3;
 
     private int mType = TYPE_NORMAL;
 
@@ -95,7 +97,10 @@
     private LoaderCallbacks<DirectoryResult> mCallbacks;
 
     private static final String EXTRA_TYPE = "type";
-    private static final String EXTRA_URI = "uri";
+    private static final String EXTRA_AUTHORITY = "authority";
+    private static final String EXTRA_ROOT_ID = "rootId";
+    private static final String EXTRA_DOC_ID = "docId";
+    private static final String EXTRA_QUERY = "query";
 
     private static AtomicInteger sLoaderId = new AtomicInteger(4000);
 
@@ -104,24 +109,26 @@
     private final int mLoaderId = sLoaderId.incrementAndGet();
 
     public static void showNormal(FragmentManager fm, Uri uri) {
-        show(fm, TYPE_NORMAL, uri);
+        show(fm, TYPE_NORMAL, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri), null);
     }
 
     public static void showSearch(FragmentManager fm, Uri uri, String query) {
-        final Uri searchUri = DocumentsContract.buildSearchDocumentsUri(
-                uri.getAuthority(), DocumentsContract.getDocumentId(uri), query);
-        show(fm, TYPE_SEARCH, searchUri);
+        show(fm, TYPE_SEARCH, uri.getAuthority(), null, DocumentsContract.getDocumentId(uri),
+                query);
     }
 
-    @Deprecated
     public static void showRecentsOpen(FragmentManager fm) {
-        // TODO: new recents behavior
+        show(fm, TYPE_RECENT_OPEN, null, null, null, null);
     }
 
-    private static void show(FragmentManager fm, int type, Uri uri) {
+    private static void show(FragmentManager fm, int type, String authority, String rootId,
+            String docId, String query) {
         final Bundle args = new Bundle();
         args.putInt(EXTRA_TYPE, type);
-        args.putParcelable(EXTRA_URI, uri);
+        args.putString(EXTRA_AUTHORITY, authority);
+        args.putString(EXTRA_ROOT_ID, rootId);
+        args.putString(EXTRA_DOC_ID, docId);
+        args.putString(EXTRA_QUERY, query);
 
         final DirectoryFragment fragment = new DirectoryFragment();
         fragment.setArguments(args);
@@ -160,9 +167,8 @@
         super.onActivityCreated(savedInstanceState);
 
         final Context context = getActivity();
-        final Uri uri = getArguments().getParcelable(EXTRA_URI);
 
-        mAdapter = new DocumentsAdapter(uri.getAuthority());
+        mAdapter = new DocumentsAdapter();
         mType = getArguments().getInt(EXTRA_TYPE);
 
         mCallbacks = new LoaderCallbacks<DirectoryResult>() {
@@ -170,15 +176,26 @@
             public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
                 final State state = getDisplayState(DirectoryFragment.this);
 
-                Uri contentsUri;
-                if (mType == TYPE_NORMAL) {
-                    contentsUri = DocumentsContract.buildChildDocumentsUri(
-                            uri.getAuthority(), DocumentsContract.getDocumentId(uri));
-                } else {
-                    contentsUri = uri;
-                }
+                final String authority = getArguments().getString(EXTRA_AUTHORITY);
+                final String rootId = getArguments().getString(EXTRA_ROOT_ID);
+                final String docId = getArguments().getString(EXTRA_DOC_ID);
+                final String query = getArguments().getString(EXTRA_QUERY);
 
-                return new DirectoryLoader(context, contentsUri, state.sortOrder);
+                Uri contentsUri;
+                switch (mType) {
+                    case TYPE_NORMAL:
+                        contentsUri = DocumentsContract.buildChildDocumentsUri(authority, docId);
+                        return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
+                    case TYPE_SEARCH:
+                        contentsUri = DocumentsContract.buildSearchDocumentsUri(
+                                authority, docId, query);
+                        return new DirectoryLoader(context, rootId, contentsUri, state.sortOrder);
+                    case TYPE_RECENT_OPEN:
+                        return new RecentLoader(context);
+                    default:
+                        throw new IllegalStateException("Unknown type " + mType);
+
+                }
             }
 
             @Override
@@ -246,8 +263,7 @@
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             final Cursor cursor = mAdapter.getItem(position);
-            final Uri uri = getArguments().getParcelable(EXTRA_URI);
-            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
+            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
             if (mFilter.apply(doc)) {
                 ((DocumentsActivity) getActivity()).onDocumentPicked(doc);
             }
@@ -285,8 +301,7 @@
             for (int i = 0; i < size; i++) {
                 if (checked.valueAt(i)) {
                     final Cursor cursor = mAdapter.getItem(checked.keyAt(i));
-                    final Uri uri = getArguments().getParcelable(EXTRA_URI);
-                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(uri, cursor);
+                    final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
                     docs.add(doc);
                 }
             }
@@ -401,14 +416,8 @@
     }
 
     private class DocumentsAdapter extends BaseAdapter {
-        private final String mAuthority;
-
         private Cursor mCursor;
 
-        public DocumentsAdapter(String authority) {
-            mAuthority = authority;
-        }
-
         public void swapCursor(Cursor cursor) {
             mCursor = cursor;
 
@@ -443,6 +452,8 @@
 
             final Cursor cursor = getItem(position);
 
+            final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
+            final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
             final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
             final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
             final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
@@ -466,7 +477,7 @@
             }
 
             if ((docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0) {
-                final Uri uri = DocumentsContract.buildDocumentUri(mAuthority, docId);
+                final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
                 final Bitmap cachedResult = thumbs.get(uri);
                 if (cachedResult != null) {
                     icon.setImageBitmap(cachedResult);
@@ -477,19 +488,27 @@
                     task.execute(uri);
                 }
             } else if (docIcon != 0) {
-                icon.setImageDrawable(DocumentInfo.loadIcon(context, mAuthority, docIcon));
+                icon.setImageDrawable(DocumentInfo.loadIcon(context, docAuthority, docIcon));
             } else {
                 icon.setImageDrawable(RootsCache.resolveDocumentIcon(context, docMimeType));
             }
 
             title.setText(docDisplayName);
 
-            icon1.setVisibility(View.GONE);
-            if (docSummary != null) {
-                summary.setText(docSummary);
+            if (mType == TYPE_RECENT_OPEN) {
+                final RootInfo root = roots.getRoot(docAuthority, docRootId);
+                icon1.setVisibility(View.VISIBLE);
+                icon1.setImageDrawable(root.loadIcon(context));
+                summary.setText(root.getDirectoryString());
                 summary.setVisibility(View.VISIBLE);
             } else {
-                summary.setVisibility(View.INVISIBLE);
+                icon1.setVisibility(View.GONE);
+                if (docSummary != null) {
+                    summary.setText(docSummary);
+                    summary.setVisibility(View.VISIBLE);
+                } else {
+                    summary.setVisibility(View.INVISIBLE);
+                }
             }
 
             if (summaryGrid != null) {
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index fa674d5..3f016b5 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -48,14 +48,16 @@
 public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
     private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
 
+    private final String mRootId;
     private final Uri mUri;
     private final int mSortOrder;
 
     private CancellationSignal mSignal;
     private DirectoryResult mResult;
 
-    public DirectoryLoader(Context context, Uri uri, int sortOrder) {
+    public DirectoryLoader(Context context, String rootId, Uri uri, int sortOrder) {
         super(context);
+        mRootId = rootId;
         mUri = uri;
         mSortOrder = sortOrder;
     }
@@ -69,12 +71,16 @@
             mSignal = new CancellationSignal();
         }
         final DirectoryResult result = new DirectoryResult();
+        final String authority = mUri.getAuthority();
         try {
             result.client = getContext()
-                    .getContentResolver().acquireUnstableContentProviderClient(mUri.getAuthority());
+                    .getContentResolver().acquireUnstableContentProviderClient(authority);
             final Cursor cursor = result.client.query(
-                    mUri, null, null, null, getQuerySortOrder(), mSignal);
-            result.cursor = new SortingCursorWrapper(cursor, mSortOrder);
+                    mUri, null, null, null, getQuerySortOrder(mSortOrder), mSignal);
+            final Cursor withRoot = new RootCursorWrapper(mUri.getAuthority(), mRootId, cursor, -1);
+            final Cursor sorted = new SortingCursorWrapper(withRoot, mSortOrder);
+
+            result.cursor = sorted;
             result.cursor.registerContentObserver(mObserver);
         } catch (Exception e) {
             result.exception = e;
@@ -149,8 +155,8 @@
         getContext().getContentResolver().unregisterContentObserver(mObserver);
     }
 
-    private String getQuerySortOrder() {
-        switch (mSortOrder) {
+    public static String getQuerySortOrder(int sortOrder) {
+        switch (sortOrder) {
             case SORT_ORDER_DISPLAY_NAME:
                 return Document.COLUMN_DISPLAY_NAME + " ASC";
             case SORT_ORDER_LAST_MODIFIED:
diff --git a/src/com/android/documentsui/RecentLoader.java b/src/com/android/documentsui/RecentLoader.java
new file mode 100644
index 0000000..5f6fd13
--- /dev/null
+++ b/src/com/android/documentsui/RecentLoader.java
@@ -0,0 +1,253 @@
+/*
+ * 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.DocumentsActivity.TAG;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
+import android.util.Log;
+
+import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.model.RootInfo;
+import com.google.android.collect.Maps;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.AbstractFuture;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
+
+    public static final int MAX_OUTSTANDING_RECENTS = 2;
+
+    /**
+     * Time to wait for first pass to complete before returning partial results.
+     */
+    public static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;
+
+    /**
+     * Maximum documents from a single root.
+     */
+    public static final int MAX_DOCS_FROM_ROOT = 24;
+
+    private static final ExecutorService sExecutor = buildExecutor();
+
+    /**
+     * Create a bounded thread pool for fetching recents; it creates threads as
+     * needed (up to maximum) and reclaims them when finished.
+     */
+    private static ExecutorService buildExecutor() {
+        // Create a bounded thread pool for fetching recents; it creates
+        // threads as needed (up to maximum) and reclaims them when finished.
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                MAX_OUTSTANDING_RECENTS, MAX_OUTSTANDING_RECENTS, 10, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>());
+        executor.allowCoreThreadTimeOut(true);
+        return executor;
+    }
+
+    private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
+
+    private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;
+
+    private CountDownLatch mFirstPassLatch;
+    private volatile boolean mFirstPassDone;
+
+    private DirectoryResult mResult;
+
+    // TODO: create better transfer of ownership around cursor to ensure its
+    // closed in all edge cases.
+
+    public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable {
+        public final String authority;
+        public final String rootId;
+
+        private Cursor mWithRoot;
+
+        public RecentTask(String authority, String rootId) {
+            this.authority = authority;
+            this.rootId = rootId;
+        }
+
+        @Override
+        public void run() {
+            if (isCancelled()) return;
+
+            final ContentResolver resolver = getContext().getContentResolver();
+            final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                    authority);
+            try {
+                final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
+                final Cursor cursor = client.query(
+                        uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
+                mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
+                set(mWithRoot);
+
+                mFirstPassLatch.countDown();
+                if (mFirstPassDone) {
+                    onContentChanged();
+                }
+
+            } catch (Exception e) {
+                setException(e);
+            } finally {
+                ContentProviderClient.closeQuietly(client);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            IoUtils.closeQuietly(mWithRoot);
+        }
+    }
+
+    public RecentLoader(Context context) {
+        super(context);
+    }
+
+    @Override
+    public DirectoryResult loadInBackground() {
+        if (mFirstPassLatch == null) {
+            // First time through we kick off all the recent tasks, and wait
+            // around to see if everyone finishes quickly.
+
+            final RootsCache roots = DocumentsApplication.getRootsCache(getContext());
+            for (RootInfo root : roots.getRoots()) {
+                if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
+                    final RecentTask task = new RecentTask(root.authority, root.rootId);
+                    mTasks.put(root, task);
+                }
+            }
+
+            mFirstPassLatch = new CountDownLatch(mTasks.size());
+            for (RecentTask task : mTasks.values()) {
+                sExecutor.execute(task);
+            }
+
+            try {
+                mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
+                mFirstPassDone = true;
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        // Collect all finished tasks
+        List<Cursor> cursors = Lists.newArrayList();
+        for (RecentTask task : mTasks.values()) {
+            if (task.isDone()) {
+                try {
+                    cursors.add(task.get());
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                } catch (ExecutionException e) {
+                    Log.w(TAG, "Failed to load " + task.authority + ", " + task.rootId, e);
+                }
+            }
+        }
+
+        final DirectoryResult result = new DirectoryResult();
+        if (cursors.size() > 0) {
+            final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
+            final SortingCursorWrapper sorted = new SortingCursorWrapper(
+                    merged, State.SORT_ORDER_LAST_MODIFIED) {
+                @Override
+                public void close() {
+                    // Ignored, since we manage cursor lifecycle internally
+                }
+            };
+            result.cursor = sorted;
+        }
+        return result;
+    }
+
+    @Override
+    public void cancelLoadInBackground() {
+        super.cancelLoadInBackground();
+    }
+
+    @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();
+
+        for (RecentTask task : mTasks.values()) {
+            IoUtils.closeQuietly(task);
+        }
+
+        IoUtils.closeQuietly(mResult);
+        mResult = null;
+    }
+}
diff --git a/src/com/android/documentsui/RootCursorWrapper.java b/src/com/android/documentsui/RootCursorWrapper.java
new file mode 100644
index 0000000..d0e5ff6
--- /dev/null
+++ b/src/com/android/documentsui/RootCursorWrapper.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.database.AbstractCursor;
+import android.database.Cursor;
+
+/**
+ * Cursor wrapper that adds columns to identify which root a document came from.
+ */
+public class RootCursorWrapper extends AbstractCursor {
+    private final String mAuthority;
+    private final String mRootId;
+
+    private final Cursor mCursor;
+    private final int mCount;
+
+    private final String[] mColumnNames;
+
+    private final int mAuthorityIndex;
+    private final int mRootIdIndex;
+
+    public static final String COLUMN_AUTHORITY = "android:authority";
+    public static final String COLUMN_ROOT_ID = "android:rootId";
+
+    public RootCursorWrapper(String authority, String rootId, Cursor cursor, int maxCount) {
+        mAuthority = authority;
+        mRootId = rootId;
+        mCursor = cursor;
+
+        final int count = cursor.getCount();
+        if (maxCount > 0 && count > maxCount) {
+            mCount = maxCount;
+        } else {
+            mCount = count;
+        }
+
+        if (cursor.getColumnIndex(COLUMN_AUTHORITY) != -1
+                || cursor.getColumnIndex(COLUMN_ROOT_ID) != -1) {
+            throw new IllegalArgumentException("Cursor contains internal columns!");
+        }
+        final String[] before = cursor.getColumnNames();
+        mColumnNames = new String[before.length + 2];
+        System.arraycopy(before, 0, mColumnNames, 0, before.length);
+        mAuthorityIndex = before.length;
+        mRootIdIndex = before.length + 1;
+        mColumnNames[mAuthorityIndex] = COLUMN_AUTHORITY;
+        mColumnNames[mRootIdIndex] = COLUMN_ROOT_ID;
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        mCursor.close();
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        return mCursor.moveToPosition(newPosition);
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mColumnNames;
+    }
+
+    @Override
+    public int getCount() {
+        return mCount;
+    }
+
+    @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) {
+        if (column == mAuthorityIndex) {
+            return mAuthority;
+        } else if (column == mRootIdIndex) {
+            return mRootId;
+        } else {
+            return mCursor.getString(column);
+        }
+    }
+
+    @Override
+    public int getType(int column) {
+        return mCursor.getType(column);
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return mCursor.isNull(column);
+    }
+
+}
diff --git a/src/com/android/documentsui/RootsCache.java b/src/com/android/documentsui/RootsCache.java
index f67c309..ac3b740 100644
--- a/src/com/android/documentsui/RootsCache.java
+++ b/src/com/android/documentsui/RootsCache.java
@@ -50,7 +50,7 @@
     // TODO: cache roots in local provider to avoid spinning up backends
     // TODO: root updates should trigger UI refresh
 
-    private static final boolean RECENTS_ENABLED = false;
+    private static final boolean RECENTS_ENABLED = true;
 
     private final Context mContext;
 
@@ -126,6 +126,16 @@
     }
 
     @GuardedBy("ActivityThread")
+    public RootInfo getRoot(String authority, String rootId) {
+        for (RootInfo root : mRoots) {
+            if (Objects.equal(root.authority, authority) && Objects.equal(root.rootId, rootId)) {
+                return root;
+            }
+        }
+        return null;
+    }
+
+    @GuardedBy("ActivityThread")
     public RootInfo getRecentsRoot() {
         return mRecentsRoot;
     }
diff --git a/src/com/android/documentsui/SortingCursorWrapper.java b/src/com/android/documentsui/SortingCursorWrapper.java
index 257c106..b434a35 100644
--- a/src/com/android/documentsui/SortingCursorWrapper.java
+++ b/src/com/android/documentsui/SortingCursorWrapper.java
@@ -54,11 +54,6 @@
                 throw new IllegalArgumentException();
         }
 
-        final int mimeTypeIndex = cursor.getColumnIndex(Document.COLUMN_MIME_TYPE);
-        final int displayNameIndex = cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME);
-        final int lastModifiedIndex = cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED);
-        final int sizeIndex = cursor.getColumnIndex(Document.COLUMN_SIZE);
-
         cursor.moveToPosition(-1);
         for (int i = 0; i < count; i++) {
             cursor.moveToNext();
@@ -66,8 +61,10 @@
 
             switch (sortOrder) {
                 case SORT_ORDER_DISPLAY_NAME:
-                    final String mimeType = cursor.getString(mimeTypeIndex);
-                    final String displayName = cursor.getString(displayNameIndex);
+                    final String mimeType = cursor.getString(
+                            cursor.getColumnIndex(Document.COLUMN_MIME_TYPE));
+                    final String displayName = cursor.getString(
+                            cursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
                     if (Document.MIME_TYPE_DIR.equals(mimeType)) {
                         mValueString[i] = '\001' + displayName;
                     } else {
@@ -75,10 +72,11 @@
                     }
                     break;
                 case SORT_ORDER_LAST_MODIFIED:
-                    mValueLong[i] = cursor.getLong(lastModifiedIndex);
+                    mValueLong[i] = cursor.getLong(
+                            cursor.getColumnIndex(Document.COLUMN_LAST_MODIFIED));
                     break;
                 case SORT_ORDER_SIZE:
-                    mValueLong[i] = cursor.getLong(sizeIndex);
+                    mValueLong[i] = cursor.getLong(cursor.getColumnIndex(Document.COLUMN_SIZE));
                     break;
             }
         }
diff --git a/src/com/android/documentsui/model/DocumentInfo.java b/src/com/android/documentsui/model/DocumentInfo.java
index feccadc..7721bcc 100644
--- a/src/com/android/documentsui/model/DocumentInfo.java
+++ b/src/com/android/documentsui/model/DocumentInfo.java
@@ -27,6 +27,7 @@
 import android.provider.DocumentsContract.Document;
 
 import com.android.documentsui.RecentsProvider;
+import com.android.documentsui.RootCursorWrapper;
 
 import libcore.io.IoUtils;
 
@@ -101,9 +102,9 @@
         out.writeInt(icon);
     }
 
-    public static DocumentInfo fromDirectoryCursor(Uri parent, Cursor cursor) {
+    public static DocumentInfo fromDirectoryCursor(Cursor cursor) {
         final DocumentInfo doc = new DocumentInfo();
-        final String authority = parent.getAuthority();
+        final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
         final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
         doc.uri = DocumentsContract.buildDocumentUri(authority, docId);
         doc.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
diff --git a/src/com/android/documentsui/model/RootInfo.java b/src/com/android/documentsui/model/RootInfo.java
index 9728838..189284b 100644
--- a/src/com/android/documentsui/model/RootInfo.java
+++ b/src/com/android/documentsui/model/RootInfo.java
@@ -25,6 +25,8 @@
 import android.graphics.drawable.Drawable;
 import android.provider.DocumentsContract.Root;
 
+import java.util.Objects;
+
 /**
  * Representation of a {@link Root}.
  */
@@ -56,4 +58,23 @@
     public Drawable loadIcon(Context context) {
         return DocumentInfo.loadIcon(context, authority, icon);
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof RootInfo) {
+            final RootInfo root = (RootInfo) o;
+            return Objects.equals(authority, root.authority) && Objects.equals(rootId, root.rootId);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(authority, rootId);
+    }
+
+    public String getDirectoryString() {
+        return (summary != null) ? summary : title;
+    }
 }