Fetch roots from all users and filter them by action

If we enable the compile-time feature flag and open pick activity,
we can see "duplicate" roots on a device with managed profiles.
Those roots will navigate to the directory of the respective user.

Bug: 148264331
Test: atest DocumentsUIGoogleTests:com.android.documentsui.UserIdManagerTest
Test: atest DocumentsUIGoogleTests
Test: manual

Change-Id: Ie2d87009e96d6ffd2c634cb10945239de1d96720
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index fe903a4..f0bf52d 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -97,6 +97,7 @@
     protected Injector<?> mInjector;
 
     protected ProvidersCache mProviders;
+    protected UserIdManager mUserIdManager;
     protected DocumentsAccess mDocs;
     protected DrawerController mDrawer;
 
@@ -154,6 +155,7 @@
         mDrawer = DrawerController.create(this, mInjector.config);
         Metrics.logActivityLaunch(mState, intent);
 
+        mUserIdManager = DocumentsApplication.getUserIdManager(this);
         mProviders = DocumentsApplication.getProvidersCache(this);
         mDocs = DocumentsAccess.create(this);
 
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 65204b7..fb479ac 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -31,6 +31,7 @@
 import android.util.Log;
 
 import com.android.documentsui.base.Lookup;
+import com.android.documentsui.base.UserId;
 import com.android.documentsui.clipping.ClipStorage;
 import com.android.documentsui.clipping.ClipStore;
 import com.android.documentsui.clipping.DocumentClipper;
@@ -48,6 +49,7 @@
     private ClipStorage mClipStore;
     private DocumentClipper mClipper;
     private DragAndDropManager mDragAndDropManager;
+    private UserIdManager mUserIdManager;
     private Lookup<String, String> mFileTypeLookup;
 
     public static ProvidersCache getProvidersCache(Context context) {
@@ -78,6 +80,10 @@
         return ((DocumentsApplication) context.getApplicationContext()).mClipStore;
     }
 
+    public static UserIdManager getUserIdManager(Context context) {
+        return ((DocumentsApplication) context.getApplicationContext()).mUserIdManager;
+    }
+
     public static DragAndDropManager getDragAndDropManager(Context context) {
         return ((DocumentsApplication) context.getApplicationContext()).mDragAndDropManager;
     }
@@ -105,7 +111,9 @@
             Log.w(TAG, "Can't obtain OverlayManager from System Service!");
         }
 
-        mProviders = new ProvidersCache(this);
+        mUserIdManager = UserIdManager.create(this);
+
+        mProviders = new ProvidersCache(this, mUserIdManager);
         mProviders.updateAsync(false);
 
         mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4);
@@ -148,7 +156,7 @@
             final Uri data = intent.getData();
             if (data != null) {
                 final String packageName = data.getSchemeSpecificPart();
-                mProviders.updatePackageAsync(packageName);
+                mProviders.updatePackageAsync(UserId.DEFAULT_USER, packageName);
             } else {
                 mProviders.updateAsync(true);
             }
diff --git a/src/com/android/documentsui/DrawerController.java b/src/com/android/documentsui/DrawerController.java
index 6822422..7ed8d51 100644
--- a/src/com/android/documentsui/DrawerController.java
+++ b/src/com/android/documentsui/DrawerController.java
@@ -30,6 +30,7 @@
 
 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".
@@ -204,7 +205,7 @@
             mToggle.onDrawerOpened(drawerView);
             // Update the information for Storage's root
             DocumentsApplication.getProvidersCache(drawerView.getContext()).updateAuthorityAsync(
-                    Providers.AUTHORITY_STORAGE);
+                    UserId.DEFAULT_USER, Providers.AUTHORITY_STORAGE);
         }
 
         @Override
diff --git a/src/com/android/documentsui/UserIdManager.java b/src/com/android/documentsui/UserIdManager.java
new file mode 100644
index 0000000..d489712
--- /dev/null
+++ b/src/com/android/documentsui/UserIdManager.java
@@ -0,0 +1,151 @@
+/*
+ * 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;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import static com.android.documentsui.base.SharedMinimal.DEBUG;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.core.os.BuildCompat;
+
+import com.android.documentsui.base.UserId;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Interface to query user ids.
+ */
+public interface UserIdManager {
+
+    /**
+     * Returns the {@UserId} of each profile which should be queried for documents. This
+     * will always include {@link UserId#CURRENT_USER}.
+     */
+    List<UserId> getUserIds();
+
+    /**
+     * Creates an implementation of {@link UserIdManager}.
+     */
+    static UserIdManager create(Context context) {
+        return new RuntimeUserIdManager(context);
+    }
+
+    /**
+     * Implementation of {@link UserIdManager}.
+     */
+    final class RuntimeUserIdManager implements UserIdManager {
+
+        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;
+
+        private RuntimeUserIdManager(Context context) {
+            this(context, UserId.CURRENT_USER,
+                    ENABLE_MULTI_PROFILES && isDeviceSupported(context));
+        }
+
+        @VisibleForTesting
+        RuntimeUserIdManager(Context context, UserId currentUser, boolean isDeviceSupported) {
+            mContext = context.getApplicationContext();
+            mCurrentUser = checkNotNull(currentUser);
+            mIsDeviceSupported = isDeviceSupported;
+        }
+
+        @Override
+        public List<UserId> getUserIds() {
+            final List<UserId> result = new ArrayList<>();
+            result.add(mCurrentUser);
+
+            // If the feature is disabled, return a list just containing the current user.
+            if (!mIsDeviceSupported) {
+                return result;
+            }
+
+            UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+            if (userManager == null) {
+                Log.e(TAG, "cannot obtain user manager");
+                return result;
+            }
+
+            final List<UserHandle> userProfiles = userManager.getUserProfiles();
+            if (userProfiles.size() < 2) {
+                return result;
+            }
+
+            UserId systemUser = null;
+            UserId managedUser = null;
+            for (UserHandle userHandle : userProfiles) {
+                if (userHandle.isSystem()) {
+                    systemUser = UserId.of(userHandle);
+                    continue;
+                }
+                if (managedUser == null
+                        && userManager.isManagedProfile(userHandle.getIdentifier())) {
+                    managedUser = UserId.of(userHandle);
+                }
+            }
+
+            if (mCurrentUser.isSystem()) {
+                // 1. If the current user is system (personal), add the managed user.
+                if (managedUser != null) {
+                    result.add(managedUser);
+                }
+            } else if (mCurrentUser.isManagedProfile(userManager)) {
+                // 2. If the current user is a managed user, add the personal user.
+                // Since we don't have MANAGED_USERS permission to get the parent user, we will
+                // treat the system as personal although the system can theoretically in the profile
+                // group but not being the parent user(personal) of the managed user.
+                if (systemUser != null) {
+                    result.add(0, systemUser);
+                }
+            } else {
+                // 3. If we cannot resolve the users properly, we will disable the cross-profile
+                // feature by returning just the current user.
+                if (DEBUG) {
+                    Log.w(TAG, "The current user " + UserId.CURRENT_USER
+                            + " is neither system nor managed user. has system user: "
+                            + (systemUser != null));
+                }
+            }
+            return result;
+        }
+
+        private static boolean isDeviceSupported(Context context) {
+            // The feature requires Android R DocumentsContract APIs and INTERACT_ACROSS_USERS
+            // permission.
+            return (BuildCompat.isAtLeastR()
+                    || (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 30))
+                    && context.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS)
+                    == PackageManager.PERMISSION_GRANTED;
+        }
+    }
+}
diff --git a/src/com/android/documentsui/UserPackage.java b/src/com/android/documentsui/UserPackage.java
new file mode 100644
index 0000000..9d3fb8e
--- /dev/null
+++ b/src/com/android/documentsui/UserPackage.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import com.android.documentsui.base.UserId;
+
+import java.util.Objects;
+
+/**
+ * Data class storing a user id and a package name.
+ */
+public class UserPackage {
+    final UserId userId;
+    final String packageName;
+
+    public UserPackage(UserId userId, String packageName) {
+        this.userId = checkNotNull(userId);
+        this.packageName = checkNotNull(packageName);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) {
+            return false;
+        }
+
+        if (this == o) {
+            return true;
+        }
+
+        if (o instanceof UserPackage) {
+            UserPackage other = (UserPackage) o;
+            return Objects.equals(userId, other.userId)
+                    && Objects.equals(packageName, other.packageName);
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(userId, packageName);
+    }
+}
diff --git a/src/com/android/documentsui/base/State.java b/src/com/android/documentsui/base/State.java
index 0a57579..489d1b7 100644
--- a/src/com/android/documentsui/base/State.java
+++ b/src/com/android/documentsui/base/State.java
@@ -139,6 +139,13 @@
         return true;
     }
 
+    /**
+     * Returns true if the action of the {@link State} can support cross-profile by DocsUI.
+     */
+    public boolean supportsCrossProfile() {
+        return action == ACTION_GET_CONTENT;
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/src/com/android/documentsui/base/UserId.java b/src/com/android/documentsui/base/UserId.java
index f1d0cdc..89a38d2 100644
--- a/src/com/android/documentsui/base/UserId.java
+++ b/src/com/android/documentsui/base/UserId.java
@@ -24,6 +24,7 @@
 import android.net.Uri;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 
 import androidx.annotation.VisibleForTesting;
 import androidx.loader.content.CursorLoader;
@@ -39,12 +40,12 @@
 public final class UserId {
 
     // A unspecified user is used as when the user's value is uninitialized. e.g. rootInfo.reset()
-    public static UserId UNSPECIFIED_USER = UserId.of(UserHandle.of(-1000));
+    public static final UserId UNSPECIFIED_USER = UserId.of(UserHandle.of(-1000));
     // A current user represents the user of the app's process. It is mainly used for comparison.
-    public static UserId CURRENT_USER = UserId.of(Process.myUserHandle());
+    public static final UserId CURRENT_USER = UserId.of(Process.myUserHandle());
     // A default user represents the user of the app's process. It is mainly used for operation
     // which supports only the current user only.
-    public static UserId DEFAULT_USER = CURRENT_USER;
+    public static final UserId DEFAULT_USER = CURRENT_USER;
 
     private static final int VERSION_INIT = 1;
 
@@ -58,12 +59,10 @@
     /**
      * Returns a {@link UserId} for a given {@link UserHandle}.
      */
-    @VisibleForTesting
-    static UserId of(UserHandle userHandle) {
+    public static UserId of(UserHandle userHandle) {
         return new UserId(userHandle);
     }
 
-
     /**
      * Returns a {@link UserId} for the given user id identifier.
      *
@@ -106,6 +105,20 @@
     }
 
     /**
+     * Returns true if this user refers to the system user; false otherwise.
+     */
+    public boolean isSystem() {
+        return mUserHandle.isSystem();
+    }
+
+    /**
+     * Returns true if the this user is a managed profile.
+     */
+    public boolean isManagedProfile(UserManager userManager) {
+        return userManager.isManagedProfile(mUserHandle.getIdentifier());
+    }
+
+    /**
      * Returns an identifier stored in this user id. This can be used to recreate the {@link UserId}
      * by {@link UserId#of(int)}.
      */
diff --git a/src/com/android/documentsui/roots/ProvidersAccess.java b/src/com/android/documentsui/roots/ProvidersAccess.java
index 822ce4d..7c933f1 100644
--- a/src/com/android/documentsui/roots/ProvidersAccess.java
+++ b/src/com/android/documentsui/roots/ProvidersAccess.java
@@ -118,6 +118,13 @@
                 continue;
             }
 
+            if (!UserId.CURRENT_USER.equals(root.userId) && !state.supportsCrossProfile()) {
+                if (VERBOSE) {
+                    Log.v(tag, "Excluding root because: action does not support cross profile.");
+                }
+                continue;
+            }
+
             final boolean overlap =
                     MimeTypes.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
                     MimeTypes.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
diff --git a/src/com/android/documentsui/roots/ProvidersCache.java b/src/com/android/documentsui/roots/ProvidersCache.java
index cf822da..b474d34 100644
--- a/src/com/android/documentsui/roots/ProvidersCache.java
+++ b/src/com/android/documentsui/roots/ProvidersCache.java
@@ -51,6 +51,8 @@
 
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.R;
+import com.android.documentsui.UserIdManager;
+import com.android.documentsui.UserPackage;
 import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.base.LookupApplicationName;
 import com.android.documentsui.base.Providers;
@@ -111,8 +113,11 @@
     @GuardedBy("mObservedAuthoritiesDetails")
     private final Map<UserAuthority, PackageDetails> mObservedAuthoritiesDetails = new HashMap<>();
 
-    public ProvidersCache(Context context) {
+    private final UserIdManager mUserIdManager;
+
+    public ProvidersCache(Context context, UserIdManager userIdManager) {
         mContext = context;
+        mUserIdManager = userIdManager;
 
         // Create a new anonymous "Recents" RootInfo. It's a faker.
         mRecentsRoot = new RootInfo() {{
@@ -154,7 +159,7 @@
             if (DEBUG) {
                 Log.i(TAG, "Updating roots due to change on user " + mUserId + "at " + uri);
             }
-            updateAuthorityAsync(uri.getAuthority());
+            updateAuthorityAsync(mUserId, uri.getAuthority());
         }
     }
 
@@ -184,18 +189,19 @@
         assert(mRecentsRoot.flags == (Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD));
         assert(mRecentsRoot.availableBytes == -1);
 
-        new UpdateTask(forceRefreshAll, null)
-                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        new UpdateTask(forceRefreshAll, null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
     }
 
-    public void updatePackageAsync(String packageName) {
-        new UpdateTask(false, packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    public void updatePackageAsync(UserId userId, String packageName) {
+        new UpdateTask(false, new UserPackage(userId, packageName)).executeOnExecutor(
+                AsyncTask.THREAD_POOL_EXECUTOR);
     }
 
-    public void updateAuthorityAsync(String authority) {
-        final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
+    public void updateAuthorityAsync(UserId userId, String authority) {
+        final ProviderInfo info = userId.getPackageManager(mContext).resolveContentProvider(
+                authority, 0);
         if (info != null) {
-            updatePackageAsync(info.packageName);
+            updatePackageAsync(userId, info.packageName);
         }
     }
 
@@ -215,7 +221,7 @@
      * Block until the first {@link UpdateTask} pass has finished.
      *
      * @return {@code true} if cached roots is ready to roll, otherwise
-     *         {@code false} if we timed out while waiting.
+     * {@code false} if we timed out while waiting.
      */
     private boolean waitForFirstLoad() {
         boolean success = false;
@@ -316,7 +322,7 @@
             final Bundle systemCache = resolver.getCache(rootsUri);
             if (systemCache != null) {
                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
-                assert(cachedRoots != null);
+                assert (cachedRoots != null);
                 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) {
                     if (VERBOSE) Log.v(TAG, "System cache hit for " + authority);
                     return cachedRoots;
@@ -464,8 +470,8 @@
 
     private class UpdateTask extends AsyncTask<Void, Void, Void> {
         private final boolean mForceRefreshAll;
-        private final String mForceRefreshPackage;
-        private final UserId mUserId = UserId.DEFAULT_USER;
+        @Nullable
+        private final UserPackage mForceRefreshUserPackage;
 
         private final Multimap<UserAuthority, RootInfo> mTaskRoots = ArrayListMultimap.create();
         private final HashSet<UserAuthority> mTaskStoppedAuthorities = new HashSet<>();
@@ -475,12 +481,12 @@
          *
          * @param forceRefreshAll when true, all previously cached values for
          *            all packages should be ignored.
-         * @param forceRefreshPackage when non-null, all previously cached
-         *            values for this specific package should be ignored.
+         * @param forceRefreshUserPackage when non-null, all previously cached
+         *            values for this specific user package should be ignored.
          */
-        public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) {
+        UpdateTask(boolean forceRefreshAll, @Nullable UserPackage forceRefreshUserPackage) {
             mForceRefreshAll = forceRefreshAll;
-            mForceRefreshPackage = forceRefreshPackage;
+            mForceRefreshUserPackage = forceRefreshUserPackage;
         }
 
         @Override
@@ -489,16 +495,17 @@
 
             mTaskRoots.put(new UserAuthority(mRecentsRoot.userId, mRecentsRoot.authority),
                     mRecentsRoot);
+            for (UserId userId : mUserIdManager.getUserIds()) {
+                final PackageManager pm = userId.getPackageManager(mContext);
 
-            final PackageManager pm = mUserId.getPackageManager(mContext);
-
-            // Pick up provider with action string
-            final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
-            final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
-            for (ResolveInfo info : providers) {
-                ProviderInfo providerInfo = info.providerInfo;
-                if (providerInfo.authority != null) {
-                    handleDocumentsProvider(providerInfo);
+                // Pick up provider with action string
+                final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
+                final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
+                for (ResolveInfo info : providers) {
+                    ProviderInfo providerInfo = info.providerInfo;
+                    if (providerInfo.authority != null) {
+                        handleDocumentsProvider(providerInfo, userId);
+                    }
                 }
             }
 
@@ -519,21 +526,21 @@
             return null;
         }
 
-        private void handleDocumentsProvider(ProviderInfo info) {
-            UserAuthority userAuthority = new UserAuthority(mUserId, info.authority);
+        private void handleDocumentsProvider(ProviderInfo info, UserId userId) {
+            UserAuthority userAuthority = new UserAuthority(userId, info.authority);
             // Ignore stopped packages for now; we might query them
             // later during UI interaction.
             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
                 if (VERBOSE) {
-                    Log.v(TAG,
-                            "Ignoring stopped authority " + info.authority + ", user " + mUserId);
+                    Log.v(TAG, "Ignoring stopped authority " + info.authority + ", user " + userId);
                 }
                 mTaskStoppedAuthorities.add(userAuthority);
                 return;
             }
 
             final boolean forceRefresh = mForceRefreshAll
-                    || Objects.equals(info.packageName, mForceRefreshPackage);
+                    || Objects.equals(new UserPackage(userId, info.packageName),
+                    mForceRefreshUserPackage);
             mTaskRoots.putAll(userAuthority, loadRootsForAuthority(userAuthority, forceRefresh));
         }
 
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 3af1473..1aee9be 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -16,8 +16,6 @@
 
 package com.android.documentsui.sidebar;
 
-import static androidx.core.util.Preconditions.checkNotNull;
-
 import static com.android.documentsui.base.Shared.compareToIgnoreCaseNullable;
 import static com.android.documentsui.base.SharedMinimal.DEBUG;
 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
@@ -64,6 +62,7 @@
 import com.android.documentsui.Injector.Injected;
 import com.android.documentsui.ItemDragListener;
 import com.android.documentsui.R;
+import com.android.documentsui.UserPackage;
 import com.android.documentsui.base.BooleanConsumer;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
@@ -367,8 +366,8 @@
                 handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
 
         final List<Item> rootList = new ArrayList<>();
-        final Map<UserPackageName, ResolveInfo> appsMapping = new HashMap<>();
-        final Map<UserPackageName, Item> appItems = new HashMap<>();
+        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
@@ -383,8 +382,8 @@
             final String packageName = info.activityInfo.packageName;
             if (!context.getPackageName().equals(packageName) &&
                     !TextUtils.equals(excludePackage, packageName)) {
-                UserPackageName userPackageName = new UserPackageName(userId, packageName);
-                appsMapping.put(userPackageName, info);
+                UserPackage userPackage = new UserPackage(userId, packageName);
+                appsMapping.put(userPackage, info);
 
                 // for change personal profile root.
                 if (PROFILE_TARGET_ACTIVITY.equals(info.activityInfo.targetActivity)) {
@@ -393,7 +392,7 @@
                 } else {
                     final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId,
                             mActionHandler);
-                    appItems.put(userPackageName, item);
+                    appItems.put(userPackage, item);
                     if (VERBOSE) Log.v(TAG, "Adding handler app: " + item);
                 }
             }
@@ -401,14 +400,14 @@
 
         // If there are some providers and apps has the same package name, combine them as one item.
         for (RootItem rootItem : otherProviders) {
-            final UserPackageName userPackageName = new UserPackageName(rootItem.userId,
+            final UserPackage userPackage = new UserPackage(rootItem.userId,
                     rootItem.getPackageName());
-            final ResolveInfo resolveInfo = appsMapping.get(userPackageName);
+            final ResolveInfo resolveInfo = appsMapping.get(userPackage);
 
             final Item item;
             if (resolveInfo != null) {
                 item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler);
-                appItems.remove(userPackageName);
+                appItems.remove(userPackage);
             } else {
                 item = rootItem;
             }
@@ -445,7 +444,7 @@
         // Update the information for Storage's root
         if (context != null) {
             DocumentsApplication.getProvidersCache(context).updateAuthorityAsync(
-                    Providers.AUTHORITY_STORAGE);
+                    UserId.DEFAULT_USER, Providers.AUTHORITY_STORAGE);
         }
         onDisplayStateChanged();
     }
@@ -635,38 +634,4 @@
     interface RootUpdater {
         void updateDocInfoForRoot(DocumentInfo doc);
     }
-
-    private static class UserPackageName {
-        final UserId userId;
-        final String packageName;
-
-        UserPackageName(UserId userId, String packageName) {
-            this.userId = checkNotNull(userId);
-            this.packageName = checkNotNull(packageName);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (o == null) {
-                return false;
-            }
-
-            if (this == o) {
-                return true;
-            }
-
-            if (o instanceof UserPackageName) {
-                UserPackageName other = (UserPackageName) o;
-                return Objects.equals(userId, other.userId)
-                        && Objects.equals(packageName, other.packageName);
-            }
-
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(userId, packageName);
-        }
-    }
 }
diff --git a/tests/unit/com/android/documentsui/UserIdManagerTest.java b/tests/unit/com/android/documentsui/UserIdManagerTest.java
new file mode 100644
index 0000000..78337a7
--- /dev/null
+++ b/tests/unit/com/android/documentsui/UserIdManagerTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.documentsui.base.UserId;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+public class UserIdManagerTest {
+
+    private final UserHandle systemUser = UserHandle.SYSTEM;
+    private final UserHandle managedUser1 = UserHandle.of(100);
+    private final UserHandle nonManagedUser1 = UserHandle.of(200);
+    private final UserHandle nonManagedUser2 = UserHandle.of(201);
+
+    private final Context mockContext = mock(Context.class);
+    private final UserManager mockUserManager = mock(UserManager.class);
+
+    private UserIdManager mUserIdManager;
+
+    @Before
+    public void setUp() throws Exception {
+        when(mockContext.getApplicationContext()).thenReturn(mockContext);
+
+        when(mockUserManager.isManagedProfile(managedUser1.getIdentifier())).thenReturn(true);
+        when(mockUserManager.isManagedProfile(systemUser.getIdentifier())).thenReturn(false);
+        when(mockUserManager.isManagedProfile(nonManagedUser1.getIdentifier())).thenReturn(false);
+        when(mockUserManager.isManagedProfile(nonManagedUser2.getIdentifier())).thenReturn(false);
+
+        when(mockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mockUserManager);
+    }
+
+    // common cases for getUserIds
+    @Test
+    public void testGetUserIds_systemUser_currentUserIsSystemUser() {
+        // Returns the current user if there is only system user.
+        UserId currentUser = UserId.of(systemUser);
+        initializeUserIdManager(currentUser, Arrays.asList(systemUser));
+        assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(systemUser));
+    }
+
+    @Test
+    public void testGetUserIds_systemUserAndManagedUser_currentUserIsSystemUser() {
+        // Returns the both if there are system and managed users.
+        UserId currentUser = UserId.of(systemUser);
+        initializeUserIdManager(currentUser, Arrays.asList(systemUser, managedUser1));
+        assertThat(mUserIdManager.getUserIds())
+                .containsExactly(UserId.of(systemUser), UserId.of(managedUser1)).inOrder();
+    }
+
+    @Test
+    public void testGetUserIds_systemUserAndManagedUser_currentUserIsManagedUser() {
+        // Returns the both if there are system and managed users.
+        UserId currentUser = UserId.of(managedUser1);
+        initializeUserIdManager(currentUser, Arrays.asList(systemUser, managedUser1));
+        assertThat(mUserIdManager.getUserIds())
+                .containsExactly(UserId.of(systemUser), UserId.of(managedUser1)).inOrder();
+    }
+
+    @Test
+    public void testGetUserIds_managedUserAndSystemUser_currentUserIsSystemUser() {
+        // Returns the both if there are system and managed users, regardless of input list order.
+        UserId currentUser = UserId.of(systemUser);
+        initializeUserIdManager(currentUser, Arrays.asList(managedUser1, systemUser));
+        assertThat(mUserIdManager.getUserIds())
+                .containsExactly(UserId.of(systemUser), UserId.of(managedUser1)).inOrder();
+    }
+
+    // other cases for getUserIds
+    @Test
+    public void testGetUserIds_NormalUser1AndNormalUser2_currentUserIsNormalUser2() {
+        // When there is no managed user, returns the current user.
+        UserId currentUser = UserId.of(nonManagedUser2);
+        initializeUserIdManager(currentUser, Arrays.asList(nonManagedUser1, nonManagedUser2));
+        assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(nonManagedUser2));
+    }
+
+    @Test
+    public void testGetUserIds_NormalUserAndManagedUser_currentUserIsNormalUser() {
+        // When there is no system user, returns the current user.
+        // This is a case theoretically can happen but we don't expect. So we return the current
+        // user only.
+        UserId currentUser = UserId.of(nonManagedUser1);
+        initializeUserIdManager(currentUser, Arrays.asList(nonManagedUser1, managedUser1));
+        assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(nonManagedUser1));
+    }
+
+    @Test
+    public void testGetUserIds_NormalUserAndManagedUser_currentUserIsManagedUser() {
+        // When there is no system user, returns the current user.
+        // This is a case theoretically can happen but we don't expect. So we return the current
+        // user only.
+        UserId currentUser = UserId.of(managedUser1);
+        initializeUserIdManager(currentUser, Arrays.asList(nonManagedUser1, managedUser1));
+        assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(managedUser1));
+    }
+
+    @Test
+    public void testGetUserIds_deviceNotSupported() {
+        // we should return the current user when device is not supported.
+        UserId currentUser = UserId.of(systemUser);
+        when(mockUserManager.getUserProfiles()).thenReturn(Arrays.asList(systemUser, managedUser1));
+        mUserIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, currentUser, false);
+        assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(systemUser));
+    }
+
+    @Test
+    public void testGetUserIds_deviceWithoutPermission() {
+        // This test only tests for Android R or later. This test case always passes before R.
+        if (BuildCompat.isAtLeastR()
+                || (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 30)) {
+            // When permission is denied, only returns the current user.
+            when(mockContext.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS))
+                    .thenReturn(PackageManager.PERMISSION_DENIED);
+            UserId currentUser = UserId.of(systemUser);
+            when(mockUserManager.getUserProfiles()).thenReturn(
+                    Arrays.asList(systemUser, managedUser1));
+            mUserIdManager = UserIdManager.create(mockContext);
+            assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(systemUser));
+        }
+    }
+
+    private void initializeUserIdManager(UserId current, List<UserHandle> usersOnDevice) {
+        when(mockUserManager.getUserProfiles()).thenReturn(usersOnDevice);
+        mUserIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, current, true);
+    }
+}
diff --git a/tests/unit/com/android/documentsui/roots/ProvidersAccessTest.java b/tests/unit/com/android/documentsui/roots/ProvidersAccessTest.java
index a52f2b6..46b5016 100644
--- a/tests/unit/com/android/documentsui/roots/ProvidersAccessTest.java
+++ b/tests/unit/com/android/documentsui/roots/ProvidersAccessTest.java
@@ -17,23 +17,45 @@
 package com.android.documentsui.roots;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.truth.Truth.assertThat;
 
 import android.provider.DocumentsContract;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import androidx.annotation.Nullable;
+
 import com.android.documentsui.base.Providers;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
+import com.android.documentsui.base.UserId;
 
 import com.google.common.collect.Lists;
+import com.google.common.truth.Correspondence;
 
 import java.util.List;
+import java.util.Objects;
 
 @SmallTest
 public class ProvidersAccessTest extends AndroidTestCase {
 
-    private static RootInfo mNull = new RootInfo();
+    private static final UserId OTHER_USER = UserId.of(UserId.DEFAULT_USER.getIdentifier() + 1);
+    private static final Correspondence<RootInfo, RootInfo> USER_ID_MIME_TYPES_CORRESPONDENCE =
+            new Correspondence<RootInfo, RootInfo>() {
+                @Override
+                public boolean compare(@Nullable RootInfo actual, @Nullable RootInfo expected) {
+                    return actual != null && expected != null
+                            && Objects.equals(actual.userId, expected.userId)
+                            && Objects.equals(actual.derivedMimeTypes, expected.derivedMimeTypes);
+                }
+
+                @Override
+                public String toString() {
+                    return "has same userId and mimeTypes as in";
+                }
+            };
+
+    private static RootInfo mNull = buildForMimeTypes((String[]) null);
     private static RootInfo mEmpty = buildForMimeTypes();
     private static RootInfo mWild = buildForMimeTypes("*/*");
     private static RootInfo mImages = buildForMimeTypes("image/*");
@@ -43,6 +65,7 @@
             "application/msword", "application/vnd.ms-excel");
     private static RootInfo mMalformed1 = buildForMimeTypes("meow");
     private static RootInfo mMalformed2 = buildForMimeTypes("*/meow");
+    private static RootInfo mImagesOtherUser = buildForMimeTypes(OTHER_USER, "image/*");
 
     private List<RootInfo> mRoots;
 
@@ -53,7 +76,8 @@
         super.setUp();
 
         mRoots = Lists.newArrayList(
-                mNull, mWild, mEmpty, mImages, mAudio, mDocs, mMalformed1, mMalformed2);
+                mNull, mWild, mEmpty, mImages, mAudio, mDocs, mMalformed1, mMalformed2,
+                mImagesOtherUser);
 
         mState = new State();
         mState.action = State.ACTION_OPEN;
@@ -62,7 +86,7 @@
     }
 
     public void testMatchingRoots_Everything() throws Exception {
-        mState.acceptMimes = new String[] { "*/*" };
+        mState.acceptMimes = new String[]{"*/*"};
         assertContainsExactly(
                 newArrayList(mNull, mWild, mImages, mAudio, mDocs, mMalformed1, mMalformed2),
                 ProvidersAccess.getMatchingRoots(mRoots, mState));
@@ -117,6 +141,14 @@
                 ProvidersAccess.getMatchingRoots(mRoots, mState));
     }
 
+    public void testMatchingRoots_FlacOrPng_crossProfile() throws Exception {
+        mState.action = State.ACTION_GET_CONTENT;
+        mState.acceptMimes = new String[]{"application/x-flac", "image/png"};
+        assertContainsExactly(
+                newArrayList(mNull, mWild, mAudio, mImages, mImagesOtherUser),
+                ProvidersAccess.getMatchingRoots(mRoots, mState));
+    }
+
     public void testDefaultRoot() {
         mState.acceptMimes = new String[] { "*/*" };
         assertNull(ProvidersAccess.getDefaultRoot(mRoots, mState));
@@ -145,6 +177,7 @@
         // Set up some roots
         for (int i = 0; i < 5; ++i) {
             RootInfo root = new RootInfo();
+            root.userId = UserId.DEFAULT_USER;
             root.authority = "authority" + i;
             roots.add(root);
         }
@@ -164,15 +197,19 @@
             ProvidersAccess.getMatchingRoots(roots, mState));
     }
 
-    private static void assertContainsExactly(List<?> expected, List<?> actual) {
-        assertEquals(expected.size(), actual.size());
-        for (Object o : expected) {
-            assertTrue(actual.contains(o));
-        }
+    private static void assertContainsExactly(List<RootInfo> expected, List<RootInfo> actual) {
+        assertThat(actual)
+                .comparingElementsUsing(USER_ID_MIME_TYPES_CORRESPONDENCE)
+                .containsExactlyElementsIn(expected);
     }
 
     private static RootInfo buildForMimeTypes(String... mimeTypes) {
+        return buildForMimeTypes(UserId.DEFAULT_USER, mimeTypes);
+    }
+
+    private static RootInfo buildForMimeTypes(UserId userId, String... mimeTypes) {
         final RootInfo root = new RootInfo();
+        root.userId = userId;
         root.derivedMimeTypes = mimeTypes;
         return root;
     }