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;
}