Features around findPath API.

* Add folders into search result
* Allow callers passing data to launch pickers at specfic location
* Rewire loadDocument() to LoadDocStackTask
* Remove VIEW intent for FilesActivity and related OpenUriForViewTask
* Add a ProviderAccess to enable testing in LoadDocStackTask
* Fix a wrong assertion in files/ActionHandlerTest

Change-Id: Iacc2b99dc68cbb4a40a4c445c69473973123c5bf
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index f541775..390dd00 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -22,10 +22,12 @@
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Parcelable;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
+import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
-import com.android.documentsui.archives.ArchivesProvider;
+import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback;
 import com.android.documentsui.base.BooleanConsumer;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -37,7 +39,6 @@
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.files.LauncherActivity;
-import com.android.documentsui.files.OpenUriForViewTask;
 import com.android.documentsui.roots.LoadRootTask;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.selection.Selection;
@@ -45,6 +46,7 @@
 import com.android.documentsui.sidebar.EjectRootTask;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
@@ -53,10 +55,13 @@
 public abstract class AbstractActionHandler<T extends Activity & CommonAddons>
         implements ActionHandler {
 
+    private static final String TAG = "AbstractActionHandler";
+
     protected final T mActivity;
     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;
@@ -66,6 +71,7 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
+            ProviderAccess providers,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors) {
@@ -73,13 +79,16 @@
         assert(activity != null);
         assert(state != null);
         assert(roots != null);
+        assert(providers != null);
         assert(selectionMgr != null);
+        assert(searchMgr != null);
         assert(docs != null);
 
         mActivity = activity;
         mState = state;
         mRoots = roots;
         mDocs = docs;
+        mProviders = providers;
         mSelectionMgr = selectionMgr;
         mSearchMgr = searchMgr;
         mExecutors = executors;
@@ -159,6 +168,50 @@
     @Override
     public void openContainerDocument(DocumentInfo doc) {
         assert(doc.isContainer());
+
+        if (mSearchMgr.isSearching()) {
+            loadDocument(
+                    doc.derivedUri,
+                    (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc));
+        } else {
+            openChildContainer(doc);
+        }
+    }
+
+    private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) {
+        if (stack == null) {
+            mState.popDocumentsToRoot();
+
+            // 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);
+        } else {
+            if (!Objects.equals(mState.stack.root, stack.root)) {
+                Log.w(TAG, "Provider returns " + stack.root + " rather than expected "
+                        + mState.stack.root);
+            }
+
+            mState.stack.clear();
+            // 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);
+        }
+
+        // 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)
+                ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
+        mActivity.refreshCurrentRootAndDirectory(anim);
+    }
+
+    private void openChildContainer(DocumentInfo doc) {
         DocumentInfo currentDoc = null;
 
         if (doc.isDirectory()) {
@@ -171,8 +224,8 @@
 
         assert(currentDoc != null);
         mActivity.notifyDirectoryNavigated(currentDoc.derivedUri);
-        mState.pushDocument(currentDoc);
 
+        mState.pushDocument(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.
@@ -191,10 +244,15 @@
         throw new UnsupportedOperationException("Share not supported!");
     }
 
-    @Override
-    public final void loadDocument(Uri uri) {
-        new OpenUriForViewTask<>(mActivity, mState, mRoots, mDocs, uri)
-                .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
+    protected final void loadDocument(Uri uri, LoadDocStackCallback callback) {
+        new LoadDocStackTask(
+                mActivity,
+                uri,
+                mRoots,
+                mDocs,
+                mProviders,
+                callback
+                ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
     }
 
     @Override
@@ -225,6 +283,9 @@
         DocumentInfo getCurrentDirectory();
         void setRootsDrawerOpen(boolean open);
 
+        // TODO: Let navigator listens to State
+        void updateNavigator();
+
         @VisibleForTesting
         void notifyDirectoryNavigated(Uri docUri);
     }
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 98d24ed..7f6f923 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -49,8 +49,6 @@
 
     void loadRoot(Uri uri);
 
-    void loadDocument(Uri uri);
-
     void openSelectedInNewWindow();
 
     void openInNewWindow(DocumentStack path);
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 235757e..d0160ce 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -413,6 +413,12 @@
                 && !root.isDownloads();
     }
 
+    // TODO: make navigator listen to state
+    @Override
+    public final void updateNavigator() {
+        mNavigator.update();
+    }
+
     /**
      * Refreshes the content of the director and the menu/action bar.
      * The current directory name and selection will get updated.
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index 56e76bb..8af1ab3 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -33,6 +33,7 @@
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.FilteringCursorWrapper;
 import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.Shared;
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.sorting.SortModel;
 
@@ -101,8 +102,8 @@
 
             cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1);
 
-            if (mSearchMode) {
-                // Filter directories out of search results, for now
+            if (mSearchMode && !Shared.ENABLE_OMC_API_FEATURES) {
+                // There is no findPath API. Enable filtering on folders in search mode.
                 cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
             }
 
diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java
index 5d72ab8..8cfaa5a 100644
--- a/src/com/android/documentsui/DocumentsAccess.java
+++ b/src/com/android/documentsui/DocumentsAccess.java
@@ -17,8 +17,11 @@
 package com.android.documentsui;
 
 import android.annotation.Nullable;
+import android.content.ContentProviderClient;
 import android.content.Context;
+import android.database.Cursor;
 import android.net.Uri;
+import android.os.RemoteException;
 import android.provider.DocumentsContract;
 import android.util.Log;
 
@@ -27,6 +30,8 @@
 import com.android.documentsui.base.RootInfo;
 
 import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Provides synchronous access to {@link DocumentInfo} instances given some identifying information.
@@ -34,10 +39,11 @@
 public interface DocumentsAccess {
 
     @Nullable DocumentInfo getRootDocument(RootInfo root);
-    @Nullable DocumentInfo getRootDocument(Uri uri);
     @Nullable DocumentInfo getDocument(Uri uri);
     @Nullable DocumentInfo getArchiveDocument(Uri uri);
 
+    @Nullable List<DocumentInfo> getDocuments(String authority, List<String> docIds);
+
     public static DocumentsAccess create(Context context) {
         return new RuntimeDocumentAccess(context);
     }
@@ -54,22 +60,12 @@
 
         @Override
         public @Nullable DocumentInfo getRootDocument(RootInfo root) {
-            return getRootDocument(
+            return getDocument(
                     DocumentsContract.buildDocumentUri(root.authority, root.documentId));
         }
 
         @Override
-        public @Nullable DocumentInfo getRootDocument(Uri uri) {
-            try {
-                return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
-            } catch (FileNotFoundException e) {
-                Log.w(TAG, "Failed to find root", e);
-                return null;
-            }
-        }
-
-        @Override
-        public DocumentInfo getDocument(Uri uri) {
+        public @Nullable DocumentInfo getDocument(Uri uri) {
             try {
                 return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
             } catch (FileNotFoundException e) {
@@ -80,6 +76,34 @@
         }
 
         @Override
+        public @Nullable List<DocumentInfo> getDocuments(String authority, List<String> docIds) {
+            try(final ContentProviderClient client = DocumentsApplication
+                    .acquireUnstableProviderOrThrow(mContext.getContentResolver(), authority)) {
+
+                List<DocumentInfo> result = new ArrayList<>(docIds.size());
+                for (String docId : docIds) {
+                    final Uri uri = DocumentsContract.buildDocumentUri(authority, docId);
+                    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;
+                        }
+
+                        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;
+            }
+        }
+
+        @Override
         public DocumentInfo getArchiveDocument(Uri uri) {
             return getDocument(ArchivesProvider.buildUriForArchive(uri));
         }
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 3f1d48b..64eeb14 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -28,6 +28,7 @@
 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;
@@ -42,6 +43,8 @@
     private ClipStorage mClipStore;
     private DocumentClipper mClipper;
 
+    private ProviderAccess mProviderAccess;
+
     public static RootsCache getRootsCache(Context context) {
         return ((DocumentsApplication) context.getApplicationContext()).mRoots;
     }
@@ -51,6 +54,11 @@
         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(
@@ -87,6 +95,8 @@
                 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/LoadDocStackTask.java b/src/com/android/documentsui/LoadDocStackTask.java
new file mode 100644
index 0000000..e849a1b
--- /dev/null
+++ b/src/com/android/documentsui/LoadDocStackTask.java
@@ -0,0 +1,115 @@
+/*
+ * 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.annotation.Nullable;
+import android.app.Activity;
+import android.net.Uri;
+import android.provider.DocumentsContract.Path;
+import android.util.Log;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.PairedTask;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.roots.RootsAccess;
+
+import java.util.List;
+
+/**
+ * Loads {@link DocumentStack} for given document. It provides its best effort to find the path of
+ * the given document.
+ *
+ * If it fails to load correct path it calls callback with different result
+ * depending on the nullness of given root. If given root is null it calls callback with null. If
+ * 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> {
+    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) {
+            try {
+                final Path path = mProviders.findPath(mDocUri);
+                if (path != null) {
+                    return buildStack(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.
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public void finish(@Nullable DocumentStack stack){
+        mCallback.onDocumentStackLoaded(stack);
+    }
+
+    private @Nullable DocumentStack buildStack(Path path) {
+        final String rootId = path.getRootId();
+        if (rootId == null) {
+            Log.e(TAG, "Provider doesn't provide root id.");
+            return null;
+        }
+
+        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;
+        }
+
+        return new DocumentStack(root, docs);
+    }
+
+    @FunctionalInterface
+    public interface LoadDocStackCallback {
+        void onDocumentStackLoaded(@Nullable DocumentStack stack);
+    }
+}
diff --git a/src/com/android/documentsui/ProviderAccess.java b/src/com/android/documentsui/ProviderAccess.java
new file mode 100644
index 0000000..67a0e3f
--- /dev/null
+++ b/src/com/android/documentsui/ProviderAccess.java
@@ -0,0 +1,48 @@
+/*
+ * 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/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java
index e978409..c3a13e0 100644
--- a/src/com/android/documentsui/base/DocumentStack.java
+++ b/src/com/android/documentsui/base/DocumentStack.java
@@ -28,6 +28,7 @@
 import java.net.ProtocolException;
 import java.util.Collection;
 import java.util.LinkedList;
+import java.util.List;
 
 /**
  * Representation of a stack of {@link DocumentInfo}, usually the result of a
@@ -53,6 +54,14 @@
         this.root = root;
     }
 
+    public DocumentStack(RootInfo root, List<DocumentInfo> docs) {
+        for (DocumentInfo doc : docs) {
+            push(doc);
+        }
+
+        this.root = 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.
diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java
index cfa102d..63f2886 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -47,6 +47,8 @@
 
     public static final boolean DEBUG = true;
 
+    public static final boolean ENABLE_OMC_API_FEATURES = true;
+
     /** Intent action name to pick a copy destination. */
     public static final String ACTION_PICK_COPY_DESTINATION =
             "com.android.documentsui.PICK_COPY_DESTINATION";
@@ -59,7 +61,7 @@
     public static final String EXTRA_PRODUCTIVITY_MODE = "com.android.documentsui.PRODUCTIVITY";
 
     /**
-     * Extra boolean flag for {@link ACTION_PICK_COPY_DESTINATION}, which
+     * Extra boolean flag for {@link #ACTION_PICK_COPY_DESTINATION}, which
      * specifies if the destination directory needs to create new directory or not.
      */
     public static final String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY";
diff --git a/src/com/android/documentsui/base/State.java b/src/com/android/documentsui/base/State.java
index 969b057..f0d47bb 100644
--- a/src/com/android/documentsui/base/State.java
+++ b/src/com/android/documentsui/base/State.java
@@ -147,6 +147,14 @@
         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;
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index d59e2a7..403be64 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -41,6 +41,7 @@
 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.State;
 import com.android.documentsui.clipping.ClipStore;
@@ -86,6 +87,7 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
+            ProviderAccess providers,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
@@ -95,7 +97,7 @@
             DocumentClipper clipper,
             ClipStore clipStore) {
 
-        super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
+        super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors);
 
         mActionModeAddons = actionModeAddons;
         mDialogs = dialogs;
@@ -297,11 +299,6 @@
             return;
         }
 
-        if (launchToDocument(intent)) {
-            if (DEBUG) Log.d(TAG, "Launched to root for viewing (likely a ZIP).");
-            return;
-        }
-
         if (launchToRoot(intent)) {
             if (DEBUG) Log.d(TAG, "Launched to root for browsing.");
             return;
@@ -336,19 +333,6 @@
         return true;
     }
 
-    // Zips in downloads are not opened inline, because of Downloads no-folders policy.
-    // So we're registered to handle VIEWs of zips.
-    private boolean launchToDocument(Intent intent) {
-        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
-            Uri uri = intent.getData();
-            assert(uri != null);
-            loadDocument(uri);
-            return true;
-        }
-
-        return false;
-    }
-
     private boolean launchToRoot(Intent intent) {
         if (DocumentsContract.ACTION_BROWSE.equals(intent.getAction())) {
             Uri uri = intent.getData();
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 30bced7..e934d48 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -112,6 +112,7 @@
                 mState,
                 mRoots,
                 mDocs,
+                DocumentsApplication.getProviderAccess(this),
                 mSelectionMgr,
                 mSearchManager,
                 ProviderExecutor::forAuthority,
diff --git a/src/com/android/documentsui/files/OpenUriForViewTask.java b/src/com/android/documentsui/files/OpenUriForViewTask.java
deleted file mode 100644
index 2a830c5..0000000
--- a/src/com/android/documentsui/files/OpenUriForViewTask.java
+++ /dev/null
@@ -1,92 +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.files;
-
-import android.app.Activity;
-import android.net.Uri;
-import android.util.Log;
-
-import com.android.documentsui.AbstractActionHandler.CommonAddons;
-import com.android.documentsui.DocumentsAccess;
-import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.PairedTask;
-import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.base.State;
-import com.android.documentsui.dirlist.AnimationView;
-import com.android.documentsui.roots.RootsAccess;
-
-import java.util.Collection;
-
-import javax.annotation.Nullable;
-
-/**
- * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible
- * to know which root to select. Also, the stack doesn't contain intermediate directories.
- * It's primarly used for opening ZIP archives from Downloads app.
- */
-public final class OpenUriForViewTask<T extends Activity & CommonAddons>
-        extends PairedTask<T, Void, Void> {
-
-    private static final String TAG = "OpenUriForViewTask";
-
-    private final T mActivity;
-    private final State mState;
-    private final RootsAccess mRoots;
-    private final DocumentsAccess mDocs;
-    private final Uri mUri;
-
-    public OpenUriForViewTask(
-            T activity, State state, RootsAccess roots, DocumentsAccess docs, Uri uri) {
-        super(activity);
-        mActivity = activity;
-        mState = state;
-        mRoots = roots;
-        mDocs = docs;
-        mUri = uri;
-    }
-
-    @Override
-    public Void run(Void... params) {
-
-        final String authority = mUri.getAuthority();
-        final Collection<RootInfo> roots = mRoots.getRootsForAuthorityBlocking(authority);
-
-        if (roots.isEmpty()) {
-            Log.e(TAG, "Failed to find root for the requested Uri: " + mUri);
-            return null;
-        }
-
-        assert(mState.stack.isEmpty());
-
-        // NOTE: There's no guarantee that this root will be the correct root for the doc.
-        final RootInfo root = roots.iterator().next();
-        mState.stack.root = root;
-        mState.stack.add(mDocs.getRootDocument(root));
-        @Nullable DocumentInfo doc = mDocs.getDocument(mUri);
-        if (doc == null) {
-            Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + mUri);
-        } else {
-            mState.stack.add(doc);
-        }
-
-        return null;
-    }
-
-    @Override
-    public void finish(Void result) {
-        mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
-    }
-}
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 03c8a90..906b26a 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -37,6 +37,7 @@
 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;
@@ -66,12 +67,13 @@
             State state,
             RootsAccess roots,
             DocumentsAccess docs,
+            ProviderAccess providers,
             SelectionManager selectionMgr,
             SearchViewManager searchMgr,
             Lookup<String, Executor> executors,
             ActivityConfig activityConfig) {
 
-        super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
+        super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors);
 
         mConfig = activityConfig;
         mScope = new ContentScope(this::onModelLoaded);
@@ -94,13 +96,36 @@
             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 {
-                if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
-                new LoadLastAccessedStackTask<>(mActivity, mState, mRoots).execute();
+                loadLastAccessedStack();
             }
         }
     }
 
+    private void onStackLoaded(Uri uri, @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);
+        } else {
+            Log.w(TAG, "Failed to launch into the given uri: " + uri);
+            loadLastAccessedStack();
+        }
+    }
+
+    private void loadLastAccessedStack() {
+        if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
+        new LoadLastAccessedStackTask<>(mActivity, mState, mRoots).execute();
+    }
+
     @Override
     public void showAppDetails(ResolveInfo info) {
         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 17e6bb1..645b7f7 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -101,6 +101,7 @@
                 mState,
                 mRoots,
                 mDocs,
+                DocumentsApplication.getProviderAccess(this),
                 mSelectionMgr,
                 mSearchManager,
                 ProviderExecutor::forAuthority,