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/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index a3cd89b..815b2f6 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -471,7 +471,7 @@
         }
 
         Intent intent = new QuickViewIntentBuilder(
-                mActivity.getPackageManager(),
+                mActivity,
                 mActivity.getResources(),
                 doc,
                 mModel,
@@ -575,6 +575,7 @@
             mState.stack.push(doc);
         } else {
             if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) {
+                // It is now possible when opening cross-profile folder.
                 Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected "
                         + mState.stack.getRoot());
             }
@@ -757,17 +758,11 @@
 
     @Override
     public void loadDocumentsForCurrentStack() {
-        DocumentStack stack = mState.stack;
-        if (!stack.isRecents() && stack.isEmpty()) {
-            // TODO: we may also need to reload cross-profile supported root with empty stack
-            DirectoryResult result = new DirectoryResult();
-
-            // TODO (b/35996595): Consider plumbing through the actual exception, though it might
-            // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()).
-            result.exception = new IllegalStateException("Failed to load root document.");
-            mInjector.getModel().update(result);
-            return;
-        }
+        // mState.stack may be empty when we cannot load the root document.
+        // However, we still want to restart loader because we may need to perform search in a
+        // cross-profile scenario.
+        // For RecentsLoader and GlobalSearchLoader, they do not require rootDoc so it is no-op.
+        // For DirectoryLoader, the loader needs to handle the case when stack.peek() returns null.
 
         mActivity.getSupportLoaderManager().restartLoader(LOADER_ID, null, mBindings);
     }
@@ -890,14 +885,24 @@
                 loader.setObserver(observer);
                 return loader;
             } else {
+                // There maybe no root docInfo
+                DocumentInfo rootDoc = mState.stack.peek();
+
+                String authority = rootDoc == null
+                        ? mState.stack.getRoot().authority
+                        : rootDoc.authority;
+                String documentId = rootDoc == null
+                        ? mState.stack.getRoot().documentId
+                        : rootDoc.documentId;
+
                 Uri contentsUri = mSearchMgr.isSearching()
                         ? DocumentsContract.buildSearchDocumentsUri(
-                            mState.stack.getRoot().authority,
-                            mState.stack.getRoot().rootId,
-                            mSearchMgr.getCurrentSearch())
+                        mState.stack.getRoot().authority,
+                        mState.stack.getRoot().rootId,
+                        mSearchMgr.getCurrentSearch())
                         : DocumentsContract.buildChildDocumentsUri(
-                                mState.stack.peek().authority,
-                                mState.stack.peek().documentId);
+                                authority,
+                                documentId);
 
                 final Bundle queryArgs = mSearchMgr.isSearching()
                         ? mSearchMgr.buildQueryArgs()
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 65aa7e7..3b09659 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -267,13 +267,38 @@
         });
 
         mNavigator.setProfileTabsListener(userId -> {
-            // Reload the roots with the selected user is changed.
+            // There are several possible cases that may trigger this callback.
+            // 1. A user click on tab layout.
+            // 2. A user click on tab layout, when filter is checked. (searching = true)
+            // 3. A user click on a open a dir of a different user in search (stack size > 1)
+            // 4. After tab layout is initialized.
+
+            if (!mState.stack.isInitialized()) {
+                return;
+            }
+
+            // Reload the roots when the selected user is changed.
+            // After reloading, we have visually same roots in the drawer. But they are
+            // different by holding different userId. Next time when user select a root, it can
+            // bring the user to correct root doc.
             final RootsFragment roots = RootsFragment.get(getSupportFragmentManager());
             if (roots != null) {
                 roots.onSelectedUserChanged();
             }
 
-            mInjector.actions.loadCrossProfileRoot(getCurrentRoot(), userId);
+            if (mState.stack.size() <= 1) {
+                // We do not load cross-profile root if the stack contains two documents. The
+                // stack may contain >1 docs when the user select a folder of the other user in
+                // search. In that case, we don't want to reload the root. The whole stack
+                // and the root will be updated in openFolderInSearchResult.
+
+                // When a user filters files by search chips on the root doc, we will be in
+                // searching mode and with stack size 1 (0 if rootDoc cannot be loaded).
+                // The activity will clear search on root picked. If we don't clear the search,
+                // user may see the search result screen show up briefly and then get cleared.
+                mSearchManager.cancelSearch();
+                mInjector.actions.loadCrossProfileRoot(getCurrentRoot(), userId);
+            }
         });
 
         mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index 6de41a9..9a9cba4 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -70,6 +70,7 @@
     private final Bundle mQueryArgs;
     private final boolean mPhotoPicking;
 
+    @Nullable
     private DocumentInfo mDoc;
     private CancellationSignal mSignal;
     private DirectoryResult mResult;
@@ -113,7 +114,6 @@
             mSignal = new CancellationSignal();
         }
 
-        final ContentResolver resolver = mDoc.userId.getContentResolver(getContext());
         final String authority = mUri.getAuthority();
 
         final DirectoryResult result = new DirectoryResult();
@@ -138,24 +138,31 @@
                 }
             }
             if (userIds.isEmpty()) {
-                userIds.add(mDoc.userId);
+                userIds.add(mRoot.userId);
             }
 
             if (userIds.size() == 1) {
-                if (!mState.canInteractWith(mDoc.userId)) {
+                if (!mState.canInteractWith(mRoot.userId)) {
                     result.exception = new CrossProfileNoPermissionException();
                     return result;
-                } else if (mDoc.userId.isQuietModeEnabled(getContext())) {
+                } else if (mRoot.userId.isQuietModeEnabled(getContext())) {
                     result.exception = new CrossProfileQuietModeException();
                     return result;
+                } else if (mDoc == null) {
+                    // TODO (b/35996595): Consider plumbing through the actual exception, though it
+                    // might not be very useful (always pointing to
+                    // DatabaseUtils#readExceptionFromParcel()).
+                    result.exception = new IllegalStateException("Failed to load root document.");
+                    return result;
                 }
             }
 
-            client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
-            if (mDoc.isInArchive()) {
+            if (mDoc != null && mDoc.isInArchive()) {
+                final ContentResolver resolver = mRoot.userId.getContentResolver(getContext());
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
                 ArchivesProvider.acquireArchive(client, mUri);
+                result.client = client;
             }
-            result.client = client;
 
             if (mFeatures.isContentPagingEnabled()) {
                 // TODO: At some point we don't want forced flags to override real paging...
diff --git a/src/com/android/documentsui/IconUtils.java b/src/com/android/documentsui/IconUtils.java
index 5a829ff..1763fd2 100644
--- a/src/com/android/documentsui/IconUtils.java
+++ b/src/com/android/documentsui/IconUtils.java
@@ -38,7 +38,7 @@
             } else {
                 packageIcon = userId.getDrawable(context, icon);
             }
-            if (maybeShowBadge) {
+            if (packageIcon != null && maybeShowBadge) {
                 return userId.getUserBadgedIcon(context, packageIcon);
             } else {
                 return packageIcon;
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index ca436a3..29595a6 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -18,7 +18,6 @@
 
 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
 
-import android.app.Activity;
 import android.content.res.Resources;
 import android.graphics.Outline;
 import android.graphics.drawable.Drawable;
@@ -60,7 +59,7 @@
     private final boolean mShowSearchBar;
 
     public NavigationViewManager(
-            Activity activity,
+            BaseActivity activity,
             DrawerController drawer,
             State state,
             NavigationViewManager.Environment env,
@@ -74,7 +73,7 @@
         mEnv = env;
         mBreadcrumb = breadcrumb;
         mBreadcrumb.setup(env, state, this::onNavigationItemSelected);
-        mProfileTabs = new ProfileTabs(tabLayout, mState, userIdManager, mEnv);
+        mProfileTabs = new ProfileTabs(tabLayout, mState, userIdManager, mEnv, activity);
 
         mToolbar.setNavigationOnClickListener(
                 new View.OnClickListener() {
diff --git a/src/com/android/documentsui/ProfileTabs.java b/src/com/android/documentsui/ProfileTabs.java
index 6c2094f..44e66e2 100644
--- a/src/com/android/documentsui/ProfileTabs.java
+++ b/src/com/android/documentsui/ProfileTabs.java
@@ -23,10 +23,12 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.base.UserId;
 
 import com.google.android.material.tabs.TabLayout;
+import com.google.common.base.Objects;
 
 import java.util.Collections;
 import java.util.List;
@@ -40,23 +42,28 @@
     private final TabLayout mTabs;
     private final State mState;
     private final NavigationViewManager.Environment mEnv;
+    private final AbstractActionHandler.CommonAddons mCommonAddons;
     private final UserIdManager mUserIdManager;
     private List<UserId> mUserIds;
     @Nullable
     private Listener mListener;
+    private TabLayout.OnTabSelectedListener mOnTabSelectedListener;
 
     public ProfileTabs(TabLayout tabLayout, State state, UserIdManager userIdManager,
-            NavigationViewManager.Environment env) {
+            NavigationViewManager.Environment env,
+            AbstractActionHandler.CommonAddons commonAddons) {
         mTabs = checkNotNull(tabLayout);
         mState = checkNotNull(state);
         mEnv = checkNotNull(env);
+        mCommonAddons = checkNotNull(commonAddons);
         mUserIdManager = checkNotNull(userIdManager);
         mTabs.removeAllTabs();
         mUserIds = Collections.singletonList(UserId.CURRENT_USER);
-        mTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+        mOnTabSelectedListener = new TabLayout.OnTabSelectedListener() {
             @Override
             public void onTabSelected(TabLayout.Tab tab) {
                 if (mListener != null) {
+                    // find a way to identify user iteraction
                     mListener.onUserSelected((UserId) tab.getTag());
                 }
             }
@@ -68,7 +75,8 @@
             @Override
             public void onTabReselected(TabLayout.Tab tab) {
             }
-        });
+        };
+        mTabs.addOnTabSelectedListener(mOnTabSelectedListener);
     }
 
     /**
@@ -76,6 +84,15 @@
      */
     public void updateView() {
         updateTabsIfNeeded();
+        RootInfo currentRoot = mCommonAddons.getCurrentRoot();
+        if (mTabs.getSelectedTabPosition() == -1
+                || !Objects.equal(currentRoot.userId, getSelectedUser())) {
+            // Update the layout according to the current root if necessary.
+            // Make sure we do not invoke callback. Otherwise, it is likely to cause infinite loop.
+            mTabs.removeOnTabSelectedListener(mOnTabSelectedListener);
+            mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(currentRoot.userId)));
+            mTabs.addOnTabSelectedListener(mOnTabSelectedListener);
+        }
         mTabs.setVisibility(shouldShow() ? View.VISIBLE : View.GONE);
     }
 
@@ -98,7 +115,6 @@
                 mTabs.addTab(createTab(R.string.work_tab,
                         mUserIdManager.getManagedUser()), /* setSelected= */false);
             }
-            mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(UserId.CURRENT_USER)));
         }
     }
 
diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java
index 14740e1..be9af0f 100644
--- a/src/com/android/documentsui/base/DocumentStack.java
+++ b/src/com/android/documentsui/base/DocumentStack.java
@@ -163,6 +163,7 @@
         // Add this for keep stack size is 1 on recent root.
         if (root.isRecents()) {
             DocumentInfo rootRecent = new DocumentInfo();
+            rootRecent.userId = root.userId;
             rootRecent.deriveFields();
             push(rootRecent);
         }
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 9d574d4..65b8960 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -23,13 +23,16 @@
 import static com.android.documentsui.base.State.MODE_LIST;
 
 import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcelable;
+import android.os.UserHandle;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.util.Log;
@@ -52,6 +55,7 @@
 import androidx.fragment.app.FragmentActivity;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 import androidx.recyclerview.selection.MutableSelection;
 import androidx.recyclerview.selection.Selection;
@@ -82,6 +86,7 @@
 import com.android.documentsui.ProfileTabsController;
 import com.android.documentsui.R;
 import com.android.documentsui.ThumbnailCache;
+import com.android.documentsui.TimeoutTask;
 import com.android.documentsui.base.DocumentFilters;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -91,6 +96,7 @@
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 import com.android.documentsui.base.State.ViewMode;
+import com.android.documentsui.base.UserId;
 import com.android.documentsui.clipping.ClipStore;
 import com.android.documentsui.clipping.DocumentClipper;
 import com.android.documentsui.clipping.UrisSupplier;
@@ -103,6 +109,8 @@
 import com.android.documentsui.sorting.SortDimension;
 import com.android.documentsui.sorting.SortModel;
 
+import com.google.common.base.Objects;
+
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -211,6 +219,26 @@
         return true;
     };
 
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (isManagedProfileAction(action)) {
+                UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+                if (Objects.equal(mActivity.getSelectedUser(), UserId.of(userHandle))) {
+                    // We only need to refresh the layout when the selected user is equal to the
+                    // received profile user.
+                    onRefresh();
+                }
+            }
+        }
+    };
+
+    private static boolean isManagedProfileAction(String action) {
+        return Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)
+                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action);
+    }
+
     @Override
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -272,6 +300,9 @@
     @Override
     public void onDestroyView() {
         mInjector.actions.unregisterDisplayStateChangedListener(mOnDisplayStateChanged);
+        if (mState.supportsCrossProfile()) {
+            LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mReceiver);
+        }
 
         // Cancel any outstanding thumbnail requests
         final int count = mRecView.getChildCount();
@@ -427,6 +458,16 @@
 
         // Kick off loader at least once
         mActions.loadDocumentsForCurrentStack();
+
+        if (mState.supportsCrossProfile()) {
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
+            filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
+            // DocumentsApplication will resend the broadcast locally after roots are updated.
+            // Register to a local broadcast manager to avoid this fragment from updating before
+            // roots are updated.
+            LocalBroadcastManager.getInstance(mActivity).registerReceiver(mReceiver, filter);
+        }
     }
 
     @Override
@@ -1165,6 +1206,12 @@
         }
 
         final DocumentInfo doc = mActivity.getCurrentDirectory();
+        if (doc == null) {
+            // If there is no root doc, try to reload the root doc from root info.
+            Log.w(TAG, "No root document. Try to get root document.");
+            getRootDocumentAndMaybeRefreshDocument();
+            return;
+        }
         mActions.refreshDocument(doc, (boolean refreshSupported) -> {
             if (refreshSupported) {
                 mRefreshLayout.setRefreshing(false);
@@ -1175,6 +1222,26 @@
         });
     }
 
+    private void getRootDocumentAndMaybeRefreshDocument() {
+        // If we can reload the root doc successfully, we will push it to the stack and load the
+        // stack.
+        final RootInfo emptyDocRoot = mActivity.getCurrentRoot();
+        mInjector.actions.getRootDocument(
+                emptyDocRoot,
+                TimeoutTask.DEFAULT_TIMEOUT,
+                rootDoc -> {
+                    mRefreshLayout.setRefreshing(false);
+                    if (rootDoc != null && mActivity.getCurrentDirectory() == null) {
+                        // Make sure the stack does not change during task was running.
+                        Log.d(TAG, "Root doc is retrieved. Pushing to the stack");
+                        mState.stack.push(rootDoc);
+                        mActivity.updateNavigator();
+                        mActions.loadDocumentsForCurrentStack();
+                    }
+                }
+        );
+    }
+
     private final class ModelUpdateListener implements EventListener<Model.Update> {
 
         @Override
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index f399a28..e506535 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -22,6 +22,7 @@
 
 import android.content.ClipData;
 import android.content.ClipDescription;
+import android.content.Context;
 import android.content.Intent;
 import android.content.QuickViewConstants;
 import android.content.pm.PackageManager;
@@ -80,18 +81,18 @@
     private final boolean mFromPicker;
 
     public QuickViewIntentBuilder(
-            PackageManager packageMgr,
+            Context context,
             Resources resources,
             DocumentInfo doc,
             Model model,
             boolean fromPicker) {
 
-        assert(packageMgr != null);
+        assert(context != null);
         assert(resources != null);
         assert(doc != null);
         assert(model != null);
 
-        mPackageMgr = packageMgr;
+        mPackageMgr = doc.userId.getPackageManager(context);
         mResources = resources;
         mDocument = doc;
         mModel = model;
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 8f55dde..e5c7eb8 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -62,6 +62,7 @@
     public static DocumentInfo FILE_READ_ONLY;
 
     public static class OtherUser {
+        public static DocumentInfo FOLDER_0;
         public static DocumentInfo FILE_PNG;
     }
 
@@ -72,6 +73,7 @@
     public final TestFocusHandler focusHandler = new TestFocusHandler();
     public final TestDialogController dialogs = new TestDialogController();
     public final TestModel model;
+    public final TestModel modelOtherUser;
     public final TestModel archiveModel;
     public final DocsSelectionHelper selectionMgr;
     public final TestSearchViewManager searchViewManager;
@@ -83,13 +85,14 @@
     public final MockContentResolver contentResolver;
     public final Map<String, TestDocumentsProvider> mockProviders;
 
-    private TestEnv(Context context, Features features, String authority, UserId userId) {
+    private TestEnv(Context context, Features features, String authority) {
         this.features = features;
-        this.userId = userId;
+        this.userId = TestProvidersAccess.USER_ID;
         userHandle = UserHandle.of(userId.getIdentifier());
         state.sortModel = SortModel.createModel();
         mExecutor = new TestScheduledExecutorService();
         model = new TestModel(userId, authority, features);
+        modelOtherUser = new TestModel(TestProvidersAccess.OtherUser.USER_ID, authority, features);
         archiveModel = new TestModel(userId, ArchivesProvider.AUTHORITY, features);
         selectionMgr = SelectionHelpers.createTestInstance();
         searchViewManager = new TestSearchViewManager();
@@ -142,7 +145,7 @@
     }
 
     private static TestEnv create(Context context, Features features, String authority) {
-        TestEnv env = new TestEnv(context, features, authority, TestProvidersAccess.USER_ID);
+        TestEnv env = new TestEnv(context, features, authority);
         env.reset();
         return env;
     }
@@ -178,8 +181,8 @@
                         | Document.FLAG_SUPPORTS_DELETE
                         | Document.FLAG_SUPPORTS_RENAME);
 
-        OtherUser.FILE_PNG = model.createFile("work.png");
-        OtherUser.FILE_PNG.userId = TestProvidersAccess.OtherUser.USER_ID;
+        OtherUser.FOLDER_0 = modelOtherUser.createFolder("folder 0");
+        OtherUser.FILE_PNG = modelOtherUser.createFile("work.png");
 
         archiveModel.update();
         model.update();
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();