Lift loader to activity level.

Also fix a bug that leaves DocumentsUI in a weird state if it fails to
obtain root document.

Change-Id: Ibb67bfd0114f45f41c0000078ca56767b5a4542b
Tests: Manual tests and auto tests.
Bug: 35934082
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 9e69c09..d4e964c 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -18,12 +18,17 @@
 
 import static com.android.documentsui.base.DocumentInfo.getCursorInt;
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
+import static com.android.documentsui.base.Shared.DEBUG;
 
 import android.app.Activity;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
 import android.content.Intent;
+import android.content.Loader;
 import android.content.pm.ResolveInfo;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.support.annotation.VisibleForTesting;
@@ -44,7 +49,6 @@
 import com.android.documentsui.dirlist.AnimationView.AnimationType;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.dirlist.FocusHandler;
-import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.files.LauncherActivity;
 import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.LoadRootTask;
@@ -66,6 +70,9 @@
 public abstract class AbstractActionHandler<T extends Activity & CommonAddons>
         implements ActionHandler {
 
+    @VisibleForTesting
+    static final int LOADER_ID = 42;
+
     private static final String TAG = "AbstractActionHandler";
     private static final int REFRESH_SPINNER_TIMEOUT = 500;
 
@@ -79,8 +86,12 @@
     protected final Lookup<String, Executor> mExecutors;
     protected final Injector mInjector;
 
+    private final LoaderBindings mBindings;
+
     private Runnable mDisplayStateChangedListener;
 
+    private DirectoryReloadLock mDirectoryReloadLock;
+
     @Override
     public void registerDisplayStateChangedListener(Runnable l) {
         mDisplayStateChangedListener = l;
@@ -117,6 +128,8 @@
         mSearchMgr = searchMgr;
         mExecutors = executors;
         mInjector = injector;
+
+        mBindings = new LoaderBindings();
     }
 
     @Override
@@ -243,6 +256,18 @@
     }
 
     @Override
+    public void openRootDocument(@Nullable DocumentInfo rootDoc) {
+        if (rootDoc == null) {
+            // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root
+            // document. Either case we should call refreshCurrentRootAndDirectory() to let
+            // DirectoryFragment update UI.
+            mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+        } else {
+            openContainerDocument(rootDoc);
+        }
+    }
+
+    @Override
     public void openContainerDocument(DocumentInfo doc) {
         assert(doc.isContainer());
 
@@ -346,6 +371,22 @@
                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
     }
 
+    @Override
+    public void loadDocumentsForCurrentStack() {
+        DocumentStack stack = mState.stack;
+        if (!stack.isRecents() && stack.isEmpty()) {
+            DirectoryResult result = new DirectoryResult();
+
+            // TODO (b/35996595): Consider plumbing through the actual exception, though it might
+            // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()).
+            result.exception = new IllegalStateException("Failed to load root document.");
+            mInjector.getModel().update(result);
+            return;
+        }
+
+        mActivity.getLoaderManager().restartLoader(LOADER_ID, null, mBindings);
+    }
+
     protected final boolean launchToDocument(Uri uri) {
         // We don't support launching to a document in an archive.
         if (!Providers.isArchiveUri(uri)) {
@@ -385,6 +426,65 @@
         return mSelectionMgr.getSelection(new Selection());
     }
 
+    public ActionHandler reset(DirectoryReloadLock reloadLock) {
+        mDirectoryReloadLock = reloadLock;
+        mActivity.getLoaderManager().destroyLoader(LOADER_ID);
+        return this;
+    }
+
+    private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
+
+        @Override
+        public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
+            Context context = mActivity;
+
+            if (mState.stack.isRecents()) {
+
+                if (DEBUG) Log.d(TAG, "Creating new loader recents.");
+                return new RecentsLoader(context, mRoots, mState, mInjector.features);
+
+            } else {
+
+                Uri contentsUri = mSearchMgr.isSearching()
+                        ? DocumentsContract.buildSearchDocumentsUri(
+                            mState.stack.getRoot().authority,
+                            mState.stack.getRoot().rootId,
+                            mSearchMgr.getCurrentSearch())
+                        : DocumentsContract.buildChildDocumentsUri(
+                                mState.stack.peek().authority,
+                                mState.stack.peek().documentId);
+
+                if (mInjector.config.managedModeEnabled(mState.stack)) {
+                    contentsUri = DocumentsContract.setManageMode(contentsUri);
+                }
+
+                if (DEBUG) Log.d(TAG,
+                        "Creating new directory loader for: "
+                                + DocumentInfo.debugString(mState.stack.peek()));
+
+                return new DirectoryLoader(
+                        context,
+                        mState.stack.getRoot(),
+                        mState.stack.peek(),
+                        contentsUri,
+                        mState.sortModel,
+                        mDirectoryReloadLock,
+                        mSearchMgr.isSearching());
+            }
+        }
+
+        @Override
+        public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+            if (DEBUG) Log.d(TAG, "Loader has finished for: "
+                    + DocumentInfo.debugString(mState.stack.peek()));
+            assert(result != null);
+
+            mInjector.getModel().update(result);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<DirectoryResult> loader) {}
+    }
     /**
      * A class primarily for the support of isolating our tests
      * from our concrete activity implementations.
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 7b0eef4..33f1d7d 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -27,7 +27,6 @@
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.dirlist.Model;
 
 import javax.annotation.Nullable;
 
@@ -84,6 +83,8 @@
 
     void showChooserForDoc(DocumentInfo doc);
 
+    void openRootDocument(@Nullable DocumentInfo rootDoc);
+
     void openContainerDocument(DocumentInfo doc);
 
     void cutToClipboard();
@@ -107,9 +108,11 @@
     void registerDisplayStateChangedListener(Runnable l);
     void unregisterDisplayStateChangedListener(Runnable l);
 
+    void loadDocumentsForCurrentStack();
+
     /**
      * Allow action handler to be initialized in a new scope.
-     * @return
+     * @return this
      */
-    <T extends ActionHandler> T reset(Model model);
+    <T extends ActionHandler> T reset(DirectoryReloadLock reloadLock);
 }
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 05365d5..d49908b 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -22,7 +22,6 @@
 
 import android.app.Activity;
 import android.app.Fragment;
-import android.app.FragmentManager;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -32,7 +31,6 @@
 import android.os.Bundle;
 import android.os.MessageQueue.IdleHandler;
 import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.Root;
 import android.support.annotation.CallSuper;
 import android.support.annotation.LayoutRes;
 import android.support.annotation.Nullable;
@@ -52,7 +50,6 @@
 import com.android.documentsui.base.State.ViewMode;
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DirectoryFragment;
-import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.prefs.LocalPreferences;
 import com.android.documentsui.prefs.PreferencesMonitor;
 import com.android.documentsui.queries.DebugCommandProcessor;
@@ -155,7 +152,11 @@
              */
             @Override
             public void onSearchChanged(@Nullable String query) {
-                reloadSearch(query);
+                if (query != null) {
+                    Metrics.logUserAction(BaseActivity.this, Metrics.USER_ACTION_SEARCH);
+                }
+
+                mInjector.actions.loadDocumentsForCurrentStack();
             }
 
             @Override
@@ -302,7 +303,7 @@
             new GetRootDocumentTask(
                     root,
                     this,
-                    mInjector.actions::openContainerDocument)
+                    mInjector.actions::openRootDocument)
                     .executeOnExecutor(getExecutorForCurrentDirectory());
         }
     }
@@ -405,14 +406,6 @@
         invalidateOptionsMenu();
     }
 
-    private void reloadSearch(String query) {
-        FragmentManager fm = getFragmentManager();
-        RootInfo root = getCurrentRoot();
-        DocumentInfo cwd = getCurrentDirectory();
-
-        DirectoryFragment.reloadSearch(fm, root, cwd, query);
-    }
-
     private final List<String> getExcludedAuthorities() {
         List<String> authorities = new ArrayList<>();
         if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index 0a4dff1..4fbb3ff 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -29,6 +29,7 @@
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.OperationCanceledException;
 import android.os.RemoteException;
 import android.provider.DocumentsContract.Document;
@@ -220,7 +221,7 @@
         private final Runnable mContentChangedCallback;
 
         public LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback) {
-            super(new Handler());
+            super(new Handler(Looper.getMainLooper()));
             mLock = lock;
             mContentChangedCallback = contentChangedCallback;
         }
diff --git a/src/com/android/documentsui/DragShadowBuilder.java b/src/com/android/documentsui/DragShadowBuilder.java
index 2bc6dde..3ba09d0 100644
--- a/src/com/android/documentsui/DragShadowBuilder.java
+++ b/src/com/android/documentsui/DragShadowBuilder.java
@@ -30,7 +30,6 @@
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.dirlist.IconHelper;
-import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.selection.Selection;
 
 import java.util.List;
diff --git a/src/com/android/documentsui/FocusManager.java b/src/com/android/documentsui/FocusManager.java
index 8126e4d..fe41aa9 100644
--- a/src/com/android/documentsui/FocusManager.java
+++ b/src/com/android/documentsui/FocusManager.java
@@ -47,8 +47,7 @@
 import com.android.documentsui.dirlist.DocumentHolder;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.dirlist.FocusHandler;
-import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.Model.Update;
 import com.android.documentsui.selection.SelectionManager;
 
 import java.util.ArrayList;
diff --git a/src/com/android/documentsui/Injector.java b/src/com/android/documentsui/Injector.java
index 0a3f5bb..8f22d22 100644
--- a/src/com/android/documentsui/Injector.java
+++ b/src/com/android/documentsui/Injector.java
@@ -27,12 +27,13 @@
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Features;
 import com.android.documentsui.dirlist.DocumentsAdapter;
-import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.prefs.ScopedPreferences;
+import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
 import com.android.documentsui.ui.DialogController;
 import com.android.documentsui.ui.MessageBuilder;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
@@ -49,6 +50,7 @@
 
     public MenuManager menuManager;
     public DialogController dialogs;
+    public SearchViewManager searchManager;
 
     @ContentScoped
     public ActionModeController actionModeController;
@@ -72,14 +74,24 @@
             ScopedPreferences prefs,
             MessageBuilder messages,
             DialogController dialogs) {
+        this(features, config, prefs, messages, dialogs, new Model(features));
+    }
+
+    @VisibleForTesting
+    public Injector(
+            Features features,
+            ActivityConfig config,
+            ScopedPreferences prefs,
+            MessageBuilder messages,
+            DialogController dialogs,
+            Model model) {
 
         this.features = features;
         this.config = config;
         this.prefs = prefs;
         this.messages = messages;
         this.dialogs = dialogs;
-
-        mModel = new Model(this.features);
+        this.mModel = model;
     }
 
     public Model getModel() {
@@ -101,14 +113,22 @@
         return actionModeController.reset(selectionDetails, menuItemClicker, view);
     }
 
-    public T getActionHandler(@Nullable Model model) {
+    /**
+     * Obtains action handler and resets it if necessary.
+     * @param reloadLock the lock held by {@link com.android.documentsui.selection.BandController}
+     *                   to prevent loader from updating result during band selection. May be
+     *                   {@code null} if called from
+     *                   {@link com.android.documentsui.sidebar.RootsFragment}.
+     * @return the action handler
+     */
+    public T getActionHandler(@Nullable DirectoryReloadLock reloadLock) {
 
         // provide our friend, RootsFragment, early access to this special feature!
-        if (model == null) {
+        if (reloadLock == null) {
             return actions;
         }
 
-        return actions.reset(model);
+        return actions.reset(reloadLock);
     }
 
     /**
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/Model.java
similarity index 96%
rename from src/com/android/documentsui/dirlist/Model.java
rename to src/com/android/documentsui/Model.java
index 43aed9a..9cc6972 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/Model.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.documentsui.dirlist;
+package com.android.documentsui;
 
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
 import static com.android.documentsui.base.Shared.DEBUG;
@@ -58,11 +58,12 @@
 
     private static final String TAG = "Model";
 
-    @Nullable String info;
-    @Nullable String error;
-    @Nullable DocumentInfo doc;
+    public @Nullable String info;
+    public @Nullable String error;
+    public @Nullable DocumentInfo doc;
 
     private final Features mFeatures;
+
     /** Maps Model ID to cursor positions, for looking up items by Model ID. */
     private final Map<String, Integer> mPositions = new HashMap<>();
     private final Set<String> mFileNames = new HashSet<>();
@@ -98,7 +99,7 @@
         }
     }
 
-    void reset() {
+    public void reset() {
         mCursor = null;
         mCursorCount = 0;
         mIds = new String[0];
@@ -111,7 +112,8 @@
         notifyUpdateListeners();
     }
 
-    void update(DirectoryResult result) {
+    @VisibleForTesting
+    protected void update(DirectoryResult result) {
         assert(result != null);
 
         if (DEBUG) Log.i(TAG, "Updating model with new result set.");
@@ -139,7 +141,7 @@
     }
 
     @VisibleForTesting
-    int getItemCount() {
+    public int getItemCount() {
         return mCursorCount;
     }
 
@@ -200,7 +202,7 @@
         return mCursorCount == 0;
     }
 
-    boolean isLoading() {
+    public boolean isLoading() {
         return mIsLoading;
     }
 
diff --git a/src/com/android/documentsui/base/DocumentInfo.java b/src/com/android/documentsui/base/DocumentInfo.java
index 006553b..3f35ba3 100644
--- a/src/com/android/documentsui/base/DocumentInfo.java
+++ b/src/com/android/documentsui/base/DocumentInfo.java
@@ -272,6 +272,10 @@
         return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
     }
 
+    public boolean prefersSortByLastModified() {
+        return (flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0;
+    }
+
     @Override
     public int hashCode() {
         return derivedUri.hashCode() + mimeType.hashCode();
diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java
index 1423666..9aa15c2 100644
--- a/src/com/android/documentsui/base/DocumentStack.java
+++ b/src/com/android/documentsui/base/DocumentStack.java
@@ -168,7 +168,7 @@
     }
 
     public boolean isRecents() {
-        return isEmpty();
+        return mRoot != null && mRoot.isRecents();
     }
 
     public void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException {
diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java
index 7616edb..0e24eda 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -77,11 +77,6 @@
     public static final String EXTRA_STATE = "state";
 
     /**
-     * Extra flag used to store type of DirectoryFragment's type ResultType type in the bundle.
-     */
-    public static final String EXTRA_TYPE = "type";
-
-    /**
      * Extra flag used to store root of type RootInfo in the bundle.
      */
     public static final String EXTRA_ROOT = "root";
@@ -97,11 +92,6 @@
     public static final String EXTRA_SELECTION = "selection";
 
     /**
-     * Extra flag used to store DirectoryFragment's search mode of boolean type in the bundle.
-     */
-    public static final String EXTRA_SEARCH_MODE = "searchMode";
-
-    /**
      * Extra flag used to store DirectoryFragment's ignore state of boolean type in the bundle.
      */
     public static final String EXTRA_IGNORE_STATE = "ignoreState";
diff --git a/src/com/android/documentsui/clipping/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
index 1e307ce..0c1b59c 100644
--- a/src/com/android/documentsui/clipping/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -254,7 +254,7 @@
      */
     public void copyFromClipData(
             final RootInfo root,
-            final DocumentInfo destination,
+            final @Nullable DocumentInfo destination,
             final @Nullable ClipData clipData,
             final FileOperations.Callback callback) {
         DocumentStack dstStack = new DocumentStack(root, destination);
@@ -271,7 +271,7 @@
      * @param callback callback to notify when operation finishes
      */
     public void copyFromClipData(
-            final DocumentInfo destination,
+            final @Nullable DocumentInfo destination,
             final DocumentStack docStack,
             final @Nullable ClipData clipData,
             final FileOperations.Callback callback) {
@@ -283,7 +283,7 @@
     /**
      * Copies documents from given clip data to a folder.
      *
-     * @param docStack the document stack to the destination folder, including the destination
+     * @param dstStack the document stack to the destination folder, including the destination
      *            folder.
      * @param clipData the clipData to copy from
      * @param callback callback to notify when operation finishes
@@ -340,12 +340,8 @@
      *
      * @return true if the list of files can be copied to destination.
      */
-    private static boolean canCopy(DocumentInfo dest) {
-        if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
-            return false;
-        }
-
-        return true;
+    private static boolean canCopy(@Nullable DocumentInfo dest) {
+        return dest != null && dest.isDirectory() && dest.isCreateSupported();
     }
 
     public static @OpType int getOpType(ClipData data) {
diff --git a/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java b/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
index 51bf589..a551423 100644
--- a/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
@@ -20,10 +20,11 @@
 import android.support.v7.widget.RecyclerView.AdapterDataObserver;
 import android.view.ViewGroup;
 
+import com.android.documentsui.Model;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.dirlist.Message.HeaderMessage;
 import com.android.documentsui.dirlist.Message.InflateMessage;
-import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.Model.Update;
 
 import java.util.List;
 
@@ -174,7 +175,6 @@
             return;
         }
 
-
         // Walk down the list of IDs till we encounter something that's not a directory, and
         // insert a whitespace element - this introduces a visual break in the grid between
         // folders and documents.
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 3da4276..8a0ba0d 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -31,11 +31,9 @@
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
-import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ClipData;
 import android.content.Context;
 import android.content.Intent;
-import android.content.Loader;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
@@ -66,9 +64,7 @@
 import com.android.documentsui.ActionModeController;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.BaseActivity.RetainedState;
-import com.android.documentsui.DirectoryLoader;
 import com.android.documentsui.DirectoryReloadLock;
-import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.DragAndDropHelper;
 import com.android.documentsui.FocusManager;
@@ -77,8 +73,8 @@
 import com.android.documentsui.Injector.Injected;
 import com.android.documentsui.ItemDragListener;
 import com.android.documentsui.Metrics;
+import com.android.documentsui.Model;
 import com.android.documentsui.R;
-import com.android.documentsui.RecentsLoader;
 import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -96,7 +92,6 @@
 import com.android.documentsui.clipping.UrisSupplier;
 import com.android.documentsui.dirlist.AnimationView.AnimationType;
 import com.android.documentsui.picker.PickActivity;
-import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.selection.BandController;
 import com.android.documentsui.selection.GestureSelector;
 import com.android.documentsui.selection.Selection;
@@ -122,14 +117,8 @@
 public class DirectoryFragment extends Fragment
         implements ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
 
-    @IntDef(flag = true, value = {
-            TYPE_NORMAL,
-            TYPE_RECENT_OPEN
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ResultType {}
-    public static final int TYPE_NORMAL = 1;
-    public static final int TYPE_RECENT_OPEN = 2;
+    static final int TYPE_NORMAL = 1;
+    static final int TYPE_RECENT_OPEN = 2;
 
     @IntDef(flag = true, value = {
             REQUEST_COPY_DESTINATION
@@ -150,7 +139,6 @@
     private Model mModel;
     private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener();
     private final DocumentsAdapter.Environment mAdapterEnv = new AdapterEnvironment();
-    private final LoaderCallbacks<DirectoryResult> mLoaderCallbacks = new LoaderBindings();
 
     @Injected
     @ContentScoped
@@ -201,7 +189,7 @@
     private SortModel.UpdateListener mSortListener = (model, updateType) -> {
         // Only when sort order has changed do we need to trigger another loading.
         if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) {
-            getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
+            mActions.loadDocumentsForCurrentStack();
         }
     };
 
@@ -259,6 +247,9 @@
             cancelThumbnailTask(view);
         }
 
+        mModel.removeUpdateListener(mModelUpdateListener);
+        mModel.removeUpdateListener(mAdapter.getModelUpdateListener());
+
         super.onDestroyView();
     }
 
@@ -312,7 +303,7 @@
 
         mSelectionMgr = mInjector.getSelectionManager(mAdapter, this::canSetSelectionState);
         mFocusManager = mInjector.getFocusManager(mRecView, mModel);
-        mActions = mInjector.getActionHandler(mModel);
+        mActions = mInjector.getActionHandler(mReloadLock);
 
         mRecView.setAccessibilityDelegateCompat(
                 new AccessibilityEventRouter(mRecView,
@@ -382,14 +373,13 @@
 
         final ActivityManager am = (ActivityManager) mActivity.getSystemService(
                 Context.ACTIVITY_SERVICE);
-        boolean svelte = am.isLowRamDevice() && (mLocalState.mType == TYPE_RECENT_OPEN);
+        boolean svelte = am.isLowRamDevice() && (mState.stack.isRecents());
         mIconHelper.setThumbnailsEnabled(!svelte);
 
         // If mDocument is null, we sort it by last modified by default because it's in Recents.
         final boolean prefersLastModified =
-                (mLocalState.mDocument != null)
-                        ? (mLocalState.mDocument.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0
-                        : true;
+                (mLocalState.mDocument == null)
+                || mLocalState.mDocument.prefersSortByLastModified();
         // Call this before adding the listener to avoid restarting the loader one more time
         mState.sortModel.setDefaultDimension(
                 prefersLastModified
@@ -397,7 +387,7 @@
                         : SortModel.SORT_DIMENSION_ID_TITLE);
 
         // Kick off loader at least once
-        getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
+        mActions.loadDocumentsForCurrentStack();
     }
 
     @Override
@@ -1073,36 +1063,17 @@
     public static void showDirectory(
             FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
         if (DEBUG) Log.d(TAG, "Showing directory: " + DocumentInfo.debugString(doc));
-        create(fm, TYPE_NORMAL, root, doc, null, anim);
+        create(fm, root, doc, anim);
     }
 
     public static void showRecentsOpen(FragmentManager fm, int anim) {
-        create(fm, TYPE_RECENT_OPEN, null, null, null, anim);
-    }
-
-    public static void reloadSearch(FragmentManager fm, RootInfo root, DocumentInfo doc,
-            String query) {
-        DirectoryFragment df = get(fm);
-
-        df.mLocalState.update(root, doc, query);
-        df.getLoaderManager().restartLoader(LOADER_ID, null, df.mLoaderCallbacks);
-    }
-
-    public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
-            String query) {
-        if (DEBUG) Log.d(TAG, "Reloading directory: " + DocumentInfo.debugString(doc));
-        DirectoryFragment df = get(fm);
-
-        df.mLocalState.update(type, root, doc, query);
-        df.getLoaderManager().restartLoader(LOADER_ID, null, df.mLoaderCallbacks);
+        create(fm, null, null, anim);
     }
 
     public static void create(
             FragmentManager fm,
-            int type,
             RootInfo root,
             @Nullable DocumentInfo doc,
-            String query,
             @AnimationType int anim) {
 
         if (DEBUG) {
@@ -1114,10 +1085,8 @@
         }
 
         final Bundle args = new Bundle();
-        args.putInt(Shared.EXTRA_TYPE, type);
         args.putParcelable(Shared.EXTRA_ROOT, root);
         args.putParcelable(Shared.EXTRA_DOC, doc);
-        args.putString(Shared.EXTRA_QUERY, query);
         args.putParcelable(Shared.EXTRA_SELECTION, new Selection());
 
         final FragmentTransaction ft = fm.beginTransaction();
@@ -1160,7 +1129,7 @@
                 mRefreshLayout.setRefreshing(false);
             } else {
                 // If Refresh API isn't available, we will explicitly reload the loader
-                getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
+                mActions.loadDocumentsForCurrentStack();
             }
         });
     }
@@ -1173,131 +1142,10 @@
 
             mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
 
-            mAdapter.notifyDataSetChanged();
-
-            if (!mModel.isLoading()) {
-                mActivity.notifyDirectoryLoaded(
-                        mModel.doc != null ? mModel.doc.derivedUri : null);
-            }
-        }
-    }
-
-    private final class AdapterEnvironment implements DocumentsAdapter.Environment {
-
-        @Override
-        public Features getFeatures() {
-            return mInjector.features;
-        }
-
-        @Override
-        public Context getContext() {
-            return mActivity;
-        }
-
-        @Override
-        public State getDisplayState() {
-            return mState;
-        }
-
-        @Override
-        public boolean isInSearchMode() {
-            return mLocalState.mSearchMode;
-        }
-
-        @Override
-        public Model getModel() {
-            return mModel;
-        }
-
-        @Override
-        public int getColumnCount() {
-            return mColumnCount;
-        }
-
-        @Override
-        public boolean isSelected(String id) {
-            return mSelectionMgr.getSelection().contains(id);
-        }
-
-        @Override
-        public boolean isDocumentEnabled(String mimeType, int flags) {
-            return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
-        }
-
-        @Override
-        public void initDocumentHolder(DocumentHolder holder) {
-            holder.addKeyEventListener(mInputHandler);
-            holder.itemView.setOnFocusChangeListener(mFocusManager);
-        }
-
-        @Override
-        public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
-            setupDragAndDropOnDocumentView(holder.itemView, cursor);
-        }
-    }
-
-    private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
-
-        @Override
-        public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
-            Context context = getActivity();
-
-            Uri contentsUri;
-            switch (mLocalState.mType) {
-                case TYPE_NORMAL:
-                    contentsUri = mLocalState.mSearchMode
-                            ? DocumentsContract.buildSearchDocumentsUri(
-                                    mLocalState.mRoot.authority,
-                                    mLocalState.mRoot.rootId,
-                                    mLocalState.mQuery)
-                            : DocumentsContract.buildChildDocumentsUri(
-                                    mLocalState.mDocument.authority,
-                                    mLocalState.mDocument.documentId);
-
-                    if (mInjector.config.managedModeEnabled(mState.stack)) {
-                        contentsUri = DocumentsContract.setManageMode(contentsUri);
-                    }
-
-                    if (DEBUG) Log.d(TAG,
-                            "Creating new directory loader for: "
-                            + DocumentInfo.debugString(mLocalState.mDocument));
-
-                    return new DirectoryLoader(
-                            context,
-                            mLocalState.mRoot,
-                            mLocalState.mDocument,
-                            contentsUri,
-                            mState.sortModel,
-                            mReloadLock,
-                            mLocalState.mSearchMode);
-
-                case TYPE_RECENT_OPEN:
-                    if (DEBUG) Log.d(TAG, "Creating new loader recents.");
-                    final RootsAccess roots = DocumentsApplication.getRootsCache(context);
-                    return new RecentsLoader(context, roots, mState, mInjector.features);
-
-                default:
-                    throw new IllegalStateException("Unknown type " + mLocalState.mType);
-            }
-        }
-
-        @Override
-        public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
-            if (DEBUG) Log.d(TAG, "Loader has finished for: "
-                    + DocumentInfo.debugString(mLocalState.mDocument));
-            assert(result != null);
-
-            if (!isAdded()) return;
-
-            if (mLocalState.mSearchMode) {
-                Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
-            }
-
-            mAdapter.notifyDataSetChanged();
-            mModel.update(result);
-
             updateLayout(mState.derivedMode);
 
+            mAdapter.notifyDataSetChanged();
+
             if (mRestoredSelection != null) {
                 mSelectionMgr.restoreSelection(mRestoredSelection);
                 // Note, we'll take care of cleaning up retained selection
@@ -1330,14 +1178,65 @@
                         () -> mRefreshLayout.setRefreshing(false),
                         REFRESH_SPINNER_TIMEOUT);
             }
+
+            if (!mModel.isLoading()) {
+                mActivity.notifyDirectoryLoaded(
+                        mModel.doc != null ? mModel.doc.derivedUri : null);
+            }
+        }
+    }
+
+    private final class AdapterEnvironment implements DocumentsAdapter.Environment {
+
+        @Override
+        public Features getFeatures() {
+            return mInjector.features;
         }
 
         @Override
-        public void onLoaderReset(Loader<DirectoryResult> loader) {
-            if (DEBUG) Log.d(TAG, "Resetting loader for: "
-                        + DocumentInfo.debugString(mLocalState.mDocument));
+        public Context getContext() {
+            return mActivity;
+        }
 
-            mRefreshLayout.setRefreshing(false);
+        @Override
+        public State getDisplayState() {
+            return mState;
+        }
+
+        @Override
+        public boolean isInSearchMode() {
+            return mInjector.searchManager.isSearching();
+        }
+
+        @Override
+        public Model getModel() {
+            return mModel;
+        }
+
+        @Override
+        public int getColumnCount() {
+            return mColumnCount;
+        }
+
+        @Override
+        public boolean isSelected(String id) {
+            return mSelectionMgr.getSelection().contains(id);
+        }
+
+        @Override
+        public boolean isDocumentEnabled(String mimeType, int flags) {
+            return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
+        }
+
+        @Override
+        public void initDocumentHolder(DocumentHolder holder) {
+            holder.addKeyEventListener(mInputHandler);
+            holder.itemView.setOnFocusChangeListener(mFocusManager);
+        }
+
+        @Override
+        public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
+            setupDragAndDropOnDocumentView(holder.itemView, cursor);
         }
     }
 }
diff --git a/src/com/android/documentsui/dirlist/DirectoryState.java b/src/com/android/documentsui/dirlist/DirectoryState.java
index 6b849a2..b27a4b8 100644
--- a/src/com/android/documentsui/dirlist/DirectoryState.java
+++ b/src/com/android/documentsui/dirlist/DirectoryState.java
@@ -20,7 +20,6 @@
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
-import com.android.documentsui.dirlist.DirectoryFragment.ResultType;
 import com.android.documentsui.services.FileOperation;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.sorting.SortModel;
@@ -33,38 +32,28 @@
     private static final String EXTRA_SORT_DIMENSION_ID = "sortDimensionId";
     private static final String EXTRA_SORT_DIRECTION = "sortDirection";
 
-    // Directory fragment state is defined by: root, document, query, type, selection
-    @ResultType int mType = DirectoryFragment.TYPE_NORMAL;
-    RootInfo mRoot;
     // Null when viewing Recents directory.
     @Nullable DocumentInfo mDocument;
-    String mQuery = null;
     // Here we save the clip details of moveTo/copyTo actions when picker shows up.
     // This will be written to saved instance.
     @Nullable FileOperation mPendingOperation;
-    boolean mSearchMode;
     int mLastSortDimensionId = SortModel.SORT_DIMENSION_ID_UNKNOWN;
     @SortDirection int mLastSortDirection;
 
+    private RootInfo mRoot;
     private String mConfigKey;
 
     public void restore(Bundle bundle) {
         mRoot = bundle.getParcelable(Shared.EXTRA_ROOT);
         mDocument = bundle.getParcelable(Shared.EXTRA_DOC);
-        mQuery = bundle.getString(Shared.EXTRA_QUERY);
-        mType = bundle.getInt(Shared.EXTRA_TYPE);
-        mSearchMode = bundle.getBoolean(Shared.EXTRA_SEARCH_MODE);
         mPendingOperation = bundle.getParcelable(FileOperationService.EXTRA_OPERATION);
         mLastSortDimensionId = bundle.getInt(EXTRA_SORT_DIMENSION_ID);
         mLastSortDirection = bundle.getInt(EXTRA_SORT_DIRECTION);
     }
 
     public void save(Bundle bundle) {
-        bundle.putInt(Shared.EXTRA_TYPE, mType);
         bundle.putParcelable(Shared.EXTRA_ROOT, mRoot);
         bundle.putParcelable(Shared.EXTRA_DOC, mDocument);
-        bundle.putString(Shared.EXTRA_QUERY, mQuery);
-        bundle.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
         bundle.putParcelable(FileOperationService.EXTRA_OPERATION, mPendingOperation);
         bundle.putInt(EXTRA_SORT_DIMENSION_ID, mLastSortDimensionId);
         bundle.putInt(EXTRA_SORT_DIRECTION, mLastSortDirection);
@@ -76,16 +65,9 @@
         return op;
     }
 
-    public void update(int type, RootInfo root, DocumentInfo doc, String query) {
-        mType = type;
-        update(root, doc, query);
-    }
-
-    public void update(RootInfo root, DocumentInfo doc, String query) {
-        mQuery = query;
+    public void update(RootInfo root, DocumentInfo doc) {
         mRoot = root;
         mDocument = doc;
-        mSearchMode =  query != null;
     }
 
     String getConfigKey() {
diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
index 9b1794c..297584f 100644
--- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
@@ -24,6 +24,7 @@
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 
+import com.android.documentsui.Model;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Features;
 import com.android.documentsui.base.State;
diff --git a/src/com/android/documentsui/dirlist/DragStartListener.java b/src/com/android/documentsui/dirlist/DragStartListener.java
index 6598a87..a0b0f64 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -26,6 +26,7 @@
 import android.view.View;
 
 import com.android.documentsui.DragShadowBuilder;
+import com.android.documentsui.Model;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.Events.InputEvent;
diff --git a/src/com/android/documentsui/dirlist/Message.java b/src/com/android/documentsui/dirlist/Message.java
index d8044bb..fdaf786 100644
--- a/src/com/android/documentsui/dirlist/Message.java
+++ b/src/com/android/documentsui/dirlist/Message.java
@@ -25,7 +25,7 @@
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.dirlist.DocumentsAdapter.Environment;
-import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.Model.Update;
 
 /**
  * Data object used by {@link InflateMessageDocumentHolder} and {@link HeaderMessageDocumentHolder}.
diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
index c7c2526..1e080bf 100644
--- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
@@ -18,7 +18,6 @@
 
 import static com.android.documentsui.base.DocumentInfo.getCursorInt;
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
-import static com.android.documentsui.base.Shared.DEBUG;
 import static com.android.documentsui.base.State.MODE_GRID;
 import static com.android.documentsui.base.State.MODE_LIST;
 
@@ -27,14 +26,13 @@
 import android.util.Log;
 import android.view.ViewGroup;
 
+import com.android.documentsui.Model;
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.State;
-import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.Model.Update;
 
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 /**
  * Adapts from dirlist.Model to something RecyclerView understands.
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 5720ea1..6926fb8 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -53,7 +53,7 @@
 import com.android.documentsui.clipping.UrisSupplier;
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.Model;
 import com.android.documentsui.files.ActionHandler.Addons;
 import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.GetRootDocumentTask;
@@ -84,7 +84,7 @@
     private final DialogController mDialogs;
     private final DocumentClipper mClipper;
     private final ClipStore mClipStore;
-    private @Nullable Model mModel;
+    private final Model mModel;
 
     ActionHandler(
             T activity,
@@ -106,6 +106,7 @@
         mDialogs = injector.dialogs;
         mClipper = clipper;
         mClipStore = clipStore;
+        mModel = injector.getModel();
     }
 
     @Override
@@ -113,7 +114,6 @@
         new GetRootDocumentTask(
                 root,
                 mActivity,
-                mActivity::isDestroyed,
                 (DocumentInfo rootDoc) -> dropOnCallback(event, rootDoc, root)
         ).executeOnExecutor(mExecutors.lookup(root.authority));
         return true;
@@ -150,12 +150,11 @@
         new GetRootDocumentTask(
                 root,
                 mActivity,
-                mActivity::isDestroyed,
                 (DocumentInfo doc) -> pasteIntoFolder(root, doc)
         ).executeOnExecutor(mExecutors.lookup(root.authority));
     }
 
-    private void pasteIntoFolder(RootInfo root, DocumentInfo doc) {
+    private void pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc) {
         DocumentClipper clipper = DocumentsApplication.getDocumentClipper(mActivity);
         DocumentStack stack = new DocumentStack(root, doc);
         clipper.copyFromClipboard(doc, stack, mDialogs::showFileOperationStatus);
@@ -620,16 +619,6 @@
         return intent;
     }
 
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public ActionHandler<T> reset(Model model) {
-        assert(model != null);
-        mModel = model;
-
-        return this;
-    }
-
     public interface Addons extends CommonAddons {
     }
 }
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index b5989b2..c909488 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -134,6 +134,8 @@
                 DocumentsApplication.getClipStore(this),
                 mInjector);
 
+        mInjector.searchManager = mSearchManager;
+
         mActivityInputHandler =
                 new ActivityInputHandler(mInjector.actions::deleteSelectedDocuments);
         mSharedInputHandler =
@@ -274,7 +276,7 @@
 
         assert(!mSearchManager.isSearching());
 
-        if (cwd == null) {
+        if (mState.stack.isRecents()) {
             DirectoryFragment.showRecentsOpen(fm, anim);
         } else {
             // Normal boring directory
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index c85af8a..189a889 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -38,7 +38,7 @@
 import com.android.documentsui.R;
 import com.android.documentsui.base.DebugFlags;
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.Model;
 import com.android.documentsui.roots.RootCursorWrapper;
 
 import java.util.ArrayList;
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index a92685a..c00345c 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -39,7 +39,7 @@
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.Model;
 import com.android.documentsui.picker.ActionHandler.Addons;
 import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.RootsAccess;
@@ -190,16 +190,6 @@
         return false;
     }
 
-    @SuppressWarnings("unchecked")
-    @Override
-    public ActionHandler<T> reset(Model model) {
-        assert(model != null);
-        mModel = model;
-
-        return this;
-    }
-
-
     public interface Addons extends CommonAddons {
         void onAppPicked(ResolveInfo info);
         void onDocumentPicked(DocumentInfo doc);
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 40c307a..1c7fc1e 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -111,11 +111,13 @@
                 getColor(R.color.accent_dark));
 
         mInjector.menuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
+
         mInjector.actionModeController = new ActionModeController(
                 this,
                 mInjector.selectionMgr,
                 mInjector.menuManager,
                 mInjector.messages);
+
         mInjector.actions = new ActionHandler<>(
                 this,
                 mState,
@@ -125,6 +127,8 @@
                 ProviderExecutor::forAuthority,
                 mInjector);
 
+        mInjector.searchManager = mSearchManager;
+
         Intent intent = getIntent();
 
         mSharedInputHandler =
@@ -277,8 +281,7 @@
         final RootInfo root = getCurrentRoot();
         final DocumentInfo cwd = getCurrentDirectory();
 
-        if (cwd == null) {
-            // No directory means recents
+        if (mState.stack.isRecents()) {
             if (mState.action == ACTION_CREATE ||
                 mState.action == ACTION_PICK_COPY_DESTINATION) {
                 mInjector.actions.loadRoot(Shared.getDefaultRootUri(this));
diff --git a/src/com/android/documentsui/queries/SearchViewManager.java b/src/com/android/documentsui/queries/SearchViewManager.java
index 1b773ea..64ec168 100644
--- a/src/com/android/documentsui/queries/SearchViewManager.java
+++ b/src/com/android/documentsui/queries/SearchViewManager.java
@@ -307,7 +307,7 @@
         return true;
     }
 
-    String getCurrentSearch() {
+    public String getCurrentSearch() {
         return mCurrentSearch;
     }
 
diff --git a/src/com/android/documentsui/roots/GetRootDocumentTask.java b/src/com/android/documentsui/roots/GetRootDocumentTask.java
index 1503ae1..e7fdadf 100644
--- a/src/com/android/documentsui/roots/GetRootDocumentTask.java
+++ b/src/com/android/documentsui/roots/GetRootDocumentTask.java
@@ -41,30 +41,15 @@
     private final RootInfo mRootInfo;
     private final Context mContext;
     private final Consumer<DocumentInfo> mCallback;
-    private boolean mForceCallback;
 
     public GetRootDocumentTask(
             RootInfo rootInfo, Activity activity, Consumer<DocumentInfo> callback) {
-        this(rootInfo, activity, activity::isDestroyed, callback);
-    }
-
-    public GetRootDocumentTask(
-            RootInfo rootInfo, Fragment fragment, Consumer<DocumentInfo> callback) {
-        this(rootInfo, fragment.getContext(), fragment::isDetached, callback);
-    }
-
-    public GetRootDocumentTask(
-            RootInfo rootInfo, Context context, Check check, Consumer<DocumentInfo> callback) {
-        super(check);
+        super(activity::isDestroyed);
         mRootInfo = rootInfo;
-        mContext = context.getApplicationContext();
+        mContext = activity.getApplicationContext();
         mCallback = callback;
     }
 
-    public void setForceCallback(boolean forceCallback) {
-        mForceCallback = forceCallback;
-    }
-
     @Override
     public @Nullable DocumentInfo run(Void... rootInfo) {
         return mRootInfo.getRootDocumentBlocking(mContext);
@@ -74,11 +59,9 @@
     public void finish(@Nullable DocumentInfo documentInfo) {
         if (documentInfo == null) {
             Log.e(TAG,
-                    "Cannot find document info for root: " + mRootInfo + " in the given timeout");
+                    "Cannot find document info for root: " + mRootInfo);
         }
 
-        if (documentInfo != null || mForceCallback) {
-            mCallback.accept(documentInfo);
-        }
+        mCallback.accept(documentInfo);
     }
 }
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 7fbe8e9..1d4950f 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -480,7 +480,6 @@
                     updater.updateDocInfoForRoot(doc);
                 });
         task.setTimeout(CONTEXT_MENU_ITEM_TIMEOUT);
-        task.setForceCallback(true);
         task.executeOnExecutor(getBaseActivity().getExecutorForCurrentDirectory());
     }
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index bdc0125..6df4c90 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -56,6 +56,19 @@
                 <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
             </intent-filter>
        </provider>
+
+        <!-- Provider that has broken behavior -->
+        <provider
+            android:name="com.android.documentsui.BrokenProvider"
+            android:authorities="com.android.documentsui.broken"
+            android:exported="true"
+            android:grantUriPermissions="true"
+            android:permission="android.permission.MANAGE_DOCUMENTS"
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+            </intent-filter>
+        </provider>
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/tests/common/com/android/documentsui/BrokenProvider.java b/tests/common/com/android/documentsui/BrokenProvider.java
new file mode 100644
index 0000000..272645e
--- /dev/null
+++ b/tests/common/com/android/documentsui/BrokenProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import android.database.Cursor;
+import android.provider.DocumentsContract.Root;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Test provider that provides different kinds of broken behaviors DocumentsUI may encounter.
+ */
+public class BrokenProvider extends TestRootProvider {
+
+    // Root information for a root that throws when querying its root document
+    private static final String BROKEN_ROOT_DOCUMENT_ID = "BROKEN_ROOT_DOCUMENT";
+    private static final String BROKEN_ROOT_DOCUMENT_TITLE = "Broken Root Doc";
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    public BrokenProvider() {
+        super(
+                BROKEN_ROOT_DOCUMENT_TITLE,
+                BROKEN_ROOT_DOCUMENT_ID,
+                Root.FLAG_SUPPORTS_CREATE,
+                BROKEN_ROOT_DOCUMENT_ID);
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        if (BROKEN_ROOT_DOCUMENT_ID.equals(documentId)) {
+            throw new FileNotFoundException();
+        }
+
+        return null;
+    }
+
+    @Override
+    public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
+            String sortOrder) throws FileNotFoundException {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 15a724e..93e8b44 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -19,6 +19,7 @@
 import static junit.framework.Assert.assertEquals;
 
 import android.app.Activity;
+import android.app.LoaderManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -26,17 +27,16 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.Uri;
-import android.os.Bundle;
-import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.testing.TestEnv;
 import com.android.documentsui.testing.TestEventListener;
+import com.android.documentsui.testing.TestLoaderManager;
 import com.android.documentsui.testing.TestPackageManager;
 import com.android.documentsui.testing.TestResources;
-import com.android.documentsui.testing.TestRootsAccess;
 
 import org.mockito.Mockito;
 
@@ -51,7 +51,7 @@
     public Intent intent;
     public RootInfo currentRoot;
     public MockContentResolver contentResolver;
-    public MockContentProvider contentProvider;
+    public TestLoaderManager loaderManager;
 
     public TestEventListener<Intent> startActivity;
     public TestEventListener<Intent> startService;
@@ -60,13 +60,13 @@
     public TestEventListener<Boolean> setRootsDrawerOpen;
     public TestEventListener<Uri> notifyDirectoryNavigated;
 
-    public static TestActivity create() {
+    public static TestActivity create(TestEnv env) {
         TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
-        activity.init();
+        activity.init(env);
         return activity;
     }
 
-   public void init() {
+   public void init(TestEnv env) {
        resources = TestResources.create();
        packageMgr = TestPackageManager.create();
        intent = new Intent();
@@ -77,10 +77,8 @@
        refreshCurrentRootAndDirectory =  new TestEventListener<>();
        setRootsDrawerOpen = new TestEventListener<>();
        notifyDirectoryNavigated = new TestEventListener<>();
-       contentResolver = new MockContentResolver();
-       contentProvider = new DocsMockContentProvider();
-       contentResolver.addProvider(TestRootsAccess.HOME.authority, contentProvider);
-
+       contentResolver = env.contentResolver;
+       loaderManager = new TestLoaderManager();
    }
 
     @Override
@@ -169,15 +167,13 @@
 
     @Override
     public final void updateNavigator() {}
+
+    @Override
+    public final LoaderManager getLoaderManager() {
+        return loaderManager;
+    }
 }
 
 // Trick Mockito into finding our Addons methods correctly. W/o this
 // hack, Mockito thinks Addons methods are not implemented.
 abstract class AbstractBase extends Activity implements CommonAddons {}
-
-class DocsMockContentProvider extends MockContentProvider {
-    @Override
-    public boolean refresh(Uri url, Bundle args) {
-        return true;
-    }
-}
diff --git a/tests/common/com/android/documentsui/dirlist/TestContentProvider.java b/tests/common/com/android/documentsui/dirlist/TestContentProvider.java
deleted file mode 100644
index c0505f2..0000000
--- a/tests/common/com/android/documentsui/dirlist/TestContentProvider.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2015 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.dirlist;
-
-import static org.junit.Assert.assertTrue;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.DocumentsContract;
-import android.test.mock.MockContentProvider;
-
-import com.android.documentsui.base.DocumentInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A very simple test double for ContentProvider. Useful in this package only.
- */
-class TestContentProvider extends MockContentProvider {
-    List<Uri> mDeleted = new ArrayList<>();
-
-    @Override
-    public Bundle call(String method, String arg, Bundle extras) {
-        // Intercept and log delete method calls.
-        if (DocumentsContract.METHOD_DELETE_DOCUMENT.equals(method)) {
-            final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
-            mDeleted.add(documentUri);
-            return new Bundle();
-        } else {
-            return super.call(method, arg, extras);
-        }
-    }
-
-    public void assertWasDeleted(DocumentInfo doc) {
-        assertTrue(mDeleted.contains(doc.derivedUri));
-    }
-}
\ No newline at end of file
diff --git a/tests/common/com/android/documentsui/dirlist/TestContext.java b/tests/common/com/android/documentsui/dirlist/TestContext.java
index 714062d..d5122f2 100644
--- a/tests/common/com/android/documentsui/dirlist/TestContext.java
+++ b/tests/common/com/android/documentsui/dirlist/TestContext.java
@@ -21,6 +21,8 @@
 import android.content.ContextWrapper;
 import android.test.mock.MockContentResolver;
 
+import com.android.documentsui.testing.TestDocumentsProvider;
+
 public final class TestContext {
 
     /**
@@ -28,7 +30,7 @@
      */
     static Context createStorageTestContext(Context context, String authority) {
         final MockContentResolver testResolver = new MockContentResolver();
-        TestContentProvider provider = new TestContentProvider();
+        TestDocumentsProvider provider = new TestDocumentsProvider(authority);
         testResolver.addProvider(authority, provider);
 
         return new ContextWrapper(context) {
diff --git a/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java b/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java
index c702c9a..760d0e2 100644
--- a/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java
+++ b/tests/common/com/android/documentsui/dirlist/TestDocumentsAdapter.java
@@ -19,7 +19,7 @@
 import android.view.ViewGroup;
 
 import com.android.documentsui.base.EventListener;
-import com.android.documentsui.dirlist.Model.Update;
+import com.android.documentsui.Model.Update;
 import com.android.documentsui.testing.TestEventListener;
 
 import java.util.ArrayList;
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 5dfc855..4912475 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -23,7 +23,7 @@
 import com.android.documentsui.TestActivity;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.Model;
 
 public class TestActionHandler extends AbstractActionHandler<TestActivity> {
 
@@ -38,7 +38,7 @@
 
     public TestActionHandler(TestEnv env) {
         super(
-                TestActivity.create(),
+                TestActivity.create(env),
                 env.state,
                 env.roots,
                 env.docs,
@@ -81,9 +81,4 @@
     protected void launchToDefaultLocation() {
         throw new UnsupportedOperationException();
     }
-
-    @Override
-    public <T extends ActionHandler> T reset(Model model) {
-        return null;
-    }
 }
diff --git a/tests/common/com/android/documentsui/testing/TestActivityConfig.java b/tests/common/com/android/documentsui/testing/TestActivityConfig.java
new file mode 100644
index 0000000..1dc8985
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestActivityConfig.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 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 com.android.documentsui.ActivityConfig;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.State;
+
+public class TestActivityConfig extends ActivityConfig {
+
+    public boolean nextSelectType = false;
+    public boolean nextDocumentEnabled = false;
+    public boolean nextManagedModeEnabled = false;
+    public boolean nextDragAndDropEnabled = false;
+
+    public boolean canSelectType(String docMimeType, int docFlags, State state) {
+        return nextSelectType;
+    }
+
+    public boolean isDocumentEnabled(String docMimeType, int docFlags, State state) {
+        return nextDocumentEnabled;
+    }
+
+    /**
+     * When managed mode is enabled, active downloads will be visible in the UI.
+     * Presumably this should only be true when in the downloads directory.
+     */
+    public boolean managedModeEnabled(DocumentStack stack) {
+        return nextManagedModeEnabled;
+    }
+
+    /**
+     * Whether drag n' drop is allowed in this context
+     */
+    public boolean dragAndDropEnabled() {
+        return nextDragAndDropEnabled;
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java
new file mode 100644
index 0000000..19c3c2b
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 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.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsProvider;
+
+import com.android.documentsui.base.DocumentInfo;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Test doubles of {@link DocumentsProvider} to isolate document providers. This is not registered
+ * or exposed through AndroidManifest, but only used locally.
+ */
+public class TestDocumentsProvider extends DocumentsProvider {
+
+    private String[] DOCUMENTS_PROJECTION = new String[] {
+            Document.COLUMN_DOCUMENT_ID,
+            Document.COLUMN_MIME_TYPE,
+            Document.COLUMN_DISPLAY_NAME,
+            Document.COLUMN_LAST_MODIFIED,
+            Document.COLUMN_FLAGS,
+            Document.COLUMN_SUMMARY,
+            Document.COLUMN_SIZE,
+            Document.COLUMN_ICON
+    };
+
+    private Cursor mNextChildDocuments;
+
+    public TestDocumentsProvider(String authority) {
+        ProviderInfo info = new ProviderInfo();
+        info.authority = authority;
+        attachInfoForTesting(null, info);
+    }
+
+    @Override
+    public boolean refresh(Uri url, Bundle args, CancellationSignal signal) {
+        return true;
+    }
+
+    @Override
+    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+        return null;
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        return null;
+    }
+
+    @Override
+    public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
+            String sortOrder) throws FileNotFoundException {
+        return mNextChildDocuments;
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(String documentId, String mode,
+            CancellationSignal signal) throws FileNotFoundException {
+        return null;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    /**
+     * Sets the next return value for {@link #queryChildDocuments(String, String[], String)}.
+     * @param docs docs to return for next query.
+     */
+    public void setNextChildDocumentsReturns(DocumentInfo... docs) {
+        mNextChildDocuments = createDocumentsCursor(docs);
+    }
+
+    private Cursor createDocumentsCursor(DocumentInfo... docs) {
+        MatrixCursor cursor = new MatrixCursor(DOCUMENTS_PROJECTION);
+        for (DocumentInfo doc : docs) {
+            cursor.newRow()
+                    .add(Document.COLUMN_DOCUMENT_ID, doc.documentId)
+                    .add(Document.COLUMN_MIME_TYPE, doc.mimeType)
+                    .add(Document.COLUMN_DISPLAY_NAME, doc.displayName)
+                    .add(Document.COLUMN_LAST_MODIFIED, doc.lastModified)
+                    .add(Document.COLUMN_FLAGS, doc.flags)
+                    .add(Document.COLUMN_SUMMARY, doc.summary)
+                    .add(Document.COLUMN_SIZE, doc.size)
+                    .add(Document.COLUMN_ICON, doc.icon);
+        }
+
+        return cursor;
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 3118487..3e177d5 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -18,21 +18,24 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.DocumentsContract.Document;
+import android.test.mock.MockContentResolver;
 
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.Injector;
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.Features;
+import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.TestFocusHandler;
-import com.android.documentsui.dirlist.TestModel;
 import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.sorting.SortModel;
 import com.android.documentsui.ui.TestDialogController;
 
 import junit.framework.Assert;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 
@@ -62,7 +65,11 @@
     public final Injector injector;
     public final TestFeatures features;
 
+    public final MockContentResolver contentResolver;
+    public final Map<String, TestDocumentsProvider> providers;
+
     private TestEnv(String authority) {
+        state.sortModel = SortModel.createModel();
         mExecutor = new TestScheduledExecutorService();
         features = new TestFeatures();
         model = new TestModel(authority, features);
@@ -70,12 +77,28 @@
         searchViewManager = new TestSearchViewManager();
         injector = new Injector(
                 features,
-                null,      // a Config is not required for tests
-                null,      // ScopedPreferences are not required for tests
-                null,      // a MessageBuilder is not required for tests
-                new TestDialogController());
+                new TestActivityConfig(),
+                null,       //ScopedPreferences are not required for tests
+                null,   //a MessageBuilder is not required for tests
+                new TestDialogController(),
+                model);
         injector.selectionMgr = selectionMgr;
         injector.focusManager = new FocusManager(features, selectionMgr, null, null, 0);
+        injector.searchManager = searchViewManager;
+
+        contentResolver = new MockContentResolver();
+        providers = new HashMap<>(roots.getRootsBlocking().size());
+        registerProviders();
+    }
+
+    private void registerProviders() {
+        for (RootInfo root : roots.getRootsBlocking()) {
+            if (!providers.containsKey(root.authority)) {
+                TestDocumentsProvider provider = new TestDocumentsProvider(root.authority);
+                contentResolver.addProvider(root.authority, provider);
+                providers.put(root.authority, provider);
+            }
+        }
     }
 
     public static TestEnv create() {
diff --git a/tests/common/com/android/documentsui/testing/TestLoaderManager.java b/tests/common/com/android/documentsui/testing/TestLoaderManager.java
new file mode 100644
index 0000000..3344398
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestLoaderManager.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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.app.LoaderManager;
+import android.content.AsyncTaskLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.os.Bundle;
+import android.util.SparseArray;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A test double of {@link LoaderManager} that doesn't kick off loading when {@link Loader} is
+ * created. If caller needs to kick off loading caller can obtain the loader initialized and
+ * explicitly call {@link Loader#startLoading()}.
+ */
+public class TestLoaderManager extends LoaderManager {
+
+    private final SparseArray<Loader> mLoaders = new SparseArray<>();
+    private final SparseArray<OnLoadCompleteListener> mListeners = new SparseArray<>();
+
+    @Override
+    public <D> Loader<D> initLoader(int id, Bundle args,
+            LoaderCallbacks<D> callback) {
+        Loader<D> loader = mLoaders.get(id);
+        OnLoadCompleteListener<D> listener = callback::onLoadFinished;
+        if (loader == null) {
+            loader = callback.onCreateLoader(id, args);
+            mLoaders.put(id, loader);
+        } else {
+            loader.unregisterListener(mListeners.get(id));
+        }
+
+        loader.registerListener(id, listener);
+        mListeners.put(id, listener);
+
+        return loader;
+    }
+
+    @Override
+    public <D> Loader<D> restartLoader(int id, Bundle args,
+            LoaderCallbacks<D> callback) {
+        if (mLoaders.get(id) != null) {
+            destroyLoader(id);
+        }
+
+        return initLoader(id, args, callback);
+    }
+
+    @Override
+    public void destroyLoader(int id) {
+        Loader loader = getLoader(id);
+        if (loader != null) {
+            loader.abandon();
+            mLoaders.remove(id);
+            mListeners.remove(id);
+        }
+    }
+
+    @Override
+    public <D> Loader<D> getLoader(int id) {
+        return mLoaders.get(id);
+    }
+
+    public <D> OnLoadCompleteListener<D> getListener(int id) {
+        return mListeners.get(id);
+    }
+
+    public void runAsyncTaskLoader(int id) {
+        AsyncTaskLoader loader = (AsyncTaskLoader) getLoader(id);
+        loader.startLoading();
+        loader.waitForLoader();
+    }
+
+    @Override
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+
+    }
+}
diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/testing/TestModel.java
similarity index 96%
rename from tests/common/com/android/documentsui/dirlist/TestModel.java
rename to tests/common/com/android/documentsui/testing/TestModel.java
index 462f5bf..517b1f4 100644
--- a/tests/common/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/common/com/android/documentsui/testing/TestModel.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.documentsui.dirlist;
+package com.android.documentsui.testing;
 
 import android.database.MatrixCursor;
 import android.os.Bundle;
@@ -22,6 +22,7 @@
 import android.provider.DocumentsContract.Document;
 
 import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.Model;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Features;
 import com.android.documentsui.roots.RootCursorWrapper;
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index 18b4bb9..2a25953 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -28,17 +28,17 @@
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
-import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
-import com.android.documentsui.base.DocumentStackTest;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.files.LauncherActivity;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
 import com.android.documentsui.testing.DocumentStackAsserts;
 import com.android.documentsui.testing.Roots;
 import com.android.documentsui.testing.TestEnv;
+import com.android.documentsui.testing.TestEventHandler;
 import com.android.documentsui.testing.TestRootsAccess;
 
 import org.junit.Before;
@@ -46,7 +46,6 @@
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
-import java.util.List;
 
 /**
  * A unit test *for* AbstractActionHandler, not an abstract test baseclass.
@@ -61,8 +60,8 @@
 
     @Before
     public void setUp() {
-        mActivity = TestActivity.create();
         mEnv = TestEnv.create();
+        mActivity = TestActivity.create(mEnv);
         mHandler = new AbstractActionHandler<TestActivity>(
                 mActivity,
                 mEnv.state,
@@ -91,11 +90,6 @@
             protected void launchToDefaultLocation() {
                 throw new UnsupportedOperationException();
             }
-
-            @Override
-            public <T extends ActionHandler> T reset(Model model) {
-                return null;
-            }
         };
     }
 
@@ -204,4 +198,39 @@
         mEnv.docs.lastUri.assertLastArgument(TestEnv.FILE_GIF.derivedUri);
         mActivity.refreshCurrentRootAndDirectory.assertCalled();
     }
+
+    @Test
+    public void testLoadChildrenDocuments() throws Exception {
+        mEnv.state.stack.changeRoot(TestRootsAccess.HOME);
+        mEnv.state.stack.push(TestEnv.FOLDER_0);
+
+        mEnv.state.sortModel.sortByUser(
+                SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
+
+        mEnv.providers.get(TestRootsAccess.HOME.authority)
+                .setNextChildDocumentsReturns(TestEnv.FILE_APK, TestEnv.FILE_GIF);
+
+        mHandler.loadDocumentsForCurrentStack();
+        mActivity.loaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+
+        assertEquals(2, mEnv.model.getItemCount());
+        String[] modelIds = mEnv.model.getModelIds();
+        assertEquals(TestEnv.FILE_APK, mEnv.model.getDocument(modelIds[0]));
+        assertEquals(TestEnv.FILE_GIF, mEnv.model.getDocument(modelIds[1]));
+    }
+
+    @Test
+    public void testLoadChildrenDocuments_failsWithNonRecentsAndEmptyStack() throws Exception {
+        mEnv.state.stack.changeRoot(TestRootsAccess.HOME);
+
+        mEnv.providers.get(TestRootsAccess.HOME.authority)
+                .setNextChildDocumentsReturns(TestEnv.FILE_APK, TestEnv.FILE_GIF);
+
+        TestEventHandler<Model.Update> listener = new TestEventHandler<>();
+        mEnv.model.addUpdateListener(listener::accept);
+
+        mHandler.loadDocumentsForCurrentStack();
+
+        assertTrue(listener.getLastValue().hasException());
+    }
 }
diff --git a/tests/unit/com/android/documentsui/FocusManagerTest.java b/tests/unit/com/android/documentsui/FocusManagerTest.java
index 886e634..d4d7648 100644
--- a/tests/unit/com/android/documentsui/FocusManagerTest.java
+++ b/tests/unit/com/android/documentsui/FocusManagerTest.java
@@ -21,7 +21,7 @@
 
 import com.android.documentsui.base.Features;
 import com.android.documentsui.dirlist.TestData;
-import com.android.documentsui.dirlist.TestModel;
+import com.android.documentsui.testing.TestModel;
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.testing.SelectionManagers;
 import com.android.documentsui.testing.TestFeatures;
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelTest.java b/tests/unit/com/android/documentsui/ModelTest.java
similarity index 88%
rename from tests/unit/com/android/documentsui/dirlist/ModelTest.java
rename to tests/unit/com/android/documentsui/ModelTest.java
index d004e13..c99b9cd 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelTest.java
+++ b/tests/unit/com/android/documentsui/ModelTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2017 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.
@@ -14,21 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.documentsui.dirlist;
+package com.android.documentsui;
 
-import android.content.ContentResolver;
-import android.content.ContextWrapper;
 import android.database.Cursor;
 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 com.android.documentsui.DirectoryResult;
 import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.Features;
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.testing.TestEventListener;
 import com.android.documentsui.testing.TestFeatures;
@@ -67,21 +62,11 @@
 
     private Cursor cursor;
     private Model model;
-    private TestContentProvider provider;
     private TestFeatures features;
 
     @Override
     public void setUp() {
-        final MockContentResolver resolver = new MockContentResolver();
         features = new TestFeatures();
-        new ContextWrapper(getContext()) {
-            @Override
-            public ContentResolver getContentResolver() {
-                return resolver;
-            }
-        };
-        provider = new TestContentProvider();
-        resolver.addProvider(AUTHORITY, provider);
 
         Random rand = new Random();
 
diff --git a/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
index df66899..b61fea1 100644
--- a/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
@@ -25,6 +25,7 @@
 import android.test.AndroidTestCase;
 import android.view.ViewGroup;
 
+import com.android.documentsui.Model;
 import com.android.documentsui.base.Features;
 import com.android.documentsui.base.State;
 import com.android.documentsui.testing.TestEnv;
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 548b0d0..0395732 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -21,6 +21,7 @@
 import android.support.test.filters.MediumTest;
 import android.test.AndroidTestCase;
 
+import com.android.documentsui.Model;
 import com.android.documentsui.base.Features;
 import com.android.documentsui.base.State;
 import com.android.documentsui.testing.TestEnv;
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 865a09f..3f1eb81 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -69,31 +69,18 @@
     @Before
     public void setUp() {
         mEnv = TestEnv.create();
-        mActivity = TestActivity.create();
+        mActivity = TestActivity.create(mEnv);
         mActionModeAddons = new TestActionModeAddons();
         mDialogs = new TestDialogController();
         mCallback = new TestConfirmationCallback();
         mEnv.roots.configurePm(mActivity.packageMgr);
         mEnv.injector.dialogs = mDialogs;
 
-        mHandler = new ActionHandler<>(
-                mActivity,
-                mEnv.state,
-                mEnv.roots,
-                mEnv.docs,
-                mEnv.searchViewManager,
-                mEnv::lookupExecutor,
-                mActionModeAddons,
-                null,  // clipper, only used in drag/drop
-                null,  // clip storage, not utilized unless we venture into *jumbo* clip terratory.
-                mEnv.injector
-                );
+        mHandler = createHandler();
 
         mDialogs.confirmNext();
 
         mEnv.selectDocument(TestEnv.FILE_GIF);
-
-        mHandler.reset(mEnv.model);
     }
 
     @Test
@@ -190,7 +177,7 @@
     @Test
     public void testShareSelectedDocuments_ArchivedFile() {
         mEnv = TestEnv.create(ArchivesProvider.AUTHORITY);
-        mHandler.reset(mEnv.model);
+        mHandler = createHandler();
 
         mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
         mEnv.selectionMgr.clearSelection();
@@ -407,4 +394,19 @@
         assertNotNull(root);
         assertEquals(expectedUri, root.getUri());
     }
+
+    private ActionHandler<TestActivity> createHandler() {
+        return new ActionHandler<>(
+                mActivity,
+                mEnv.state,
+                mEnv.roots,
+                mEnv.docs,
+                mEnv.searchViewManager,
+                mEnv::lookupExecutor,
+                mActionModeAddons,
+                null,  // clipper, only used in drag/drop
+                null,  // clip storage, not utilized unless we venture into *jumbo* clip terratory.
+                mEnv.injector
+        );
+    }
 }
diff --git a/tests/unit/com/android/documentsui/files/TestActivity.java b/tests/unit/com/android/documentsui/files/TestActivity.java
index 809830f..4d49649 100644
--- a/tests/unit/com/android/documentsui/files/TestActivity.java
+++ b/tests/unit/com/android/documentsui/files/TestActivity.java
@@ -17,14 +17,15 @@
 package com.android.documentsui.files;
 
 import com.android.documentsui.files.ActionHandler;
+import com.android.documentsui.testing.TestEnv;
 
 import org.mockito.Mockito;
 
 public abstract class TestActivity extends AbstractBase {
 
-    public static TestActivity create() {
+    public static TestActivity create(TestEnv env) {
         TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
-        activity.init();
+        activity.init(env);
         return activity;
     }
 }
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index d7d6405..a5bef37 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -58,7 +58,7 @@
     @Before
     public void setUp() {
         mEnv = TestEnv.create();
-        mActivity = TestActivity.create();
+        mActivity = TestActivity.create(mEnv);
         mDialogs = new TestDialogController();
         mEnv.roots.configurePm(mActivity.packageMgr);
 
@@ -75,8 +75,6 @@
         mDialogs.confirmNext();
 
         mEnv.selectionMgr.toggleSelection("1");
-
-        mHandler.reset(mEnv.model);
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/picker/TestActivity.java b/tests/unit/com/android/documentsui/picker/TestActivity.java
index 056eb18..b1622ae 100644
--- a/tests/unit/com/android/documentsui/picker/TestActivity.java
+++ b/tests/unit/com/android/documentsui/picker/TestActivity.java
@@ -16,13 +16,15 @@
 
 package com.android.documentsui.picker;
 
+import com.android.documentsui.testing.TestEnv;
+
 import org.mockito.Mockito;
 
 public abstract class TestActivity extends AbstractBase {
 
-    public static TestActivity create() {
+    public static TestActivity create(TestEnv env) {
         TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
-        activity.init();
+        activity.init(env);
         return activity;
     }
 }