Merge "Enable navigation between profile tabs, roots"
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index b18fabb..59878ec 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -711,12 +711,44 @@
     }
 
     @Override
-    public final void loadRoot(Uri uri) {
-        new LoadRootTask<>(mActivity, mProviders, uri, this::onRootLoaded)
+    public final void loadRoot(Uri uri, UserId userId) {
+        new LoadRootTask<>(mActivity, mProviders, uri, userId, this::onRootLoaded)
                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
     }
 
     @Override
+    public final void loadCrossProfileRoot(RootInfo info, UserId selectedUser) {
+        if (info.isRecents()) {
+            openRoot(mProviders.getRecentsRoot(selectedUser));
+            return;
+        }
+        new LoadRootTask<>(mActivity, mProviders, info.getUri(), selectedUser,
+                new LoadCrossProfileRootCallback(info, selectedUser))
+                .executeOnExecutor(mExecutors.lookup(info.getUri().getAuthority()));
+    }
+
+    private class LoadCrossProfileRootCallback implements LoadRootTask.LoadRootCallback {
+        private final RootInfo mOriginalRoot;
+        private final UserId mSelectedUserId;
+
+        LoadCrossProfileRootCallback(RootInfo rootInfo, UserId selectedUser) {
+            mOriginalRoot = rootInfo;
+            mSelectedUserId = selectedUser;
+        }
+
+        @Override
+        public void onRootLoaded(@Nullable RootInfo root) {
+            if (root == null) {
+                // There is no such root in the other profile. Maybe the provider is missing on
+                // the other profile. Create a dummy root and open it to show error message.
+                root = RootInfo.copyRootInfo(mOriginalRoot);
+                root.userId = mSelectedUserId;
+            }
+            openRoot(root);
+        }
+    }
+
+    @Override
     public final void loadFirstRoot(Uri uri) {
         new LoadFirstRootTask<>(mActivity, mProviders, uri, this::onRootLoaded)
                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
@@ -799,7 +831,7 @@
     }
 
     protected final void loadHomeDir() {
-        loadRoot(Shared.getDefaultRootUri(mActivity));
+        loadRoot(Shared.getDefaultRootUri(mActivity), UserId.DEFAULT_USER);
     }
 
     protected final void loadRecent() {
@@ -918,6 +950,7 @@
         void onDocumentPicked(DocumentInfo doc);
         RootInfo getCurrentRoot();
         DocumentInfo getCurrentDirectory();
+        UserId getSelectedUser();
         /**
          * Check whether current directory is root of recent.
          */
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index f3bd1fe..189608a 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -95,7 +95,9 @@
 
     void openRoot(ResolveInfo app);
 
-    void loadRoot(Uri uri);
+    void loadRoot(Uri uri, UserId userId);
+
+    void loadCrossProfileRoot(RootInfo info, UserId selectedUser);
 
     void loadFirstRoot(Uri uri);
 
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 079f332..d0568c3 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -267,6 +267,16 @@
             mNavigator.update();
         });
 
+        mNavigator.setProfileTabsListener(userId -> {
+            // Reload the roots with the selected user is changed.
+            final RootsFragment roots = RootsFragment.get(getSupportFragmentManager());
+            if (roots != null) {
+                roots.onSelectedUserChanged();
+            }
+
+            mInjector.actions.loadCrossProfileRoot(getCurrentRoot(), userId);
+        });
+
         mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
 
         mPreferencesMonitor = new PreferencesMonitor(
@@ -751,12 +761,16 @@
     }
 
     @Override
+    public UserId getSelectedUser() {
+        return mNavigator.getSelectedUser();
+    }
+
     public RootInfo getCurrentRoot() {
         RootInfo root = mState.stack.getRoot();
         if (root != null) {
             return root;
         } else {
-            return mProviders.getRecentsRoot(UserId.DEFAULT_USER);
+            return mProviders.getRecentsRoot(getSelectedUser());
         }
     }
 
diff --git a/src/com/android/documentsui/DrawerController.java b/src/com/android/documentsui/DrawerController.java
index 7ed8d51..fb2822a 100644
--- a/src/com/android/documentsui/DrawerController.java
+++ b/src/com/android/documentsui/DrawerController.java
@@ -30,7 +30,6 @@
 
 import com.android.documentsui.base.Display;
 import com.android.documentsui.base.Providers;
-import com.android.documentsui.base.UserId;
 
 /**
  * A facade over the various pieces comprising "roots fragment in a Drawer".
@@ -49,7 +48,7 @@
     /**
      * Returns a controller suitable for {@code Layout}.
      */
-    public static DrawerController create(Activity activity, ActivityConfig activityConfig) {
+    public static DrawerController create(BaseActivity activity, ActivityConfig activityConfig) {
 
         DrawerLayout layout = (DrawerLayout) activity.findViewById(R.id.drawer_layout);
 
@@ -68,7 +67,8 @@
                 R.string.drawer_open,
                 R.string.drawer_close);
 
-        return new RuntimeDrawerController(layout, drawer, toggle, toolbar, activityConfig);
+        return new RuntimeDrawerController(layout, drawer, toggle, toolbar, activityConfig,
+                activity);
     }
 
     /**
@@ -101,19 +101,22 @@
         private DrawerLayout mLayout;
         private View mDrawer;
         private Toolbar mToolbar;
+        private AbstractActionHandler.CommonAddons mCommonAddons;
 
         public RuntimeDrawerController(
                 DrawerLayout layout,
                 View drawer,
                 ActionBarDrawerToggle toggle,
                 Toolbar drawerToolbar,
-                ActivityConfig activityConfig) {
+                ActivityConfig activityConfig,
+                AbstractActionHandler.CommonAddons commonAddons) {
             mToolbar = drawerToolbar;
             assert(layout != null);
 
             mLayout = layout;
             mDrawer = drawer;
             mToggle = toggle;
+            mCommonAddons = commonAddons;
 
             mLayout.setDrawerListener(this);
 
@@ -205,7 +208,7 @@
             mToggle.onDrawerOpened(drawerView);
             // Update the information for Storage's root
             DocumentsApplication.getProvidersCache(drawerView.getContext()).updateAuthorityAsync(
-                    UserId.DEFAULT_USER, Providers.AUTHORITY_STORAGE);
+                    mCommonAddons.getSelectedUser(), Providers.AUTHORITY_STORAGE);
         }
 
         @Override
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index ed4acf6..ca436a3 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -31,6 +31,7 @@
 
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
+import com.android.documentsui.base.UserId;
 import com.android.documentsui.dirlist.AnimationView;
 
 import com.google.android.material.appbar.AppBarLayout;
@@ -110,6 +111,13 @@
         return mProfileTabs;
     }
 
+    /**
+     * Sets a listener to the profile tabs.
+     */
+    public void setProfileTabsListener(ProfileTabs.Listener listener) {
+        mProfileTabs.setListener(listener);
+    }
+
     private void onNavigationIconClicked() {
         if (mDrawer.isPresent()) {
             mDrawer.setOpen(true);
@@ -127,6 +135,10 @@
         }
     }
 
+    public UserId getSelectedUser() {
+        return mProfileTabs.getSelectedUser();
+    }
+
     public void update() {
         updateScrollFlag();
         updateToolbar();
diff --git a/src/com/android/documentsui/ProfileTabs.java b/src/com/android/documentsui/ProfileTabs.java
index f9e195b..6c2094f 100644
--- a/src/com/android/documentsui/ProfileTabs.java
+++ b/src/com/android/documentsui/ProfileTabs.java
@@ -21,7 +21,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import androidx.annotation.VisibleForTesting;
+import androidx.annotation.Nullable;
 
 import com.android.documentsui.base.State;
 import com.android.documentsui.base.UserId;
@@ -42,6 +42,8 @@
     private final NavigationViewManager.Environment mEnv;
     private final UserIdManager mUserIdManager;
     private List<UserId> mUserIds;
+    @Nullable
+    private Listener mListener;
 
     public ProfileTabs(TabLayout tabLayout, State state, UserIdManager userIdManager,
             NavigationViewManager.Environment env) {
@@ -51,6 +53,22 @@
         mUserIdManager = checkNotNull(userIdManager);
         mTabs.removeAllTabs();
         mUserIds = Collections.singletonList(UserId.CURRENT_USER);
+        mTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+            @Override
+            public void onTabSelected(TabLayout.Tab tab) {
+                if (mListener != null) {
+                    mListener.onUserSelected((UserId) tab.getTag());
+                }
+            }
+
+            @Override
+            public void onTabUnselected(TabLayout.Tab tab) {
+            }
+
+            @Override
+            public void onTabReselected(TabLayout.Tab tab) {
+            }
+        });
     }
 
     /**
@@ -61,6 +79,10 @@
         mTabs.setVisibility(shouldShow() ? View.VISIBLE : View.GONE);
     }
 
+    public void setListener(@Nullable Listener listener) {
+        mListener = listener;
+    }
+
     private void updateTabsIfNeeded() {
         List<UserId> userIds = mUserIdManager.getUserIds();
         // Add tabs if the userIds is not equals to cached mUserIds.
@@ -70,9 +92,13 @@
             mUserIds = userIds;
             mTabs.removeAllTabs();
             if (mUserIds.size() > 1) {
-                mTabs.addTab(createTab(R.string.personal_tab, mUserIdManager.getSystemUser()));
-                mTabs.addTab(createTab(R.string.work_tab, mUserIdManager.getManagedUser()));
+                // set setSelected to false otherwise it will trigger callback.
+                mTabs.addTab(createTab(R.string.personal_tab,
+                        mUserIdManager.getSystemUser()), /* setSelected= */false);
+                mTabs.addTab(createTab(R.string.work_tab,
+                        mUserIdManager.getManagedUser()), /* setSelected= */false);
             }
+            mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(UserId.CURRENT_USER)));
         }
     }
 
@@ -80,7 +106,6 @@
      * Returns the user represented by the selected tab. If there is no tab, return the
      * current user.
      */
-    @VisibleForTesting
     public UserId getSelectedUser() {
         if (mTabs.getTabCount() > 1 && mTabs.getSelectedTabPosition() >= 0) {
             return (UserId) mTabs.getTabAt(mTabs.getSelectedTabPosition()).getTag();
@@ -121,4 +146,14 @@
             }
         }
     }
+
+    /**
+     * Interface definition for a callback to be invoked.
+     */
+    interface Listener {
+        /**
+         * Called when a user tab has been selected.
+         */
+        void onUserSelected(UserId userId);
+    }
 }
diff --git a/src/com/android/documentsui/ProfileTabsController.java b/src/com/android/documentsui/ProfileTabsController.java
index d10806b..41e3b99 100644
--- a/src/com/android/documentsui/ProfileTabsController.java
+++ b/src/com/android/documentsui/ProfileTabsController.java
@@ -16,11 +16,13 @@
 
 package com.android.documentsui;
 
+import static androidx.core.util.Preconditions.checkNotNull;
+
 import androidx.recyclerview.selection.SelectionTracker;
 import androidx.recyclerview.selection.SelectionTracker.SelectionObserver;
 
 /**
- * A controller that listens to selection changes and control profile tabs behavior.
+ * A controller that listens to selection changes and controls profile tabs behavior.
  */
 public class ProfileTabsController extends SelectionObserver<String> {
 
@@ -32,17 +34,21 @@
     public ProfileTabsController(
             SelectionTracker<String> selectionMgr,
             ProfileTabsAddons profileTabsAddons) {
-        mSelectionMgr = selectionMgr;
-        mProfileTabsAddons = profileTabsAddons;
+        mSelectionMgr = checkNotNull(selectionMgr);
+        mProfileTabsAddons = checkNotNull(profileTabsAddons);
     }
 
     @Override
     public void onSelectionChanged() {
-        mProfileTabsAddons.setEnabled(mSelectionMgr.getSelection().isEmpty());
+        onSelectionUpdated();
     }
 
     @Override
     public void onSelectionRestored() {
-        onSelectionChanged();
+        onSelectionUpdated();
+    }
+
+    private void onSelectionUpdated() {
+        mProfileTabsAddons.setEnabled(mSelectionMgr.getSelection().isEmpty());
     }
 }
diff --git a/src/com/android/documentsui/UserIdManager.java b/src/com/android/documentsui/UserIdManager.java
index e73eda9..f1ce903 100644
--- a/src/com/android/documentsui/UserIdManager.java
+++ b/src/com/android/documentsui/UserIdManager.java
@@ -36,6 +36,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.core.os.BuildCompat;
 
+import com.android.documentsui.base.Features;
 import com.android.documentsui.base.UserId;
 
 import java.util.ArrayList;
@@ -80,8 +81,6 @@
 
         private static final String TAG = "UserIdManager";
 
-        private static final boolean ENABLE_MULTI_PROFILES = false; // compile-time feature flag
-
         private final Context mContext;
         private final UserId mCurrentUser;
         private final boolean mIsDeviceSupported;
@@ -105,7 +104,7 @@
 
         private RuntimeUserIdManager(Context context) {
             this(context, UserId.CURRENT_USER,
-                    ENABLE_MULTI_PROFILES && isDeviceSupported(context));
+                    Features.CROSS_PROFILE_TABS && isDeviceSupported(context));
         }
 
         @VisibleForTesting
diff --git a/src/com/android/documentsui/base/Features.java b/src/com/android/documentsui/base/Features.java
index dd2c314..6b880f4 100644
--- a/src/com/android/documentsui/base/Features.java
+++ b/src/com/android/documentsui/base/Features.java
@@ -29,6 +29,11 @@
  */
 public interface Features {
 
+    /**
+     * Temporary compile-time feature flag to enable in-app cross-profile browsing for some intent.
+     */
+    boolean CROSS_PROFILE_TABS = false;
+
     boolean isArchiveCreationEnabled();
     boolean isCommandInterceptorEnabled();
     boolean isContentPagingEnabled();
diff --git a/src/com/android/documentsui/base/RootInfo.java b/src/com/android/documentsui/base/RootInfo.java
index 4f01235..8e5a197 100644
--- a/src/com/android/documentsui/base/RootInfo.java
+++ b/src/com/android/documentsui/base/RootInfo.java
@@ -200,6 +200,30 @@
         }
     };
 
+    /**
+     * Returns a new root info copied from the provided root info.
+     */
+    public static RootInfo copyRootInfo(RootInfo root) {
+        final RootInfo newRoot = new RootInfo();
+        newRoot.userId = root.userId;
+        newRoot.authority = root.authority;
+        newRoot.rootId = root.rootId;
+        newRoot.flags = root.flags;
+        newRoot.icon = root.icon;
+        newRoot.title = root.title;
+        newRoot.summary = root.summary;
+        newRoot.documentId = root.documentId;
+        newRoot.availableBytes = root.availableBytes;
+        newRoot.mimeTypes = root.mimeTypes;
+        newRoot.queryArgs = root.queryArgs;
+
+        // derived fields
+        newRoot.derivedType = root.derivedType;
+        newRoot.derivedIcon = root.derivedIcon;
+        newRoot.derivedMimeTypes = root.derivedMimeTypes;
+        return newRoot;
+    }
+
     public static RootInfo fromRootsCursor(UserId userId, String authority, Cursor cursor) {
         final RootInfo root = new RootInfo();
         root.userId = userId;
@@ -469,7 +493,7 @@
     public String toString() {
         return "Root{"
                 + "userId=" + userId
-                + "authority=" + authority
+                + ", authority=" + authority
                 + ", rootId=" + rootId
                 + ", title=" + title
                 + ", isUsb=" + isUsb()
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 7b17246..20b8318 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -497,7 +497,7 @@
                 }
                 // If we've got a specific root to display, restore that root using a dedicated
                 // authority. That way a misbehaving provider won't result in an ANR.
-                loadRoot(uri);
+                loadRoot(uri, UserId.DEFAULT_USER);
                 return true;
             } else if (DocumentsContract.isRootsUri(mActivity, uri)) {
                 if (DEBUG) {
@@ -527,7 +527,7 @@
         if (DownloadManager.ACTION_VIEW_DOWNLOADS.equals(intent.getAction())) {
             Uri uri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS,
                     Providers.ROOT_ID_DOWNLOADS);
-            loadRoot(uri);
+            loadRoot(uri, UserId.DEFAULT_USER);
             return true;
         }
 
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index fcc6e89..bcbd8e2 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -158,7 +158,7 @@
         Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
         if (uri != null) {
             if (DocumentsContract.isRootUri(mActivity, uri)) {
-                loadRoot(uri);
+                loadRoot(uri, UserId.DEFAULT_USER);
                 return true;
             } else if (DocumentsContract.isDocumentUri(mActivity, uri)) {
                 return launchToDocument(uri);
diff --git a/src/com/android/documentsui/picker/LastAccessedProvider.java b/src/com/android/documentsui/picker/LastAccessedProvider.java
index a382256..36c00f3 100644
--- a/src/com/android/documentsui/picker/LastAccessedProvider.java
+++ b/src/com/android/documentsui/picker/LastAccessedProvider.java
@@ -38,6 +38,7 @@
 
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.DurableUtils;
+import com.android.documentsui.base.UserId;
 
 import java.io.IOException;
 import java.util.HashSet;
@@ -122,9 +123,15 @@
     static void setLastAccessed(
             ContentResolver resolver, String packageName, DocumentStack stack) {
         final ContentValues values = new ContentValues();
-        final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack);
         values.clear();
-        values.put(Columns.STACK, rawStack);
+        if (stack.getRoot() != null && !UserId.CURRENT_USER.equals(stack.getRoot().userId)) {
+            // Do not remember and clear the stack if it is not from the current user. Next time
+            // it will launch into default root.
+            values.put(Columns.STACK, (Byte) null);
+        } else {
+            final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack);
+            values.put(Columns.STACK, rawStack);
+        }
         values.put(Columns.EXTERNAL, 0);
         resolver.insert(buildLastAccessed(packageName), values);
     }
diff --git a/src/com/android/documentsui/roots/LoadFirstRootTask.java b/src/com/android/documentsui/roots/LoadFirstRootTask.java
index c6c04d1..0c2f0c2 100644
--- a/src/com/android/documentsui/roots/LoadFirstRootTask.java
+++ b/src/com/android/documentsui/roots/LoadFirstRootTask.java
@@ -33,7 +33,7 @@
             ProvidersAccess providers,
             Uri rootUri,
             LoadRootCallback callback) {
-        super(activity, providers, rootUri, callback);
+        super(activity, providers, rootUri, UserId.DEFAULT_USER, callback);
     }
 
     @Override
diff --git a/src/com/android/documentsui/roots/LoadRootTask.java b/src/com/android/documentsui/roots/LoadRootTask.java
index 3b4c23c..f0069de 100644
--- a/src/com/android/documentsui/roots/LoadRootTask.java
+++ b/src/com/android/documentsui/roots/LoadRootTask.java
@@ -36,27 +36,29 @@
 
     protected final ProvidersAccess mProviders;
     private final Uri mRootUri;
+    private final UserId mUserId;
     private final LoadRootCallback mCallback;
 
     public LoadRootTask(
             T activity,
             ProvidersAccess providers,
             Uri rootUri,
+            UserId userId,
             LoadRootCallback callback) {
         super(activity);
         mProviders = providers;
         mRootUri = rootUri;
+        mUserId = userId;
         mCallback = callback;
     }
 
     @Override
     protected RootInfo run(Void... params) {
         if (DEBUG) {
-            Log.d(TAG, "Loading root: " + mRootUri);
+            Log.d(TAG, "Loading root: " + mRootUri + " on user " + mUserId);
         }
 
-        return mProviders.getRootOneshot(UserId.DEFAULT_USER, mRootUri.getAuthority(),
-                getRootId(mRootUri));
+        return mProviders.getRootOneshot(mUserId, mRootUri.getAuthority(), getRootId(mRootUri));
     }
 
     @Override
@@ -66,7 +68,7 @@
                 Log.d(TAG, "Loaded root: " + root);
             }
         } else {
-            Log.w(TAG, "Failed to find root: " + mRootUri);
+            Log.w(TAG, "Failed to find root: " + mRootUri + " on user " + mUserId);
         }
 
         mCallback.onRootLoaded(root);
diff --git a/src/com/android/documentsui/sidebar/RootItem.java b/src/com/android/documentsui/sidebar/RootItem.java
index 343b4ff..777334b 100644
--- a/src/com/android/documentsui/sidebar/RootItem.java
+++ b/src/com/android/documentsui/sidebar/RootItem.java
@@ -36,11 +36,15 @@
 import com.android.documentsui.R;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.UserId;
+
+import java.util.Objects;
 
 /**
  * An {@link Item} for each root provided by {@link DocumentsProvider}s.
  */
 public class RootItem extends Item {
+    private static final String TAG = "RootItem";
     private static final String STRING_ID_FORMAT = "RootItem{%s/%s}";
 
     public final RootInfo root;
@@ -175,4 +179,41 @@
                 + ", docInfo=" + docInfo
                 + "}";
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) {
+            return false;
+        }
+
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof RootItem) {
+            RootItem other = (RootItem) o;
+            return Objects.equals(root, other.root)
+                    && Objects.equals(docInfo, other.docInfo)
+                    && Objects.equals(mActionHandler, other.mActionHandler)
+                    && Objects.equals(mPackageName, other.mPackageName);
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(root, docInfo, mActionHandler, mPackageName);
+    }
+
+    /**
+     * Creates a dummy root item for a user. A dummy root item is used as a place holder when
+     * there is no such root available. We can therefore show the item on the UI.
+     */
+    public static RootItem createDummyItem(RootItem item, UserId targetUser) {
+        RootInfo dummyRootInfo = RootInfo.copyRootInfo(item.root);
+        dummyRootInfo.userId = targetUser;
+        RootItem dummy = new RootItem(dummyRootInfo, item.mActionHandler);
+        return dummy;
+    }
 }
diff --git a/src/com/android/documentsui/sidebar/RootItemListBuilder.java b/src/com/android/documentsui/sidebar/RootItemListBuilder.java
new file mode 100644
index 0000000..4bdce15
--- /dev/null
+++ b/src/com/android/documentsui/sidebar/RootItemListBuilder.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 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.sidebar;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import com.android.documentsui.base.UserId;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A builder to build a list of root items to be displayed on the {@link RootsFragment}. The list
+ * should contain roots that the were added to this builder, except for those which support
+ * cross-profile.
+ *
+ * <p>If the root supports cross-profile, the list would only contain the root of the
+ * selected user.
+ *
+ * <p>If no root of the selected user was added but that of the other user was added,
+ * a dummy root of that root for the selected user will be generated.
+ *
+ * <p>The builder group the roots using {@link Item#stringId} as key.
+ *
+ * <p>For example, if these items were added to the builder: itemA[0], itemA[10], itemB[0],
+ * itemC[10], itemX[0],itemY[10] where root itemX, itemY do not support cross profile.
+ *
+ * <p>When the selected user is user 0, {@link #getList()} returns itemA[0], itemB[0],
+ * dummyC[0], itemX[0], itemY[10].
+ *
+ * <p>When the selected user is user 10, {@link #getList()} returns itemA[10], dummyB[10],
+ * itemC[10], itemX[0], itemY[10].
+ */
+class RootItemListBuilder {
+    private final UserId mSelectedUser;
+    private final List<UserId> mUserIds;
+    private final Multimap<String, RootItem> mItems = ArrayListMultimap.create();
+
+    RootItemListBuilder(UserId selectedUser, List<UserId> userIds) {
+        mSelectedUser = checkNotNull(selectedUser);
+        mUserIds = userIds;
+    }
+
+    public void add(RootItem item) {
+        mItems.put(item.stringId, item);
+    }
+
+    /**
+     * Returns a lists of root items generated from the provided root items.
+     */
+    public List<RootItem> getList() {
+        if (mUserIds.size() < 2) {
+            // If we only have one user, simply return everything that has added to this builder.
+            return Lists.newArrayList(mItems.values());
+        }
+        List<RootItem> result = Lists.newArrayList();
+        // Iterates through items by item.stringId.
+        for (Collection<RootItem> items : mItems.asMap().values()) {
+            result.addAll(getRootItemForSelectedUser(items));
+        }
+        return result;
+    }
+
+    private Collection<RootItem> getRootItemForSelectedUser(Collection<RootItem> items) {
+        RootItem testRootItem = items.iterator().next();
+        if (!testRootItem.root.supportsCrossProfile()) {
+            // If the root does not support cross-profile, return the entire list.
+            return items;
+        }
+
+        // If the root supports cross-profile, we return the added root or create a dummy root if
+        // it was not added for the selected user.
+        for (RootItem item : items) {
+            if (item.userId.equals(mSelectedUser)) {
+                // If the item has and return the item if it is already in the items list.
+                return Collections.singletonList(item);
+            }
+        }
+
+        return Collections.singletonList(RootItem.createDummyItem(testRootItem, mSelectedUser));
+    }
+}
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 21bb5fd..04b2d05 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -67,6 +67,7 @@
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.Events;
+import com.android.documentsui.base.Features;
 import com.android.documentsui.base.Providers;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
@@ -247,7 +248,9 @@
                         intent.getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false);
                 final String excludePackage = excludeSelf ? activity.getCallingPackage() : null;
                 List<Item> sortedItems = sortLoadResult(roots, excludePackage, handlerAppIntent,
-                        DocumentsApplication.getProvidersCache(getContext()));
+                        DocumentsApplication.getProvidersCache(getContext()),
+                        getBaseActivity().getSelectedUser(),
+                        DocumentsApplication.getUserIdManager(getContext()).getUserIds());
 
                 // Get the first visible position and offset
                 final int firstPosition = mList.getFirstVisiblePosition();
@@ -289,11 +292,14 @@
             Collection<RootInfo> roots,
             @Nullable String excludePackage,
             @Nullable Intent handlerAppIntent,
-            ProvidersAccess providersAccess) {
+            ProvidersAccess providersAccess,
+            UserId selectedUser,
+            List<UserId> userIds) {
         final List<Item> result = new ArrayList<>();
 
-        final List<RootItem> libraries = new ArrayList<>();
-        final List<RootItem> storageProviders = new ArrayList<>();
+        final RootItemListBuilder librariesBuilder = new RootItemListBuilder(selectedUser, userIds);
+        final RootItemListBuilder storageProvidersBuilder = new RootItemListBuilder(selectedUser,
+                userIds);
         final List<RootItem> otherProviders = new ArrayList<>();
 
         for (final RootInfo root : roots) {
@@ -301,10 +307,10 @@
 
             if (root.isLibrary() || root.isDownloads()) {
                 item = new RootItem(root, mActionHandler);
-                libraries.add(item);
+                librariesBuilder.add(item);
             } else if (root.isStorage()) {
                 item = new RootItem(root, mActionHandler);
-                storageProviders.add(item);
+                storageProvidersBuilder.add(item);
             } else {
                 item = new RootItem(root, mActionHandler,
                         providersAccess.getPackageName(root.userId, root.authority));
@@ -312,6 +318,9 @@
             }
         }
 
+        final List<RootItem> libraries = librariesBuilder.getList();
+        final List<RootItem> storageProviders = storageProvidersBuilder.getList();
+
         final RootComparator comp = new RootComparator();
         Collections.sort(libraries, comp);
         Collections.sort(storageProviders, comp);
@@ -328,7 +337,7 @@
 
         // Include apps that can handle this intent too.
         if (handlerAppIntent != null) {
-            includeHandlerApps(handlerAppIntent, excludePackage, result, otherProviders);
+            includeHandlerApps(handlerAppIntent, excludePackage, result, otherProviders, userIds);
         } else {
             // Only add providers
             Collections.sort(otherProviders, comp);
@@ -343,7 +352,6 @@
                 mApplicationItemList.add(item);
             }
         }
-
         return result;
     }
 
@@ -353,46 +361,50 @@
      */
     private void includeHandlerApps(
             Intent handlerAppIntent, @Nullable String excludePackage, List<Item> result,
-            List<RootItem> otherProviders) {
+            List<RootItem> otherProviders, List<UserId> userIds) {
         if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent);
 
-        UserId userId = UserId.DEFAULT_USER;
         Context context = getContext();
-        final PackageManager pm = userId.getPackageManager(context);
-        final List<ResolveInfo> infos = pm.queryIntentActivities(
-                handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
-
         final List<Item> rootList = new ArrayList<>();
+        final List<Item> rootListOtherUser = new ArrayList<>();
         final Map<UserPackage, ResolveInfo> appsMapping = new HashMap<>();
         final Map<UserPackage, Item> appItems = new HashMap<>();
         ProfileItem profileItem = null;
 
-        // Omit ourselves and maybe calling package from the list
-        for (ResolveInfo info : infos) {
-            if (!info.activityInfo.exported) {
-                if (VERBOSE) {
-                    Log.v(TAG, "Non exported activity: " + info.activityInfo);
+        final String myPackageName = context.getPackageName();
+        for (UserId userId : userIds) {
+            final PackageManager pm = userId.getPackageManager(context);
+            final List<ResolveInfo> infos = pm.queryIntentActivities(
+                    handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
+
+            // Omit ourselves and maybe calling package from the list
+            for (ResolveInfo info : infos) {
+                if (!info.activityInfo.exported) {
+                    if (VERBOSE) {
+                        Log.v(TAG, "Non exported activity: " + info.activityInfo);
+                    }
+                    continue;
                 }
-                continue;
-            }
 
-            final String packageName = info.activityInfo.packageName;
-            if (!context.getPackageName().equals(packageName) &&
-                    !TextUtils.equals(excludePackage, packageName)) {
-                UserPackage userPackage = new UserPackage(userId, packageName);
-                appsMapping.put(userPackage, info);
+                final String packageName = info.activityInfo.packageName;
+                if (!myPackageName.equals(packageName)
+                        && !TextUtils.equals(excludePackage, packageName)) {
+                    UserPackage userPackage = new UserPackage(userId, packageName);
+                    appsMapping.put(userPackage, info);
 
-                // for change personal profile root.
-                if (PROFILE_TARGET_ACTIVITY.equals(info.activityInfo.targetActivity)) {
-                    // TODO: only set in current user
-                    getBaseActivity().getDisplayState().canShareAcrossProfile = true;
-                    profileItem = new ProfileItem(info, info.loadLabel(pm).toString(),
-                            mActionHandler);
-                } else {
-                    final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId,
-                            mActionHandler);
-                    appItems.put(userPackage, item);
-                    if (VERBOSE) Log.v(TAG, "Adding handler app: " + item);
+                    // for change personal profile root.
+                    if (PROFILE_TARGET_ACTIVITY.equals(info.activityInfo.targetActivity)) {
+                        if (UserId.CURRENT_USER.equals(userId)) {
+                            getBaseActivity().getDisplayState().canShareAcrossProfile = true;
+                            profileItem = new ProfileItem(info, info.loadLabel(pm).toString(),
+                                    mActionHandler);
+                        }
+                    } else {
+                        final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId,
+                                mActionHandler);
+                        appItems.put(userPackage, item);
+                        if (VERBOSE) Log.v(TAG, "Adding handler app: " + item);
+                    }
                 }
             }
         }
@@ -411,11 +423,27 @@
                 item = rootItem;
             }
 
-            if (VERBOSE) Log.v(TAG, "Adding provider : " + item);
-            rootList.add(item);
+            if (UserId.CURRENT_USER.equals(item.userId)) {
+                if (VERBOSE) Log.v(TAG, "Adding provider : " + item);
+                rootList.add(item);
+            } else {
+                if (VERBOSE) Log.v(TAG, "Adding provider to other users : " + item);
+                rootListOtherUser.add(item);
+            }
         }
 
-        rootList.addAll(appItems.values());
+        for (Item item : appItems.values()) {
+            if (UserId.CURRENT_USER.equals(item.userId)) {
+                rootList.add(item);
+            } else {
+                rootListOtherUser.add(item);
+            }
+        }
+
+        if (profileItem != null && Features.CROSS_PROFILE_TABS) {
+            // Combine lists only if we enabled profile tab feature.
+            rootList.addAll(rootListOtherUser);
+        }
 
         if (!result.isEmpty() && !rootList.isEmpty()) {
             result.add(new SpacerItem());
@@ -430,7 +458,8 @@
 
         mApplicationItemList = rootList;
 
-        if (profileItem != null) {
+        if (profileItem != null && !Features.CROSS_PROFILE_TABS) {
+            // Add profile item if we don't support cross-profile tab.
             result.add(new SpacerItem());
             result.add(profileItem);
         }
@@ -443,7 +472,7 @@
         // Update the information for Storage's root
         if (context != null) {
             DocumentsApplication.getProvidersCache(context).updateAuthorityAsync(
-                    UserId.DEFAULT_USER, Providers.AUTHORITY_STORAGE);
+                    ((BaseActivity) context).getSelectedUser(), Providers.AUTHORITY_STORAGE);
         }
         onDisplayStateChanged();
     }
@@ -483,6 +512,13 @@
     }
 
     /**
+     * Called when the selected user is changed. It reloads roots with the current user.
+     */
+    public void onSelectedUserChanged() {
+        LoaderManager.getInstance(this).restartLoader(/* id= */ 2, /* args= */ null, mCallbacks);
+    }
+
+    /**
      * Attempts to shift focus back to the navigation drawer.
      */
     public boolean requestFocus() {
diff --git a/tests/common/com/android/documentsui/testing/TestProvidersAccess.java b/tests/common/com/android/documentsui/testing/TestProvidersAccess.java
index 6d7a1ac..f041f3b 100644
--- a/tests/common/com/android/documentsui/testing/TestProvidersAccess.java
+++ b/tests/common/com/android/documentsui/testing/TestProvidersAccess.java
@@ -49,6 +49,7 @@
     public static final RootInfo DOCUMENT;
     public static final RootInfo EXTERNALSTORAGE;
     public static final RootInfo NO_TREE_ROOT;
+    public static final RootInfo SD_CARD;
 
     static {
         UserId userId = USER_ID;
@@ -153,6 +154,15 @@
         NO_TREE_ROOT.title = "No Tree Title";
         NO_TREE_ROOT.derivedType = RootInfo.TYPE_LOCAL;
         NO_TREE_ROOT.flags = Root.FLAG_LOCAL_ONLY;
+
+        SD_CARD = new RootInfo();
+        SD_CARD.userId = userId;
+        SD_CARD.authority = Providers.AUTHORITY_STORAGE;
+        SD_CARD.rootId = Providers.ROOT_ID_DOCUMENTS;
+        SD_CARD.title = "SD card";
+        SD_CARD.derivedType = RootInfo.TYPE_SD;
+        SD_CARD.flags = Root.FLAG_LOCAL_ONLY
+                | Root.FLAG_SUPPORTS_IS_CHILD;
     }
 
     public static class OtherUser {
@@ -161,6 +171,10 @@
         public static final UserId USER_ID = UserId.of(OtherUser.USER_HANDLE);
 
         public static final RootInfo DOWNLOADS;
+        public static final RootInfo HOME;
+        public static final RootInfo IMAGE;
+        public static final RootInfo PICKLES;
+        public static final RootInfo MTP_ROOT;
 
         static {
             UserId userId = OtherUser.USER_ID;
@@ -174,6 +188,41 @@
             DOWNLOADS.flags = Root.FLAG_LOCAL_ONLY
                     | Root.FLAG_SUPPORTS_CREATE
                     | Root.FLAG_SUPPORTS_RECENTS;
+
+            HOME = new RootInfo();
+            HOME.userId = userId;
+            HOME.authority = Providers.AUTHORITY_STORAGE;
+            HOME.rootId = Providers.ROOT_ID_HOME;
+            HOME.title = "Home";
+            HOME.derivedType = RootInfo.TYPE_LOCAL;
+            HOME.flags = Root.FLAG_LOCAL_ONLY
+                    | Root.FLAG_SUPPORTS_CREATE
+                    | Root.FLAG_SUPPORTS_IS_CHILD
+                    | Root.FLAG_SUPPORTS_RECENTS;
+
+            IMAGE = new RootInfo();
+            IMAGE.userId = userId;
+            IMAGE.authority = Providers.AUTHORITY_MEDIA;
+            IMAGE.rootId = Providers.ROOT_ID_IMAGES;
+            IMAGE.title = "Images";
+            IMAGE.derivedType = RootInfo.TYPE_IMAGES;
+
+            PICKLES = new RootInfo();
+            PICKLES.userId = userId;
+            PICKLES.authority = "yummies";
+            PICKLES.rootId = "pickles";
+            PICKLES.title = "Pickles";
+            PICKLES.summary = "Yummy pickles";
+
+            MTP_ROOT = new RootInfo();
+            MTP_ROOT.userId = userId;
+            MTP_ROOT.authority = Providers.AUTHORITY_MTP;
+            MTP_ROOT.rootId = Providers.ROOT_ID_DOCUMENTS;
+            MTP_ROOT.title = "MTP";
+            MTP_ROOT.derivedType = RootInfo.TYPE_MTP;
+            MTP_ROOT.flags = Root.FLAG_SUPPORTS_CREATE
+                    | Root.FLAG_LOCAL_ONLY
+                    | Root.FLAG_SUPPORTS_IS_CHILD;
         }
     }
 
diff --git a/tests/unit/com/android/documentsui/ProfileTabsControllerTest.java b/tests/unit/com/android/documentsui/ProfileTabsControllerTest.java
index 2c6b2f9..92f901e 100644
--- a/tests/unit/com/android/documentsui/ProfileTabsControllerTest.java
+++ b/tests/unit/com/android/documentsui/ProfileTabsControllerTest.java
@@ -28,9 +28,10 @@
 public class ProfileTabsControllerTest {
 
     private SelectionTracker<String> mSelectionMgr = SelectionHelpers.createTestInstance();
-    private Boolean testEnabledValue = null;
+
+    private FakeProfileTabs mFakeProfileTabs = new FakeProfileTabs();
     private ProfileTabsController mController = new ProfileTabsController(mSelectionMgr,
-            enabled -> testEnabledValue = enabled);
+            enabled -> mFakeProfileTabs.enabled = enabled);
 
     @Before
     public void setUp() throws Exception {
@@ -40,14 +41,14 @@
     @Test
     public void testNoSelection_enable() {
         mController.onSelectionRestored();
-        assertThat(testEnabledValue).isTrue();
+        assertThat(mFakeProfileTabs.isEnabled()).isTrue();
     }
 
     @Test
     public void testClearSelection_enable() {
         mSelectionMgr.select("foo");
         mSelectionMgr.clearSelection();
-        assertThat(testEnabledValue).isTrue();
+        assertThat(mFakeProfileTabs.isEnabled()).isTrue();
     }
 
     @Test
@@ -56,13 +57,13 @@
         mSelectionMgr.select("bar");
         mSelectionMgr.deselect("foo");
         mSelectionMgr.deselect("bar");
-        assertThat(testEnabledValue).isTrue();
+        assertThat(mFakeProfileTabs.isEnabled()).isTrue();
     }
 
     @Test
     public void testSelection_disable() {
         mSelectionMgr.select("foo");
-        assertThat(testEnabledValue).isFalse();
+        assertThat(mFakeProfileTabs.isEnabled()).isFalse();
     }
 
     @Test
@@ -70,7 +71,7 @@
         mSelectionMgr.select("foo");
         mSelectionMgr.select("bar");
         mSelectionMgr.select("apple");
-        assertThat(testEnabledValue).isFalse();
+        assertThat(mFakeProfileTabs.isEnabled()).isFalse();
     }
 
     @Test
@@ -78,6 +79,17 @@
         mSelectionMgr.select("foo");
         mSelectionMgr.select("bar");
         mSelectionMgr.deselect("bar");
-        assertThat(testEnabledValue).isFalse();
+        assertThat(mFakeProfileTabs.isEnabled()).isFalse();
+    }
+
+    private static class FakeProfileTabs {
+        Boolean enabled = null;
+
+        boolean isEnabled() {
+            if (enabled == null) {
+                throw new IllegalArgumentException("Not initialized");
+            }
+            return enabled;
+        }
     }
 }
diff --git a/tests/unit/com/android/documentsui/base/RootInfoTest.java b/tests/unit/com/android/documentsui/base/RootInfoTest.java
new file mode 100644
index 0000000..4639b45
--- /dev/null
+++ b/tests/unit/com/android/documentsui/base/RootInfoTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.base;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RootInfoTest {
+
+    @Test
+    public void testEquals_sameUser() {
+        RootInfo rootInfo = new RootInfo();
+        rootInfo.userId = UserId.of(100);
+        rootInfo.authority = "authority";
+        rootInfo.rootId = "root";
+
+        RootInfo rootInfo2 = new RootInfo();
+        rootInfo2.userId = UserId.of(100);
+        rootInfo2.authority = "authority";
+        rootInfo2.rootId = "root";
+
+        assertThat(rootInfo).isEqualTo(rootInfo2);
+    }
+
+    @Test
+    public void testNotEquals_differentUser() {
+        RootInfo rootInfo = new RootInfo();
+        rootInfo.userId = UserId.of(100);
+        rootInfo.authority = "authority";
+        rootInfo.rootId = "root";
+
+        RootInfo rootInfo2 = new RootInfo();
+        rootInfo2.userId = UserId.of(101);
+        rootInfo2.authority = "authority";
+        rootInfo2.rootId = "root";
+
+        assertThat(rootInfo).isNotEqualTo(rootInfo2);
+    }
+
+    @Test
+    public void testCopyInfo_equal() {
+        RootInfo rootInfo = new RootInfo();
+        rootInfo.userId = UserId.of(100);
+        rootInfo.authority = "authority";
+        rootInfo.rootId = "root";
+
+        RootInfo copied = RootInfo.copyRootInfo(rootInfo);
+        assertThat(copied).isEqualTo(rootInfo);
+        assertThat(copied).isNotSameAs(rootInfo);
+    }
+}
diff --git a/tests/unit/com/android/documentsui/sidebar/RootItemListBuilderTest.java b/tests/unit/com/android/documentsui/sidebar/RootItemListBuilderTest.java
new file mode 100644
index 0000000..7f227c8
--- /dev/null
+++ b/tests/unit/com/android/documentsui/sidebar/RootItemListBuilderTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2020 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.sidebar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.base.UserId;
+import com.android.documentsui.testing.TestProvidersAccess;
+
+import com.google.common.collect.Lists;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A unit test for {@link RootItemListBuilderTest}.
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class RootItemListBuilderTest {
+
+    private static final RootItem DOWNLOADS_DEFAULT_USER =
+            new RootItem(TestProvidersAccess.DOWNLOADS, null);
+    private static final RootItem HAMMY_DEFAULT_USER =
+            new RootItem(TestProvidersAccess.HAMMY, null);
+    private static final RootItem HOME_DEFAULT_USER =
+            new RootItem(TestProvidersAccess.HOME, null);
+    private static final RootItem IMAGE_DEFAULT_USER =
+            new RootItem(TestProvidersAccess.IMAGE, null);
+    private static final RootItem PICKLES_DEFAULT_USER =
+            new RootItem(TestProvidersAccess.PICKLES, null);
+    private static final RootItem SDCARD_DEFAULT_USER =
+            new RootItem(TestProvidersAccess.SD_CARD, null);
+
+    private static final RootItem DOWNLOADS_OTHER_USER =
+            new RootItem(TestProvidersAccess.OtherUser.DOWNLOADS, null);
+    private static final RootItem HOME_OTHER_USER =
+            new RootItem(TestProvidersAccess.OtherUser.HOME, null);
+    private static final RootItem IMAGE_OTHER_USER =
+            new RootItem(TestProvidersAccess.OtherUser.IMAGE, null);
+    private static final RootItem PICKLES_OTHER_USER =
+            new RootItem(TestProvidersAccess.OtherUser.PICKLES, null);
+
+    private RootItemListBuilder mBuilder;
+
+    @Test
+    public void testGetList_empty() {
+        mBuilder = new RootItemListBuilder(
+                UserId.DEFAULT_USER, Collections.singletonList(UserId.DEFAULT_USER));
+
+        List<RootItem> result = mBuilder.getList();
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void testGetList_singleUser() {
+        mBuilder = new RootItemListBuilder(
+                UserId.DEFAULT_USER, Collections.singletonList(UserId.DEFAULT_USER));
+
+        mBuilder.add(HAMMY_DEFAULT_USER);
+        mBuilder.add(PICKLES_DEFAULT_USER);
+        mBuilder.add(HOME_DEFAULT_USER);
+
+        List<RootItem> result = mBuilder.getList();
+        assertThat(result)
+                .containsExactly(HAMMY_DEFAULT_USER, PICKLES_DEFAULT_USER, HOME_DEFAULT_USER);
+    }
+
+    @Test
+    public void testGetList_twoUsers_allMultiProfileRootsMatchOther_defaultUser() {
+        mBuilder = new RootItemListBuilder(UserId.DEFAULT_USER,
+                Lists.newArrayList(UserId.DEFAULT_USER, TestProvidersAccess.OtherUser.USER_ID));
+
+        mBuilder.add(DOWNLOADS_DEFAULT_USER); // support multi-profile
+        mBuilder.add(HAMMY_DEFAULT_USER);
+        mBuilder.add(HOME_DEFAULT_USER); // support multi-profile
+        mBuilder.add(IMAGE_DEFAULT_USER); // support multi-profile
+        mBuilder.add(PICKLES_DEFAULT_USER);
+
+        mBuilder.add(DOWNLOADS_OTHER_USER); // support multi-profile
+        mBuilder.add(HOME_OTHER_USER); // support multi-profile
+        mBuilder.add(IMAGE_OTHER_USER); // support multi-profile
+        mBuilder.add(PICKLES_OTHER_USER);
+
+        List<RootItem> result = mBuilder.getList();
+        assertThat(result).containsExactly(
+                DOWNLOADS_DEFAULT_USER,
+                HAMMY_DEFAULT_USER,
+                HOME_DEFAULT_USER,
+                IMAGE_DEFAULT_USER,
+                PICKLES_DEFAULT_USER,
+                PICKLES_OTHER_USER);
+    }
+
+    @Test
+    public void testGetList_twoUsers_allMultiProfileRootsMatchOther_otherUser() {
+        mBuilder = new RootItemListBuilder(TestProvidersAccess.OtherUser.USER_ID,
+                Lists.newArrayList(UserId.DEFAULT_USER, TestProvidersAccess.OtherUser.USER_ID));
+
+        mBuilder.add(DOWNLOADS_DEFAULT_USER); // support multi-profile
+        mBuilder.add(SDCARD_DEFAULT_USER);
+        mBuilder.add(HOME_DEFAULT_USER); // support multi-profile
+        mBuilder.add(IMAGE_DEFAULT_USER); // support multi-profile
+        mBuilder.add(PICKLES_DEFAULT_USER);
+
+        mBuilder.add(DOWNLOADS_OTHER_USER); // support multi-profile
+        mBuilder.add(HOME_OTHER_USER); // support multi-profile
+        mBuilder.add(IMAGE_OTHER_USER); // support multi-profile
+
+        List<RootItem> result = mBuilder.getList();
+        assertThat(result).containsExactly(
+                DOWNLOADS_OTHER_USER,
+                SDCARD_DEFAULT_USER,
+                HOME_OTHER_USER,
+                IMAGE_OTHER_USER,
+                PICKLES_DEFAULT_USER);
+    }
+
+    @Test
+    public void testGetList_twoUsers_defaultUserHasAllMatchingRoots() {
+        mBuilder = new RootItemListBuilder(UserId.DEFAULT_USER,
+                Lists.newArrayList(UserId.DEFAULT_USER, TestProvidersAccess.OtherUser.USER_ID));
+
+        mBuilder.add(DOWNLOADS_DEFAULT_USER); // support multi-profile
+        mBuilder.add(SDCARD_DEFAULT_USER);
+        mBuilder.add(HOME_DEFAULT_USER); // support multi-profile
+        mBuilder.add(IMAGE_DEFAULT_USER); // support multi-profile
+        mBuilder.add(PICKLES_DEFAULT_USER);
+
+        mBuilder.add(IMAGE_OTHER_USER); // support multi-profile
+
+        List<RootItem> result = mBuilder.getList();
+        assertThat(result).containsExactly(
+                DOWNLOADS_DEFAULT_USER,
+                SDCARD_DEFAULT_USER,
+                HOME_DEFAULT_USER,
+                IMAGE_DEFAULT_USER,
+                PICKLES_DEFAULT_USER);
+    }
+
+    @Test
+    public void testGetList_twoUsers_secondUserFillsUpNonMatchingRoots() {
+        mBuilder = new RootItemListBuilder(TestProvidersAccess.OtherUser.USER_ID,
+                Lists.newArrayList(UserId.DEFAULT_USER, TestProvidersAccess.OtherUser.USER_ID));
+
+        mBuilder.add(DOWNLOADS_DEFAULT_USER); // support multi-profile
+        mBuilder.add(SDCARD_DEFAULT_USER);
+        mBuilder.add(HOME_DEFAULT_USER); // support multi-profile
+        mBuilder.add(IMAGE_DEFAULT_USER); // support multi-profile
+        mBuilder.add(PICKLES_DEFAULT_USER);
+
+        mBuilder.add(IMAGE_OTHER_USER); // support multi-profile
+
+        List<RootItem> result = mBuilder.getList();
+        assertThat(result).containsExactlyElementsIn(Lists.newArrayList(
+                RootItem.createDummyItem(
+                        DOWNLOADS_DEFAULT_USER, TestProvidersAccess.OtherUser.USER_ID),
+                SDCARD_DEFAULT_USER,
+                RootItem.createDummyItem(HOME_DEFAULT_USER, TestProvidersAccess.OtherUser.USER_ID),
+                IMAGE_OTHER_USER,
+                PICKLES_DEFAULT_USER));
+    }
+}
diff --git a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java
index 1162961..af9a66c 100644
--- a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java
+++ b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java
@@ -69,7 +69,8 @@
     @Test
     public void testSortLoadResult_WithCorrectOrder() {
         List<Item> items = mRootsFragment.sortLoadResult(createFakeRootInfoList(),
-                null /* excludePackage */, null /* handlerAppIntent */, new TestProvidersAccess());
+                null /* excludePackage */, null /* handlerAppIntent */, new TestProvidersAccess(),
+                UserId.DEFAULT_USER, Collections.singletonList(UserId.DEFAULT_USER));
         assertTrue(assertSortedResult(items));
     }