Enable DirectoryFragment.onRefresh works when there is no root doc.

This enables us to reload the directory correctly. On next CL,
we will refresh directory when we listened to some event. (e.g. work
profile is unlocked)

Also now we can perform search in directory loader on a disabled user.

* In DirectoryFragment, refresh stack without root doc
DirectoryFragment will now try to reload rootDoc and push it to stack
onRefresh. This is useful when the root doc was temporarily unable to
load (e.g. work profile was turned off)

* Search across profile on an empty stack
Directory takes care of empty stack now. We will restart loader in
loadDocumentsForCurrentStack. If there is only one queriable user,
the same exception message will be updated.

* ProfileTabs
The tab layout will be updated if the current root does not match.
This could happen when opening a search folder from the other user.

* QuickViewIntentBuilder
Now use a correct packageManager to test intent

Bug: 148270816
Bug: 150600030
Bug: 150799134
Test: atest DocumentsUIGoogleTests
Test: manual

Change-Id: Ib6684e4dd2a257f92ee3784297b5568cbd3d21b2
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index c5c3ac3..3eb8ab2 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -16,10 +16,13 @@
 
 package com.android.documentsui;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
 
 import android.content.Intent;
 import android.net.Uri;
@@ -32,8 +35,10 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.EventListener;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
+import com.android.documentsui.base.State;
 import com.android.documentsui.files.LauncherActivity;
 import com.android.documentsui.sorting.SortDimension;
 import com.android.documentsui.sorting.SortModel;
@@ -274,6 +279,145 @@
     }
 
     @Test
+    public void testCrossProfileDocuments_success() throws Exception {
+        mEnv.state.action = State.ACTION_GET_CONTENT;
+        mEnv.state.canShareAcrossProfile = true;
+        mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
+        mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
+
+        mEnv.state.sortModel.sortByUser(
+                SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
+
+        // Currently mock provider does not have cross profile concept, this will always return
+        // the supplied docs without checking for the user. But this should not be a problem for
+        // this test case.
+        mEnv.mockProviders.get(TestProvidersAccess.OtherUser.HOME.authority)
+                .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG);
+
+        mHandler.loadDocumentsForCurrentStack();
+        CountDownLatch latch = new CountDownLatch(1);
+        mEnv.model.addUpdateListener(event -> latch.countDown());
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+
+        latch.await(1, TimeUnit.SECONDS);
+        assertEquals(1, mEnv.model.getItemCount());
+        String[] modelIds = mEnv.model.getModelIds();
+        assertEquals(TestEnv.OtherUser.FILE_PNG, mEnv.model.getDocument(modelIds[0]));
+    }
+
+    @Test
+    public void testLoadCrossProfileDoc_failsWithQuietModeException() throws Exception {
+        mEnv.state.action = State.ACTION_GET_CONTENT;
+        mEnv.state.canShareAcrossProfile = true;
+        mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
+        mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
+        // Turn off the other user.
+        when(mActivity.userManager.isQuietModeEnabled(TestProvidersAccess.OtherUser.USER_HANDLE))
+                .thenReturn(true);
+
+        TestEventHandler<Model.Update> listener = new TestEventHandler<>();
+        mEnv.model.addUpdateListener(listener::accept);
+
+        mHandler.loadDocumentsForCurrentStack();
+        CountDownLatch latch = new CountDownLatch(1);
+        mEnv.model.addUpdateListener(event -> latch.countDown());
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+
+        latch.await(1, TimeUnit.SECONDS);
+        assertThat(listener.getLastValue().getException())
+                .isInstanceOf(CrossProfileQuietModeException.class);
+    }
+
+    @Test
+    public void testLoadCrossProfileDoc_failsWithNoPermissionException() throws Exception {
+        mEnv.state.action = State.ACTION_GET_CONTENT;
+        mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
+        mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
+        // Disallow sharing across profile
+        mEnv.state.canShareAcrossProfile = false;
+
+        TestEventHandler<Model.Update> listener = new TestEventHandler<>();
+        mEnv.model.addUpdateListener(listener::accept);
+
+        mHandler.loadDocumentsForCurrentStack();
+        CountDownLatch latch = new CountDownLatch(1);
+        mEnv.model.addUpdateListener(event -> latch.countDown());
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+
+        latch.await(1, TimeUnit.SECONDS);
+        assertThat(listener.getLastValue().getException())
+                .isInstanceOf(CrossProfileNoPermissionException.class);
+    }
+
+    @Test
+    public void testLoadCrossProfileDoc_bothError_showNoPermissionException() throws Exception {
+        mEnv.state.action = State.ACTION_GET_CONTENT;
+        mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
+        mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
+        // Disallow sharing
+        mEnv.state.canShareAcrossProfile = false;
+        // Turn off the other user.
+        when(mActivity.userManager.isQuietModeEnabled(TestProvidersAccess.OtherUser.USER_HANDLE))
+                .thenReturn(true);
+
+        TestEventHandler<Model.Update> listener = new TestEventHandler<>();
+        mEnv.model.addUpdateListener(listener::accept);
+
+        mHandler.loadDocumentsForCurrentStack();
+        CountDownLatch latch = new CountDownLatch(1);
+        mEnv.model.addUpdateListener(event -> latch.countDown());
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+
+        latch.await(1, TimeUnit.SECONDS);
+        assertThat(listener.getLastValue().getException())
+                .isInstanceOf(CrossProfileNoPermissionException.class);
+    }
+
+    @Test
+    public void testCrossProfileDocuments_reloadSuccessAfterCrossProfileError() throws Exception {
+        mEnv.state.action = State.ACTION_GET_CONTENT;
+        mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME);
+        mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0);
+
+        mEnv.state.sortModel.sortByUser(
+                SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
+
+        // Currently mock provider does not have cross profile concept, this will always return
+        // the supplied docs without checking for the user. But this should not be a problem for
+        // this test case.
+        mEnv.mockProviders.get(TestProvidersAccess.OtherUser.HOME.authority)
+                .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG);
+
+        // Disallow sharing across profile
+        mEnv.state.canShareAcrossProfile = false;
+
+        TestEventHandler<Model.Update> listener = new TestEventHandler<>();
+        mEnv.model.addUpdateListener(listener::accept);
+
+        mHandler.loadDocumentsForCurrentStack();
+        CountDownLatch latch1 = new CountDownLatch(1);
+        EventListener<Model.Update> updateEventListener1 = update -> latch1.countDown();
+        mEnv.model.addUpdateListener(updateEventListener1);
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+        latch1.await(1, TimeUnit.SECONDS);
+        assertThat(listener.getLastValue().getException())
+                .isInstanceOf(CrossProfileNoPermissionException.class);
+
+        // Allow sharing across profile.
+        mEnv.state.canShareAcrossProfile = true;
+
+        CountDownLatch latch2 = new CountDownLatch(1);
+        mEnv.model.addUpdateListener(update -> latch2.countDown());
+        mHandler.loadDocumentsForCurrentStack();
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
+
+        latch2.await(1, TimeUnit.SECONDS);
+        assertEquals(1, mEnv.model.getItemCount());
+        String[] modelIds = mEnv.model.getModelIds();
+        assertEquals(TestEnv.OtherUser.FILE_PNG, mEnv.model.getDocument(modelIds[0]));
+    }
+
+    @Test
     public void testLoadChildrenDocuments_failsWithNonRecentsAndEmptyStack() throws Exception {
         mEnv.state.stack.changeRoot(TestProvidersAccess.HOME);
 
@@ -284,7 +428,11 @@
         mEnv.model.addUpdateListener(listener::accept);
 
         mHandler.loadDocumentsForCurrentStack();
+        CountDownLatch latch = new CountDownLatch(1);
+        mEnv.model.addUpdateListener(event -> latch.countDown());
+        mActivity.supportLoaderManager.runAsyncTaskLoader(AbstractActionHandler.LOADER_ID);
 
+        latch.await(1, TimeUnit.SECONDS);
         assertTrue(listener.getLastValue().hasException());
     }
 
diff --git a/tests/unit/com/android/documentsui/ProfileTabsTest.java b/tests/unit/com/android/documentsui/ProfileTabsTest.java
index 5330e9c..156c7ca 100644
--- a/tests/unit/com/android/documentsui/ProfileTabsTest.java
+++ b/tests/unit/com/android/documentsui/ProfileTabsTest.java
@@ -19,12 +19,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.view.LayoutInflater;
 import android.view.View;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.base.UserId;
@@ -38,6 +40,7 @@
 import org.junit.Test;
 
 import java.util.Collections;
+import java.util.List;
 
 public class ProfileTabsTest {
 
@@ -51,6 +54,8 @@
     private TestEnvironment mTestEnv;
     private State mState;
     private TestUserIdManager mTestUserIdManager;
+    private TestCommonAddons mTestCommonAddons;
+    private boolean mIsListenerInvoked;
 
     @Before
     public void setUp() {
@@ -70,6 +75,8 @@
         mTestEnv.isSearching = false;
 
         mTestUserIdManager = new TestUserIdManager();
+        mTestCommonAddons = new TestCommonAddons();
+        mTestCommonAddons.mCurrentRoot = TestProvidersAccess.DOWNLOADS;
     }
 
     @Test
@@ -167,6 +174,23 @@
     }
 
     @Test
+    public void testUpdateView_afterCurrentRootChanged_shouldChangeSelectedUser() {
+        initializeWithUsers(systemUser, managedUser);
+        mProfileTabs.updateView();
+
+        assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser);
+
+        RootInfo newRoot = RootInfo.copyRootInfo(mTestCommonAddons.mCurrentRoot);
+        newRoot.userId = managedUser;
+        mTestCommonAddons.mCurrentRoot = newRoot;
+        mProfileTabs.updateView();
+
+        assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser);
+        // updating view should not trigger listener callback.
+        assertThat(mIsListenerInvoked).isFalse();
+    }
+
+    @Test
     public void testgetSelectedUser_twoUsers() {
         initializeWithUsers(systemUser, managedUser);
 
@@ -175,6 +199,19 @@
 
         mTabLayout.selectTab(mTabLayout.getTabAt(1));
         assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser);
+        assertThat(mIsListenerInvoked).isTrue();
+    }
+
+    @Test
+    public void testReselectedUser_doesNotInvokeListener() {
+        initializeWithUsers(systemUser, managedUser);
+
+        assertThat(mTabLayout.getSelectedTabPosition()).isAtLeast(0);
+        assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser);
+
+        mTabLayout.selectTab(mTabLayout.getTabAt(0));
+        assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser);
+        assertThat(mIsListenerInvoked).isFalse();
     }
 
     @Test
@@ -194,8 +231,10 @@
             }
         }
 
-        mProfileTabs = new ProfileTabs(mTabLayout, mState, mTestUserIdManager, mTestEnv);
+        mProfileTabs = new ProfileTabs(mTabLayout, mState, mTestUserIdManager, mTestEnv,
+                mTestCommonAddons);
         mProfileTabs.updateView();
+        mProfileTabs.setListener(userId -> mIsListenerInvoked = true);
     }
 
     /**
@@ -231,5 +270,70 @@
         }
 
     }
+
+    private static class TestCommonAddons implements AbstractActionHandler.CommonAddons {
+
+        private RootInfo mCurrentRoot;
+
+        @Override
+        public void restoreRootAndDirectory() {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void refreshCurrentRootAndDirectory(int anim) {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void onRootPicked(RootInfo root) {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void onDocumentsPicked(List<DocumentInfo> docs) {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void onDocumentPicked(DocumentInfo doc) {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public RootInfo getCurrentRoot() {
+            return mCurrentRoot;
+        }
+
+        @Override
+        public DocumentInfo getCurrentDirectory() {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public UserId getSelectedUser() {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public boolean isInRecents() {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void setRootsDrawerOpen(boolean open) {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void updateNavigator() {
+            throw new UnsupportedOperationException("not implemented");
+        }
+
+        @Override
+        public void notifyDirectoryNavigated(Uri docUri) {
+            throw new UnsupportedOperationException("not implemented");
+        }
+    }
 }
 
diff --git a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java
index 4679ee8..7f3c4c4 100644
--- a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java
+++ b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java
@@ -3,10 +3,15 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
 import android.content.Intent;
 import android.content.QuickViewConstants;
 import android.content.pm.PackageManager;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -14,8 +19,6 @@
 import com.android.documentsui.testing.TestPackageManager;
 import com.android.documentsui.testing.TestResources;
 
-import androidx.test.InstrumentationRegistry;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,6 +32,7 @@
 public class QuickViewIntentBuilderTest {
 
     private static String mTargetPackageName;
+    private Context mContext = mock(Context.class);
     private PackageManager mPm;
     private TestEnv mEnv;
     private TestResources mRes;
@@ -38,6 +42,7 @@
         mTargetPackageName =
                 InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName();
         mPm = TestPackageManager.create();
+        when(mContext.getPackageManager()).thenReturn(mPm);
         mEnv = TestEnv.create();
         mRes = TestResources.create();
 
@@ -48,7 +53,7 @@
     public void testSetsNoFeatures_InArchiveDocument() {
         QuickViewIntentBuilder builder =
                 new QuickViewIntentBuilder(
-                        mPm, mRes, TestEnv.FILE_IN_ARCHIVE, mEnv.archiveModel, false);
+                        mContext, mRes, TestEnv.FILE_IN_ARCHIVE, mEnv.archiveModel, false);
 
         Intent intent = builder.build();
 
@@ -59,7 +64,7 @@
     @Test
     public void testSetsFullFeatures_RegularDocument() {
         QuickViewIntentBuilder builder =
-                new QuickViewIntentBuilder(mPm, mRes, TestEnv.FILE_JPG, mEnv.model, false);
+                new QuickViewIntentBuilder(mContext, mRes, TestEnv.FILE_JPG, mEnv.model, false);
 
         Intent intent = builder.build();
 
@@ -79,7 +84,7 @@
     public void testPickerFeatures_RegularDocument() {
 
         QuickViewIntentBuilder builder =
-                new QuickViewIntentBuilder(mPm, mRes, TestEnv.FILE_JPG, mEnv.model, true);
+                new QuickViewIntentBuilder(mContext, mRes, TestEnv.FILE_JPG, mEnv.model, true);
 
         Intent intent = builder.build();