Merge "Address comments in ag/1514806." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 131d2f7..1a26bcc 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -62,7 +62,6 @@
     protected final State mState;
     protected final RootsAccess mRoots;
     protected final DocumentsAccess mDocs;
-    protected final ProviderAccess mProviders;
     protected final SelectionManager mSelectionMgr;
     protected final SearchViewManager mSearchMgr;
     protected final Lookup<String, Executor> mExecutors;
@@ -72,7 +71,6 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors) {
@@ -80,7 +78,6 @@
         assert(activity != null);
         assert(state != null);
         assert(roots != null);
-        assert(providers != null);
         assert(selectionMgr != null);
         assert(searchMgr != null);
         assert(docs != null);
@@ -89,7 +86,6 @@
         mState = state;
         mRoots = roots;
         mDocs = docs;
-        mProviders = providers;
         mSelectionMgr = selectionMgr;
         mSearchMgr = searchMgr;
         mExecutors = executors;
@@ -181,33 +177,33 @@
 
     private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) {
         if (stack == null) {
-            mState.popDocumentsToRoot();
+            mState.stack.popToRootDocument();
 
             // Update navigator to give horizontal breadcrumb a chance to update documents. It
             // doesn't update its content if the size of document stack doesn't change.
             // TODO: update breadcrumb to take range update.
             mActivity.updateNavigator();
 
-            mState.pushDocument(doc);
+            mState.stack.push(doc);
         } else {
-            if (!Objects.equals(mState.stack.root, stack.root)) {
-                Log.w(TAG, "Provider returns " + stack.root + " rather than expected "
-                        + mState.stack.root);
+            if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) {
+                Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected "
+                        + mState.stack.getRoot());
             }
 
-            mState.stack.clear();
+            mState.stack.reset();
             // Update navigator to give horizontal breadcrumb a chance to update documents. It
             // doesn't update its content if the size of document stack doesn't change.
             // TODO: update breadcrumb to take range update.
             mActivity.updateNavigator();
 
-            mState.setStack(stack);
+            mState.stack.reset(stack);
         }
 
         // Show an opening animation only if pressing "back" would get us back to the
         // previous directory. Especially after opening a root document, pressing
         // back, wouldn't go to the previous root, but close the activity.
-        final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
+        final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
         mActivity.refreshCurrentRootAndDirectory(anim);
     }
@@ -226,11 +222,11 @@
         assert(currentDoc != null);
         mActivity.notifyDirectoryNavigated(currentDoc.derivedUri);
 
-        mState.pushDocument(currentDoc);
+        mState.stack.push(currentDoc);
         // Show an opening animation only if pressing "back" would get us back to the
         // previous directory. Especially after opening a root document, pressing
         // back, wouldn't go to the previous root, but close the activity.
-        final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
+        final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
         mActivity.refreshCurrentRootAndDirectory(anim);
     }
@@ -248,12 +244,10 @@
     protected final void loadDocument(Uri uri, LoadDocStackCallback callback) {
         new LoadDocStackTask(
                 mActivity,
-                uri,
                 mRoots,
                 mDocs,
-                mProviders,
                 callback
-                ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
+                ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri);
     }
 
     @Override
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 3820db5..d0bc30c 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -335,7 +335,7 @@
                 root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
 
         // Clear entire backstack and start in new root
-        mState.onRootChanged(root);
+        mState.stack.changeRoot(root);
 
         // Recents is always in memory, so we just load it directly.
         // Otherwise we delegate loading data from disk to a task
@@ -564,8 +564,9 @@
 
     @Override
     public RootInfo getCurrentRoot() {
-        if (mState.stack.root != null) {
-            return mState.stack.root;
+        RootInfo root = mState.stack.getRoot();
+        if (root != null) {
+            return root;
         } else {
             return mRoots.getRecentsRoot();
         }
diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java
index 8cfaa5a..80d4589 100644
--- a/src/com/android/documentsui/DocumentsAccess.java
+++ b/src/com/android/documentsui/DocumentsAccess.java
@@ -18,11 +18,13 @@
 
 import android.annotation.Nullable;
 import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.RemoteException;
 import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Path;
 import android.util.Log;
 
 import com.android.documentsui.archives.ArchivesProvider;
@@ -34,7 +36,8 @@
 import java.util.List;
 
 /**
- * Provides synchronous access to {@link DocumentInfo} instances given some identifying information.
+ * Provides synchronous access to {@link DocumentInfo} instances given some identifying information
+ * and some documents API.
  */
 public interface DocumentsAccess {
 
@@ -42,7 +45,10 @@
     @Nullable DocumentInfo getDocument(Uri uri);
     @Nullable DocumentInfo getArchiveDocument(Uri uri);
 
-    @Nullable List<DocumentInfo> getDocuments(String authority, List<String> docIds);
+    boolean isDocumentUri(Uri uri);
+    @Nullable Path findPath(Uri uri) throws RemoteException;
+
+    List<DocumentInfo> getDocuments(String authority, List<String> docIds) throws RemoteException;
 
     public static DocumentsAccess create(Context context) {
         return new RuntimeDocumentAccess(context);
@@ -76,7 +82,9 @@
         }
 
         @Override
-        public @Nullable List<DocumentInfo> getDocuments(String authority, List<String> docIds) {
+        public List<DocumentInfo> getDocuments(String authority, List<String> docIds)
+                throws RemoteException {
+
             try(final ContentProviderClient client = DocumentsApplication
                     .acquireUnstableProviderOrThrow(mContext.getContentResolver(), authority)) {
 
@@ -86,20 +94,14 @@
                     try (final Cursor cursor = client.query(uri, null, null, null, null)) {
                         if (!cursor.moveToNext()) {
                             Log.e(TAG, "Couldn't create DocumentInfo for Uri: " + uri);
-                            return null;
+                            throw new RemoteException("Failed to move cursor.");
                         }
 
                         result.add(DocumentInfo.fromCursor(cursor, authority));
-                    } catch (Exception e) {
-                        Log.e(TAG, "Couldn't create DocumentInfo for Uri: " + uri);
-                        return null;
                     }
                 }
 
                 return result;
-            } catch (RemoteException e) {
-                Log.w(TAG, "Couldn't get a content provider client." ,e);
-                return null;
             }
         }
 
@@ -107,5 +109,19 @@
         public DocumentInfo getArchiveDocument(Uri uri) {
             return getDocument(ArchivesProvider.buildUriForArchive(uri));
         }
+
+        @Override
+        public boolean isDocumentUri(Uri uri) {
+            return DocumentsContract.isDocumentUri(mContext, uri);
+        }
+
+        @Override
+        public Path findPath(Uri docUri) throws RemoteException {
+            final ContentResolver resolver = mContext.getContentResolver();
+            try (final ContentProviderClient client = DocumentsApplication
+                    .acquireUnstableProviderOrThrow(resolver, docUri.getAuthority())) {
+                return DocumentsContract.findPath(client, docUri);
+            }
+        }
     }
 }
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 64eeb14..3f1d48b 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -28,7 +28,6 @@
 import android.os.RemoteException;
 import android.text.format.DateUtils;
 
-import com.android.documentsui.ProviderAccess.RuntimeProviderAccess;
 import com.android.documentsui.clipping.ClipStorage;
 import com.android.documentsui.clipping.ClipStore;
 import com.android.documentsui.clipping.DocumentClipper;
@@ -43,8 +42,6 @@
     private ClipStorage mClipStore;
     private DocumentClipper mClipper;
 
-    private ProviderAccess mProviderAccess;
-
     public static RootsCache getRootsCache(Context context) {
         return ((DocumentsApplication) context.getApplicationContext()).mRoots;
     }
@@ -54,11 +51,6 @@
         return app.mThumbnailCache;
     }
 
-    public static ProviderAccess getProviderAccess(Context context) {
-        final DocumentsApplication app = (DocumentsApplication) context.getApplicationContext();
-        return app.mProviderAccess;
-    }
-
     public static ContentProviderClient acquireUnstableProviderOrThrow(
             ContentResolver resolver, String authority) throws RemoteException {
         final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
@@ -95,8 +87,6 @@
                 getSharedPreferences(ClipStorage.PREF_NAME, 0));
         mClipper = new DocumentClipper(this, mClipStore);
 
-        mProviderAccess = new RuntimeProviderAccess(getContentResolver());
-
         final IntentFilter packageFilter = new IntentFilter();
         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
diff --git a/src/com/android/documentsui/DropdownBreadcrumb.java b/src/com/android/documentsui/DropdownBreadcrumb.java
index 98597b8..03b6d09 100644
--- a/src/com/android/documentsui/DropdownBreadcrumb.java
+++ b/src/com/android/documentsui/DropdownBreadcrumb.java
@@ -107,7 +107,7 @@
 
         @Override
         public DocumentInfo getItem(int position) {
-            return mState.stack.get(mState.stack.size() - position - 1);
+            return mState.stack.get(position);
         }
 
         @Override
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index 881fbbc..240a0d3 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -195,7 +195,7 @@
         }
 
         private DocumentInfo getItem(int position) {
-            return mState.stack.get(mState.stack.size() - position - 1);
+            return mState.stack.get(position);
         }
 
         @Override
diff --git a/src/com/android/documentsui/LoadDocStackTask.java b/src/com/android/documentsui/LoadDocStackTask.java
index e849a1b..a73945c 100644
--- a/src/com/android/documentsui/LoadDocStackTask.java
+++ b/src/com/android/documentsui/LoadDocStackTask.java
@@ -40,45 +40,37 @@
  * given root is not null it calls callback with a {@link DocumentStack} as if the given doc lives
  * under the root doc.
  */
-public class LoadDocStackTask extends PairedTask<Activity, Void, DocumentStack> {
+public class LoadDocStackTask extends PairedTask<Activity, Uri, DocumentStack> {
     private static final String TAG = "LoadDocStackTask";
 
     private final RootsAccess mRoots;
     private final DocumentsAccess mDocs;
-    private final Uri mDocUri;
-    private final String mAuthority;
-    private final ProviderAccess mProviders;
     private final LoadDocStackCallback mCallback;
 
     public LoadDocStackTask(
             Activity activity,
-            Uri docUri,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
             LoadDocStackCallback callback) {
         super(activity);
         mRoots = roots;
         mDocs = docs;
-        mDocUri = docUri;
-        mAuthority = docUri.getAuthority();
-        mProviders = providers;
         mCallback = callback;
     }
 
     @Override
-    public @Nullable DocumentStack run(Void... args) {
-        if (Shared.ENABLE_OMC_API_FEATURES) {
+    public @Nullable DocumentStack run(Uri... uris) {
+        final Uri docUri = uris[0];
+        if (Shared.ENABLE_OMC_API_FEATURES && mDocs.isDocumentUri(docUri)) {
             try {
-                final Path path = mProviders.findPath(mDocUri);
+                final Path path = mDocs.findPath(docUri);
                 if (path != null) {
-                    return buildStack(path);
+                    return buildStack(docUri.getAuthority(), path);
                 } else {
                     Log.i(TAG, "Remote provider doesn't support findPath.");
                 }
             } catch (Exception e) {
-                Log.e(TAG, "Failed to build document stack for uri: " + mDocUri, e);
-                // Fallback to old behavior.
+                Log.e(TAG, "Failed to build document stack for uri: " + docUri, e);
             }
         }
 
@@ -90,21 +82,20 @@
         mCallback.onDocumentStackLoaded(stack);
     }
 
-    private @Nullable DocumentStack buildStack(Path path) {
+    private DocumentStack buildStack(String authority, Path path) throws Exception {
         final String rootId = path.getRootId();
         if (rootId == null) {
-            Log.e(TAG, "Provider doesn't provide root id.");
-            return null;
+            throw new IllegalStateException("Provider doesn't provider root id.");
         }
 
-        RootInfo root = mRoots.getRootOneshot(mAuthority, path.getRootId());
-        List<DocumentInfo> docs = mDocs.getDocuments(mAuthority, path.getPath());
-
-        if (root == null || docs == null) {
-            Log.e(TAG, "Either root: " + root + " or docs: " + docs + " failed to load.");
-            return null;
+        RootInfo root = mRoots.getRootOneshot(authority, path.getRootId());
+        if (root == null) {
+            throw new IllegalStateException("Failed to load root for authority: " + authority +
+                    " and root ID: " + path.getRootId() + ".");
         }
 
+        List<DocumentInfo> docs = mDocs.getDocuments(authority, path.getPath());
+
         return new DocumentStack(root, docs);
     }
 
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index 4f97111..65f9000 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -75,7 +75,7 @@
         boolean changed = false;
         while (mState.stack.size() > position + 1) {
             changed = true;
-            mState.popDocument();
+            mState.stack.pop();
         }
         if (changed) {
             mEnv.refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
diff --git a/src/com/android/documentsui/ProviderAccess.java b/src/com/android/documentsui/ProviderAccess.java
deleted file mode 100644
index 67a0e3f..0000000
--- a/src/com/android/documentsui/ProviderAccess.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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;
-
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.Path;
-
-/**
- * Provides synchronous {@link android.provider.DocumentsProvider} access.
- */
-public interface ProviderAccess {
-    Path findPath(Uri docUri) throws RemoteException;
-
-    class RuntimeProviderAccess implements ProviderAccess {
-
-        private final ContentResolver mResolver;
-
-        RuntimeProviderAccess(ContentResolver resolver) {
-            mResolver = resolver;
-        }
-
-        @Override
-        public Path findPath(Uri docUri) throws RemoteException {
-            try (final ContentProviderClient client = DocumentsApplication
-                    .acquireUnstableProviderOrThrow(mResolver, docUri.getAuthority())) {
-                return DocumentsContract.findPath(client, docUri);
-            }
-        }
-    }
-}
diff --git a/src/com/android/documentsui/RootsMonitor.java b/src/com/android/documentsui/RootsMonitor.java
index da48794..a0d4d0b 100644
--- a/src/com/android/documentsui/RootsMonitor.java
+++ b/src/com/android/documentsui/RootsMonitor.java
@@ -135,7 +135,7 @@
             }
 
             // Clear entire backstack and start in new root.
-            mState.onRootChanged(defaultRoot);
+            mState.stack.changeRoot(defaultRoot);
             mSearchMgr.update(defaultRoot);
 
             if (defaultRoot.isRecents()) {
diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java
index c3a13e0..9e0c580 100644
--- a/src/com/android/documentsui/base/DocumentStack.java
+++ b/src/com/android/documentsui/base/DocumentStack.java
@@ -16,10 +16,13 @@
 
 package com.android.documentsui.base;
 
+import static com.android.documentsui.base.Shared.DEBUG;
+
 import android.content.ContentResolver;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.DocumentsProvider;
+import android.util.Log;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -34,51 +37,131 @@
  * Representation of a stack of {@link DocumentInfo}, usually the result of a
  * user-driven traversal.
  */
-public class DocumentStack extends LinkedList<DocumentInfo> implements Durable, Parcelable {
+public class DocumentStack implements Durable, Parcelable {
+
+    private static final String TAG = "DocumentStack";
+
     private static final int VERSION_INIT = 1;
     private static final int VERSION_ADD_ROOT = 2;
 
-    public RootInfo root;
+    private LinkedList<DocumentInfo> mList;
+    private RootInfo mRoot;
 
-    public DocumentStack() {};
+    private boolean mInitialRootChanged;
+    private boolean mInitialDocChanged;
+    private boolean mStackTouched;
+
+    public DocumentStack() {
+        mList = new LinkedList<>();
+    }
 
     /**
      * Creates an instance, and pushes all docs to it in the same order as they're passed as
      * parameters, i.e. the last document will be at the top of the stack.
      */
     public DocumentStack(RootInfo root, DocumentInfo... docs) {
-        for (DocumentInfo doc : docs) {
-            push(doc);
+        mList = new LinkedList<>();
+        for (int i = 0; i < docs.length; ++i) {
+            mList.add(docs[i]);
         }
 
-        this.root = root;
-    }
-
-    public DocumentStack(RootInfo root, List<DocumentInfo> docs) {
-        for (DocumentInfo doc : docs) {
-            push(doc);
-        }
-
-        this.root = root;
+        mRoot = root;
     }
 
     /**
-     * Makes a new copy, and pushes all docs to the new copy in the same order as they're passed
-     * as parameters, i.e. the last document will be at the top of the stack.
+     * Same as {@link #DocumentStack(DocumentStack, DocumentInfo...)} except it takes a {@link List}
+     * instead of an array.
+     */
+    public DocumentStack(RootInfo root, List<DocumentInfo> docs) {
+        mList = new LinkedList<>(docs);
+        mRoot = root;
+    }
+
+    /**
+     * Makes a new shallow copy, and pushes all docs to the new copy in the same order as they're
+     * passed as parameters, i.e. the last document will be at the top of the stack.
      */
     public DocumentStack(DocumentStack src, DocumentInfo... docs) {
-        super(src);
+        mList = src.mList;
         for (DocumentInfo doc : docs) {
-            push(doc);
+            mList.addLast(doc);
         }
 
-        root = src.root;
+        mRoot = src.mRoot;
+    }
+
+    public RootInfo getRoot() {
+        return mRoot;
+    }
+
+    public boolean isEmpty() {
+        return mList.isEmpty();
+    }
+
+    public int size() {
+        return mList.size();
+    }
+
+    public DocumentInfo peek() {
+        return mList.peekLast();
+    }
+
+    /**
+     * Returns {@link DocumentInfo} at index counted from the bottom of this stack.
+     */
+    public DocumentInfo get(int index) {
+        return mList.get(index);
+    }
+
+    public void push(DocumentInfo info) {
+        if (DEBUG) Log.d(TAG, "Adding doc to stack: " + info);
+        if (!mInitialDocChanged && !isEmpty() && !info.equals(peek())) {
+            mInitialDocChanged = true;
+        }
+        mList.addLast(info);
+        mStackTouched = true;
+    }
+
+    public DocumentInfo pop() {
+        if (DEBUG) Log.d(TAG, "Popping doc off stack.");
+        final DocumentInfo result = mList.removeLast();
+        mStackTouched = true;
+
+        return result;
+    }
+
+    public void popToRootDocument() {
+        if (DEBUG) Log.d(TAG, "Popping docs to root folder.");
+        while (mList.size() > 1) {
+            mList.removeLast();
+        }
+        mStackTouched = true;
+    }
+
+    public void changeRoot(RootInfo root) {
+        if (DEBUG) Log.d(TAG, "Root changed to: " + root);
+        if (!mInitialRootChanged && mRoot != null && !root.equals(mRoot)) {
+            mInitialRootChanged = true;
+        }
+        reset();
+        mRoot = root;
+    }
+
+    /** This will return true even when the initial location is set.
+     * To get a read on if the user has changed something, use {@link #hasInitialLocationChanged()}.
+     */
+    public boolean hasLocationChanged() {
+        return mStackTouched;
+    }
+
+    public boolean hasInitialLocationChanged() {
+        return mInitialRootChanged || mInitialDocChanged;
     }
 
     public String getTitle() {
-        if (size() == 1 && root != null) {
-            return root.title;
-        } else if (size() > 1) {
+        if (mList.size() == 1 && mRoot != null) {
+            return mRoot.title;
+        } else if (mList.size() > 1) {
             return peek().displayName;
         } else {
             return null;
@@ -86,17 +169,17 @@
     }
 
     public boolean isRecents() {
-        return size() == 0;
+        return isEmpty();
     }
 
     public void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException {
         for (RootInfo root : matchingRoots) {
-            if (root.equals(this.root)) {
-                this.root = root;
+            if (root.equals(this.mRoot)) {
+                this.mRoot = root;
                 return;
             }
         }
-        throw new FileNotFoundException("Failed to find matching root for " + root);
+        throw new FileNotFoundException("Failed to find matching mRoot for " + mRoot);
     }
 
     /**
@@ -104,34 +187,38 @@
      * {@link DocumentsProvider}.
      */
     public void updateDocuments(ContentResolver resolver) throws FileNotFoundException {
-        for (DocumentInfo info : this) {
+        for (DocumentInfo info : mList) {
             info.updateSelf(resolver);
         }
     }
 
     /**
-     * Build key that uniquely identifies this stack. It omits most of the raw
-     * details included in {@link #write(DataOutputStream)}, since they change
-     * too regularly to be used as a key.
+     * Resets this stack to the given stack. It takes the reference of {@link #mList} and
+     * {@link #mRoot} instead of making a copy.
      */
-    public String buildKey() {
-        final StringBuilder builder = new StringBuilder();
-        if (root != null) {
-            builder.append(root.authority).append('#');
-            builder.append(root.rootId).append('#');
-        } else {
-            builder.append("[null]").append('#');
-        }
-        for (DocumentInfo doc : this) {
-            builder.append(doc.documentId).append('#');
-        }
-        return builder.toString();
+    public void reset(DocumentStack stack) {
+        if (DEBUG) Log.d(TAG, "Resetting the whole darn stack to: " + stack);
+
+        mList = stack.mList;
+        mRoot = stack.mRoot;
+        mStackTouched = true;
+    }
+
+    @Override
+    public String toString() {
+        return "DocumentStack{"
+                + "root=" + mRoot
+                + ", docStack=" + mList
+                + ", stackTouched=" + mStackTouched
+                + ", initialDocChanged=" + mInitialDocChanged
+                + ", initialRootChanged=" + mInitialRootChanged
+                + "}";
     }
 
     @Override
     public void reset() {
-        clear();
-        root = null;
+        mList.clear();
+        mRoot = null;
     }
 
     @Override
@@ -142,15 +229,18 @@
                 throw new ProtocolException("Ignored upgrade");
             case VERSION_ADD_ROOT:
                 if (in.readBoolean()) {
-                    root = new RootInfo();
-                    root.read(in);
+                    mRoot = new RootInfo();
+                    mRoot.read(in);
                 }
                 final int size = in.readInt();
                 for (int i = 0; i < size; i++) {
                     final DocumentInfo doc = new DocumentInfo();
                     doc.read(in);
-                    add(doc);
+                    mList.add(doc);
                 }
+                mStackTouched = in.readInt() != 0;
+                mInitialRootChanged = in.readInt() != 0;
+                mInitialDocChanged = in.readInt() != 0;
                 break;
             default:
                 throw new ProtocolException("Unknown version " + version);
@@ -160,18 +250,21 @@
     @Override
     public void write(DataOutputStream out) throws IOException {
         out.writeInt(VERSION_ADD_ROOT);
-        if (root != null) {
+        if (mRoot != null) {
             out.writeBoolean(true);
-            root.write(out);
+            mRoot.write(out);
         } else {
             out.writeBoolean(false);
         }
-        final int size = size();
+        final int size = mList.size();
         out.writeInt(size);
         for (int i = 0; i < size; i++) {
-            final DocumentInfo doc = get(i);
+            final DocumentInfo doc = mList.get(i);
             doc.write(out);
         }
+        out.writeInt(mStackTouched ? 1 : 0);
+        out.writeInt(mInitialRootChanged ? 1 : 0);
+        out.writeInt(mInitialDocChanged ? 1 : 0);
     }
 
     @Override
diff --git a/src/com/android/documentsui/base/State.java b/src/com/android/documentsui/base/State.java
index f0d47bb..fff50c9 100644
--- a/src/com/android/documentsui/base/State.java
+++ b/src/com/android/documentsui/base/State.java
@@ -16,13 +16,10 @@
 
 package com.android.documentsui.base;
 
-import static com.android.documentsui.base.Shared.DEBUG;
-
 import android.annotation.IntDef;
 import android.content.Intent;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.documentsui.services.FileOperationService;
@@ -102,10 +99,7 @@
     public @OpType int copyOperationSubType = FileOperationService.OPERATION_UNKNOWN;
 
     /** Current user navigation stack; empty implies recents. */
-    public DocumentStack stack = new DocumentStack();
-    private boolean mStackTouched;
-    private boolean mInitialRootChanged;
-    private boolean mInitialDocChanged;
+    public final DocumentStack stack = new DocumentStack();
 
     /** Instance configs for every shown directory */
     public HashMap<String, SparseArray<Parcelable>> dirConfigs = new HashMap<>();
@@ -122,55 +116,6 @@
         }
     }
 
-    public void onRootChanged(RootInfo root) {
-        if (DEBUG) Log.d(TAG, "Root changed to: " + root);
-        if (!mInitialRootChanged && stack.root != null && !root.equals(stack.root)) {
-            mInitialRootChanged = true;
-        }
-        stack.root = root;
-        stack.clear();
-        mStackTouched = true;
-    }
-
-    public void pushDocument(DocumentInfo info) {
-        if (DEBUG) Log.d(TAG, "Adding doc to stack: " + info);
-        if (!mInitialDocChanged && stack.size() > 0 && !info.equals(stack.peek())) {
-            mInitialDocChanged = true;
-        }
-        stack.push(info);
-        mStackTouched = true;
-    }
-
-    public void popDocument() {
-        if (DEBUG) Log.d(TAG, "Popping doc off stack.");
-        stack.pop();
-        mStackTouched = true;
-    }
-
-    public void popDocumentsToRoot() {
-        if (DEBUG) Log.d(TAG, "Popping docs to root folder.");
-        while (stack.size() > 1) {
-            stack.pop();
-        }
-        mStackTouched = true;
-    }
-
-    public void setStack(DocumentStack stack) {
-        if (DEBUG) Log.d(TAG, "Setting the whole darn stack to: " + stack);
-        this.stack = stack;
-        mStackTouched = true;
-    }
-
-    // This will return true even when the initial location is set.
-    // To get a read on if the user has changed something, use #hasInitialLocationChanged.
-    public boolean hasLocationChanged() {
-        return mStackTouched;
-    }
-
-    public boolean hasInitialLocationChanged() {
-        return mInitialRootChanged || mInitialDocChanged;
-    }
-
     @Override
     public int describeContents() {
         return 0;
@@ -190,9 +135,6 @@
         out.writeMap(dirConfigs);
         out.writeList(excludedAuthorities);
         out.writeInt(openableOnly ? 1 : 0);
-        out.writeInt(mStackTouched ? 1 : 0);
-        out.writeInt(mInitialRootChanged ? 1 : 0);
-        out.writeInt(mInitialDocChanged ? 1 : 0);
         out.writeParcelable(sortModel, 0);
     }
 
@@ -217,9 +159,6 @@
             in.readMap(state.dirConfigs, loader);
             in.readList(state.excludedAuthorities, loader);
             state.openableOnly = in.readInt() != 0;
-            state.mStackTouched = in.readInt() != 0;
-            state.mInitialRootChanged = in.readInt() != 0;
-            state.mInitialDocChanged = in.readInt() != 0;
             state.sortModel = in.readParcelable(getClass().getClassLoader());
             return state;
         }
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 84d8b05..b7f24f5 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -1154,7 +1154,7 @@
 
             if (mModel.isEmpty()) {
                 if (mLocalState.mSearchMode) {
-                    showNoResults(mState.stack.root);
+                    showNoResults(mState.stack.getRoot());
                 } else {
                     showEmptyDirectory();
                 }
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index c2da8f5..afea4cd 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -40,7 +40,6 @@
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.MimeTypes;
-import com.android.documentsui.ProviderAccess;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
@@ -88,7 +87,6 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
@@ -98,7 +96,7 @@
             DocumentClipper clipper,
             ClipStore clipStore) {
 
-        super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors);
+        super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
 
         mActionModeAddons = actionModeAddons;
         mDialogs = dialogs;
@@ -321,12 +319,12 @@
     // Any other URI is *sorta* unexpected...except when browsing an archive
     // in downloads.
     private boolean launchToStackLocation(DocumentStack stack) {
-        if (stack == null || stack.root == null) {
+        if (stack == null || stack.getRoot() == null) {
             return false;
         }
 
         if (mState.stack.isEmpty()) {
-            mActivity.onRootPicked(mState.stack.root);
+            mActivity.onRootPicked(mState.stack.getRoot());
         } else {
             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
         }
@@ -512,7 +510,7 @@
     private void onModelLoaded(Model.Update update) {
         // When launched into empty root, open drawer.
         if (mScope.model.isEmpty()
-                && !mState.hasInitialLocationChanged()
+                && !mState.stack.hasInitialLocationChanged()
                 && !mScope.searchMode
                 && !mScope.modelLoadObserved) {
             // Opens the drawer *if* an openable drawer is present
diff --git a/src/com/android/documentsui/files/Config.java b/src/com/android/documentsui/files/Config.java
index 69da89e..30d468a 100644
--- a/src/com/android/documentsui/files/Config.java
+++ b/src/com/android/documentsui/files/Config.java
@@ -16,8 +16,9 @@
 
 package com.android.documentsui.files;
 
-import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.ActivityConfig;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
 
 /**
  * Provides support for Files activity specific specializations.
@@ -30,8 +31,9 @@
         // And while we don't allow folders in Downloads, we do allow Zip files in
         // downloads that themselves can be opened and viewed like directories.
         // This method helps us understand when to kick in on those special behaviors.
-        return stack.root != null
-                && stack.root.isDownloads()
+        final RootInfo root = stack.getRoot();
+        return root != null
+                && root.isDownloads()
                 && stack.size() == 1;
     }
 
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 0de44ea..42afa7f 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -124,7 +124,6 @@
                 mState,
                 mRoots,
                 mDocs,
-                DocumentsApplication.getProviderAccess(this),
                 mSelectionMgr,
                 mSearchManager,
                 ProviderExecutor::forAuthority,
@@ -176,7 +175,7 @@
 
         final DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
         if (stack != null) {
-            state.stack = stack;
+            state.stack.reset(stack);
         }
     }
 
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index d37e34c..6e1b9d4 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -36,7 +36,6 @@
 import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.MimeTypes;
-import com.android.documentsui.ProviderAccess;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
@@ -67,13 +66,12 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
             ActivityConfig activityConfig) {
 
-        super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors);
+        super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
 
         mConfig = activityConfig;
         mScope = new ContentScope(this::onModelLoaded);
@@ -81,42 +79,65 @@
 
     @Override
     public void initLocation(Intent intent) {
-        if (mState.restored) {
-            if (DEBUG) Log.d(TAG, "Stack already resolved");
-        } else {
-            // We set the activity title in AsyncTask.onPostExecute().
-            // To prevent talkback from reading aloud the default title, we clear it here.
-            mActivity.setTitle("");
+        assert(intent != null);
 
-            // As a matter of policy we don't load the last used stack for the copy
-            // destination picker (user is already in Files app).
-            // Concensus was that the experice was too confusing.
-            // In all other cases, where the user is visiting us from another app
-            // we restore the stack as last used from that app.
-            if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) {
-                if (DEBUG) Log.d(TAG, "Launching directly into Home directory.");
-                loadHomeDir();
-            } else if (intent.getData() != null) {
-                Uri uri = intent.getData();
-                loadDocument(
-                        uri,
-                        (@Nullable DocumentStack stack) -> onStackLoaded(uri, stack));
-            } else {
-                loadLastAccessedStack();
-            }
+        if (mState.restored) {
+            if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
+            return;
         }
+
+        // We set the activity title in AsyncTask.onPostExecute().
+        // To prevent talkback from reading aloud the default title, we clear it here.
+        mActivity.setTitle("");
+
+        if (launchHomeForCopyDestination(intent)) {
+            if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination.");
+            return;
+        }
+
+        if (launchToDocument(intent)) {
+            if (DEBUG) Log.d(TAG, "Launched to a document.");
+            return;
+        }
+
+        if (DEBUG) Log.d(TAG, "Load last accessed stack.");
+        loadLastAccessedStack();
     }
 
-    private void onStackLoaded(Uri uri, @Nullable DocumentStack stack) {
+    private boolean launchHomeForCopyDestination(Intent intent) {
+        // As a matter of policy we don't load the last used stack for the copy
+        // destination picker (user is already in Files app).
+        // Consensus was that the experice was too confusing.
+        // In all other cases, where the user is visiting us from another app
+        // we restore the stack as last used from that app.
+        if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) {
+            loadHomeDir();
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean launchToDocument(Intent intent) {
+        final Uri uri = intent.getData();
+        if (uri != null) {
+            loadDocument(uri, this::onStackLoaded);
+            return true;
+        }
+
+        return false;
+    }
+
+    private void onStackLoaded(@Nullable DocumentStack stack) {
         if (stack != null) {
             if (!stack.peek().isContainer()) {
                 // Requested document is not a container. Pop it so that we can launch into its
                 // parent.
                 stack.pop();
             }
-            mState.setStack(stack);
+            mState.stack.reset(stack);
         } else {
-            Log.w(TAG, "Failed to launch into the given uri: " + uri);
+            Log.w(TAG, "Failed to launch into the given uri. Load last accessed stack.");
             loadLastAccessedStack();
         }
     }
@@ -194,7 +215,7 @@
             showDrawer = true;
         }
 
-        if (showDrawer && !mState.hasInitialLocationChanged() && !mScope.searchMode
+        if (showDrawer && !mState.stack.hasInitialLocationChanged() && !mScope.searchMode
                 && !mScope.modelLoadObserved) {
             // This noops on layouts without drawer, so no need to guard.
             mActivity.setRootsDrawerOpen(true);
diff --git a/src/com/android/documentsui/picker/LastAccessedProvider.java b/src/com/android/documentsui/picker/LastAccessedProvider.java
index bb1a129..b77ce99 100644
--- a/src/com/android/documentsui/picker/LastAccessedProvider.java
+++ b/src/com/android/documentsui/picker/LastAccessedProvider.java
@@ -37,10 +37,10 @@
 import com.android.documentsui.base.DurableUtils;
 import com.android.internal.util.Predicate;
 
-import com.google.android.collect.Sets;
-
 import libcore.io.IoUtils;
 
+import com.google.android.collect.Sets;
+
 import java.io.IOException;
 import java.util.Set;
 
@@ -244,7 +244,7 @@
                             cursor.getColumnIndex(Columns.STACK));
                     DurableUtils.readFromArray(rawStack, stack);
 
-                    if (stack.root != null && predicate.apply(stack.root.authority)) {
+                    if (stack.getRoot() != null && predicate.apply(stack.getRoot().authority)) {
                         final String packageName = getCursorString(
                                 cursor, Columns.PACKAGE_NAME);
                         db.delete(TABLE_LAST_ACCESSED, Columns.PACKAGE_NAME + "=?",
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 3e71402..268d801 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -110,7 +110,6 @@
                 mState,
                 mRoots,
                 mDocs,
-                DocumentsApplication.getProviderAccess(this),
                 mSelectionMgr,
                 mSearchManager,
                 ProviderExecutor::forAuthority,
diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index 1551bad..b9bc651 100644
--- a/src/com/android/documentsui/services/CopyJob.java
+++ b/src/com/android/documentsui/services/CopyJob.java
@@ -276,7 +276,7 @@
             int docProcessed = 0;
             for (Uri uri : uris) {
                 DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
-                if (canCopy(doc, stack.root)) {
+                if (canCopy(doc, stack.getRoot())) {
                     mSrcs.add(doc);
                 } else {
                     onFileFailed(doc);
@@ -322,9 +322,10 @@
         if (batchSize >= 0) {
             RootsCache cache = DocumentsApplication.getRootsCache(appContext);
 
+            RootInfo root = stack.getRoot();
             // Query root info here instead of using stack.root because the number there may be
             // stale.
-            RootInfo root = cache.getRootOneshot(stack.root.authority, stack.root.rootId, true);
+            root = cache.getRootOneshot(root.authority, root.rootId, true);
             if (root.availableBytes >= 0) {
                 result = (batchSize <= root.availableBytes);
             } else {
diff --git a/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java
index b7924c8..16bda15 100644
--- a/src/com/android/documentsui/services/MoveJob.java
+++ b/src/com/android/documentsui/services/MoveJob.java
@@ -108,7 +108,7 @@
     boolean checkSpace() {
         long size = 0;
         for (DocumentInfo src : mSrcs) {
-            if (!src.authority.equals(stack.root.authority)) {
+            if (!src.authority.equals(stack.getRoot().authority)) {
                 if (src.isDirectory()) {
                     try {
                         size += calculateFileSizesRecursively(getClient(src), src.derivedUri);
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 5ad0563..69f716c 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -40,7 +40,6 @@
                 env.state,
                 env.roots,
                 env.docs,
-                env.providers,
                 env.selectionMgr,
                 env.searchViewManager,
                 (String authority) -> null);
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
index 7ded624..fb821ee 100644
--- a/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
+++ b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
@@ -16,6 +16,8 @@
 package com.android.documentsui.testing;
 
 import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.DocumentsContract.Path;
 
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.base.DocumentInfo;
@@ -31,6 +33,9 @@
     public @Nullable DocumentInfo nextDocument;
     public @Nullable List<DocumentInfo> nextDocuments;
 
+    public boolean nextIsDocumentsUri;
+    public @Nullable Path nextPath;
+
     @Override
     public DocumentInfo getRootDocument(RootInfo root) {
         return nextRootDocument;
@@ -50,4 +55,14 @@
     public DocumentInfo getArchiveDocument(Uri uri) {
         return nextDocument;
     }
+
+    @Override
+    public boolean isDocumentUri(Uri uri) {
+        return nextIsDocumentsUri;
+    }
+
+    @Override
+    public Path findPath(Uri docUri) throws RemoteException {
+        return nextPath;
+    }
 }
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 7ea147c..e0af394 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -50,7 +50,6 @@
     public final State state = new State();
     public final TestRootsAccess roots = new TestRootsAccess();
     public final TestDocumentsAccess docs = new TestDocumentsAccess();
-    public final TestProviderAccess providers = new TestProviderAccess();
     public final TestModel model;
     public final SelectionManager selectionMgr;
     public final TestSearchViewManager searchViewManager;
@@ -111,7 +110,7 @@
         Assert.assertNotNull(rootDoc);
         Assert.assertEquals(rootDoc.displayName, FOLDER_0.displayName);
 
-        state.stack.root = TestRootsAccess.HOME;
+        state.stack.changeRoot(TestRootsAccess.HOME);
         state.stack.push(rootDoc);
     }
 
diff --git a/tests/common/com/android/documentsui/testing/TestProviderAccess.java b/tests/common/com/android/documentsui/testing/TestProviderAccess.java
deleted file mode 100644
index 1887f81..0000000
--- a/tests/common/com/android/documentsui/testing/TestProviderAccess.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.annotation.Nullable;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.Path;
-
-import com.android.documentsui.ProviderAccess;
-
-public class TestProviderAccess implements ProviderAccess {
-
-    public @Nullable Path nextPath;
-
-    @Override
-    public DocumentsContract.Path findPath(Uri docUri)
-            throws RemoteException {
-        return nextPath;
-    }
-}
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index 2e56c54..5ac2741 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -59,7 +59,6 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
-                mEnv.providers,
                 mEnv.selectionMgr,
                 mEnv.searchViewManager,
                 mEnv::lookupExecutor) {
@@ -97,18 +96,18 @@
         mEnv.populateStack();
 
         mEnv.searchViewManager.isSearching = true;
-        mEnv.providers.nextPath = new Path(
+        mEnv.docs.nextPath = new Path(
                 TestRootsAccess.HOME.rootId,
                 Arrays.asList(TestEnv.FOLDER_1.documentId, TestEnv.FOLDER_2.documentId));
         mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2);
 
-        mEnv.state.pushDocument(TestEnv.FOLDER_0);
+        mEnv.state.stack.push(TestEnv.FOLDER_0);
 
         mHandler.openContainerDocument(TestEnv.FOLDER_2);
 
         mEnv.beforeAsserts();
 
-        assertEquals(mEnv.providers.nextPath.getPath().size(), mEnv.state.stack.size());
+        assertEquals(mEnv.docs.nextPath.getPath().size(), mEnv.state.stack.size());
         assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.peek());
     }
 
@@ -120,7 +119,7 @@
         mEnv.searchViewManager.isSearching = true;
         mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2);
 
-        mEnv.state.pushDocument(TestEnv.FOLDER_0);
+        mEnv.state.stack.push(TestEnv.FOLDER_0);
 
         mHandler.openContainerDocument(TestEnv.FOLDER_2);
 
diff --git a/tests/unit/com/android/documentsui/base/DocumentStackTest.java b/tests/unit/com/android/documentsui/base/DocumentStackTest.java
new file mode 100644
index 0000000..b7e3cc7
--- /dev/null
+++ b/tests/unit/com/android/documentsui/base/DocumentStackTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.base;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DocumentStackTest {
+    private static final DocumentInfo DIR_1;
+    private static final DocumentInfo DIR_2;
+
+    private DocumentStack mStack;
+
+    static {
+        DIR_1 = new DocumentInfo();
+        DIR_1.displayName = "firstDirectory";
+        DIR_2 = new DocumentInfo();
+        DIR_2.displayName = "secondDirectory";
+    }
+
+    @Before
+    public void setUp() {
+        mStack = new DocumentStack();
+    }
+
+    @Test
+    public void testInitialStateEmpty() {
+        assertFalse(mStack.hasLocationChanged());
+    }
+
+    @Test
+    public void testPushDocument_ChangesLocation() {
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+        assertTrue(mStack.hasLocationChanged());
+    }
+
+    @Test
+    public void testPushDocument_ModifiesStack() {
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+        assertEquals(DIR_2, mStack.peek());
+    }
+
+    @Test
+    public void testPopDocument_ModifiesStack() {
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+        mStack.pop();
+        assertEquals(DIR_1, mStack.peek());
+    }
+}
diff --git a/tests/unit/com/android/documentsui/base/StateTest.java b/tests/unit/com/android/documentsui/base/StateTest.java
index 1f01b67..3858ae4 100644
--- a/tests/unit/com/android/documentsui/base/StateTest.java
+++ b/tests/unit/com/android/documentsui/base/StateTest.java
@@ -16,51 +16,54 @@
 
 package com.android.documentsui.base;
 
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.documentsui.base.DocumentInfo;
+import static org.junit.Assert.assertArrayEquals;
 
+import android.content.Intent;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
 @SmallTest
-public class StateTest extends AndroidTestCase {
+public class StateTest {
 
-    private static final DocumentInfo DIR_1;
-    private static final DocumentInfo DIR_2;
+    private static final String[] MIME_TYPES = { "image/gif", "image/jpg" };
 
+    private Intent mIntent;
     private State mState;
 
-    static {
-        DIR_1 = new DocumentInfo();
-        DIR_1.displayName = "firstDirectory";
-        DIR_2 = new DocumentInfo();
-        DIR_2.displayName = "secondDirectory";
-    }
-
-    @Override
-    protected void setUp() throws Exception {
+    @Before
+    public void setUp() {
+        mIntent = new Intent();
         mState = new State();
     }
 
-    public void testInitialStateEmpty() {
-        assertFalse(mState.hasLocationChanged());
+    @Test
+    public void testAcceptAllMimeTypesByDefault() {
+        mState.initAcceptMimes(mIntent);
+
+        assertArrayEquals(new String[] { "*/*" }, mState.acceptMimes);
     }
 
-    public void testPushDocument_ChangesLocation() {
-        mState.pushDocument(DIR_1);
-        mState.pushDocument(DIR_2);
-        assertTrue(mState.hasLocationChanged());
+    @Test
+    public void testAcceptGivenMimeTypesInExtra() {
+        mIntent.putExtra(Intent.EXTRA_MIME_TYPES, MIME_TYPES);
+
+        mState.initAcceptMimes(mIntent);
+
+        assertArrayEquals(MIME_TYPES, mState.acceptMimes);
     }
 
-    public void testPushDocument_ModifiesStack() {
-        mState.pushDocument(DIR_1);
-        mState.pushDocument(DIR_2);
-        assertEquals(DIR_2, mState.stack.getFirst());
-    }
+    @Test
+    public void testAcceptIntentTypeWithoutExtra() {
+        mIntent.setType(MIME_TYPES[0]);
 
-    public void testPopDocument_ModifiesStack() {
-        mState.pushDocument(DIR_1);
-        mState.pushDocument(DIR_2);
-        mState.popDocument();
-        assertEquals(DIR_1, mState.stack.getFirst());
+        mState.initAcceptMimes(mIntent);
+
+        assertArrayEquals(new String[] { MIME_TYPES[0] }, mState.acceptMimes);
     }
 }
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 97b199d..271639b 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -71,7 +71,6 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
-                mEnv.providers,
                 mEnv.selectionMgr,
                 mEnv.searchViewManager,
                 mEnv::lookupExecutor,
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index b6897de..73cb123 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -24,10 +24,13 @@
 
 import android.content.Intent;
 import android.net.Uri;
+import android.provider.DocumentsContract.Path;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.testing.TestEnv;
@@ -38,6 +41,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Arrays;
+import java.util.List;
+
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class ActionHandlerTest {
@@ -59,7 +65,6 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
-                mEnv.providers,
                 mEnv.selectionMgr,
                 mEnv.searchViewManager,
                 mEnv::lookupExecutor,
@@ -123,6 +128,26 @@
     }
 
     @Test
+    public void testInitLocation_LaunchToDocuments() throws Exception {
+        mEnv.docs.nextIsDocumentsUri = true;
+        mEnv.docs.nextPath = new Path(
+                TestRootsAccess.HOME.rootId,
+                Arrays.asList(
+                        TestEnv.FOLDER_0.documentId,
+                        TestEnv.FOLDER_1.documentId,
+                        TestEnv.FILE_GIF.documentId));
+        mEnv.docs.nextDocuments =
+                Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
+
+        Intent intent = mActivity.getIntent();
+        intent.setAction(Intent.ACTION_GET_CONTENT);
+        intent.setData(TestEnv.FILE_GIF.derivedUri);
+        mHandler.initLocation(intent);
+
+        assertStackEquals(TestRootsAccess.HOME, Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+    }
+
+    @Test
     public void testOpenContainerDocument() {
         mHandler.openContainerDocument(TestEnv.FOLDER_0);
 
@@ -131,6 +156,17 @@
         mActivity.refreshCurrentRootAndDirectory.assertCalled();
     }
 
+    private void assertStackEquals(RootInfo root, List<DocumentInfo> docs) throws Exception {
+        mEnv.beforeAsserts();
+
+        final DocumentStack stack = mEnv.state.stack;
+        assertEquals(stack.getRoot(), root);
+        assertEquals(docs.size(), stack.size());
+        for (int i = 0; i < docs.size(); ++i) {
+            assertEquals(docs.get(i), stack.get(i));
+        }
+    }
+
     private void assertRootPicked(Uri expectedUri) throws Exception {
         mEnv.beforeAsserts();
 
diff --git a/tests/unit/com/android/documentsui/services/AbstractJobTest.java b/tests/unit/com/android/documentsui/services/AbstractJobTest.java
index c2034d2..d6b1a70 100644
--- a/tests/unit/com/android/documentsui/services/AbstractJobTest.java
+++ b/tests/unit/com/android/documentsui/services/AbstractJobTest.java
@@ -87,9 +87,8 @@
 
     final T createJob(@OpType int opType, List<Uri> srcs, Uri srcParent, Uri destination)
             throws Exception {
-        DocumentStack stack = new DocumentStack();
-        stack.push(DocumentInfo.fromUri(mResolver, destination));
-        stack.root = mSrcRoot;
+        DocumentStack stack =
+                new DocumentStack(mSrcRoot, DocumentInfo.fromUri(mResolver, destination));
 
         UrisSupplier urisSupplier = DocsProviders.createDocsProvider(srcs);
         FileOperation operation = new FileOperation.Builder()