Merge "Always pass DragEvents to the underlying DirFragment's ItemDragListener." into nyc-andromeda-dev
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml
index c865aff..3198be3 100644
--- a/res/values-uz-rUZ/strings.xml
+++ b/res/values-uz-rUZ/strings.xml
@@ -122,7 +122,7 @@
     <string name="open_external_dialog_root_request" msgid="6776729293982633">"<xliff:g id="APPNAME"><b>^1</b></xliff:g> ilovasiga <xliff:g id="STORAGE"><i>^2</i></xliff:g> xotirasidagi ma’lumotlardan, jumladan, rasmlar va videolardan foydalanishiga ruxsat berilsinmi?"</string>
     <string name="never_ask_again" msgid="525908236522201138">"Boshqa so‘ralmasin"</string>
     <string name="allow" msgid="1275746941353040309">"Ruxsat berish"</string>
-    <string name="deny" msgid="5127201668078153379">"Rad qilish"</string>
+    <string name="deny" msgid="5127201668078153379">"Rad etish"</string>
     <plurals name="elements_selected" formatted="false" msgid="4448165978637163692">
       <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> ta tanlandi</item>
       <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> ta tanlandi</item>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 81463d8..f867a02 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -37,6 +37,7 @@
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.files.LauncherActivity;
+import com.android.documentsui.files.OpenUriForViewTask;
 import com.android.documentsui.roots.LoadRootTask;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.sidebar.EjectRootTask;
@@ -53,21 +54,25 @@
     protected final T mActivity;
     protected final State mState;
     protected final RootsAccess mRoots;
+    protected final DocumentsAccess mDocs;
     protected final Lookup<String, Executor> mExecutors;
 
     public AbstractActionHandler(
             T activity,
             State state,
             RootsAccess roots,
+            DocumentsAccess docs,
             Lookup<String, Executor> executors) {
 
         assert(activity != null);
         assert(state != null);
         assert(roots != null);
+        assert(docs != null);
 
         mActivity = activity;
         mState = state;
         mRoots = roots;
+        mDocs = docs;
         mExecutors = executors;
     }
 
@@ -138,9 +143,15 @@
     }
 
     @Override
+    public final void loadDocument(Uri uri) {
+        new OpenUriForViewTask<>(mActivity, mState, mRoots, mDocs, uri)
+                .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
+    }
+
+    @Override
     public final void loadRoot(Uri uri) {
-        new LoadRootTask<>(mActivity, mRoots, mState, uri).executeOnExecutor(
-                mExecutors.lookup(uri.getAuthority()));
+        new LoadRootTask<>(mActivity, mRoots, mState, uri)
+                .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
     }
 
     protected final void loadHomeDir() {
@@ -152,10 +163,13 @@
      * from our concrete activity implementations.
      */
     public interface CommonAddons {
-       void onRootPicked(RootInfo root);
-       // TODO: Move this to PickAddons.
-       void onDocumentsPicked(List<DocumentInfo> docs);
-       void onDocumentPicked(DocumentInfo doc, Model model);
        void refreshCurrentRootAndDirectory(@AnimationType int anim);
+       void onRootPicked(RootInfo root);
+       // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
+       void onDocumentsPicked(List<DocumentInfo> docs);
+       void onDocumentPicked(DocumentInfo doc);
+       void openContainerDocument(DocumentInfo doc);
+       RootInfo getCurrentRoot();
+       DocumentInfo getCurrentDirectory();
     }
 }
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index f4aa0ca..0c33f6c 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -52,6 +52,8 @@
 
     void loadRoot(Uri uri);
 
+    void loadDocument(Uri uri);
+
     void openInNewWindow(DocumentStack path);
 
     void pasteIntoFolder(RootInfo root);
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 9f5e66c..5d1c40c 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -47,7 +47,6 @@
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.KeyEvent;
-import android.view.KeyboardShortcutGroup;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -69,6 +68,7 @@
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.MultiSelectManager;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsCache;
 import com.android.documentsui.sidebar.RootsFragment;
 import com.android.documentsui.sorting.SortController;
@@ -210,7 +210,6 @@
 
             @Override
             public void onSearchViewChanged(boolean opened) {
-                mState.sortModel.setSortEnabled(!opened);
                 mNavigator.update();
             }
         };
@@ -382,7 +381,9 @@
                 && !root.isDownloads();
     }
 
-    protected void openContainerDocument(DocumentInfo doc) {
+    // TODO: Move to ActionHandler...currently blocked by the notifyDirectory....business.
+    @Override
+    public void openContainerDocument(DocumentInfo doc) {
         assert(doc.isContainer());
 
         notifyDirectoryNavigated(doc.derivedUri);
@@ -548,6 +549,7 @@
         }
     }
 
+    @Override
     public DocumentInfo getCurrentDirectory() {
         return mState.stack.peek();
     }
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index ab79d01..d2c7d10 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -25,7 +25,6 @@
 import android.os.CancellationSignal;
 import android.os.OperationCanceledException;
 import android.os.RemoteException;
-import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.util.Log;
 
@@ -35,8 +34,6 @@
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.sorting.SortModel;
 
-import java.io.FileNotFoundException;
-
 import libcore.io.IoUtils;
 
 public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
@@ -86,7 +83,6 @@
 
         final DirectoryResult result = new DirectoryResult();
         result.doc = mDoc;
-        result.sortModel = mModel;
 
         ContentProviderClient client = null;
         Cursor cursor;
@@ -107,6 +103,8 @@
                 cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
             }
 
+            cursor = mModel.sortCursor(cursor);
+
             result.client = client;
             result.cursor = cursor;
         } catch (Exception e) {
diff --git a/src/com/android/documentsui/DirectoryResult.java b/src/com/android/documentsui/DirectoryResult.java
index 32910ce..9e4d9f3 100644
--- a/src/com/android/documentsui/DirectoryResult.java
+++ b/src/com/android/documentsui/DirectoryResult.java
@@ -20,7 +20,6 @@
 import android.database.Cursor;
 
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.sorting.SortModel;
 
 import libcore.io.IoUtils;
 
@@ -29,7 +28,6 @@
     public Cursor cursor;
     public Exception exception;
     public DocumentInfo doc;
-    public SortModel sortModel;
 
     @Override
     public void close() {
@@ -37,6 +35,6 @@
         ContentProviderClient.releaseQuietly(client);
         cursor = null;
         client = null;
-        sortModel = null;
+        doc = null;
     }
 }
diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java
new file mode 100644
index 0000000..cd9c4c9
--- /dev/null
+++ b/src/com/android/documentsui/DocumentsAccess.java
@@ -0,0 +1,80 @@
+/*
+ * 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.annotation.Nullable;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.util.Log;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.RootInfo;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Provides synchronous access to {@link DocumentInfo} instances given some identifying information.
+ */
+public interface DocumentsAccess {
+
+    @Nullable DocumentInfo getRootDocument(RootInfo root);
+    @Nullable DocumentInfo getRootDocument(Uri uri);
+    @Nullable DocumentInfo getDocument(Uri uri);
+
+    public static DocumentsAccess create(Context context) {
+        return new RuntimeDocumentAccess(context);
+    }
+
+    public final class RuntimeDocumentAccess implements DocumentsAccess {
+
+        private static final String TAG = "DocumentAccess";
+
+        private final Context mContext;
+
+        private RuntimeDocumentAccess(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public @Nullable DocumentInfo getRootDocument(RootInfo root) {
+            return getRootDocument(
+                    DocumentsContract.buildDocumentUri(root.authority, root.documentId));
+        }
+
+        @Override
+        public @Nullable DocumentInfo getRootDocument(Uri uri) {
+            try {
+                return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
+            } catch (FileNotFoundException e) {
+                Log.w(TAG, "Failed to find root", e);
+                return null;
+            }
+        }
+
+        @Override
+        public DocumentInfo getDocument(Uri uri) {
+            try {
+                return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
+            } catch (FileNotFoundException e) {
+                Log.w(TAG, "Couldn't create DocumentInfo for uri: " + uri);
+            }
+
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index a95bf72..4f97111 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -130,6 +130,7 @@
     }
 
     interface Environment {
+        @Deprecated  // Use CommonAddones#getCurrentRoot
         RootInfo getCurrentRoot();
         String getDrawerTitle();
         @Deprecated  // Use CommonAddones#refreshCurrentRootAndDirectory
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index dc4d9f2..6ce1896 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -40,10 +40,10 @@
 import com.android.documentsui.roots.RootsAccess;
 import com.android.internal.annotations.GuardedBy;
 
-import com.google.common.util.concurrent.AbstractFuture;
-
 import libcore.io.IoUtils;
 
+import com.google.common.util.concurrent.AbstractFuture;
+
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -180,7 +180,6 @@
         }
 
         final DirectoryResult result = new DirectoryResult();
-        result.sortModel = mState.sortModel;
 
         final Cursor merged;
         if (cursors.size() > 0) {
@@ -190,13 +189,16 @@
             merged = new MatrixCursor(new String[0]);
         }
 
+
+        final Cursor sorted = mState.sortModel.sortCursor(merged);
+
         // Tell the UI if this is an in-progress result. When loading is complete, another update is
         // sent with EXTRA_LOADING set to false.
         Bundle extras = new Bundle();
         extras.putBoolean(DocumentsContract.EXTRA_LOADING, !allDone);
-        merged.setExtras(extras);
+        sorted.setExtras(extras);
 
-        result.cursor = merged;
+        result.cursor = sorted;
 
         return result;
     }
diff --git a/src/com/android/documentsui/base/RootInfo.java b/src/com/android/documentsui/base/RootInfo.java
index 1b4656a..2c32b9e 100644
--- a/src/com/android/documentsui/base/RootInfo.java
+++ b/src/com/android/documentsui/base/RootInfo.java
@@ -37,6 +37,7 @@
 
 import com.android.documentsui.IconUtils;
 import com.android.documentsui.R;
+import com.android.documentsui.roots.RootsAccess;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -349,8 +350,9 @@
     }
 
     /**
-     * Gets the {@link DocumentInfo} of the root folder of this root.
+     * @deprecate use {@link RootsAccess#getRootDocumentBlocking}.
      */
+    @Deprecated
     public @Nullable DocumentInfo getRootDocumentBlocking(Context context) {
         try {
             final Uri uri = DocumentsContract.buildDocumentUri(authority, documentId);
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 32ea333..a5c2564 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -659,7 +659,7 @@
         if (docs.size() > 1) {
             activity.onDocumentsPicked(docs);
         } else {
-            activity.onDocumentPicked(docs.get(0), mModel);
+            activity.onDocumentPicked(docs.get(0));
         }
     }
 
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 7973101..6c08859 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -16,7 +16,6 @@
 
 package com.android.documentsui.dirlist;
 
-import static com.android.documentsui.base.DocumentInfo.getCursorLong;
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
 import static com.android.documentsui.base.Shared.DEBUG;
 
@@ -34,11 +33,8 @@
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.EventListener;
-import com.android.documentsui.base.Shared;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.roots.RootCursorWrapper;
-import com.android.documentsui.sorting.SortDimension;
-import com.android.documentsui.sorting.SortModel;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -60,12 +56,7 @@
     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 #mSortModel}
-     */
     private String mIds[] = new String[0];
-    private SortModel mSortModel;
 
     @Nullable String info;
     @Nullable String error;
@@ -126,7 +117,6 @@
 
         mCursor = result.cursor;
         mCursorCount = mCursor.getCount();
-        mSortModel = result.sortModel;
         doc = result.doc;
 
         updateModelData();
@@ -151,24 +141,7 @@
      * according to the current sort order.
      */
     private void updateModelData() {
-        int[] positions = new int[mCursorCount];
         mIds = new String[mCursorCount];
-        boolean[] isDirs = new boolean[mCursorCount];
-        String[] displayNames = null;
-        long[] longValues = null;
-
-        final int id = mSortModel.getSortedDimensionId();
-        switch (id) {
-            case SortModel.SORT_DIMENSION_ID_TITLE:
-                displayNames = new String[mCursorCount];
-                break;
-            case SortModel.SORT_DIMENSION_ID_DATE:
-            case SortModel.SORT_DIMENSION_ID_SIZE:
-                longValues = new long[mCursorCount];
-                break;
-        }
-
-        String mimeType;
 
         mCursor.moveToPosition(-1);
         for (int pos = 0; pos < mCursorCount; ++pos) {
@@ -176,246 +149,25 @@
                 Log.e(TAG, "Fail to move cursor to next pos: " + pos);
                 return;
             }
-            positions[pos] = pos;
-
             // 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.
             // If the cursor is a merged cursor over multiple authorities, then prefix the ids
             // with the authority to avoid collisions.
             if (mCursor instanceof MergeCursor) {
-                mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) + "|" +
-                        getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
+                mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
+                        + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
             } else {
                 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
             }
-
-            mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
-            isDirs[pos] = Document.MIME_TYPE_DIR.equals(mimeType);
-
-            switch(id) {
-                case SortModel.SORT_DIMENSION_ID_TITLE:
-                    final String displayName = getCursorString(
-                            mCursor, Document.COLUMN_DISPLAY_NAME);
-                    displayNames[pos] = displayName;
-                    break;
-                case SortModel.SORT_DIMENSION_ID_DATE:
-                    longValues[pos] = getLastModified(mCursor);
-                    break;
-                case SortModel.SORT_DIMENSION_ID_SIZE:
-                    longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
-                    break;
-            }
-        }
-
-        final SortDimension dimension = mSortModel.getDimensionById(id);
-        switch (id) {
-            case SortModel.SORT_DIMENSION_ID_TITLE:
-                binarySort(displayNames, isDirs, positions, mIds, dimension.getSortDirection());
-                break;
-            case SortModel.SORT_DIMENSION_ID_DATE:
-            case SortModel.SORT_DIMENSION_ID_SIZE:
-                binarySort(longValues, isDirs, positions, mIds, dimension.getSortDirection());
-                break;
         }
 
         // Populate the positions.
         mPositions.clear();
         for (int i = 0; i < mCursorCount; ++i) {
-            mPositions.put(mIds[i], positions[i]);
+            mPositions.put(mIds[i], i);
         }
     }
 
-    /**
-     * Sorts model data. Takes three columns of index-corresponded data. The first column is the
-     * sort key. Rows are sorted in ascending alphabetical order on the sort key.
-     * Directories are always shown first. This code is based on TimSort.binarySort().
-     *
-     * @param sortKey Data is sorted in ascending alphabetical order.
-     * @param isDirs Array saying whether an item is a directory or not.
-     * @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,
-            @SortDimension.SortDirection int direction) {
-        final int count = positions.length;
-        for (int start = 1; start < count; start++) {
-            final int pivotPosition = positions[start];
-            final String pivotValue = sortKey[start];
-            final boolean pivotIsDir = isDirs[start];
-            final String pivotId = ids[start];
-
-            int left = 0;
-            int right = start;
-
-            while (left < right) {
-                int mid = (left + right) >>> 1;
-
-                // Directories always go in front.
-                int compare = 0;
-                final boolean rhsIsDir = isDirs[mid];
-                if (pivotIsDir && !rhsIsDir) {
-                    compare = -1;
-                } else if (!pivotIsDir && rhsIsDir) {
-                    compare = 1;
-                } else {
-                    final String lhs = pivotValue;
-                    final String rhs = sortKey[mid];
-                    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) {
-                    right = mid;
-                } else {
-                    left = mid + 1;
-                }
-            }
-
-            int n = start - left;
-            switch (n) {
-                case 2:
-                    positions[left + 2] = positions[left + 1];
-                    sortKey[left + 2] = sortKey[left + 1];
-                    isDirs[left + 2] = isDirs[left + 1];
-                    ids[left + 2] = ids[left + 1];
-                case 1:
-                    positions[left + 1] = positions[left];
-                    sortKey[left + 1] = sortKey[left];
-                    isDirs[left + 1] = isDirs[left];
-                    ids[left + 1] = ids[left];
-                    break;
-                default:
-                    System.arraycopy(positions, left, positions, left + 1, n);
-                    System.arraycopy(sortKey, left, sortKey, left + 1, n);
-                    System.arraycopy(isDirs, left, isDirs, left + 1, n);
-                    System.arraycopy(ids, left, ids, left + 1, n);
-            }
-
-            positions[left] = pivotPosition;
-            sortKey[left] = pivotValue;
-            isDirs[left] = pivotIsDir;
-            ids[left] = pivotId;
-        }
-    }
-
-    /**
-     * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort
-     * key, and the second is an array of mime types. The rows are first bucketed by mime type
-     * (directories vs documents) and then each bucket is sorted independently in descending
-     * numerical order on the sort key. This code is based on TimSort.binarySort().
-     *
-     * @param sortKey Data is sorted in descending numerical order.
-     * @param isDirs Array saying whether an item is a directory or not.
-     * @param positions Cursor positions to be sorted.
-     * @param ids Model IDs to be sorted.
-     */
-    private static void binarySort(
-            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];
-            final long pivotValue = sortKey[start];
-            final boolean pivotIsDir = isDirs[start];
-            final String pivotId = ids[start];
-
-            int left = 0;
-            int right = start;
-
-            while (left < right) {
-                int mid = ((left + right) >>> 1);
-
-                // Directories always go in front.
-                int compare = 0;
-                final boolean rhsIsDir = isDirs[mid];
-                if (pivotIsDir && !rhsIsDir) {
-                    compare = -1;
-                } else if (!pivotIsDir && rhsIsDir) {
-                    compare = 1;
-                } else {
-                    final long lhs = pivotValue;
-                    final long rhs = sortKey[mid];
-                    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
-                // will yield stable results even if incoming items are continually shuffling and
-                // have identical numerical sort keys.  One common example of this scenario is seen
-                // when sorting a set of active downloads by mod time.
-                if (compare == 0) {
-                    compare = pivotId.compareTo(ids[mid]);
-                }
-
-                if (compare < 0) {
-                    right = mid;
-                } else {
-                    left = mid + 1;
-                }
-            }
-
-            int n = start - left;
-            switch (n) {
-                case 2:
-                    positions[left + 2] = positions[left + 1];
-                    sortKey[left + 2] = sortKey[left + 1];
-                    isDirs[left + 2] = isDirs[left + 1];
-                    ids[left + 2] = ids[left + 1];
-                case 1:
-                    positions[left + 1] = positions[left];
-                    sortKey[left + 1] = sortKey[left];
-                    isDirs[left + 1] = isDirs[left];
-                    ids[left + 1] = ids[left];
-                    break;
-                default:
-                    System.arraycopy(positions, left, positions, left + 1, n);
-                    System.arraycopy(sortKey, left, sortKey, left + 1, n);
-                    System.arraycopy(isDirs, left, isDirs, left + 1, n);
-                    System.arraycopy(ids, left, ids, left + 1, n);
-            }
-
-            positions[left] = pivotPosition;
-            sortKey[left] = pivotValue;
-            isDirs[left] = pivotIsDir;
-            ids[left] = pivotId;
-        }
-    }
-
-    /**
-     * @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 descending by date.
-     */
-    long getLastModified(Cursor cursor) {
-        long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
-        return (l == -1) ? Long.MAX_VALUE : l;
-    }
-
     public @Nullable Cursor getItem(String modelId) {
         Integer pos = mPositions.get(modelId);
         if (pos == null) {
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index d6c4f42..feb70d0 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -19,6 +19,7 @@
 import static com.android.documentsui.base.Shared.DEBUG;
 
 import android.app.Activity;
+import android.content.ActivityNotFoundException;
 import android.content.ClipData;
 import android.content.Intent;
 import android.net.Uri;
@@ -26,10 +27,9 @@
 import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.GetRootDocumentTask;
 import com.android.documentsui.Metrics;
-import com.android.documentsui.ProviderExecutor;
 import com.android.documentsui.base.ConfirmationCallback;
 import com.android.documentsui.base.ConfirmationCallback.Result;
 import com.android.documentsui.base.DocumentInfo;
@@ -47,6 +47,7 @@
 import com.android.documentsui.dirlist.MultiSelectManager;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.files.ActionHandler.Addons;
+import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.services.FileOperation;
 import com.android.documentsui.services.FileOperationService;
@@ -77,13 +78,14 @@
             T activity,
             State state,
             RootsAccess roots,
+            DocumentsAccess docs,
             Lookup<String, Executor> executors,
             DialogController dialogs,
             FragmentTuner tuner,
             DocumentClipper clipper,
             ClipStore clipStore) {
 
-        super(activity, state, roots, executors);
+        super(activity, state, roots, docs, executors);
 
         mDialogs = dialogs;
         mTuner = tuner;
@@ -101,7 +103,7 @@
                 mActivity::isDestroyed,
                 (DocumentInfo doc) -> mClipper.copyFromClipData(
                         root, doc, data, mDialogs::showFileOperationFailures)
-        ).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
+        ).executeOnExecutor(mExecutors.lookup(root.authority));
         return true;
     }
 
@@ -120,7 +122,7 @@
                 mActivity,
                 mActivity::isDestroyed,
                 (DocumentInfo doc) -> pasteIntoFolder(root, doc)
-        ).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
+        ).executeOnExecutor(mExecutors.lookup(root.authority));
     }
 
     private void pasteIntoFolder(RootInfo root, DocumentInfo doc) {
@@ -145,7 +147,7 @@
         }
 
         if (mTuner.isDocumentEnabled(doc.mimeType, doc.flags)) {
-            mActivity.onDocumentPicked(doc, mConfig.model);
+            onDocumentPicked(doc);
             mConfig.selectionMgr.clearSelection();
             return true;
         }
@@ -155,7 +157,7 @@
     @Override
     public boolean viewDocument(DocumentDetails details) {
         DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
-        return mActivity.viewDocument(doc);
+        return viewDocument(doc);
     }
 
     @Override
@@ -164,7 +166,7 @@
         if (doc.isContainer()) {
             return false;
         }
-        return mActivity.previewDocument(doc, mConfig.model);
+        return previewDocument(doc);
     }
 
     @Override
@@ -174,6 +176,7 @@
         assert(!selected.isEmpty());
 
         final DocumentInfo srcParent = mState.stack.peek();
+        assert(srcParent != null);
 
         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
         List<DocumentInfo> docs = model.getDocuments(selected);
@@ -268,8 +271,7 @@
         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             Uri uri = intent.getData();
             assert(uri != null);
-            new OpenUriForViewTask<>(mActivity, mState).executeOnExecutor(
-                    ProviderExecutor.forAuthority(uri.getAuthority()), uri);
+            loadDocument(uri);
             return true;
         }
 
@@ -290,6 +292,163 @@
         return false;
     }
 
+    public void showChooserForDoc(DocumentInfo doc) {
+        assert(!doc.isContainer());
+
+        if (manageDocument(doc)) {
+            Log.w(TAG, "Open with is not yet supported for managed doc.");
+            return;
+        }
+
+        Intent intent = Intent.createChooser(buildViewIntent(doc), null);
+        try {
+            mActivity.startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            mDialogs.showNoApplicationFound();
+        }
+    }
+
+    public void onDocumentPicked(DocumentInfo doc) {
+        if (doc.isContainer()) {
+            mActivity.openContainerDocument(doc);
+            return;
+        }
+
+        if (manageDocument(doc)) {
+            return;
+        }
+
+        if (previewDocument(doc)) {
+            return;
+        }
+
+        viewDocument(doc);
+    }
+
+    public boolean viewDocument(DocumentInfo doc) {
+        if (doc.isPartial()) {
+            Log.w(TAG, "Can't view partial file.");
+            return false;
+        }
+
+        if (doc.isContainer()) {
+            mActivity.openContainerDocument(doc);
+            return true;
+        }
+
+        // this is a redundant check.
+        if (manageDocument(doc)) {
+            return true;
+        }
+
+        // Fall back to traditional VIEW action...
+        Intent intent = buildViewIntent(doc);
+        if (DEBUG && intent.getClipData() != null) {
+            Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
+        }
+
+        try {
+            mActivity.startActivity(intent);
+            return true;
+        } catch (ActivityNotFoundException e) {
+            mDialogs.showNoApplicationFound();
+        }
+        return false;
+    }
+
+    public boolean previewDocument(DocumentInfo doc) {
+        if (doc.isPartial()) {
+            Log.w(TAG, "Can't view partial file.");
+            return false;
+        }
+
+        Intent intent = new QuickViewIntentBuilder(
+                mActivity.getPackageManager(),
+                mActivity.getResources(),
+                doc,
+                mConfig.model).build();
+
+        if (intent != null) {
+            // TODO: un-work around issue b/24963914. Should be fixed soon.
+            try {
+                mActivity.startActivity(intent);
+                return true;
+            } catch (SecurityException e) {
+                // Carry on to regular view mode.
+                Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
+            }
+        }
+
+        return false;
+    }
+
+    private boolean manageDocument(DocumentInfo doc) {
+        if (isManagedDownload(doc)) {
+            // First try managing the document; we expect manager to filter
+            // based on authority, so we don't grant.
+            Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
+            manage.setData(doc.derivedUri);
+            try {
+                mActivity.startActivity(manage);
+                return true;
+            } catch (ActivityNotFoundException ex) {
+                // Fall back to regular handling.
+            }
+        }
+
+        return false;
+    }
+
+    private boolean isManagedDownload(DocumentInfo doc) {
+        // Anything on downloads goes through the back through downloads manager
+        // (that's the MANAGE_DOCUMENT bit).
+        // This is done for two reasons:
+        // 1) The file in question might be a failed/queued or otherwise have some
+        //    specialized download handling.
+        // 2) For APKs, the download manager will add on some important security stuff
+        //    like origin URL.
+        // 3) For partial files, the download manager will offer to restart/retry downloads.
+
+        // All other files not on downloads, event APKs, would get no benefit from this
+        // treatment, thusly the "isDownloads" check.
+
+        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
+        // files in archives. Also, if the activity is already browsing a ZIP from downloads,
+        // then skip MANAGE_DOCUMENTS.
+        if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
+                && mState.stack.size() > 1) {
+            // viewing the contents of an archive.
+            return false;
+        }
+
+        // managament is only supported in downloads.
+        if (mActivity.getCurrentRoot().isDownloads()) {
+            // and only and only on APKs or partial files.
+            return "application/vnd.android.package-archive".equals(doc.mimeType)
+                    || doc.isPartial();
+        }
+
+        return false;
+    }
+
+    private Intent buildViewIntent(DocumentInfo doc) {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setDataAndType(doc.derivedUri, doc.mimeType);
+
+        // Downloads has traditionally added the WRITE permission
+        // in the TrampolineActivity. Since this behavior is long
+        // established, we set the same permission for non-managed files
+        // This ensures consistent behavior between the Downloads root
+        // and other roots.
+        int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+        if (doc.isWriteSupported()) {
+            flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+        }
+        intent.setFlags(flags);
+
+        return intent;
+    }
+
     ActionHandler<T> reset(Model model, MultiSelectManager selectionMgr) {
         mConfig.reset(model, selectionMgr);
         return this;
@@ -302,7 +461,8 @@
 
         public void reset(Model model, MultiSelectManager selectionMgr) {
             assert(model != null);
-            assert(selectionMgr != null);
+            // Allowed to be null in testing, for now.
+            // assert(selectionMgr != null);
 
             this.model = model;
             this.selectionMgr = selectionMgr;
@@ -310,7 +470,5 @@
     }
 
     public interface Addons extends CommonAddons {
-        boolean viewDocument(DocumentInfo doc);
-        boolean previewDocument(DocumentInfo doc, Model model);
     }
 }
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 2d1ed94..f3dfd77 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -21,13 +21,10 @@
 
 import android.app.Activity;
 import android.app.FragmentManager;
-import android.content.ActivityNotFoundException;
 import android.content.ClipData;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.provider.DocumentsContract;
-import android.support.design.widget.Snackbar;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -36,6 +33,7 @@
 import android.view.MenuItem;
 
 import com.android.documentsui.BaseActivity;
+import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -57,7 +55,6 @@
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.sidebar.RootsFragment;
 import com.android.documentsui.ui.DialogController;
-import com.android.documentsui.ui.Snackbars;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -104,6 +101,7 @@
                 this,
                 mState,
                 mRoots,
+                DocumentsAccess.create(this),
                 ProviderExecutor::forAuthority,
                 mDialogs,
                 mTuner,
@@ -252,22 +250,13 @@
         throw new UnsupportedOperationException();
     }
 
+    /**
+     * @deprecated use {@link ActionHandler#onDocumentPicked(DocumentInfo)}
+     * @param doc
+     */
     @Override
-    public void onDocumentPicked(DocumentInfo doc, Model model) {
-        if (doc.isContainer()) {
-            openContainerDocument(doc);
-            return;
-        }
-
-        if (manageDocument(doc)) {
-            return;
-        }
-
-        if (previewDocument(doc, model)) {
-            return;
-        }
-
-        viewDocument(doc);
+    public void onDocumentPicked(DocumentInfo doc) {
+        mActions.onDocumentPicked(doc);
     }
 
     @Override
@@ -283,138 +272,13 @@
         openContainerDocument(doc);
     }
 
+    /**
+     * @deprecated use {@link ActionHandler#showChooserForDoc(DocumentInfo)}
+     * @param doc
+     */
+    @Deprecated
     public void showChooserForDoc(DocumentInfo doc) {
-        assert(!doc.isContainer());
-
-        if (manageDocument(doc)) {
-            Log.w(TAG, "Open with is not yet supported for managed doc.");
-            return;
-        }
-
-        Intent intent = Intent.createChooser(buildViewIntent(doc), null);
-        try {
-            startActivity(intent);
-        } catch (ActivityNotFoundException e) {
-            Snackbars.makeSnackbar(
-                    this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
-        }
-    }
-
-    // TODO: Move to ActionHandler.
-    @Override
-    public boolean viewDocument(DocumentInfo doc) {
-        if (doc.isPartial()) {
-            Log.w(TAG, "Can't view partial file.");
-            return false;
-        }
-
-        if (doc.isContainer()) {
-            openContainerDocument(doc);
-            return true;
-        }
-
-        // this is a redundant check.
-        if (manageDocument(doc)) {
-            return true;
-        }
-
-        // Fall back to traditional VIEW action...
-        Intent intent = buildViewIntent(doc);
-        if (DEBUG && intent.getClipData() != null) {
-            Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
-        }
-
-        try {
-            startActivity(intent);
-            return true;
-        } catch (ActivityNotFoundException e) {
-            Snackbars.makeSnackbar(
-                    this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
-        }
-        return false;
-    }
-
-    // TODO: Move to ActionHandler.
-    @Override
-    public boolean previewDocument(DocumentInfo doc, Model model) {
-        if (doc.isPartial()) {
-            Log.w(TAG, "Can't view partial file.");
-            return false;
-        }
-
-        Intent intent = new QuickViewIntentBuilder(
-                getPackageManager(), getResources(), doc, model).build();
-
-        if (intent != null) {
-            // TODO: un-work around issue b/24963914. Should be fixed soon.
-            try {
-                startActivity(intent);
-                return true;
-            } catch (SecurityException e) {
-                // Carry on to regular view mode.
-                Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
-            }
-        }
-
-        return false;
-    }
-
-    private boolean manageDocument(DocumentInfo doc) {
-        if (isManagedDocument(doc)) {
-            // First try managing the document; we expect manager to filter
-            // based on authority, so we don't grant.
-            Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
-            manage.setData(doc.derivedUri);
-            try {
-                startActivity(manage);
-                return true;
-            } catch (ActivityNotFoundException ex) {
-                // Fall back to regular handling.
-            }
-        }
-
-        return false;
-    }
-
-    private boolean isManagedDocument(DocumentInfo doc) {
-        // Anything on downloads goes through the back through downloads manager
-        // (that's the MANAGE_DOCUMENT bit).
-        // This is done for two reasons:
-        // 1) The file in question might be a failed/queued or otherwise have some
-        //    specialized download handling.
-        // 2) For APKs, the download manager will add on some important security stuff
-        //    like origin URL.
-        // All other files not on downloads, event APKs, would get no benefit from this
-        // treatment, thusly the "isDownloads" check.
-
-        // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
-        // files in archives. Also, if the activity is already browsing a ZIP from downloads,
-        // then skip MANAGE_DOCUMENTS.
-        // Oh, and only launch for APKs.
-        final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction());
-        final boolean isInArchive = mState.stack.size() > 1;
-        return getCurrentRoot().isDownloads()
-                && "application/vnd.android.package-archive".equals(doc.mimeType)
-                && !isInArchive
-                && !isViewing;
-    }
-
-    private Intent buildViewIntent(DocumentInfo doc) {
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setDataAndType(doc.derivedUri, doc.mimeType);
-
-        // Downloads has traditionally added the WRITE permission
-        // in the TrampolineActivity. Since this behavior is long
-        // established, we set the same permission for non-managed files
-        // This ensures consistent behavior between the Downloads root
-        // and other roots.
-        int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
-        if (doc.isWriteSupported()) {
-            flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
-        }
-        intent.setFlags(flags);
-
-        return intent;
+        mActions.showChooserForDoc(doc);
     }
 
     @Override
diff --git a/src/com/android/documentsui/files/OpenUriForViewTask.java b/src/com/android/documentsui/files/OpenUriForViewTask.java
index f8424c1..2a830c5 100644
--- a/src/com/android/documentsui/files/OpenUriForViewTask.java
+++ b/src/com/android/documentsui/files/OpenUriForViewTask.java
@@ -20,7 +20,7 @@
 import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
-import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.PairedTask;
 import com.android.documentsui.base.RootInfo;
@@ -28,44 +28,58 @@
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.roots.RootsAccess;
 
-import java.io.FileNotFoundException;
 import java.util.Collection;
 
+import javax.annotation.Nullable;
+
 /**
  * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible
  * to know which root to select. Also, the stack doesn't contain intermediate directories.
  * It's primarly used for opening ZIP archives from Downloads app.
  */
-final class OpenUriForViewTask<T extends Activity & CommonAddons>
-        extends PairedTask<T, Uri, Void> {
+public final class OpenUriForViewTask<T extends Activity & CommonAddons>
+        extends PairedTask<T, Void, Void> {
 
+    private static final String TAG = "OpenUriForViewTask";
+
+    private final T mActivity;
     private final State mState;
-    public OpenUriForViewTask(T activity, State state) {
+    private final RootsAccess mRoots;
+    private final DocumentsAccess mDocs;
+    private final Uri mUri;
+
+    public OpenUriForViewTask(
+            T activity, State state, RootsAccess roots, DocumentsAccess docs, Uri uri) {
         super(activity);
+        mActivity = activity;
         mState = state;
+        mRoots = roots;
+        mDocs = docs;
+        mUri = uri;
     }
 
     @Override
-    public Void run(Uri... params) {
-        final Uri uri = params[0];
+    public Void run(Void... params) {
 
-        final RootsAccess rootsCache = DocumentsApplication.getRootsCache(mOwner);
-        final String authority = uri.getAuthority();
+        final String authority = mUri.getAuthority();
+        final Collection<RootInfo> roots = mRoots.getRootsForAuthorityBlocking(authority);
 
-        final Collection<RootInfo> roots =
-                rootsCache.getRootsForAuthorityBlocking(authority);
         if (roots.isEmpty()) {
-            Log.e(FilesActivity.TAG, "Failed to find root for the requested Uri: " + uri);
+            Log.e(TAG, "Failed to find root for the requested Uri: " + mUri);
             return null;
         }
 
+        assert(mState.stack.isEmpty());
+
+        // NOTE: There's no guarantee that this root will be the correct root for the doc.
         final RootInfo root = roots.iterator().next();
         mState.stack.root = root;
-        mState.stack.add(root.getRootDocumentBlocking(mOwner));
-        try {
-            mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri));
-        } catch (FileNotFoundException e) {
-            Log.e(FilesActivity.TAG, "Failed to resolve DocumentInfo from Uri: " + uri);
+        mState.stack.add(mDocs.getRootDocument(root));
+        @Nullable DocumentInfo doc = mDocs.getDocument(mUri);
+        if (doc == null) {
+            Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + mUri);
+        } else {
+            mState.stack.add(doc);
         }
 
         return null;
@@ -73,6 +87,6 @@
 
     @Override
     public void finish(Void result) {
-        mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+        mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index 75bb2c4..995d280 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -46,23 +46,34 @@
 /**
  * Provides support for gather a list of quick-viewable files into a quick view intent.
  */
-final class QuickViewIntentBuilder {
+public final class QuickViewIntentBuilder {
+
+    // trusted quick view package can be set via system property on debug builds.
+    // Unfortunately when the value is set, it interferes with testing.
+    // For that reason when trusted quick view package is set to this magic value
+    // we won't the system property. It's a gross hack, but stuff's gotta get done.
+    public static final String IGNORE_DEBUG_PROP = "*disabled*";
 
     private static final String TAG = "QuickViewIntentBuilder";
 
     private final DocumentInfo mDocument;
     private final Model mModel;
 
-    private final PackageManager mPkgManager;
+    private final PackageManager mPackageMgr;
     private final Resources mResources;
 
     public QuickViewIntentBuilder(
-            PackageManager pkgManager,
+            PackageManager packageMgr,
             Resources resources,
             DocumentInfo doc,
             Model model) {
 
-        mPkgManager = pkgManager;
+        assert(packageMgr != null);
+        assert(resources != null);
+        assert(doc != null);
+        assert(model != null);
+
+        mPackageMgr = packageMgr;
         mResources = resources;
         mDocument = doc;
         mModel = model;
@@ -121,9 +132,15 @@
 
     private String getQuickViewPackage() {
         String resValue = mResources.getString(R.string.trusted_quick_viewer_package);
-        if (Build.IS_DEBUGGABLE ) {
-            // Allow users of debug devices to override default quick viewer
-            // for the purposes of testing.
+
+        // Allow automated tests to hard-disable quick viewing.
+        if (IGNORE_DEBUG_PROP.equals(resValue)) {
+            return "";
+        }
+
+        // Allow users of debug devices to override default quick viewer
+        // for the purposes of testing.
+        if (Build.IS_DEBUGGABLE) {
             return android.os.SystemProperties.get("debug.quick_viewer", resValue);
         }
         return resValue;
@@ -196,6 +213,6 @@
 
     private boolean hasRegisteredHandler(Intent intent) {
         // Try to resolve the intent. If a matching app isn't installed, it won't resolve.
-        return intent.resolveActivity(mPkgManager) != null;
+        return intent.resolveActivity(mPackageMgr) != null;
     }
 }
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 59d0b48..369b489 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -58,10 +59,11 @@
             T activity,
             State state,
             RootsAccess roots,
+            DocumentsAccess docs,
             Lookup<String, Executor> executors,
             FragmentTuner tuner) {
 
-        super(activity, state, roots, executors);
+        super(activity, state, roots, docs, executors);
 
         mTuner = tuner;
         mConfig = new Config();
@@ -134,7 +136,7 @@
         }
 
         if (mTuner.isDocumentEnabled(doc.mimeType, doc.flags)) {
-            mActivity.onDocumentPicked(doc, mConfig.model);
+            mActivity.onDocumentPicked(doc);
             mConfig.selectionMgr.clearSelection();
             return true;
         }
@@ -162,5 +164,6 @@
 
     public interface Addons extends CommonAddons {
         void onAppPicked(ResolveInfo info);
+        void onDocumentPicked(DocumentInfo doc);
     }
 }
diff --git a/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java b/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java
index 3dadca4..bb22e08 100644
--- a/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java
+++ b/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java
@@ -106,4 +106,4 @@
         mState.external = mExternal;
         mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 1f2f69f..962d3a9 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -43,6 +43,7 @@
 import android.view.Menu;
 
 import com.android.documentsui.BaseActivity;
+import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -91,6 +92,7 @@
                 this,
                 mState,
                 DocumentsApplication.getRootsCache(this),
+                DocumentsAccess.create(this),
                 ProviderExecutor::forAuthority,
                 mTuner);
 
@@ -303,7 +305,7 @@
     }
 
     @Override
-    public void onDocumentPicked(DocumentInfo doc, Model model) {
+    public void onDocumentPicked(DocumentInfo doc) {
         final FragmentManager fm = getFragmentManager();
         if (doc.isContainer()) {
             openContainerDocument(doc);
diff --git a/src/com/android/documentsui/picker/PickFragment.java b/src/com/android/documentsui/picker/PickFragment.java
index a389942..8eb3b4c 100644
--- a/src/com/android/documentsui/picker/PickFragment.java
+++ b/src/com/android/documentsui/picker/PickFragment.java
@@ -32,9 +32,6 @@
 
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.R;
-import com.android.documentsui.R.id;
-import com.android.documentsui.R.layout;
-import com.android.documentsui.R.string;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.services.FileOperationService.OpType;
@@ -45,6 +42,27 @@
 public class PickFragment extends Fragment {
     public static final String TAG = "PickFragment";
 
+    private static final String ACTION_KEY = "action";
+    private static final String COPY_OPERATION_SUBTYPE_KEY = "copyOperationSubType";
+    private static final String PICK_TARGET_KEY = "pickTarget";
+
+    private final View.OnClickListener mPickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final PickActivity activity = PickActivity.get(PickFragment.this);
+            activity.onPickRequested(mPickTarget);
+        }
+    };
+
+    private final View.OnClickListener mCancelListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final BaseActivity activity = BaseActivity.get(PickFragment.this);
+            activity.setResult(Activity.RESULT_CANCELED);
+            activity.finish();
+        }
+    };
+
     private int mAction;
     // Only legal values are OPERATION_COPY, OPERATION_MOVE, and unset (OPERATION_UNKNOWN).
     private @OpType int mCopyOperationSubType = OPERATION_UNKNOWN;
@@ -84,22 +102,26 @@
         return mContainer;
     }
 
-    private View.OnClickListener mPickListener = new View.OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final PickActivity activity = PickActivity.get(PickFragment.this);
-            activity.onPickRequested(mPickTarget);
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        if (savedInstanceState != null) {
+            // Restore status
+            mAction = savedInstanceState.getInt(ACTION_KEY);
+            mCopyOperationSubType =
+                    savedInstanceState.getInt(COPY_OPERATION_SUBTYPE_KEY);
+            mPickTarget = savedInstanceState.getParcelable(PICK_TARGET_KEY);
+            updateView();
         }
-    };
+    }
 
-    private View.OnClickListener mCancelListener = new View.OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final BaseActivity activity = BaseActivity.get(PickFragment.this);
-            activity.setResult(Activity.RESULT_CANCELED);
-            activity.finish();
-        }
-    };
+    @Override
+    public void onSaveInstanceState(final Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(ACTION_KEY, mAction);
+        outState.putInt(COPY_OPERATION_SUBTYPE_KEY, mCopyOperationSubType);
+        outState.putParcelable(PICK_TARGET_KEY, mPickTarget);
+    }
 
     /**
      * @param action Which action defined in State is the picker shown for.
diff --git a/src/com/android/documentsui/GetRootDocumentTask.java b/src/com/android/documentsui/roots/GetRootDocumentTask.java
similarity index 96%
rename from src/com/android/documentsui/GetRootDocumentTask.java
rename to src/com/android/documentsui/roots/GetRootDocumentTask.java
index e56b110..1503ae1 100644
--- a/src/com/android/documentsui/GetRootDocumentTask.java
+++ b/src/com/android/documentsui/roots/GetRootDocumentTask.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.documentsui;
+package com.android.documentsui.roots;
 
 import android.annotation.Nullable;
 import android.app.Activity;
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.util.Log;
 
+import com.android.documentsui.TimeoutTask;
 import com.android.documentsui.base.CheckedTask;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 1174503..a250a8d 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -49,7 +49,6 @@
 import com.android.documentsui.ActionHandler;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.GetRootDocumentTask;
 import com.android.documentsui.ItemDragListener;
 import com.android.documentsui.R;
 import com.android.documentsui.base.BooleanConsumer;
@@ -59,6 +58,7 @@
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
+import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsCache;
 import com.android.documentsui.roots.RootsLoader;
 
diff --git a/src/com/android/documentsui/sorting/DropdownSortWidgetController.java b/src/com/android/documentsui/sorting/DropdownSortWidgetController.java
index 7977591..21c182c 100644
--- a/src/com/android/documentsui/sorting/DropdownSortWidgetController.java
+++ b/src/com/android/documentsui/sorting/DropdownSortWidgetController.java
@@ -21,18 +21,14 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.View.OnClickListener;
 import android.widget.ImageView;
 import android.widget.PopupMenu;
 import android.widget.TextView;
 
 import com.android.documentsui.R;
 import com.android.documentsui.sorting.SortController.WidgetController;
-import com.android.documentsui.sorting.SortDimension;
 import com.android.documentsui.sorting.SortDimension.SortDirection;
-import com.android.documentsui.sorting.SortModel;
 import com.android.documentsui.sorting.SortModel.SortDimensionId;
-import com.android.documentsui.sorting.SortModel.UpdateListener;
 import com.android.documentsui.sorting.SortModel.UpdateType;
 
 /**
@@ -49,24 +45,23 @@
     private final PopupMenu mMenu;
     private final ImageView mArrow;
 
-    private final OnClickListener mDimensionButtonClickListener = this::showMenu;
-    private final OnClickListener mArrowClickListener = this::onChangeDirection;
-    private final UpdateListener mUpdateListener = this::onModelUpdate;
-
     public DropdownSortWidgetController(SortModel model, View widget) {
         mModel = model;
         mWidget = widget;
 
         mDimensionButton = (TextView) mWidget.findViewById(R.id.sort_dimen_dropdown);
+        mDimensionButton.setOnClickListener(this::showMenu);
+
         mMenu = new PopupMenu(widget.getContext(), mDimensionButton, Gravity.END | Gravity.TOP);
         mMenu.setOnMenuItemClickListener(this::onSelectDimension);
 
         mArrow = (ImageView) mWidget.findViewById(R.id.sort_arrow);
+        mArrow.setOnClickListener(this::onChangeDirection);
 
         populateMenuItems();
         onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
 
-        mModel.addListener(mUpdateListener);
+        mModel.addListener(this::onModelUpdate);
     }
 
     @Override
@@ -92,10 +87,6 @@
     private void onModelUpdate(SortModel model, @UpdateType int updateType) {
         final @SortDimensionId int sortedId = model.getSortedDimensionId();
 
-        if ((updateType & SortModel.UPDATE_TYPE_STATUS) != 0) {
-            setEnabled(mModel.isSortEnabled());
-        }
-
         if ((updateType & SortModel.UPDATE_TYPE_VISIBILITY) != 0) {
             updateVisibility();
         }
@@ -106,17 +97,6 @@
         }
     }
 
-    private void setEnabled(boolean enabled) {
-        if (enabled) {
-            mDimensionButton.setOnClickListener(mDimensionButtonClickListener);
-            mArrow.setOnClickListener(mArrowClickListener);
-        } else {
-            mMenu.dismiss();
-            mDimensionButton.setOnClickListener(null);
-            mArrow.setOnClickListener(null);
-        }
-    }
-
     private void updateVisibility() {
         Menu menu = mMenu.getMenu();
 
diff --git a/src/com/android/documentsui/sorting/SortModel.java b/src/com/android/documentsui/sorting/SortModel.java
index 793dd60..a5c8459 100644
--- a/src/com/android/documentsui/sorting/SortModel.java
+++ b/src/com/android/documentsui/sorting/SortModel.java
@@ -20,6 +20,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
+import android.database.Cursor;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.DocumentsContract.Document;
@@ -59,7 +60,6 @@
     @IntDef(flag = true, value = {
             UPDATE_TYPE_NONE,
             UPDATE_TYPE_UNSPECIFIED,
-            UPDATE_TYPE_STATUS,
             UPDATE_TYPE_VISIBILITY,
             UPDATE_TYPE_SORTING
     })
@@ -70,18 +70,14 @@
      */
     public static final int UPDATE_TYPE_NONE = 0;
     /**
-     * Indicates the status of sorting has changed, i.e. whether soring is enabled.
-     */
-    public static final int UPDATE_TYPE_STATUS = 1;
-    /**
      * Indicates the visibility of at least one dimension has changed.
      */
-    public static final int UPDATE_TYPE_VISIBILITY = 1 << 1;
+    public static final int UPDATE_TYPE_VISIBILITY = 1;
     /**
      * Indicates the sorting order has changed, either because the sorted dimension has changed or
      * the sort direction has changed.
      */
-    public static final int UPDATE_TYPE_SORTING = 1 << 2;
+    public static final int UPDATE_TYPE_SORTING = 1 << 1;
     /**
      * Anything can be changed if the type is unspecified.
      */
@@ -98,8 +94,6 @@
     private boolean mIsUserSpecified = false;
     private @Nullable SortDimension mSortedDimension;
 
-    private boolean mIsSortEnabled = true;
-
     public SortModel(Collection<SortDimension> columns) {
         mDimensions = new SparseArray<>(columns.size());
 
@@ -145,16 +139,6 @@
                 : SortDimension.SORT_DIRECTION_NONE;
     }
 
-    public void setSortEnabled(boolean enabled) {
-        mIsSortEnabled = enabled;
-
-        notifyListeners(UPDATE_TYPE_STATUS);
-    }
-
-    public boolean isSortEnabled() {
-        return mIsSortEnabled;
-    }
-
     /**
      * Sort by the default direction of the given dimension if user has never specified any sort
      * direction before.
@@ -181,10 +165,6 @@
      * @param direction the direction to sort docs in
      */
     public void sortByUser(int dimensionId, @SortDirection int direction) {
-        if (!mIsSortEnabled) {
-            throw new IllegalStateException("Sort is not enabled.");
-        }
-
         SortDimension dimension = mDimensions.get(dimensionId);
         if (dimension == null) {
             throw new IllegalArgumentException("Unknown column id: " + dimensionId);
@@ -239,6 +219,14 @@
         notifyListeners(UPDATE_TYPE_VISIBILITY);
     }
 
+    public Cursor sortCursor(Cursor cursor) {
+        if (mSortedDimension != null) {
+            return new SortingCursorWrapper(cursor, mSortedDimension);
+        } else {
+            return cursor;
+        }
+    }
+
     public @Nullable String getDocumentSortQuery() {
         final int id = getSortedDimensionId();
         final String columnName;
@@ -290,17 +278,6 @@
         mListeners.remove(listener);
     }
 
-    public void clearSortDirection() {
-        if (mSortedDimension != null) {
-            mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
-            mSortedDimension = null;
-        }
-
-        mIsUserSpecified = false;
-
-        sortOnDefault();
-    }
-
     /**
      * Sort by default dimension and direction if there is no history of user specifying a sort
      * order.
@@ -340,7 +317,6 @@
         }
 
         return mDefaultDimensionId == other.mDefaultDimensionId
-                && mIsSortEnabled == other.mIsSortEnabled
                 && (mSortedDimension == other.mSortedDimension
                     || mSortedDimension.equals(other.mSortedDimension));
     }
@@ -349,8 +325,7 @@
     public String toString() {
         return new StringBuilder()
                 .append("SortModel{")
-                .append("enabled=").append(mIsSortEnabled)
-                .append(", dimensions=").append(mDimensions)
+                .append("dimensions=").append(mDimensions)
                 .append(", defaultDimensionId=").append(mDefaultDimensionId)
                 .append(", sortedDimension=").append(mSortedDimension)
                 .append("}")
@@ -370,7 +345,6 @@
         }
 
         out.writeInt(mDefaultDimensionId);
-        out.writeInt(mIsSortEnabled ? 1 : 0);
         out.writeInt(getSortedDimensionId());
     }
 
@@ -386,7 +360,6 @@
             SortModel model = new SortModel(columns);
 
             model.mDefaultDimensionId = in.readInt();
-            model.mIsSortEnabled = (in.readInt() == 1);
             model.mSortedDimension = model.getDimensionById(in.readInt());
 
             return model;
diff --git a/src/com/android/documentsui/sorting/SortingCursorWrapper.java b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
new file mode 100644
index 0000000..89869d9
--- /dev/null
+++ b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2016 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.sorting;
+
+import static com.android.documentsui.base.DocumentInfo.getCursorLong;
+import static com.android.documentsui.base.DocumentInfo.getCursorString;
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.provider.DocumentsContract.Document;
+
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.sorting.SortModel.SortDimensionId;
+
+/**
+ * Cursor wrapper that presents a sorted view of the underlying cursor. Handles
+ * common {@link Document} sorting modes, such as ordering directories first.
+ */
+class SortingCursorWrapper extends AbstractCursor {
+    private final Cursor mCursor;
+
+    private final int[] mPosition;
+
+    public SortingCursorWrapper(Cursor cursor, SortDimension dimension) {
+        mCursor = cursor;
+
+        final int count = cursor.getCount();
+        mPosition = new int[count];
+        boolean[] isDirs = new boolean[count];
+        String[] displayNames = null;
+        long[] longValues = null;
+        String[] ids = null;
+
+        final @SortDimensionId int id = dimension.getId();
+        switch (id) {
+            case SortModel.SORT_DIMENSION_ID_TITLE:
+                displayNames = new String[count];
+                break;
+            case SortModel.SORT_DIMENSION_ID_DATE:
+            case SortModel.SORT_DIMENSION_ID_SIZE:
+                longValues = new long[count];
+                ids = new String[count];
+                break;
+        }
+
+        cursor.moveToPosition(-1);
+        for (int i = 0; i < count; i++) {
+            cursor.moveToNext();
+            mPosition[i] = i;
+
+            final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
+            isDirs[i] = Document.MIME_TYPE_DIR.equals(mimeType);
+
+            switch(id) {
+                case SortModel.SORT_DIMENSION_ID_TITLE:
+                    final String displayName = getCursorString(
+                            mCursor, Document.COLUMN_DISPLAY_NAME);
+                    displayNames[i] = displayName;
+                    break;
+                case SortModel.SORT_DIMENSION_ID_DATE:
+                    longValues[i] = getLastModified(mCursor);
+                    ids[i] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
+                    break;
+                case SortModel.SORT_DIMENSION_ID_SIZE:
+                    longValues[i] = getCursorLong(mCursor, Document.COLUMN_SIZE);
+                    ids[i] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
+                    break;
+            }
+
+        }
+
+        switch (id) {
+            case SortModel.SORT_DIMENSION_ID_TITLE:
+                binarySort(displayNames, isDirs, mPosition, dimension.getSortDirection());
+                break;
+            case SortModel.SORT_DIMENSION_ID_DATE:
+            case SortModel.SORT_DIMENSION_ID_SIZE:
+                binarySort(longValues, isDirs, mPosition, ids, dimension.getSortDirection());
+                break;
+        }
+
+    }
+
+    @Override
+    public void close() {
+        super.close();
+        mCursor.close();
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        return mCursor.moveToPosition(mPosition[newPosition]);
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mCursor.getColumnNames();
+    }
+
+    @Override
+    public int getCount() {
+        return mCursor.getCount();
+    }
+
+    @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) {
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public int getType(int column) {
+        return mCursor.getType(column);
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return mCursor.isNull(column);
+    }
+
+    /**
+     * @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 descending by date.
+     */
+    private static long getLastModified(Cursor cursor) {
+        long l = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+        return (l == -1) ? Long.MAX_VALUE : l;
+    }
+
+    /**
+     * Borrowed from TimSort.binarySort(), but modified to sort two column
+     * dataset.
+     */
+    private static void binarySort(
+            String[] sortKey,
+            boolean[] isDirs,
+            int[] positions,
+            @SortDimension.SortDirection int direction) {
+        final int count = positions.length;
+        for (int start = 1; start < count; start++) {
+            final int pivotPosition = positions[start];
+            final String pivotValue = sortKey[start];
+            final boolean pivotIsDir = isDirs[start];
+
+            int left = 0;
+            int right = start;
+
+            while (left < right) {
+                int mid = (left + right) >>> 1;
+
+                // Directories always go in front.
+                int compare = 0;
+                final boolean rhsIsDir = isDirs[mid];
+                if (pivotIsDir && !rhsIsDir) {
+                    compare = -1;
+                } else if (!pivotIsDir && rhsIsDir) {
+                    compare = 1;
+                } else {
+                    final String lhs = pivotValue;
+                    final String rhs = sortKey[mid];
+                    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) {
+                    right = mid;
+                } else {
+                    left = mid + 1;
+                }
+            }
+
+            int n = start - left;
+            switch (n) {
+                case 2:
+                    positions[left + 2] = positions[left + 1];
+                    sortKey[left + 2] = sortKey[left + 1];
+                    isDirs[left + 2] = isDirs[left + 1];
+                case 1:
+                    positions[left + 1] = positions[left];
+                    sortKey[left + 1] = sortKey[left];
+                    isDirs[left + 1] = isDirs[left];
+                    break;
+                default:
+                    System.arraycopy(positions, left, positions, left + 1, n);
+                    System.arraycopy(sortKey, left, sortKey, left + 1, n);
+                    System.arraycopy(isDirs, left, isDirs, left + 1, n);
+            }
+
+            positions[left] = pivotPosition;
+            sortKey[left] = pivotValue;
+            isDirs[left] = pivotIsDir;
+        }
+    }
+
+    /**
+     * Borrowed from TimSort.binarySort(), but modified to sort two column
+     * dataset.
+     */
+    private static void binarySort(
+            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];
+            final long pivotValue = sortKey[start];
+            final boolean pivotIsDir = isDirs[start];
+            final String pivotId = ids[start];
+
+            int left = 0;
+            int right = start;
+
+            while (left < right) {
+                int mid = ((left + right) >>> 1);
+
+                // Directories always go in front.
+                int compare = 0;
+                final boolean rhsIsDir = isDirs[mid];
+                if (pivotIsDir && !rhsIsDir) {
+                    compare = -1;
+                } else if (!pivotIsDir && rhsIsDir) {
+                    compare = 1;
+                } else {
+                    final long lhs = pivotValue;
+                    final long rhs = sortKey[mid];
+                    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
+                // will yield stable results even if incoming items are continually shuffling and
+                // have identical numerical sort keys.  One common example of this scenario is seen
+                // when sorting a set of active downloads by mod time.
+                if (compare == 0) {
+                    compare = pivotId.compareTo(ids[mid]);
+                }
+
+                if (compare < 0) {
+                    right = mid;
+                } else {
+                    left = mid + 1;
+                }
+            }
+
+            int n = start - left;
+            switch (n) {
+                case 2:
+                    positions[left + 2] = positions[left + 1];
+                    sortKey[left + 2] = sortKey[left + 1];
+                    isDirs[left + 2] = isDirs[left + 1];
+                    ids[left + 2] = ids[left + 1];
+                case 1:
+                    positions[left + 1] = positions[left];
+                    sortKey[left + 1] = sortKey[left];
+                    isDirs[left + 1] = isDirs[left];
+                    ids[left + 1] = ids[left];
+                    break;
+                default:
+                    System.arraycopy(positions, left, positions, left + 1, n);
+                    System.arraycopy(sortKey, left, sortKey, left + 1, n);
+                    System.arraycopy(isDirs, left, isDirs, left + 1, n);
+                    System.arraycopy(ids, left, ids, left + 1, n);
+            }
+
+            positions[left] = pivotPosition;
+            sortKey[left] = pivotValue;
+            isDirs[left] = pivotIsDir;
+            ids[left] = pivotId;
+        }
+    }
+}
diff --git a/src/com/android/documentsui/sorting/TableHeaderController.java b/src/com/android/documentsui/sorting/TableHeaderController.java
index c5957ce..da82a6e 100644
--- a/src/com/android/documentsui/sorting/TableHeaderController.java
+++ b/src/com/android/documentsui/sorting/TableHeaderController.java
@@ -76,8 +76,7 @@
         cell.setTag(dimension);
 
         cell.onBind(dimension);
-        if (mModel.isSortEnabled()
-                && dimension.getVisibility() == View.VISIBLE
+        if (dimension.getVisibility() == View.VISIBLE
                 && dimension.getSortCapability() != SortDimension.SORT_CAPABILITY_NONE) {
             cell.setOnClickListener(mOnCellClickListener);
         } else {
diff --git a/src/com/android/documentsui/ui/DialogController.java b/src/com/android/documentsui/ui/DialogController.java
index e684618..90b7777 100644
--- a/src/com/android/documentsui/ui/DialogController.java
+++ b/src/com/android/documentsui/ui/DialogController.java
@@ -18,7 +18,7 @@
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
-import android.content.DialogInterface.OnShowListener;
+import android.support.design.widget.Snackbar;
 import android.widget.Button;
 import android.widget.TextView;
 
@@ -45,10 +45,16 @@
         public void showFileOperationFailures(int status, int opType, int docCount) {
             throw new UnsupportedOperationException();
         }
+
+        @Override
+        public void showNoApplicationFound() {
+            throw new UnsupportedOperationException();
+        }
     };
 
     void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback);
     void showFileOperationFailures(int status, int opType, int docCount);
+    void showNoApplicationFound();
 
     // Should be private, but Java doesn't like me treating an interface like a mini-package.
     public static final class RuntimeDialogController implements DialogController {
@@ -99,7 +105,8 @@
         }
 
         @Override
-        public void showFileOperationFailures(@Status int status, @OpType int opType, int docCount) {
+        public void showFileOperationFailures(
+                @Status int status, @OpType int opType, int docCount) {
             if (status == FileOperations.Callback.STATUS_REJECTED) {
                 Snackbars.showPasteFailed(mActivity);
                 return;
@@ -123,7 +130,13 @@
                 default:
                     throw new UnsupportedOperationException("Unsupported Operation: " + opType);
             }
-        };
+        }
+
+        @Override
+        public void showNoApplicationFound() {
+            Snackbars.makeSnackbar(
+                    mActivity, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
+        }
     }
 
     static DialogController create(Activity activity) {
diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 4cca190..3987636 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import static junit.framework.Assert.assertEquals;
+
 import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -23,6 +25,7 @@
 import android.content.res.Resources;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.testing.TestEventListener;
 import com.android.documentsui.testing.android.TestPackageManager;
@@ -39,10 +42,13 @@
     public TestResources resources;
     public TestPackageManager packageMgr;
     public Intent intent;
+    public RootInfo currentRoot;
 
     public TestEventListener<Intent> startActivity;
     public TestEventListener<Intent> startService;
     public TestEventListener<RootInfo> rootPicked;
+    public TestEventListener<DocumentInfo> openContainer;
+    public TestEventListener<Integer> refreshCurrentRootAndDirectory;
 
     public static TestActivity create() {
         TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
@@ -58,6 +64,8 @@
        startActivity = new TestEventListener<>();
        startService = new TestEventListener<>();
        rootPicked = new TestEventListener<>();
+       openContainer = new TestEventListener<>();
+       refreshCurrentRootAndDirectory =  new TestEventListener<>();
    }
 
     @Override
@@ -70,12 +78,20 @@
         startActivity.accept(intent);
     }
 
+    public final void assertActivityStarted(String expectedAction) {
+        assertEquals(expectedAction, startActivity.getLastValue().getAction());
+    }
+
     @Override
     public final ComponentName startService(Intent intent) {
         startService.accept(intent);
         return null;
     }
 
+    public final void assertServiceStarted(String expectedAction) {
+        assertEquals(expectedAction, startService.getLastValue().getAction());
+    }
+
     @Override
     public final Intent getIntent() {
         return intent;
@@ -87,7 +103,7 @@
     }
 
     @Override
-    public PackageManager getPackageManager() {
+    public final PackageManager getPackageManager() {
         return packageMgr;
     }
 
@@ -95,6 +111,26 @@
     public final void onRootPicked(RootInfo root) {
         rootPicked.accept(root);
     }
+
+    @Override
+    public final void onDocumentPicked(DocumentInfo doc) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public final void openContainerDocument(DocumentInfo doc) {
+        openContainer.accept(doc);
+    }
+
+    @Override
+    public final void refreshCurrentRootAndDirectory(int anim) {
+        refreshCurrentRootAndDirectory.accept(anim);
+    }
+
+    @Override
+    public final RootInfo getCurrentRoot() {
+        return currentRoot;
+    }
 }
 
 // Trick Mockito into finding our Addons methods correctly. W/o this
diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/dirlist/TestModel.java
index 0c3ca92..abe4d5f 100644
--- a/tests/common/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/common/com/android/documentsui/dirlist/TestModel.java
@@ -17,11 +17,14 @@
 package com.android.documentsui.dirlist;
 
 import android.database.MatrixCursor;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 
 import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.roots.RootCursorWrapper;
-import com.android.documentsui.testing.SortModels;
+
+import libcore.net.MimeUtils;
 
 import java.util.Random;
 
@@ -37,34 +40,94 @@
     };
 
     private final String mAuthority;
+    private int mLastId = 0;
+    private Random mRand = new Random();
+    private MatrixCursor mCursor;
 
     public TestModel(String authority) {
         super();
         mAuthority = authority;
+        reset();
     }
 
-    public void update(String... names) {
-        Random rand = new Random();
+    public void reset() {
+        mLastId = 0;
+        mCursor = new MatrixCursor(COLUMNS);
+    }
 
-        MatrixCursor c = new MatrixCursor(COLUMNS);
-        for (int i = 0; i < names.length; i++) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, mAuthority);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
-            // Generate random document names and sizes. This forces the model's internal sort code
-            // to actually do something.
-            row.add(Document.COLUMN_DISPLAY_NAME, names[i]);
-            row.add(Document.COLUMN_SIZE, rand.nextInt());
+    public void update() {
+        DirectoryResult r = new DirectoryResult();
+        r.cursor = mCursor;
+        super.update(r);
+    }
+
+    public DocumentInfo createFile(String name) {
+        return createFile(
+                name,
+                Document.FLAG_SUPPORTS_WRITE
+                        | Document.FLAG_SUPPORTS_DELETE
+                        | Document.FLAG_SUPPORTS_RENAME);
+    }
+
+    public DocumentInfo createFile(String name, int flags) {
+        return createDocument(
+                name,
+                guessMimeType(name),
+                flags);
+    }
+
+    public DocumentInfo createFolder(String name) {
+        return createFolder(
+                name,
+                Document.FLAG_SUPPORTS_WRITE
+                        | Document.FLAG_SUPPORTS_DELETE
+                        | Document.FLAG_SUPPORTS_REMOVE
+                        | Document.FLAG_DIR_SUPPORTS_CREATE);
+    }
+
+    public DocumentInfo createFolder(String name, int flags) {
+        return createDocument(
+                name,
+                DocumentsContract.Document.MIME_TYPE_DIR,
+                flags);
+    }
+
+    public DocumentInfo createDocument(String name, String mimeType, int flags) {
+        DocumentInfo doc = new DocumentInfo();
+        doc.authority = mAuthority;
+        doc.documentId = Integer.toString(++mLastId);
+        doc.displayName = name;
+        doc.mimeType = mimeType;
+        doc.flags = flags;
+        doc.size = mRand.nextInt();
+
+        addToCursor(doc);
+
+        return doc;
+    }
+
+    private void addToCursor(DocumentInfo doc) {
+        MatrixCursor.RowBuilder row = mCursor.newRow();
+        row.add(Document.COLUMN_DOCUMENT_ID, doc.documentId);
+        row.add(RootCursorWrapper.COLUMN_AUTHORITY, doc.authority);
+        row.add(Document.COLUMN_DISPLAY_NAME, doc.displayName);
+        row.add(Document.COLUMN_MIME_TYPE, doc.mimeType);
+        row.add(Document.COLUMN_FLAGS, doc.flags);
+        row.add(Document.COLUMN_SIZE, doc.size);
+    }
+
+    private static String guessMimeType(String name) {
+        int i = name.indexOf('.');
+
+        while(i != -1) {
+            name = name.substring(i + 1);
+            String type = MimeUtils.guessMimeTypeFromExtension(name);
+            if (type != null) {
+                return type;
+            }
+            i = name.indexOf('.');
         }
 
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = SortModels.createTestSortModel();
-        update(r);
-    }
-
-    String idForPosition(int p) {
-        return Integer.toString(p);
+        return "text/plain";
     }
 }
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 4581a5c..08bee36 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -34,7 +34,7 @@
     }
 
     public TestActionHandler(TestEnv env) {
-        super(TestActivity.create(), env.state, env.roots, (String authority) -> null);
+        super(TestActivity.create(), env.state, env.roots, env.docs, (String authority) -> null);
     }
 
     @Override
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
new file mode 100644
index 0000000..40f799e
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.testing;
+
+import android.net.Uri;
+
+import com.android.documentsui.DocumentsAccess;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.RootInfo;
+
+import javax.annotation.Nullable;
+
+public class TestDocumentsAccess implements DocumentsAccess {
+
+    public @Nullable DocumentInfo nextRootDocument;
+    public @Nullable DocumentInfo nextDocument;
+
+    @Override
+    public DocumentInfo getRootDocument(Uri uri) {
+        return nextRootDocument;
+    }
+
+    @Override
+    public DocumentInfo getRootDocument(RootInfo root) {
+        return nextRootDocument;
+    }
+
+    @Override
+    public DocumentInfo getDocument(Uri uri) {
+        return nextDocument;
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index f898e6b..10de113 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -17,35 +17,94 @@
 
 import android.os.Handler;
 import android.os.Looper;
+import android.provider.DocumentsContract.Document;
 
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.TestModel;
 
+import junit.framework.Assert;
+
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 
 public class TestEnv {
 
-    public static final String AUTHORITY = "hullabaloo";
+    public static DocumentInfo FOLDER_0;
+    public static DocumentInfo FOLDER_1;
+    public static DocumentInfo FOLDER_2;
+    public static DocumentInfo FILE_TXT;
+    public static DocumentInfo FILE_PNG;
+    public static DocumentInfo FILE_JPG;
+    public static DocumentInfo FILE_GIF;
+    public static DocumentInfo FILE_PDF;
+    public static DocumentInfo FILE_APK;
+    public static DocumentInfo FILE_PARTIAL;
+    public static DocumentInfo FILE_ARCHIVE;
+    public static DocumentInfo FILE_VIRTUAL;
 
     public final TestScheduledExecutorService mExecutor;
     public final State state = new State();
     public final TestRootsAccess roots = new TestRootsAccess();
-    public final TestModel model = new TestModel(AUTHORITY);
+    public final TestDocumentsAccess docs = new TestDocumentsAccess();
+    public final TestModel model;
 
-    private TestEnv() {
+    private TestEnv(String authority) {
         mExecutor = new TestScheduledExecutorService();
+        model = new TestModel(authority);
     }
 
     public static TestEnv create() {
-        TestEnv env = new TestEnv();
+        return create(TestRootsAccess.HOME.authority);
+    }
+
+    public static TestEnv create(String authority) {
+        TestEnv env = new TestEnv(authority);
         env.reset();
         return env;
     }
 
+    public void clear() {
+        model.reset();
+        model.update();
+    }
+
     public void reset() {
-        model.update("a", "b", "c", "x", "y", "z");
-        state.stack.push(model.getDocument("1"));
+        model.reset();
+        FOLDER_0 = model.createFolder("folder 0");
+        FOLDER_1 = model.createFolder("folder 1");
+        FOLDER_2 = model.createFolder("folder 2");
+        FILE_TXT = model.createFile("woowoo.txt");
+        FILE_PNG = model.createFile("peppey.png");
+        FILE_JPG = model.createFile("jiffy.jpg");
+        FILE_GIF = model.createFile("glibby.gif");
+        FILE_PDF = model.createFile("busy.pdf");
+        FILE_APK = model.createFile("becareful.apk");
+        FILE_PARTIAL = model.createFile(
+                "UbuntuFlappyBird.iso",
+                Document.FLAG_SUPPORTS_DELETE
+                        | Document.FLAG_PARTIAL);
+        FILE_ARCHIVE = model.createFile(
+                "whatsinthere.zip",
+                Document.FLAG_ARCHIVE
+                        | Document.FLAG_SUPPORTS_DELETE);
+        FILE_VIRTUAL = model.createDocument(
+                "virtualdoc.vnd",
+                "application/vnd.google-apps.document",
+                Document.FLAG_VIRTUAL_DOCUMENT
+                        | Document.FLAG_SUPPORTS_DELETE
+                        | Document.FLAG_SUPPORTS_RENAME);
+
+        model.update();
+    }
+
+    public void populateStack() {
+        DocumentInfo rootDoc = model.getDocument("1");
+        Assert.assertNotNull(rootDoc);
+        Assert.assertEquals(rootDoc.displayName, FOLDER_0.displayName);
+
+        state.stack.root = TestRootsAccess.HOME;
+        state.stack.push(rootDoc);
     }
 
     public void beforeAsserts() throws Exception {
diff --git a/tests/common/com/android/documentsui/testing/android/TestPackageManager.java b/tests/common/com/android/documentsui/testing/android/TestPackageManager.java
index a078c6c..a5b28b1 100644
--- a/tests/common/com/android/documentsui/testing/android/TestPackageManager.java
+++ b/tests/common/com/android/documentsui/testing/android/TestPackageManager.java
@@ -16,7 +16,10 @@
 
 package com.android.documentsui.testing.android;
 
+import android.annotation.UserIdInt;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
@@ -38,8 +41,6 @@
 
     public Map<String, ResolveInfo> contentProviders;
 
-    private TestPackageManager() {}
-
     public void addStubContentProviderForRoot(RootInfo... roots) {
         for (RootInfo root : roots) {
             // only one entry per authority is required.
@@ -60,9 +61,24 @@
     }
 
     @Override
-    public List<ResolveInfo> queryIntentContentProviders(Intent intent, int flags) {
+    public final List<ResolveInfo> queryIntentContentProviders(Intent intent, int flags) {
         List<ResolveInfo> result = new ArrayList<>();
         result.addAll(contentProviders.values());
         return result;
     }
+
+    public final ResolveInfo resolveActivity(Intent intent, int flags) {
+        ResolveInfo info = new ResolveInfo();
+        info.activityInfo = new ActivityInfo();
+        info.activityInfo.packageName = intent.getPackage();
+        info.activityInfo.applicationInfo = new ApplicationInfo();
+        info.activityInfo.applicationInfo.packageName = intent.getPackage();
+        info.activityInfo.name = "Fake Quick Viewer";
+        return info;
+    }
+
+    public final ResolveInfo resolveActivityAsUser(
+            Intent intent, int flags, @UserIdInt int userId) {
+        return resolveActivity(intent, flags);
+    }
 }
diff --git a/tests/common/com/android/documentsui/testing/android/TestResources.java b/tests/common/com/android/documentsui/testing/android/TestResources.java
index d72aee1..246b4e8 100644
--- a/tests/common/com/android/documentsui/testing/android/TestResources.java
+++ b/tests/common/com/android/documentsui/testing/android/TestResources.java
@@ -16,11 +16,20 @@
 
 package com.android.documentsui.testing.android;
 
+import android.annotation.BoolRes;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
 import android.content.res.Resources;
+import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
+import com.android.documentsui.R;
+import com.android.documentsui.files.QuickViewIntentBuilder;
+
 import org.mockito.Mockito;
 
+import javax.annotation.Nullable;
+
 /**
  * Abstract to avoid having to implement unnecessary Activity stuff.
  * Instances are created using {@link #create()}.
@@ -28,20 +37,48 @@
 public abstract class TestResources extends Resources {
 
     public SparseBooleanArray bools;
+    public SparseArray<String> strings;
 
     public TestResources() {
         super(ClassLoader.getSystemClassLoader());
     }
 
     public static TestResources create() {
-        TestResources resources = Mockito.mock(
+        TestResources res = Mockito.mock(
                 TestResources.class, Mockito.CALLS_REAL_METHODS);
-        resources.bools = new SparseBooleanArray();
-        return resources;
+        res.bools = new SparseBooleanArray();
+        res.strings = new SparseArray<>();
+
+        res.setProductivityDeviceEnabled(false);
+
+        // quick view package can be set via system property on debug builds.
+        // unfortunately that interfers with testing. For that reason we have
+        // this little hack....QuickViewIntentBuilder will check for this value
+        // and ignore
+        res.setQuickViewerPackage(QuickViewIntentBuilder.IGNORE_DEBUG_PROP);
+        return res;
+    }
+
+    public void setQuickViewerPackage(String packageName) {
+        strings.put(R.string.trusted_quick_viewer_package, packageName);
+    }
+
+    public void setProductivityDeviceEnabled(boolean enabled) {
+        bools.put(R.bool.productivity_device, enabled);
     }
 
     @Override
-    public boolean getBoolean(int id) throws NotFoundException {
+    public final boolean getBoolean(@BoolRes int id) throws NotFoundException {
         return bools.get(id);
     }
+
+    @Override
+    public final @Nullable String getString(@StringRes int id) throws NotFoundException {
+        return strings.get(id);
+    }
+
+    @NonNull
+    public final String getString(@StringRes int id, Object... formatArgs) throws NotFoundException {
+        return getString(id);
+    }
 }
diff --git a/tests/common/com/android/documentsui/ui/TestDialogController.java b/tests/common/com/android/documentsui/ui/TestDialogController.java
index c7c1bbf..dbac0f5 100644
--- a/tests/common/com/android/documentsui/ui/TestDialogController.java
+++ b/tests/common/com/android/documentsui/ui/TestDialogController.java
@@ -27,6 +27,7 @@
 
     public int mNextConfirmationCode;
     private boolean mFileOpFailed;
+    private boolean mNoApplicationFound;
 
     public TestDialogController() {
         // by default, always confirm
@@ -45,10 +46,19 @@
         }
     }
 
+    @Override
+    public void showNoApplicationFound() {
+        mNoApplicationFound = true;
+    }
+
     public void assertNoFileFailures() {
         Assert.assertFalse(mFileOpFailed);
     }
 
+    public void assertNoAppFoundShown() {
+        Assert.assertFalse(mNoApplicationFound);
+    }
+
     public void confirmNext() {
         mNextConfirmationCode = ConfirmationCallback.CONFIRM;
     }
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index ee9ee4d..d924a44 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -54,6 +54,7 @@
                 mActivity,
                 mEnv.state,
                 mEnv.roots,
+                mEnv.docs,
                 mEnv::lookupExecutor) {
 
             @Override
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 1208f03..eb1ee6f 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -18,36 +18,24 @@
 
 import android.content.Context;
 import android.database.Cursor;
+import android.support.test.filters.MediumTest;
 import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.documentsui.base.State;
+import com.android.documentsui.testing.TestEnv;
 
-@SmallTest
+@MediumTest
 public class ModelBackedDocumentsAdapterTest extends AndroidTestCase {
 
     private static final String AUTHORITY = "test_authority";
-    private static final String[] NAMES = new String[] {
-            "4",
-            "foo",
-            "1",
-            "bar",
-            "*(Ljifl;a",
-            "0",
-            "baz",
-            "2",
-            "3",
-            "%$%VD"
-    };
 
-    private TestModel mModel;
+    private TestEnv mEnv;
     private ModelBackedDocumentsAdapter mAdapter;
 
     public void setUp() {
 
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
-        mModel = new TestModel(AUTHORITY);
-        mModel.update(NAMES);
+        mEnv = TestEnv.create(AUTHORITY);
 
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
 
@@ -58,7 +46,7 @@
 
     // Tests that the item count is correct.
     public void testItemCount() {
-        assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
+        assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
     }
 
     private final class TestEnvironment implements DocumentsAdapter.Environment {
@@ -83,7 +71,7 @@
 
         @Override
         public Model getModel() {
-            return mModel;
+            return mEnv.model;
         }
 
         @Override
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelTest.java b/tests/unit/com/android/documentsui/dirlist/ModelTest.java
index b9dab0d..75f46e4 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelTest.java
@@ -22,9 +22,9 @@
 import android.database.MatrixCursor;
 import android.database.MergeCursor;
 import android.provider.DocumentsContract.Document;
+import android.support.test.filters.SmallTest;
 import android.test.AndroidTestCase;
 import android.test.mock.MockContentResolver;
-import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.base.DocumentInfo;
@@ -74,7 +74,6 @@
     private Cursor cursor;
     private Model model;
     private TestContentProvider provider;
-    private SortModel sortModel;
 
     @Override
     public void setUp() {
@@ -94,11 +93,9 @@
             row.add(Document.COLUMN_SIZE, rand.nextInt());
         }
         cursor = c;
-        sortModel = SortModels.createTestSortModel();
 
         DirectoryResult r = new DirectoryResult();
         r.cursor = cursor;
-        r.sortModel = sortModel;
 
         // Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
         model = new Model();
@@ -136,7 +133,6 @@
         // Update the model, then make sure it contains all the expected items.
         DirectoryResult r = new DirectoryResult();
         r.cursor = cIn;
-        r.sortModel = SortModels.createTestSortModel();
         model.update(r);
 
         assertEquals(ITEM_COUNT * 2, model.getItemCount());
@@ -174,291 +170,6 @@
             assertEquals(i, c.getPosition());
         }
     }
-
-    // Tests sorting ascending by item name.
-    public void testSort_names_ascending() {
-        BitSet seen = new BitSet(ITEM_COUNT);
-        List<String> names = new ArrayList<>();
-
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
-                SortDimension.SORT_DIRECTION_ASCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = cursor;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        for (String id: model.getModelIds()) {
-            Cursor c = model.getItem(id);
-            seen.set(c.getPosition());
-            names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME));
-        }
-
-        assertEquals(ITEM_COUNT, seen.cardinality());
-        for (int i = 0; i < names.size()-1; ++i) {
-            assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) <= 0);
-        }
-    }
-
-    // Tests sorting descending by item name.
-    public void testSort_names_descending() {
-        BitSet seen = new BitSet(ITEM_COUNT);
-        List<String> names = new ArrayList<>();
-
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
-                SortDimension.SORT_DIRECTION_DESCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = cursor;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        for (String id: model.getModelIds()) {
-            Cursor c = model.getItem(id);
-            seen.set(c.getPosition());
-            names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME));
-        }
-
-        assertEquals(ITEM_COUNT, seen.cardinality());
-        for (int i = 0; i < names.size()-1; ++i) {
-            assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) >= 0);
-        }
-    }
-
-    // Tests sorting by item size.
-    public void testSort_sizes_ascending() {
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
-                SortDimension.SORT_DIRECTION_ASCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = cursor;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        BitSet seen = new BitSet(ITEM_COUNT);
-        int previousSize = Integer.MIN_VALUE;
-        for (String id: model.getModelIds()) {
-            Cursor c = model.getItem(id);
-            seen.set(c.getPosition());
-            // Check sort order - descending numerical
-            int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
-            assertTrue(previousSize <= size);
-            previousSize = size;
-        }
-        // Check that all items were accounted for.
-        assertEquals(ITEM_COUNT, seen.cardinality());
-    }
-
-    // Tests sorting by item size.
-    public void testSort_sizes_descending() {
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
-                SortDimension.SORT_DIRECTION_DESCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = cursor;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        BitSet seen = new BitSet(ITEM_COUNT);
-        int previousSize = Integer.MAX_VALUE;
-        for (String id: model.getModelIds()) {
-            Cursor c = model.getItem(id);
-            seen.set(c.getPosition());
-            // Check sort order - descending numerical
-            int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
-            assertTrue(previousSize >= size);
-            previousSize = size;
-        }
-        // Check that all items were accounted for.
-        assertEquals(ITEM_COUNT, seen.cardinality());
-    }
-
-    // Tests that directories and files are properly bucketed when sorting by size
-    public void testSort_sizesWithBucketing_ascending() {
-        MatrixCursor c = new MatrixCursor(COLUMNS);
-
-        for (int i = 0; i < ITEM_COUNT; ++i) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_SIZE, i);
-            // Interleave directories and text files.
-            String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
-            row.add(Document.COLUMN_MIME_TYPE, mimeType);
-        }
-
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
-                SortDimension.SORT_DIRECTION_ASCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        boolean seenAllDirs = false;
-        int previousSize = Integer.MIN_VALUE;
-        BitSet seen = new BitSet(ITEM_COUNT);
-        // Iterate over items in sort order. Once we've encountered a document (i.e. not a
-        // directory), all subsequent items must also be documents. That is, all directories are
-        // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
-        for (String id: model.getModelIds()) {
-            Cursor cOut = model.getItem(id);
-            seen.set(cOut.getPosition());
-
-            String mimeType = DocumentInfo.getCursorString(cOut, Document.COLUMN_MIME_TYPE);
-            if (seenAllDirs) {
-                assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
-            } else {
-                if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
-                    seenAllDirs = true;
-                    // Reset the previous size seen, because documents are bucketed separately by
-                    // the sort.
-                    previousSize = Integer.MIN_VALUE;
-                }
-            }
-            // Check sort order - descending numerical
-            int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
-            assertTrue(previousSize <= size);
-            previousSize = size;
-        }
-
-        // Check that all items were accounted for.
-        assertEquals(ITEM_COUNT, seen.cardinality());
-    }
-
-    // Tests that directories and files are properly bucketed when sorting by size
-    public void testSort_sizesWithBucketing_descending() {
-        MatrixCursor c = new MatrixCursor(COLUMNS);
-
-        for (int i = 0; i < ITEM_COUNT; ++i) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_SIZE, i);
-            // Interleave directories and text files.
-            String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
-            row.add(Document.COLUMN_MIME_TYPE, mimeType);
-        }
-
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
-                SortDimension.SORT_DIRECTION_DESCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        boolean seenAllDirs = false;
-        int previousSize = Integer.MAX_VALUE;
-        BitSet seen = new BitSet(ITEM_COUNT);
-        // Iterate over items in sort order. Once we've encountered a document (i.e. not a
-        // directory), all subsequent items must also be documents. That is, all directories are
-        // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
-        for (String id: model.getModelIds()) {
-            Cursor cOut = model.getItem(id);
-            seen.set(cOut.getPosition());
-
-            String mimeType = DocumentInfo.getCursorString(cOut, Document.COLUMN_MIME_TYPE);
-            if (seenAllDirs) {
-                assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
-            } else {
-                if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
-                    seenAllDirs = true;
-                    // Reset the previous size seen, because documents are bucketed separately by
-                    // the sort.
-                    previousSize = Integer.MAX_VALUE;
-                }
-            }
-            // Check sort order - descending numerical
-            int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
-            assertTrue(previousSize >= size);
-            previousSize = size;
-        }
-
-        // Check that all items were accounted for.
-        assertEquals(ITEM_COUNT, seen.cardinality());
-    }
-
-    public void testSort_time_ascending() {
-        final int DL_COUNT = 3;
-        MatrixCursor c = new MatrixCursor(COLUMNS);
-        Set<String> currentDownloads = new HashSet<>();
-
-        // Add some files
-        for (int i = 0; i < ITEM_COUNT; i++) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
-        }
-        // Add some current downloads (no timestamp)
-        for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            String id = Integer.toString(i);
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, id);
-            currentDownloads.add(id);
-        }
-
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
-                SortDimension.SORT_DIRECTION_ASCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        String[] ids = model.getModelIds();
-
-        // Check that all items were accounted for
-        assertEquals(ITEM_COUNT + DL_COUNT, ids.length);
-
-        // Check that active downloads are sorted to the bottom.
-        for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
-            assertTrue(currentDownloads.contains(ids[i]));
-        }
-    }
-
-    public void testSort_time_descending() {
-        final int DL_COUNT = 3;
-        MatrixCursor c = new MatrixCursor(COLUMNS);
-        Set<String> currentDownloads = new HashSet<>();
-
-        // Add some files
-        for (int i = 0; i < ITEM_COUNT; i++) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
-        }
-        // Add some current downloads (no timestamp)
-        for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            String id = Integer.toString(i);
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, id);
-            currentDownloads.add(id);
-        }
-
-        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
-                SortDimension.SORT_DIRECTION_DESCENDING);
-
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = sortModel;
-        model.update(r);
-
-        String[] ids = model.getModelIds();
-
-        // Check that all items were accounted for
-        assertEquals(ITEM_COUNT + DL_COUNT, ids.length);
-
-        // Check that active downloads are sorted to the top.
-        for (int i = 0; i < DL_COUNT; i++) {
-            assertTrue(currentDownloads.contains(ids[i]));
-        }
-    }
-
     private void setupTestContext() {
         final MockContentResolver resolver = new MockContentResolver();
         new ContextWrapper(getContext()) {
diff --git a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index dec287b..39ab24a 100644
--- a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -18,95 +18,63 @@
 
 import android.content.Context;
 import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.provider.DocumentsContract.Document;
+import android.support.test.filters.MediumTest;
 import android.support.v7.widget.RecyclerView;
 import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.view.ViewGroup;
 
-import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.base.State;
-import com.android.documentsui.roots.RootCursorWrapper;
-import com.android.documentsui.testing.SortModels;
+import com.android.documentsui.testing.TestEnv;
 
-@SmallTest
+@MediumTest
 public class SectionBreakDocumentsAdapterWrapperTest extends AndroidTestCase {
 
     private static final String AUTHORITY = "test_authority";
-    private static final String[] NAMES = new String[] {
-            "4",
-            "foo",
-            "1",
-            "bar",
-            "*(Ljifl;a",
-            "0",
-            "baz",
-            "2",
-            "3",
-            "%$%VD"
-    };
 
-    private TestModel mModel;
+    private TestEnv mEnv;
     private SectionBreakDocumentsAdapterWrapper mAdapter;
 
     public void setUp() {
 
+        mEnv = TestEnv.create(AUTHORITY);
+        mEnv.clear();
+
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
 
-        mModel = new TestModel(AUTHORITY);
         mAdapter = new SectionBreakDocumentsAdapterWrapper(
             env,
             new ModelBackedDocumentsAdapter(
-                env, new IconHelper(testContext, State.MODE_GRID)));
+                    env, new IconHelper(testContext, State.MODE_GRID)));
 
-        mModel.addUpdateListener(mAdapter.getModelUpdateListener());
-    }
-
-    // Tests that the item count is correct for a directory containing only subdirs.
-    public void testItemCount_allDirs() {
-        MatrixCursor c = new MatrixCursor(TestModel.COLUMNS);
-
-        for (int i = 0; i < 5; ++i) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_SIZE, i);
-            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
-        }
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = SortModels.createTestSortModel();
-        mModel.update(r);
-
-        assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
-    }
-
-    // Tests that the item count is correct for a directory containing only files.
-    public void testItemCount_allFiles() {
-        mModel.update(NAMES);
-        assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
+        mEnv.model.addUpdateListener(mAdapter.getModelUpdateListener());
     }
 
     // Tests that the item count is correct for a directory containing files and subdirs.
     public void testItemCount_mixed() {
-        MatrixCursor c = new MatrixCursor(TestModel.COLUMNS);
+        mEnv.reset();  // creates a mix of folders and files for us.
 
-        for (int i = 0; i < 5; ++i) {
-            MatrixCursor.RowBuilder row = c.newRow();
-            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
-            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
-            row.add(Document.COLUMN_SIZE, i);
-            String mimeType =(i < 2) ? Document.MIME_TYPE_DIR : "text/*";
-            row.add(Document.COLUMN_MIME_TYPE, mimeType);
+        assertEquals(mEnv.model.getItemCount() + 1, mAdapter.getItemCount());
+    }
+
+    // Tests that the item count is correct for a directory containing only subdirs.
+    public void testItemCount_allDirs() {
+        String[] names = {"Trader Joe's", "Alphabeta", "Lucky", "Vons", "Gelson's"};
+        for (String name : names) {
+            mEnv.model.createFolder(name);
         }
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        r.sortModel = SortModels.createTestSortModel();
-        mModel.update(r);
+        mEnv.model.update();
+        assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
+    }
 
-        assertEquals(mModel.getItemCount() + 1, mAdapter.getItemCount());
+    // Tests that the item count is correct for a directory containing only files.
+    public void testItemCount_allFiles() {
+        String[] names = {"123.txt", "234.jpg", "abc.pdf"};
+        for (String name : names) {
+            mEnv.model.createFile(name);
+        }
+        mEnv.model.update();
+        assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
     }
 
     private final class TestEnvironment implements DocumentsAdapter.Environment {
@@ -131,7 +99,7 @@
 
         @Override
         public Model getModel() {
-            return mModel;
+            return mEnv.model;
         }
 
         @Override
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index ada1b5f..457c2ba 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -26,6 +26,7 @@
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.testing.TestConfirmationCallback;
@@ -60,6 +61,7 @@
                 mActivity,
                 mEnv.state,
                 mEnv.roots,
+                mEnv.docs,
                 mEnv::lookupExecutor,
                 mDialogs,
                 null,  // tuner, not currently used.
@@ -71,10 +73,14 @@
 
         mSelection = new Selection();
         mSelection.add("1");
+
+        mHandler.reset(mEnv.model, null);
     }
 
     @Test
     public void testDeleteDocuments() {
+        mEnv.populateStack();
+
         mHandler.deleteDocuments(mEnv.model, mSelection, mCallback);
         mDialogs.assertNoFileFailures();
         mActivity.startService.assertCalled();
@@ -83,6 +89,8 @@
 
     @Test
     public void testDeleteDocuments_Cancelable() {
+        mEnv.populateStack();
+
         mDialogs.rejectNext();
         mHandler.deleteDocuments(mEnv.model, mSelection, mCallback);
         mDialogs.assertNoFileFailures();
@@ -91,6 +99,63 @@
     }
 
     @Test
+    public void testDocumentPicked_DefaultsToView() throws Exception {
+        mActivity.currentRoot = TestRootsAccess.HOME;
+
+        mHandler.onDocumentPicked(TestEnv.FILE_GIF);
+        mActivity.assertActivityStarted(Intent.ACTION_VIEW);
+    }
+
+    @Test
+    public void testDocumentPicked_PreviewsWhenResourceSet() throws Exception {
+        mActivity.resources.setQuickViewerPackage("corptropolis.viewer");
+        mActivity.currentRoot = TestRootsAccess.HOME;
+
+        mHandler.onDocumentPicked(TestEnv.FILE_GIF);
+        mActivity.assertActivityStarted(Intent.ACTION_QUICK_VIEW);
+    }
+
+    @Test
+    public void testDocumentPicked_Downloads_ManagesApks() throws Exception {
+        mActivity.currentRoot = TestRootsAccess.DOWNLOADS;
+
+        mHandler.onDocumentPicked(TestEnv.FILE_APK);
+        mActivity.assertActivityStarted(DocumentsContract.ACTION_MANAGE_DOCUMENT);
+    }
+
+    @Test
+    public void testDocumentPicked_Downloads_ManagesPartialFiles() throws Exception {
+        mActivity.currentRoot = TestRootsAccess.DOWNLOADS;
+
+        mHandler.onDocumentPicked(TestEnv.FILE_PARTIAL);
+        mActivity.assertActivityStarted(DocumentsContract.ACTION_MANAGE_DOCUMENT);
+    }
+
+    @Test
+    public void testDocumentPicked_OpensArchives() throws Exception {
+        mActivity.currentRoot = TestRootsAccess.HOME;
+
+        mHandler.onDocumentPicked(TestEnv.FILE_ARCHIVE);
+        mActivity.openContainer.assertLastArgument(TestEnv.FILE_ARCHIVE);
+    }
+
+    @Test
+    public void testDocumentPicked_OpensDirectories() throws Exception {
+        mActivity.currentRoot = TestRootsAccess.HOME;
+
+        mHandler.onDocumentPicked(TestEnv.FOLDER_1);
+        mActivity.openContainer.assertLastArgument(TestEnv.FOLDER_1);
+    }
+
+    @Test
+    public void testShowChooser() throws Exception {
+        mActivity.currentRoot = TestRootsAccess.DOWNLOADS;
+
+        mHandler.showChooserForDoc(TestEnv.FILE_PDF);
+        mActivity.assertActivityStarted(Intent.ACTION_CHOOSER);
+    }
+
+    @Test
     public void testInitLocation_DefaultsToDownloads() throws Exception {
         mActivity.resources.bools.put(R.bool.productivity_device, false);
 
@@ -107,19 +172,39 @@
     }
 
     @Test
-    public void testInitLocation_LoadsFromRootUri() throws Exception {
-        mActivity.resources.bools.put(R.bool.productivity_device, true);
+    public void testInitLocation_ViewDocument() throws Exception {
+        Intent intent = mActivity.getIntent();
+        intent.setAction(Intent.ACTION_VIEW);
 
+        // configure DocumentsAccess to return something.
+        mEnv.docs.nextRootDocument = TestEnv.FOLDER_0;
+        mEnv.docs.nextDocument = TestEnv.FILE_GIF;
+
+        Uri destUri = mEnv.model.getItemUri(TestEnv.FILE_GIF.documentId);
+        intent.setData(destUri);
+
+        mEnv.state.stack.clear();  // Stack must be clear, we've even got an assert!
+        mHandler.initLocation(intent);
+        assertDocumentPicked(destUri);
+    }
+
+    private void assertDocumentPicked(Uri expectedUri) throws Exception {
+        mEnv.beforeAsserts();
+
+        mActivity.refreshCurrentRootAndDirectory.assertCalled();
+        DocumentInfo doc = mEnv.state.stack.peekLast();
+        Uri actualUri = mEnv.model.getItemUri(doc.documentId);
+        assertEquals(expectedUri, actualUri);
+    }
+
+    @Test
+    public void testInitLocation_BrowseRoot() throws Exception {
         Intent intent = mActivity.getIntent();
         intent.setAction(DocumentsContract.ACTION_BROWSE);
         intent.setData(TestRootsAccess.PICKLES.getUri());
 
         mHandler.initLocation(intent);
-        assertRootPicked(TestRootsAccess.PICKLES);
-    }
-
-    private void assertRootPicked(RootInfo root) throws Exception {
-        assertRootPicked(root.getUri());
+        assertRootPicked(TestRootsAccess.PICKLES.getUri());
     }
 
     private void assertRootPicked(Uri expectedUri) throws Exception {
diff --git a/tests/unit/com/android/documentsui/sorting/SortModelTest.java b/tests/unit/com/android/documentsui/sorting/SortModelTest.java
index e418848..aa50fb0 100644
--- a/tests/unit/com/android/documentsui/sorting/SortModelTest.java
+++ b/tests/unit/com/android/documentsui/sorting/SortModelTest.java
@@ -17,9 +17,7 @@
 package com.android.documentsui.sorting;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.support.annotation.Nullable;
@@ -82,11 +80,6 @@
     }
 
     @Test
-    public void testEnabledByDefault() {
-        assertTrue(mModel.isSortEnabled());
-    }
-
-    @Test
     public void testSizeEquals() {
         assertEquals(DIMENSIONS.length, mModel.getSize());
     }
@@ -193,36 +186,6 @@
     }
 
     @Test
-    public void testSetSortEnabled() {
-        mModel.setSortEnabled(false);
-
-        assertFalse(mModel.isSortEnabled());
-    }
-
-    @Test
-    public void testSetDefaultDimension_sortDisabled() {
-        mModel.setSortEnabled(false);
-
-        mModel.setDefaultDimension(DIMENSION_1.getId());
-
-        SortDimension sortedDimension = getSortedDimension();
-        assertSame(DIMENSION_1, sortedDimension);
-        assertEquals(DIMENSION_1.getDefaultSortDirection(), sortedDimension.getSortDirection());
-    }
-
-    @Test
-    public void testSortByUser_sortDisabled() {
-        mModel.setSortEnabled(false);
-
-        try {
-            mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_ASCENDING);
-            fail("Expect exception but not raised.");
-        } catch(IllegalStateException expected) {
-            // Expected
-        }
-    }
-
-    @Test
     public void testSetDefaultDimension_noSortingCapability() {
         try {
             mModel.setDefaultDimension(DIMENSION_3.getId());
diff --git a/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
new file mode 100644
index 0000000..1d6a09c
--- /dev/null
+++ b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2016 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.sorting;
+
+import static com.android.documentsui.base.DocumentInfo.getCursorString;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.DocumentsContract.Document;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.roots.RootCursorWrapper;
+import com.android.documentsui.sorting.SortModel.SortDimensionId;
+import com.android.documentsui.testing.SortModels;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class SortingCursorWrapperTest {
+    private static final int ITEM_COUNT = 10;
+    private static final String AUTHORITY = "test_authority";
+
+    private static final String[] COLUMNS = new String[]{
+            RootCursorWrapper.COLUMN_AUTHORITY,
+            Document.COLUMN_DOCUMENT_ID,
+            Document.COLUMN_FLAGS,
+            Document.COLUMN_DISPLAY_NAME,
+            Document.COLUMN_SIZE,
+            Document.COLUMN_LAST_MODIFIED,
+            Document.COLUMN_MIME_TYPE
+    };
+
+    private static final String[] NAMES = new String[] {
+            "4",
+            "foo",
+            "1",
+            "bar",
+            "*(Ljifl;a",
+            "0",
+            "baz",
+            "2",
+            "3",
+            "%$%VD"
+    };
+
+    private SortModel sortModel;
+    private Cursor cursor;
+
+    @Before
+    public void setUp() {
+        sortModel = SortModels.createTestSortModel();
+
+        Random rand = new Random();
+        MatrixCursor c = new MatrixCursor(COLUMNS);
+        for (int i = 0; i < ITEM_COUNT; ++i) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+            row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
+            // Generate random document names and sizes. This forces the model's internal sort code
+            // to actually do something.
+            row.add(Document.COLUMN_DISPLAY_NAME, NAMES[i]);
+            row.add(Document.COLUMN_SIZE, rand.nextInt());
+        }
+
+        cursor = c;
+    }
+
+    // Tests sorting ascending by item name.
+    @Test
+    public void testSort_names_ascending() {
+        BitSet seen = new BitSet(ITEM_COUNT);
+        List<String> names = new ArrayList<>();
+
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
+                SortDimension.SORT_DIRECTION_ASCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper();
+
+        for (int i = 0; i < cursor.getCount(); ++i) {
+            cursor.moveToPosition(i);
+            seen.set(Integer.parseInt(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+            names.add(getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
+        }
+
+        assertEquals(ITEM_COUNT, seen.cardinality());
+        for (int i = 0; i < names.size()-1; ++i) {
+            assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) <= 0);
+        }
+    }
+
+    // Tests sorting descending by item name.
+    @Test
+    public void testSort_names_descending() {
+        BitSet seen = new BitSet(ITEM_COUNT);
+        List<String> names = new ArrayList<>();
+
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
+                SortDimension.SORT_DIRECTION_DESCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper();
+
+        for (int i = 0; i < cursor.getCount(); ++i) {
+            cursor.moveToPosition(i);
+            seen.set(Integer.parseInt(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+            names.add(getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
+        }
+
+        assertEquals(ITEM_COUNT, seen.cardinality());
+        for (int i = 0; i < names.size()-1; ++i) {
+            assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) >= 0);
+        }
+    }
+
+    // Tests sorting by item size.
+    @Test
+    public void testSort_sizes_ascending() {
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+                SortDimension.SORT_DIRECTION_ASCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper();
+
+        BitSet seen = new BitSet(ITEM_COUNT);
+        int previousSize = Integer.MIN_VALUE;
+        for (int i = 0; i < cursor.getCount(); ++i) {
+            cursor.moveToPosition(i);
+            seen.set(Integer.parseInt(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+            // Check sort order - descending numerical
+            int size = DocumentInfo.getCursorInt(cursor, Document.COLUMN_SIZE);
+            assertTrue(previousSize <= size);
+            previousSize = size;
+        }
+        // Check that all items were accounted for.
+        assertEquals(ITEM_COUNT, seen.cardinality());
+    }
+
+    // Tests sorting by item size.
+    @Test
+    public void testSort_sizes_descending() {
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+                SortDimension.SORT_DIRECTION_DESCENDING);
+
+        Cursor cursor = createSortingCursorWrapper();
+
+        BitSet seen = new BitSet(ITEM_COUNT);
+        int previousSize = Integer.MAX_VALUE;
+        for (int i = 0; i < cursor.getCount(); ++i) {
+            cursor.moveToPosition(i);
+            seen.set(Integer.parseInt(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+            // Check sort order - descending numerical
+            int size = DocumentInfo.getCursorInt(cursor, Document.COLUMN_SIZE);
+            assertTrue(previousSize >= size);
+            previousSize = size;
+        }
+        // Check that all items were accounted for.
+        assertEquals(ITEM_COUNT, seen.cardinality());
+    }
+
+    // Tests that directories and files are properly bucketed when sorting by size
+    @Test
+    public void testSort_sizesWithBucketing_ascending() {
+        MatrixCursor c = new MatrixCursor(COLUMNS);
+
+        for (int i = 0; i < ITEM_COUNT; ++i) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+            row.add(Document.COLUMN_SIZE, i);
+            // Interleave directories and text files.
+            String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
+            row.add(Document.COLUMN_MIME_TYPE, mimeType);
+        }
+
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+                SortDimension.SORT_DIRECTION_ASCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper(c);
+
+        boolean seenAllDirs = false;
+        int previousSize = Integer.MIN_VALUE;
+        BitSet seen = new BitSet(ITEM_COUNT);
+        // Iterate over items in sort order. Once we've encountered a document (i.e. not a
+        // directory), all subsequent items must also be documents. That is, all directories are
+        // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
+        for (int i = 0; i < cursor.getCount(); ++i) {
+            cursor.moveToPosition(i);
+            seen.set(Integer.parseInt(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+
+            String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            if (seenAllDirs) {
+                assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
+            } else {
+                if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
+                    seenAllDirs = true;
+                    // Reset the previous size seen, because documents are bucketed separately by
+                    // the sort.
+                    previousSize = Integer.MIN_VALUE;
+                }
+            }
+            // Check sort order - descending numerical
+            int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
+            assertTrue(previousSize <= size);
+            previousSize = size;
+        }
+
+        // Check that all items were accounted for.
+        assertEquals(ITEM_COUNT, seen.cardinality());
+    }
+
+    // Tests that directories and files are properly bucketed when sorting by size
+    @Test
+    public void testSort_sizesWithBucketing_descending() {
+        MatrixCursor c = new MatrixCursor(COLUMNS);
+
+        for (int i = 0; i < ITEM_COUNT; ++i) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+            row.add(Document.COLUMN_SIZE, i);
+            // Interleave directories and text files.
+            String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
+            row.add(Document.COLUMN_MIME_TYPE, mimeType);
+        }
+
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+                SortDimension.SORT_DIRECTION_DESCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper(c);
+
+        boolean seenAllDirs = false;
+        int previousSize = Integer.MAX_VALUE;
+        BitSet seen = new BitSet(ITEM_COUNT);
+        // Iterate over items in sort order. Once we've encountered a document (i.e. not a
+        // directory), all subsequent items must also be documents. That is, all directories are
+        // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
+        for (int i = 0; i < cursor.getCount(); ++i) {
+            cursor.moveToPosition(i);
+            seen.set(cursor.getPosition());
+
+            String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            if (seenAllDirs) {
+                assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
+            } else {
+                if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
+                    seenAllDirs = true;
+                    // Reset the previous size seen, because documents are bucketed separately by
+                    // the sort.
+                    previousSize = Integer.MAX_VALUE;
+                }
+            }
+            // Check sort order - descending numerical
+            int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
+            assertTrue(previousSize >= size);
+            previousSize = size;
+        }
+
+        // Check that all items were accounted for.
+        assertEquals(ITEM_COUNT, seen.cardinality());
+    }
+
+    @Test
+    public void testSort_time_ascending() {
+        final int DL_COUNT = 3;
+        MatrixCursor c = new MatrixCursor(COLUMNS);
+        Set<String> currentDownloads = new HashSet<>();
+
+        // Add some files
+        for (int i = 0; i < ITEM_COUNT; i++) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+            row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
+        }
+        // Add some current downloads (no timestamp)
+        for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            String id = Integer.toString(i);
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, id);
+            currentDownloads.add(id);
+        }
+
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
+                SortDimension.SORT_DIRECTION_ASCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper(c);
+
+        // Check that all items were accounted for
+        assertEquals(ITEM_COUNT + DL_COUNT, cursor.getCount());
+
+        // Check that active downloads are sorted to the bottom.
+        for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+            assertTrue(currentDownloads.contains(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+        }
+    }
+
+    @Test
+    public void testSort_time_descending() {
+        final int DL_COUNT = 3;
+        MatrixCursor c = new MatrixCursor(COLUMNS);
+        Set<String> currentDownloads = new HashSet<>();
+
+        // Add some files
+        for (int i = 0; i < ITEM_COUNT; i++) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+            row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
+        }
+        // Add some current downloads (no timestamp)
+        for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+            MatrixCursor.RowBuilder row = c.newRow();
+            String id = Integer.toString(i);
+            row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+            row.add(Document.COLUMN_DOCUMENT_ID, id);
+            currentDownloads.add(id);
+        }
+
+        sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
+                SortDimension.SORT_DIRECTION_DESCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper(c);
+
+        // Check that all items were accounted for
+        assertEquals(ITEM_COUNT + DL_COUNT, cursor.getCount());
+
+        // Check that active downloads are sorted to the top.
+        for (int i = 0; i < DL_COUNT; i++) {
+            assertTrue(currentDownloads.contains(
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+        }
+    }
+
+    private Cursor createSortingCursorWrapper() {
+        return createSortingCursorWrapper(cursor);
+    }
+
+    private Cursor createSortingCursorWrapper(Cursor c) {
+        final @SortDimensionId int id = sortModel.getSortedDimensionId();
+        return new SortingCursorWrapper(c, sortModel.getDimensionById(id));
+    }
+}