Merge "Followups to ag/1553792/" into nyc-andromeda-dev
diff --git a/res/layout/dropdown_sort_widget.xml b/res/layout/dropdown_sort_widget.xml
index ce5e06f..3415f9c 100644
--- a/res/layout/dropdown_sort_widget.xml
+++ b/res/layout/dropdown_sort_widget.xml
@@ -30,7 +30,7 @@
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:gravity="center"
-        android:textAppearance="@android:style/TextAppearance.Material.Subhead"/>
+        android:textAppearance="@android:style/TextAppearance.Material.Subhead" />
 
     <ImageView
         android:id="@+id/sort_arrow"
@@ -38,6 +38,7 @@
         android:layout_height="@dimen/dropdown_sort_widget_icon_size"
         android:layout_marginStart="10dp"
         android:padding="3dp"
-        android:src="@drawable/ic_sort_arrow"/>
+        android:src="@drawable/ic_sort_arrow"
+        android:accessibilityTraversalAfter="@id/sort_dimen_dropdown"/>
 
 </LinearLayout>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 131d2f7..86093f9 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -38,6 +38,7 @@
 import com.android.documentsui.dirlist.AnimationView.AnimationType;
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DocumentDetails;
+import com.android.documentsui.dirlist.FocusHandler;
 import com.android.documentsui.files.LauncherActivity;
 import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.LoadRootTask;
@@ -62,17 +63,18 @@
     protected final State mState;
     protected final RootsAccess mRoots;
     protected final DocumentsAccess mDocs;
-    protected final ProviderAccess mProviders;
+    protected final FocusHandler mFocusHandler;
     protected final SelectionManager mSelectionMgr;
     protected final SearchViewManager mSearchMgr;
     protected final Lookup<String, Executor> mExecutors;
 
+
     public AbstractActionHandler(
             T activity,
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
+            FocusHandler focusHandler,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors) {
@@ -80,7 +82,7 @@
         assert(activity != null);
         assert(state != null);
         assert(roots != null);
-        assert(providers != null);
+        assert(focusHandler != null);
         assert(selectionMgr != null);
         assert(searchMgr != null);
         assert(docs != null);
@@ -89,7 +91,7 @@
         mState = state;
         mRoots = roots;
         mDocs = docs;
-        mProviders = providers;
+        mFocusHandler = focusHandler;
         mSelectionMgr = selectionMgr;
         mSearchMgr = searchMgr;
         mExecutors = executors;
@@ -181,33 +183,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,16 +228,26 @@
         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);
     }
 
     @Override
+    public void cutToClipboard() {
+        throw new UnsupportedOperationException("Cut not supported!");
+    }
+
+    @Override
+    public void copyToClipboard() {
+        throw new UnsupportedOperationException("Copy not supported!");
+    }
+
+    @Override
     public void deleteSelectedDocuments() {
         throw new UnsupportedOperationException("Delete not supported!");
     }
@@ -248,12 +260,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/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 7f6f923..d92ebbb 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -65,6 +65,13 @@
 
     void openContainerDocument(DocumentInfo doc);
 
+    void cutToClipboard();
+
+    void copyToClipboard();
+
+    /**
+     * In general, selected = selection or single focused item
+     */
     void deleteSelectedDocuments();
 
     void shareSelectedDocuments();
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index a6fa656..96b6832 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();
         }
@@ -665,20 +666,21 @@
      * locked, open/close it as appropriate.
      */
     void toggleNavDrawerFocus() {
+        boolean toogleHappened = false;
         if (mNavDrawerHasFocus) {
             mDrawer.setOpen(false);
             DirectoryFragment df = DirectoryFragment.get(getFragmentManager());
-            if (df != null) {
-                df.requestFocus();
-            }
+            assert (df != null);
+            toogleHappened = df.requestFocus();
         } else {
             mDrawer.setOpen(true);
             RootsFragment rf = RootsFragment.get(getFragmentManager());
-            if (rf != null) {
-                rf.requestFocus();
-            }
+            assert (rf != null);
+            toogleHappened = rf.requestFocus();
         }
-        mNavDrawerHasFocus = !mNavDrawerHasFocus;
+        if (toogleHappened) {
+            mNavDrawerHasFocus = !mNavDrawerHasFocus;
+        }
     }
 
     /**
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/FocusManager.java b/src/com/android/documentsui/FocusManager.java
index 2371b7f..8f5d388 100644
--- a/src/com/android/documentsui/FocusManager.java
+++ b/src/com/android/documentsui/FocusManager.java
@@ -146,6 +146,16 @@
         return mScope.lastFocusPosition;
     }
 
+    @Override
+    public @Nullable String getFocusModelId() {
+        if (mScope.lastFocusPosition != RecyclerView.NO_POSITION) {
+            DocumentHolder holder = (DocumentHolder) mScope.view
+                    .findViewHolderForAdapterPosition(mScope.lastFocusPosition);
+            return holder.getModelId();
+        }
+        return null;
+    }
+
     /**
      * Finds the destination position where the focus should land for a given navigation event.
      *
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 8059625..32d3cc2 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 e385af2..cc6a548 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -633,11 +633,11 @@
                 return true;
 
             case R.id.menu_cut_to_clipboard:
-                cutSelectedToClipboard();
+                mActions.cutToClipboard();
                 return true;
 
             case R.id.menu_copy_to_clipboard:
-                copySelectedToClipboard();
+                mActions.copyToClipboard();
                 return true;
 
             case R.id.menu_paste_from_clipboard:
@@ -838,34 +838,6 @@
         mRecView.requestFocus();
     }
 
-    public void copySelectedToClipboard() {
-        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
-
-        Selection selection = mSelectionMgr.getSelection(new Selection());
-        if (selection.isEmpty()) {
-            return;
-        }
-        mSelectionMgr.clearSelection();
-
-        mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
-
-        Snackbars.showDocumentsClipped(getActivity(), selection.size());
-    }
-
-    public void cutSelectedToClipboard() {
-        Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD);
-
-        Selection selection = mSelectionMgr.getSelection(new Selection());
-        if (selection.isEmpty()) {
-            return;
-        }
-        mSelectionMgr.clearSelection();
-
-        mClipper.clipDocumentsForCut(mModel::getItemUri, selection, mState.stack.peek());
-
-        Snackbars.showDocumentsClipped(getActivity(), selection.size());
-    }
-
     public void pasteFromClipboard() {
         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
 
@@ -920,8 +892,12 @@
     /**
      * Attempts to restore focus on the directory listing.
      */
-    public void requestFocus() {
+    public boolean requestFocus() {
+        if (mSelectionMgr.hasSelection()) {
+            return false;
+        }
         mFocusManager.restoreLastFocus();
+        return true;
     }
 
     private void setupDragAndDropOnDocumentView(View view, Cursor cursor) {
@@ -1206,7 +1182,7 @@
 
             if (mModel.isEmpty()) {
                 if (mLocalState.mSearchMode) {
-                    showNoResults(mState.stack.root);
+                    showNoResults(mState.stack.getRoot());
                 } else {
                     showEmptyDirectory();
                 }
diff --git a/src/com/android/documentsui/dirlist/FocusHandler.java b/src/com/android/documentsui/dirlist/FocusHandler.java
index 0f43ac0..1cbb8a9 100644
--- a/src/com/android/documentsui/dirlist/FocusHandler.java
+++ b/src/com/android/documentsui/dirlist/FocusHandler.java
@@ -16,6 +16,7 @@
 
 package com.android.documentsui.dirlist;
 
+import android.annotation.Nullable;
 import android.view.KeyEvent;
 import android.view.View;
 
@@ -52,4 +53,8 @@
      */
     int getFocusPosition();
 
+    /**
+     * @return The modelId of the last focused item. If no item is focused, this should return null.
+     */
+    @Nullable String getFocusModelId();
 }
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index c2da8f5..09f5ce1 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;
@@ -49,6 +48,7 @@
 import com.android.documentsui.clipping.UrisSupplier;
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DocumentDetails;
+import com.android.documentsui.dirlist.FocusHandler;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
 import com.android.documentsui.files.ActionHandler.Addons;
@@ -88,7 +88,7 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
+            FocusHandler focusHandler,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
@@ -98,7 +98,7 @@
             DocumentClipper clipper,
             ClipStore clipStore) {
 
-        super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors);
+        super(activity, state, roots, docs, focusHandler, selectionMgr, searchMgr, executors);
 
         mActionModeAddons = actionModeAddons;
         mDialogs = dialogs;
@@ -192,14 +192,57 @@
         return previewDocument(doc);
     }
 
+    private Selection getSelectedOrFocused() {
+        final Selection selection = this.getStableSelection();
+        if (selection.isEmpty()) {
+            String focusModelId = mFocusHandler.getFocusModelId();
+            if (focusModelId != null) {
+                selection.add(focusModelId);
+            }
+        }
+
+        return selection;
+    }
+
+    @Override
+    public void cutToClipboard() {
+        Metrics.logUserAction(mActivity, Metrics.USER_ACTION_CUT_CLIPBOARD);
+        Selection selection = getSelectedOrFocused();
+
+        if (selection.isEmpty()) {
+            return;
+        }
+        mSelectionMgr.clearSelection();
+
+        mClipper.clipDocumentsForCut(mScope.model::getItemUri, selection, mState.stack.peek());
+
+        mDialogs.showDocumentsClipped(selection.size());
+    }
+
+    @Override
+    public void copyToClipboard() {
+        Metrics.logUserAction(mActivity, Metrics.USER_ACTION_COPY_CLIPBOARD);
+        Selection selection = getSelectedOrFocused();
+
+        if (selection.isEmpty()) {
+            return;
+        }
+        mSelectionMgr.clearSelection();
+
+        mClipper.clipDocumentsForCopy(mScope.model::getItemUri, selection);
+
+        mDialogs.showDocumentsClipped(selection.size());
+    }
+
+
     @Override
     public void deleteSelectedDocuments() {
-        assert(mSelectionMgr.hasSelection());
-
         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_DELETE);
+        Selection selection = getSelectedOrFocused();
 
-        Selection selection = getStableSelection();
-        assert(!selection.isEmpty());
+        if (selection.isEmpty()) {
+            return;
+        }
 
         final DocumentInfo srcParent = mState.stack.peek();
         assert(srcParent != null);
@@ -321,12 +364,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 +555,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/ActivityInputHandler.java b/src/com/android/documentsui/files/ActivityInputHandler.java
index a7b0e2c..35653d2 100644
--- a/src/com/android/documentsui/files/ActivityInputHandler.java
+++ b/src/com/android/documentsui/files/ActivityInputHandler.java
@@ -18,31 +18,22 @@
 
 import android.view.KeyEvent;
 
-import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.ActionHandler;
-
 /**
  * Used by {@link FilesActivity} to manage global keyboard shortcuts tied to file actions
  */
 final class ActivityInputHandler {
 
-    private final SelectionManager mSelectionMgr;
-    private final ActionHandler mActions;
+    private final Runnable mDeleteHandler;
 
-    ActivityInputHandler(SelectionManager selectionMgr, ActionHandler actionHandler) {
-        mSelectionMgr = selectionMgr;
-        mActions = actionHandler;
+    ActivityInputHandler(Runnable deleteHandler) {
+        mDeleteHandler = deleteHandler;
     }
 
     boolean onKeyDown(int keyCode, KeyEvent event) {
         if ((keyCode == KeyEvent.KEYCODE_DEL && event.isAltPressed())
                 || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
-            if (mSelectionMgr.hasSelection()) {
-                mActions.deleteSelectedDocuments();
-                return true;
-            } else {
-                return false;
-            }
+            mDeleteHandler.run();
+            return true;
         }
         return false;
     }
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..7a2284f 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -56,6 +56,7 @@
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.selection.Selection;
 import com.android.documentsui.selection.SelectionManager;
 import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
 import com.android.documentsui.services.FileOperationService;
@@ -124,7 +125,7 @@
                 mState,
                 mRoots,
                 mDocs,
-                DocumentsApplication.getProviderAccess(this),
+                mFocusManager,
                 mSelectionMgr,
                 mSearchManager,
                 ProviderExecutor::forAuthority,
@@ -134,7 +135,7 @@
                 mClipper,
                 DocumentsApplication.getClipStore(this));
 
-        mActivityInputHandler = new ActivityInputHandler(mSelectionMgr, mActions);
+        mActivityInputHandler = new ActivityInputHandler(mActions::deleteSelectedDocuments);
 
         RootsFragment.show(getFragmentManager(), null);
 
@@ -176,7 +177,7 @@
 
         final DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
         if (stack != null) {
-            state.stack = stack;
+            state.stack.reset(stack);
         }
     }
 
@@ -325,16 +326,10 @@
                 }
                 return true;
             case KeyEvent.KEYCODE_X:
-                dir = getDirectoryFragment();
-                if (dir != null) {
-                    dir.cutSelectedToClipboard();
-                }
+                mActions.cutToClipboard();
                 return true;
             case KeyEvent.KEYCODE_C:
-                dir = getDirectoryFragment();
-                if (dir != null) {
-                    dir.copySelectedToClipboard();
-                }
+                mActions.copyToClipboard();
                 return true;
             case KeyEvent.KEYCODE_V:
                 dir = getDirectoryFragment();
diff --git a/src/com/android/documentsui/files/LauncherActivity.java b/src/com/android/documentsui/files/LauncherActivity.java
index bb25c35..e76728f 100644
--- a/src/com/android/documentsui/files/LauncherActivity.java
+++ b/src/com/android/documentsui/files/LauncherActivity.java
@@ -59,16 +59,27 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        launch();
+
+        finish();
+    }
+
+    private void launch() {
         ActivityManager activities = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
 
         Intent intent = findTask(activities);
         if (intent != null) {
-            restoreTask(intent);
-        } else {
-            startTask();
+            if (restoreTask(intent)) {
+                return;
+            } else {
+                // We failed to restore the task. It may happen when system was just updated and we
+                // moved the location of the targeted activity. Chances is that the rest of tasks
+                // can't be restored either, so clean those tasks and start a new one.
+                clearTask(activities);
+            }
         }
 
-        finish();
+        startTask();
     }
 
     private @Nullable Intent findTask(ActivityManager activities) {
@@ -92,10 +103,26 @@
         startActivity(intent);
     }
 
-    private void restoreTask(Intent intent) {
+    private boolean restoreTask(Intent intent) {
         if (DEBUG) Log.d(TAG, "Restoring existing task > " + intent.getData());
-        // TODO: This doesn't appear to restore a task once it has stopped running.
-        startActivity(intent);
+        try {
+            // TODO: This doesn't appear to restore a task once it has stopped running.
+            startActivity(intent);
+
+            return true;
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to restore task > " + intent.getData() +
+                    ". Clear all existing tasks and start a new one.", e);
+        }
+
+        return false;
+    }
+
+    private void clearTask(ActivityManager activities) {
+        List<AppTask> tasks = activities.getAppTasks();
+        for (AppTask task : tasks) {
+            task.finishAndRemoveTask();
+        }
     }
 
     public static final Intent createLaunchIntent(Activity activity) {
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index d37e34c..1999381 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -36,11 +36,11 @@
 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;
 import com.android.documentsui.dirlist.DocumentDetails;
+import com.android.documentsui.dirlist.FocusHandler;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
 import com.android.documentsui.picker.ActionHandler.Addons;
@@ -67,13 +67,13 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
-            ProviderAccess providers,
+            FocusHandler focusHandler,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
             ActivityConfig activityConfig) {
 
-        super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors);
+        super(activity, state, roots, docs, focusHandler, selectionMgr, searchMgr, executors);
 
         mConfig = activityConfig;
         mScope = new ContentScope(this::onModelLoaded);
@@ -81,42 +81,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 +217,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..eee92e5 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -110,7 +110,7 @@
                 mState,
                 mRoots,
                 mDocs,
-                DocumentsApplication.getProviderAccess(this),
+                mFocusManager,
                 mSelectionMgr,
                 mSearchManager,
                 ProviderExecutor::forAuthority,
diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index 1122f6b..30bbbf1 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 756112e..7e66cb0 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/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 8890fb3..4e9287e 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -357,8 +357,8 @@
     /**
      * Attempts to shift focus back to the navigation drawer.
      */
-    public void requestFocus() {
-        mList.requestFocus();
+    public boolean requestFocus() {
+        return mList.requestFocus();
     }
 
     private BaseActivity<?> getBaseActivity() {
diff --git a/src/com/android/documentsui/ui/DialogController.java b/src/com/android/documentsui/ui/DialogController.java
index 6c648a2..39bebfe 100644
--- a/src/com/android/documentsui/ui/DialogController.java
+++ b/src/com/android/documentsui/ui/DialogController.java
@@ -50,11 +50,17 @@
         public void showNoApplicationFound() {
             throw new UnsupportedOperationException();
         }
+
+        @Override
+        public void showDocumentsClipped(int size) {
+            throw new UnsupportedOperationException();
+        }
     };
 
     void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback);
     void showFileOperationFailures(int status, int opType, int docCount);
     void showNoApplicationFound();
+    void showDocumentsClipped(int size);
 
     // Should be private, but Java doesn't like me treating an interface like a mini-package.
     public static final class RuntimeDialogController implements DialogController {
@@ -137,6 +143,11 @@
             Snackbars.makeSnackbar(
                     mActivity, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
         }
+
+        @Override
+        public void showDocumentsClipped(int size) {
+            Snackbars.showDocumentsClipped(mActivity, size);
+        }
     }
 
     static DialogController create(Activity activity, MessageBuilder messages) {
diff --git a/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java b/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java
index 45e0655..76db4e1 100644
--- a/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java
+++ b/tests/common/com/android/documentsui/dirlist/TestFocusHandler.java
@@ -45,6 +45,11 @@
     }
 
     @Override
+    public String getFocusModelId() {
+        return null;
+    }
+
+    @Override
     public void focusDocument(String modelId) {
     }
 
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 5ad0563..bd8adac 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -40,7 +40,7 @@
                 env.state,
                 env.roots,
                 env.docs,
-                env.providers,
+                env.focusHandler,
                 env.selectionMgr,
                 env.searchViewManager,
                 (String authority) -> null);
diff --git a/tests/common/com/android/documentsui/testing/TestConfirmationCallback.java b/tests/common/com/android/documentsui/testing/TestConfirmationCallback.java
index 6ffdf70..f94f33f 100644
--- a/tests/common/com/android/documentsui/testing/TestConfirmationCallback.java
+++ b/tests/common/com/android/documentsui/testing/TestConfirmationCallback.java
@@ -48,4 +48,8 @@
     public void assertCalled() {
         Assert.assertTrue(mCalled);
     }
+
+    public void assertNeverCalled() {
+        Assert.assertFalse(mCalled);
+    }
 }
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..076e621 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -21,6 +21,7 @@
 
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.TestFocusHandler;
 import com.android.documentsui.dirlist.TestModel;
 import com.android.documentsui.selection.SelectionManager;
 
@@ -50,7 +51,7 @@
     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 TestFocusHandler focusHandler = new TestFocusHandler();
     public final TestModel model;
     public final SelectionManager selectionMgr;
     public final TestSearchViewManager searchViewManager;
@@ -111,7 +112,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/common/com/android/documentsui/ui/TestDialogController.java b/tests/common/com/android/documentsui/ui/TestDialogController.java
index dbac0f5..e3135be 100644
--- a/tests/common/com/android/documentsui/ui/TestDialogController.java
+++ b/tests/common/com/android/documentsui/ui/TestDialogController.java
@@ -28,6 +28,7 @@
     public int mNextConfirmationCode;
     private boolean mFileOpFailed;
     private boolean mNoApplicationFound;
+    private boolean mDocumentsClipped;
 
     public TestDialogController() {
         // by default, always confirm
@@ -51,6 +52,11 @@
         mNoApplicationFound = true;
     }
 
+    @Override
+    public void showDocumentsClipped(int size) {
+        mDocumentsClipped = true;
+    }
+
     public void assertNoFileFailures() {
         Assert.assertFalse(mFileOpFailed);
     }
@@ -59,6 +65,10 @@
         Assert.assertFalse(mNoApplicationFound);
     }
 
+    public void assertDocumentsClippedNotShown() {
+        Assert.assertFalse(mDocumentsClipped);
+    }
+
     public void confirmNext() {
         mNextConfirmationCode = ConfirmationCallback.CONFIRM;
     }
diff --git a/tests/functional/com/android/documentsui/BandSelectionUiTest.java b/tests/functional/com/android/documentsui/BandSelectionUiTest.java
index 61bb442..c1b44b4 100644
--- a/tests/functional/com/android/documentsui/BandSelectionUiTest.java
+++ b/tests/functional/com/android/documentsui/BandSelectionUiTest.java
@@ -18,7 +18,7 @@
 
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.test.suitebuilder.annotation.LargeTest;
+import android.support.test.filters.LargeTest;
 
 import com.android.documentsui.files.FilesActivity;
 
diff --git a/tests/functional/com/android/documentsui/FileManagementUiTest.java b/tests/functional/com/android/documentsui/FileManagementUiTest.java
index dd186cb..bd62550 100644
--- a/tests/functional/com/android/documentsui/FileManagementUiTest.java
+++ b/tests/functional/com/android/documentsui/FileManagementUiTest.java
@@ -21,11 +21,10 @@
 
 import android.net.Uri;
 import android.os.RemoteException;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.Suppress;
-import android.test.suitebuilder.annotation.LargeTest;
 import android.view.KeyEvent;
 
-import com.android.documentsui.R;
 import com.android.documentsui.files.FilesActivity;
 
 @LargeTest
diff --git a/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java b/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java
index 8fc6d27..ff86a7f 100644
--- a/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java
+++ b/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java
@@ -21,7 +21,7 @@
 
 import android.content.Intent;
 import android.provider.DocumentsContract;
-import android.test.suitebuilder.annotation.LargeTest;
+import android.support.test.filters.LargeTest;
 
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
diff --git a/tests/functional/com/android/documentsui/FilesActivityUiTest.java b/tests/functional/com/android/documentsui/FilesActivityUiTest.java
index 4f0ce13..a798f16 100644
--- a/tests/functional/com/android/documentsui/FilesActivityUiTest.java
+++ b/tests/functional/com/android/documentsui/FilesActivityUiTest.java
@@ -18,7 +18,7 @@
 
 import android.net.Uri;
 import android.os.RemoteException;
-import android.test.suitebuilder.annotation.LargeTest;
+import android.support.test.filters.LargeTest;
 
 import com.android.documentsui.files.FilesActivity;
 
diff --git a/tests/functional/com/android/documentsui/GestureSelectionUiTest.java b/tests/functional/com/android/documentsui/GestureSelectionUiTest.java
index 16c92f3..a8a1aec 100644
--- a/tests/functional/com/android/documentsui/GestureSelectionUiTest.java
+++ b/tests/functional/com/android/documentsui/GestureSelectionUiTest.java
@@ -16,7 +16,7 @@
 
 package com.android.documentsui;
 
-import android.test.suitebuilder.annotation.LargeTest;
+import android.support.test.filters.LargeTest;
 
 import com.android.documentsui.files.FilesActivity;
 
diff --git a/tests/functional/com/android/documentsui/IntegratedDownloadsUiTest.java b/tests/functional/com/android/documentsui/IntegratedDownloadsUiTest.java
index da340c2..62e5fb6 100644
--- a/tests/functional/com/android/documentsui/IntegratedDownloadsUiTest.java
+++ b/tests/functional/com/android/documentsui/IntegratedDownloadsUiTest.java
@@ -20,10 +20,10 @@
 import android.app.DownloadManager.Request;
 import android.content.Context;
 import android.net.Uri;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.Suppress;
 import android.support.test.uiautomator.Configurator;
 import android.support.test.uiautomator.UiObject;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.Suppress;
 import android.view.MotionEvent;
 
 import com.android.documentsui.files.FilesActivity;
diff --git a/tests/functional/com/android/documentsui/KeyboardNavigationUiTest.java b/tests/functional/com/android/documentsui/KeyboardNavigationUiTest.java
index f70918d..d9e2065 100644
--- a/tests/functional/com/android/documentsui/KeyboardNavigationUiTest.java
+++ b/tests/functional/com/android/documentsui/KeyboardNavigationUiTest.java
@@ -16,10 +16,9 @@
 
 package com.android.documentsui;
 
-import android.net.Uri;
 import android.os.RemoteException;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.Suppress;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.Suppress;
 import android.view.KeyEvent;
 
 import com.android.documentsui.files.FilesActivity;
diff --git a/tests/functional/com/android/documentsui/RenameDocumentUiTest.java b/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
index 0330ac3..9848bd5 100644
--- a/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
+++ b/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
@@ -16,10 +16,9 @@
 
 package com.android.documentsui;
 
+import android.support.test.filters.LargeTest;
 import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.test.suitebuilder.annotation.LargeTest;
 
-import com.android.documentsui.R;
 import com.android.documentsui.files.FilesActivity;
 
 @LargeTest
diff --git a/tests/functional/com/android/documentsui/SearchViewUiTest.java b/tests/functional/com/android/documentsui/SearchViewUiTest.java
index 1b248a9..8e54512 100644
--- a/tests/functional/com/android/documentsui/SearchViewUiTest.java
+++ b/tests/functional/com/android/documentsui/SearchViewUiTest.java
@@ -19,9 +19,9 @@
 import static com.android.documentsui.StubProvider.ROOT_0_ID;
 import static com.android.documentsui.StubProvider.ROOT_1_ID;
 
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.Suppress;
 import android.support.v7.recyclerview.R;
-import android.test.suitebuilder.annotation.LargeTest;
 
 import com.android.documentsui.files.FilesActivity;
 
diff --git a/tests/functional/com/android/documentsui/SidebarUiTest.java b/tests/functional/com/android/documentsui/SidebarUiTest.java
index 79dacea..c21c853 100644
--- a/tests/functional/com/android/documentsui/SidebarUiTest.java
+++ b/tests/functional/com/android/documentsui/SidebarUiTest.java
@@ -19,8 +19,8 @@
 import static com.android.documentsui.StubProvider.ROOT_0_ID;
 import static com.android.documentsui.StubProvider.ROOT_1_ID;
 
-import android.test.suitebuilder.annotation.LargeTest;
-import android.test.suitebuilder.annotation.Suppress;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.Suppress;
 
 import com.android.documentsui.files.FilesActivity;
 
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index 2e56c54..b3379bf 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -59,7 +59,7 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
-                mEnv.providers,
+                mEnv.focusHandler,
                 mEnv.selectionMgr,
                 mEnv.searchViewManager,
                 mEnv::lookupExecutor) {
@@ -97,18 +97,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 +120,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..c332bfd 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -36,6 +36,7 @@
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
+import com.android.documentsui.selection.Selection;
 import com.android.documentsui.testing.Roots;
 import com.android.documentsui.testing.TestConfirmationCallback;
 import com.android.documentsui.testing.TestEnv;
@@ -71,7 +72,7 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
-                mEnv.providers,
+                mEnv.focusHandler,
                 mEnv.selectionMgr,
                 mEnv.searchViewManager,
                 mEnv::lookupExecutor,
@@ -113,13 +114,32 @@
     }
 
     @Test
-    public void testDeleteSelectedDocuments() {
+    public void testCutSelectedDocuments_NoGivenSelection() {
         mEnv.populateStack();
 
+        mEnv.selectionMgr.clearSelection();
+        mHandler.cutToClipboard();
+        mDialogs.assertDocumentsClippedNotShown();
+    }
+
+    @Test
+    public void testCopySelectedDocuments_NoGivenSelection() {
+        mEnv.populateStack();
+
+        mEnv.selectionMgr.clearSelection();
+        mHandler.copyToClipboard();
+        mDialogs.assertDocumentsClippedNotShown();
+    }
+
+    @Test
+    public void testDeleteSelectedDocuments_NoSelection() {
+        mEnv.populateStack();
+
+        mEnv.selectionMgr.clearSelection();
         mHandler.deleteSelectedDocuments();
         mDialogs.assertNoFileFailures();
-        mActivity.startService.assertCalled();
-        mActionModeAddons.finishOnConfirmed.assertConfirmed();
+        mActivity.startService.assertNotCalled();
+        mActionModeAddons.finishOnConfirmed.assertNeverCalled();
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java b/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java
index 37a696f..b579dbe 100644
--- a/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.documentsui.files;
 
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.support.test.filters.MediumTest;
@@ -24,50 +23,30 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-import com.android.documentsui.dirlist.TestData;
-import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.selection.SelectionProbe;
-import com.android.documentsui.testing.SelectionManagers;
-import com.android.documentsui.testing.TestActionHandler;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.List;
-
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class ActivityInputHandlerTest {
 
-    private static final List<String> ITEMS = TestData.create(100);
-
-    private SelectionProbe mSelection;
-    private TestActionHandler mActionHandler;
     private ActivityInputHandler mActivityInputHandler;
+    private boolean mDeleteHappened;
 
     @Before
     public void setUp() {
-        SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
-        mSelection = new SelectionProbe(selectionMgr);
-        mActionHandler = new TestActionHandler();
-        mActivityInputHandler = new ActivityInputHandler(selectionMgr, mActionHandler);
+        mDeleteHappened = false;
+        mActivityInputHandler = new ActivityInputHandler(() -> {
+            mDeleteHappened = true;
+        });
     }
 
     @Test
-    public void testDelete_noSelection() {
-        KeyEvent event = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0,
-                KeyEvent.META_ALT_ON);
-        assertFalse(mActivityInputHandler.onKeyDown(event.getKeyCode(), event));
-        assertFalse(mActionHandler.mDeleteHappened);
-    }
-
-    @Test
-    public void testDelete_hasSelection() {
-        mSelection.select(1);
+    public void testDelete() {
         KeyEvent event = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0,
                 KeyEvent.META_ALT_ON);
         assertTrue(mActivityInputHandler.onKeyDown(event.getKeyCode(), event));
-        assertTrue(mActionHandler.mDeleteHappened);
+        assertTrue(mDeleteHappened);
     }
 }
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index b6897de..5e26cd8 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,7 @@
                 mEnv.state,
                 mEnv.roots,
                 mEnv.docs,
-                mEnv.providers,
+                mEnv.focusHandler,
                 mEnv.selectionMgr,
                 mEnv.searchViewManager,
                 mEnv::lookupExecutor,
@@ -123,6 +129,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 +157,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()