Merge "Add capability to launch to a document in file manager." into arc-apps
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ac5e98b..8e3b2c6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -77,6 +77,11 @@
                 <data android:mimeType="vnd.android.document/root" />
             </intent-filter>
             <intent-filter>
+                <action android:name="android.provider.action.BROWSE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.document/directory" />
+            </intent-filter>
+            <intent-filter>
                 <action android:name="android.intent.action.VIEW_DOWNLOADS" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 1461b64..c0244e0 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -22,12 +22,14 @@
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Parcelable;
+import android.provider.DocumentsContract;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
 import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback;
+import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.base.BooleanConsumer;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -287,6 +289,37 @@
                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
     }
 
+    protected final boolean launchToDocument(Uri uri) {
+        // We don't support launching to a document in an archive.
+        if (!ArchivesProvider.AUTHORITY.equals(uri.getAuthority())) {
+            loadDocument(uri, this::onStackLoaded);
+            return true;
+        }
+
+        return false;
+    }
+
+    private void onStackLoaded(@Nullable DocumentStack stack) {
+        if (stack != null) {
+            if (!stack.peek().isDirectory()) {
+                // Requested document is not a directory. Pop it so that we can launch into its
+                // parent.
+                stack.pop();
+            }
+            mState.stack.reset(stack);
+            mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+
+            Metrics.logLaunchAtLocation(mActivity, mState, stack.getRoot().getUri());
+        } else {
+            Log.w(TAG, "Failed to launch into the given uri. Launch to default location.");
+            launchToDefaultLocation();
+
+            Metrics.logLaunchAtLocation(mActivity, mState, null);
+        }
+    }
+
+    protected abstract void launchToDefaultLocation();
+
     protected final void loadHomeDir() {
         loadRoot(Shared.getDefaultRootUri(mActivity));
     }
diff --git a/src/com/android/documentsui/Metrics.java b/src/com/android/documentsui/Metrics.java
index fd64166..8f5ad4c 100644
--- a/src/com/android/documentsui/Metrics.java
+++ b/src/com/android/documentsui/Metrics.java
@@ -73,6 +73,7 @@
     private static final String COUNT_STARTUP_MS = "docsui_startup_ms";
     private static final String COUNT_DRAWER_OPENED = "docsui_drawer_opened";
     private static final String COUNT_USER_ACTION = "docsui_menu_action";
+    private static final String COUNT_BROWSE_AT_LOCATION = "docsui_browse_at_location";
     private static final String COUNT_CREATE_AT_LOCATION = "docsui_create_at_location";
     private static final String COUNT_OPEN_AT_LOCATION = "docsui_open_at_location";
     private static final String COUNT_GET_CONTENT_AT_LOCATION = "docsui_get_content_at_location";
@@ -437,6 +438,9 @@
      */
     public static void logLaunchAtLocation(Context context, State state, @Nullable Uri rootUri) {
         switch (state.action) {
+            case State.ACTION_BROWSE:
+                logHistogram(context, COUNT_BROWSE_AT_LOCATION, sanitizeRoot(rootUri));
+                break;
             case State.ACTION_CREATE:
                 logHistogram(context, COUNT_CREATE_AT_LOCATION, sanitizeRoot(rootUri));
                 break;
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 0ec98e9..9c39a64 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -24,6 +24,8 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Document;
 import android.util.Log;
 
 import com.android.documentsui.AbstractActionHandler;
@@ -354,10 +356,20 @@
             return;
         }
 
+        if (launchToDocument(intent)) {
+            if (DEBUG) Log.d(TAG, "Launched to a document.");
+            return;
+        }
+
         if (DEBUG) Log.d(TAG, "Launching directly into Home directory.");
         loadHomeDir();
     }
 
+    @Override
+    protected void launchToDefaultLocation() {
+        loadHomeDir();
+    }
+
     // If a non-empty stack is present in our state, it was read (presumably)
     // from EXTRA_STACK intent extra. In this case, we'll skip other means of
     // loading or restoring the stack (like URI).
@@ -397,6 +409,17 @@
         return false;
     }
 
+    private boolean launchToDocument(Intent intent) {
+        if (DocumentsContract.ACTION_BROWSE.equals(intent.getAction())) {
+            Uri uri = intent.getData();
+            if (DocumentsContract.isDocumentUri(mActivity, uri)) {
+                return launchToDocument(intent.getData());
+            }
+        }
+
+        return false;
+    }
+
     @Override
     public void showChooserForDoc(DocumentInfo doc) {
         assert(!doc.isContainer());
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index f412f02..9611b57 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -107,6 +107,11 @@
         loadLastAccessedStack();
     }
 
+    @Override
+    protected void launchToDefaultLocation() {
+        loadLastAccessedStack();
+    }
+
     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).
@@ -122,35 +127,14 @@
     }
 
     private boolean launchToDocument(Intent intent) {
-        final Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
+        Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
         if (uri != null) {
-            loadDocument(uri, this::onStackLoaded);
-            return true;
+            return launchToDocument(uri);
         }
 
         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.stack.reset(stack);
-            mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
-
-            Metrics.logLaunchAtLocation(mActivity, mState, stack.getRoot().getUri());
-        } else {
-            Log.w(TAG, "Failed to launch into the given uri. Load last accessed stack.");
-            loadLastAccessedStack();
-
-            Metrics.logLaunchAtLocation(mActivity, mState, null);
-        }
-
-    }
-
     private void loadLastAccessedStack() {
         if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
         new LoadLastAccessedStackTask<>(mActivity, mState, mRoots).execute();
diff --git a/tests/common/com/android/documentsui/testing/DocumentStackAsserts.java b/tests/common/com/android/documentsui/testing/DocumentStackAsserts.java
new file mode 100644
index 0000000..099ca04
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/DocumentStackAsserts.java
@@ -0,0 +1,25 @@
+package com.android.documentsui.testing;
+
+import static junit.framework.Assert.assertEquals;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+
+import java.util.List;
+
+/**
+ * Helpers for assertions on {@link DocumentStack}.
+ */
+public class DocumentStackAsserts {
+
+    private DocumentStackAsserts() {}
+
+    public static void assertEqualsTo(DocumentStack stack, RootInfo root, List<DocumentInfo> docs) {
+        assertEquals(root, stack.getRoot());
+        assertEquals(docs.size(), stack.size());
+        for (int i = 0; i < docs.size(); ++i) {
+            assertEquals(docs.get(i), stack.get(i));
+        }
+    }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index b33cea0..0985bc0 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -79,6 +79,11 @@
     }
 
     @Override
+    protected void launchToDefaultLocation() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public <T extends ActionHandler> T reset(Model model) {
         return null;
     }
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index a2f4ea5..15d68d8 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -16,20 +16,27 @@
 
 package com.android.documentsui;
 
+import static junit.framework.Assert.assertTrue;
+
 import static org.junit.Assert.assertEquals;
 
 import android.content.Intent;
+import android.net.Uri;
 import android.os.Parcelable;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Path;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.DocumentStackTest;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.files.LauncherActivity;
+import com.android.documentsui.testing.DocumentStackAsserts;
 import com.android.documentsui.testing.Roots;
 import com.android.documentsui.testing.TestEnv;
 import com.android.documentsui.testing.TestRootsAccess;
@@ -39,6 +46,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
+import java.util.List;
 
 /**
  * A unit test *for* AbstractActionHandler, not an abstract test baseclass.
@@ -81,6 +89,11 @@
             }
 
             @Override
+            protected void launchToDefaultLocation() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
             public <T extends ActionHandler> T reset(Model model) {
                 return null;
             }
@@ -136,4 +149,52 @@
         assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.pop());
         assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.pop());
     }
+
+    @Test
+    public void testLaunchToDocuments() 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);
+
+        mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
+        assertTrue(mHandler.launchToDocument(TestEnv.FILE_GIF.derivedUri));
+
+        mEnv.beforeAsserts();
+
+        DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+                Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+        mActivity.refreshCurrentRootAndDirectory.assertCalled();
+    }
+
+    @Test
+    public void testLaunchToDocuments_convertsTreeUriToDocumentUri() 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);
+
+        final Uri treeBaseUri = DocumentsContract.buildTreeDocumentUri(
+                TestRootsAccess.HOME.authority, TestEnv.FOLDER_0.documentId);
+        final Uri treeDocUri = DocumentsContract.buildDocumentUriUsingTree(
+                treeBaseUri, TestEnv.FILE_GIF.documentId);
+        assertTrue(mHandler.launchToDocument(treeDocUri));
+
+        mEnv.beforeAsserts();
+
+        DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+                Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+        mEnv.docs.lastUri.assertLastArgument(TestEnv.FILE_GIF.derivedUri);
+        mActivity.refreshCurrentRootAndDirectory.assertCalled();
+    }
 }
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index b1daae0..9955f36 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -31,6 +31,9 @@
 import android.net.Uri;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Path;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -41,7 +44,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.DocumentStackAsserts;
 import com.android.documentsui.testing.Roots;
 import com.android.documentsui.testing.TestConfirmationCallback;
 import com.android.documentsui.testing.TestEnv;
@@ -52,6 +55,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Arrays;
+
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class ActionHandlerTest {
@@ -341,6 +346,31 @@
     }
 
     @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);
+
+        mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
+        Intent intent = mActivity.getIntent();
+        intent.setAction(DocumentsContract.ACTION_BROWSE);
+        intent.setData(TestEnv.FILE_GIF.derivedUri);
+        mHandler.initLocation(intent);
+
+        mEnv.beforeAsserts();
+
+        DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+                Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+        mActivity.refreshCurrentRootAndDirectory.assertCalled();
+    }
+
+    @Test
     public void testRefresh_nullUri() throws Exception {
         refreshAnswer = true;
         mHandler.refreshDocument(null, (boolean answer) -> {
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index 731a76b..6607a00 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -34,6 +34,7 @@
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
+import com.android.documentsui.testing.DocumentStackAsserts;
 import com.android.documentsui.testing.TestEnv;
 import com.android.documentsui.testing.TestRootsAccess;
 import com.android.documentsui.ui.TestDialogController;
@@ -114,37 +115,14 @@
 
         mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
         Intent intent = mActivity.getIntent();
-        intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
+        intent.setAction(Intent.ACTION_GET_CONTENT);
         intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, TestEnv.FILE_GIF.derivedUri);
         mHandler.initLocation(intent);
 
-        assertStackEquals(TestRootsAccess.HOME, Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
-        mActivity.refreshCurrentRootAndDirectory.assertCalled();
-    }
+        mEnv.beforeAsserts();
 
-    @Test
-    public void testInitLocation_LaunchToDocuments_convertsTreeUriToDocumentUri() 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_OPEN_DOCUMENT);
-        final Uri treeBaseUri = DocumentsContract.buildTreeDocumentUri(
-                TestRootsAccess.HOME.authority, TestEnv.FOLDER_0.documentId);
-        final Uri treeDocUri = DocumentsContract.buildDocumentUriUsingTree(
-                treeBaseUri, TestEnv.FILE_GIF.documentId);
-        intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, treeDocUri);
-        mHandler.initLocation(intent);
-
-        assertStackEquals(TestRootsAccess.HOME, Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
-        mEnv.docs.lastUri.assertLastArgument(TestEnv.FILE_GIF.derivedUri);
+        DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+                Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
         mActivity.refreshCurrentRootAndDirectory.assertCalled();
     }
 
@@ -157,17 +135,6 @@
         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();