diff options
author | 2020-01-31 00:58:19 +0000 | |
---|---|---|
committer | 2020-02-10 14:07:39 +0000 | |
commit | c8b0001f754711857074ce7355372eea81cf0eb5 (patch) | |
tree | 82af23ca68b69a265f8fcc8462e8b039bc940764 | |
parent | cfb64adb61442b51b48bd89c3338109539f8020a (diff) |
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
-rw-r--r-- | src/com/android/documentsui/BaseActivity.java | 2 | ||||
-rw-r--r-- | src/com/android/documentsui/DocumentsApplication.java | 12 | ||||
-rw-r--r-- | src/com/android/documentsui/DrawerController.java | 3 | ||||
-rw-r--r-- | src/com/android/documentsui/UserIdManager.java | 151 | ||||
-rw-r--r-- | src/com/android/documentsui/UserPackage.java | 60 | ||||
-rw-r--r-- | src/com/android/documentsui/base/State.java | 7 | ||||
-rw-r--r-- | src/com/android/documentsui/base/UserId.java | 25 | ||||
-rw-r--r-- | src/com/android/documentsui/roots/ProvidersAccess.java | 7 | ||||
-rw-r--r-- | src/com/android/documentsui/roots/ProvidersCache.java | 71 | ||||
-rw-r--r-- | src/com/android/documentsui/sidebar/RootsFragment.java | 55 | ||||
-rw-r--r-- | tests/unit/com/android/documentsui/UserIdManagerTest.java | 161 | ||||
-rw-r--r-- | tests/unit/com/android/documentsui/roots/ProvidersAccessTest.java | 53 |
12 files changed, 513 insertions, 94 deletions
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index fe903a4e1..f0bf52d98 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -97,6 +97,7 @@ public abstract class BaseActivity protected Injector<?> mInjector; protected ProvidersCache mProviders; + protected UserIdManager mUserIdManager; protected DocumentsAccess mDocs; protected DrawerController mDrawer; @@ -154,6 +155,7 @@ public abstract class BaseActivity 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 65204b7e6..fb479ac02 100644 --- a/src/com/android/documentsui/DocumentsApplication.java +++ b/src/com/android/documentsui/DocumentsApplication.java @@ -31,6 +31,7 @@ import android.text.format.DateUtils; 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 @@ public class DocumentsApplication extends Application { 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 @@ public class DocumentsApplication extends Application { 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 @@ public class DocumentsApplication extends Application { 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 @@ public class DocumentsApplication extends Application { 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 682242244..7ed8d51f2 100644 --- a/src/com/android/documentsui/DrawerController.java +++ b/src/com/android/documentsui/DrawerController.java @@ -30,6 +30,7 @@ import androidx.legacy.app.ActionBarDrawerToggle; 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 @@ public abstract class DrawerController implements DrawerListener { 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 000000000..d489712af --- /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 000000000..9d3fb8e5f --- /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 0a575794a..489d1b70f 100644 --- a/src/com/android/documentsui/base/State.java +++ b/src/com/android/documentsui/base/State.java @@ -139,6 +139,13 @@ public class State implements android.os.Parcelable { 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 f1d0cdc2c..89a38d26c 100644 --- a/src/com/android/documentsui/base/UserId.java +++ b/src/com/android/documentsui/base/UserId.java @@ -24,6 +24,7 @@ import android.content.pm.PackageManager; 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 @@ import java.util.Objects; 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 @@ public final class UserId { /** * 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 @@ public final class UserId { } /** + * 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 822ce4ddc..7c933f10b 100644 --- a/src/com/android/documentsui/roots/ProvidersAccess.java +++ b/src/com/android/documentsui/roots/ProvidersAccess.java @@ -118,6 +118,13 @@ public interface ProvidersAccess { 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 cf822dafe..b474d3473 100644 --- a/src/com/android/documentsui/roots/ProvidersCache.java +++ b/src/com/android/documentsui/roots/ProvidersCache.java @@ -51,6 +51,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; 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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { @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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { 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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { 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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { * 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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { 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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { 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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { * * @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 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { mTaskRoots.put(new UserAuthority(mRecentsRoot.userId, mRecentsRoot.authority), mRecentsRoot); - - 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); + for (UserId userId : mUserIdManager.getUserIds()) { + final PackageManager pm = userId.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, userId); + } } } @@ -519,21 +526,21 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { 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 3af14731d..1aee9be9b 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; 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 @@ public class RootsFragment extends Fragment { 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 @@ public class RootsFragment extends Fragment { 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 @@ public class RootsFragment extends Fragment { } 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 @@ public class RootsFragment extends Fragment { // 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 @@ public class RootsFragment extends Fragment { // 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 @@ public class RootsFragment extends Fragment { 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 000000000..78337a7e2 --- /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 a52f2b6cf..46b501650 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 @@ public class ProvidersAccessTest extends AndroidTestCase { "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 @@ public class ProvidersAccessTest extends AndroidTestCase { 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 class ProvidersAccessTest extends AndroidTestCase { } 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 @@ public class ProvidersAccessTest extends AndroidTestCase { 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 @@ public class ProvidersAccessTest extends AndroidTestCase { // 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 @@ public class ProvidersAccessTest extends AndroidTestCase { 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; } |