Test location init with doc URI.

Make a more complete test environment.
Move GetRootDocumentTask to roots package.

Change-Id: Icb8f4c42c38c17aa97ec428ef9d46d76d2286fa7
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 81463d8..59d4bb9 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,10 @@
      * from our concrete activity implementations.
      */
     public interface CommonAddons {
+       void refreshCurrentRootAndDirectory(@AnimationType int anim);
        void onRootPicked(RootInfo root);
-       // TODO: Move this to PickAddons.
+       // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
        void onDocumentsPicked(List<DocumentInfo> docs);
        void onDocumentPicked(DocumentInfo doc, Model model);
-       void refreshCurrentRootAndDirectory(@AnimationType int anim);
     }
 }
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 d9d6cf4..340cd6b 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -68,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;
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/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/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index d6c4f42..67b5a53 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -26,10 +26,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 +46,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 +77,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 +102,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 +121,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) {
@@ -174,6 +175,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 +270,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;
         }
 
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 2d1ed94..0f497eb 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -36,6 +36,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;
@@ -104,6 +105,7 @@
                 this,
                 mState,
                 mRoots,
+                DocumentsAccess.create(this),
                 ProviderExecutor::forAuthority,
                 mDialogs,
                 mTuner,
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/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 59d0b48..cec1397 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();
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..d1b1ec1 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);
 
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/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 4cca190..9f32a76 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -43,6 +43,7 @@
     public TestEventListener<Intent> startActivity;
     public TestEventListener<Intent> startService;
     public TestEventListener<RootInfo> rootPicked;
+    public TestEventListener<Integer> refreshCurrentRootAndDirectory;
 
     public static TestActivity create() {
         TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
@@ -58,6 +59,7 @@
        startActivity = new TestEventListener<>();
        startService = new TestEventListener<>();
        rootPicked = new TestEventListener<>();
+       refreshCurrentRootAndDirectory =  new TestEventListener<>();
    }
 
     @Override
@@ -95,6 +97,11 @@
     public final void onRootPicked(RootInfo root) {
         rootPicked.accept(root);
     }
+
+    @Override
+    public final void refreshCurrentRootAndDirectory(int anim) {
+        refreshCurrentRootAndDirectory.accept(anim);
+    }
 }
 
 // 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 1204400..7f62ac7 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.roots.RootCursorWrapper;
 
+import libcore.net.MimeUtils;
+
 import java.util.Random;
 
 public class TestModel extends Model {
@@ -36,33 +39,75 @@
     };
 
     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);
+    public void update() {
+        DirectoryResult r = new DirectoryResult();
+        r.cursor = mCursor;
+        super.update(r);
+    }
+
+    public void createFolders(String... names) {
         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());
+            createFolder(i, names[i]);
+        }
+    }
+
+    public void createFiles(String... names) {
+        for (int i = 0; i < names.length; i++) {
+            create(++mLastId, names[i], guessMimeType(names[i]));
+        }
+    }
+
+    private void createFolder(int i, String name) {
+        create(
+                ++mLastId,
+                name,
+                DocumentsContract.Document.MIME_TYPE_DIR,
+                Document.FLAG_SUPPORTS_DELETE
+                        | Document.FLAG_SUPPORTS_WRITE
+                        | Document.FLAG_DIR_SUPPORTS_CREATE);
+    }
+
+    private void create(int id, String name, String mimeType) {
+        create(id, name, mimeType, Document.FLAG_SUPPORTS_DELETE);
+    }
+
+    public void create(int id, String name, String mimeType, int flags) {
+        MatrixCursor.RowBuilder row = mCursor.newRow();
+        row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(id));
+        row.add(RootCursorWrapper.COLUMN_AUTHORITY, mAuthority);
+        row.add(Document.COLUMN_DISPLAY_NAME, name);
+        row.add(Document.COLUMN_MIME_TYPE, mimeType);
+        row.add(Document.COLUMN_FLAGS, flags);
+        row.add(Document.COLUMN_SIZE, mRand.nextInt());
+    }
+
+    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;
-        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..b9e5fdf 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -18,34 +18,72 @@
 import android.os.Handler;
 import android.os.Looper;
 
+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 final String[] FOLDERS = {
+            "My Root Doc",
+            "Things",
+            "Others"
+    };
+
+    public static final String[] FILES = {
+            "abc.txt",
+            "def.png",
+            "ghi.jpg",
+            "jkl.gif",
+            "mno.psd",
+            "pqr.tiff",
+            "stu.nef",
+            "vwx.tar.gz",
+            "yx.012"
+    };
 
     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();
+        model.createFolders(FOLDERS);
+        model.createFiles(FILES);
+
+        model.update();
+
+        DocumentInfo rootDoc = model.getDocument("1");
+        Assert.assertNotNull(rootDoc);
+        Assert.assertEquals(rootDoc.displayName, FOLDERS[0]);
+        state.stack.push(rootDoc);
     }
 
     public void beforeAsserts() throws Exception {
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..8424249 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.SmallTest;
 import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.documentsui.base.State;
+import com.android.documentsui.testing.TestEnv;
 
 @SmallTest
 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 faad880..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;
diff --git a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index d44a467..f79aa35 100644
--- a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -18,93 +18,57 @@
 
 import android.content.Context;
 import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.provider.DocumentsContract.Document;
+import android.support.test.filters.SmallTest;
 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
 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;
-        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);
-        }
-        DirectoryResult r = new DirectoryResult();
-        r.cursor = c;
-        mModel.update(r);
+        assertEquals(mEnv.model.getItemCount() + 1, mAdapter.getItemCount());
+    }
 
-        assertEquals(mModel.getItemCount() + 1, mAdapter.getItemCount());
+    // Tests that the item count is correct for a directory containing only subdirs.
+    public void testItemCount_allDirs() {
+        mEnv.model.createFolders("Trader Joe's", "Alphabeta", "Lucky", "Vons", "Gelson's");
+        mEnv.model.update();
+        assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
+    }
+
+    // Tests that the item count is correct for a directory containing only files.
+    public void testItemCount_allFiles() {
+        mEnv.model.createFiles("123.txt", "234.jpg", "abc.pdf");
+        mEnv.model.update();
+        assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
     }
 
     private final class TestEnvironment implements DocumentsAdapter.Environment {
@@ -129,7 +93,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..ee8aafc 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.
@@ -107,19 +109,41 @@
     }
 
     @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);
 
+        DocumentInfo destDoc = mEnv.model.getDocument("2");
+
+        // configure DocumentsAccess to return something.
+        mEnv.docs.nextRootDocument = mEnv.model.getDocument("1");
+        mEnv.docs.nextDocument = destDoc;
+
+        Uri destUri = mEnv.model.getItemUri(destDoc.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 {