diff options
author | 2023-12-07 05:42:25 +0000 | |
---|---|---|
committer | 2024-02-01 07:07:10 +0000 | |
commit | d5f27a5ff886cc5ec7c40508d3d2e3f64b849e4d (patch) | |
tree | cf6ca969236a56bed32b770b0b84df7ed665d74b | |
parent | b1a45a549cb2891bd86c49851fec8ad32cbebc84 (diff) |
Introducing private tab in profile tabs and sidebar.
Updated ProfileTabs.java to user UserManagerState to get list of userIds
for which the tabs have to created, when the feature flag for private
space is enabled. RootsFragment and other associated classes have also
been updated to display private profile (or any other new profile)
section in the sidebar.
Bug: 309754864
Test: atest unit tests and manual testing
Change-Id: I130cda26346f7df81fabbb9c9f1ba92b1b859929
Merged-In: I130cda26346f7df81fabbb9c9f1ba92b1b859929
16 files changed, 838 insertions, 127 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index 4407a62b2..e348d6b72 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -119,6 +119,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA public void registerDisplayStateChangedListener(Runnable l) { mDisplayStateChangedListener = l; } + @Override public void unregisterDisplayStateChangedListener(Runnable l) { if (mDisplayStateChangedListener == l) { @@ -135,12 +136,12 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA Lookup<String, Executor> executors, Injector<?> injector) { - assert(activity != null); - assert(state != null); - assert(providers != null); - assert(searchMgr != null); - assert(docs != null); - assert(injector != null); + assert (activity != null); + assert (state != null); + assert (providers != null); + assert (searchMgr != null); + assert (docs != null); + assert (injector != null); mActivity = activity; mState = state; @@ -359,7 +360,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA @Override public void openContainerDocument(DocumentInfo doc) { - assert(doc.isContainer()); + assert (doc.isContainer()); if (mSearchMgr.isSearching()) { loadDocument( @@ -372,6 +373,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } // TODO: Make this private and make tests call interface method instead. + /** * Behavior when a document is opened. */ @@ -620,7 +622,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA currentDoc = mDocs.getArchiveDocument(doc.derivedUri, doc.userId); } - assert(currentDoc != null); + assert (currentDoc != null); if (currentDoc.equals(mState.stack.peek())) { Log.w(TAG, "This DocumentInfo is already in current DocumentsStack"); return; @@ -676,8 +678,8 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA ComponentName component = new ComponentName( mActivity.getPackageName(), Shared.LAUNCHER_TARGET_CLASS); pm.setComponentEnabledSetting(component, enalbled - ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED - : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED + : PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } } @@ -714,7 +716,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA mDocs, userId, callback - ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); + ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri); } @Override @@ -826,8 +828,8 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA private void onRootLoaded(@Nullable RootInfo root) { boolean invalidRootForAction = (root != null - && !root.supportsChildren() - && mState.action == State.ACTION_OPEN_TREE); + && !root.supportsChildren() + && mState.action == State.ACTION_OPEN_TREE); if (invalidRootForAction) { loadDeviceRoot(); @@ -942,8 +944,8 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA if (DEBUG) { Log.d(TAG, - "Creating new directory loader for: " - + DocumentInfo.debugString(mState.stack.peek())); + "Creating new directory loader for: " + + DocumentInfo.debugString(mState.stack.peek())); } return new DirectoryLoader( @@ -961,9 +963,9 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { if (DEBUG) { Log.d(TAG, "Loader has finished for: " - + DocumentInfo.debugString(mState.stack.peek())); + + DocumentInfo.debugString(mState.stack.peek())); } - assert(result != null); + assert (result != null); mInjector.getModel().update(result); mLoaderSemaphore.release(); @@ -974,24 +976,34 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA mLoaderSemaphore.release(); } } + /** * A class primarily for the support of isolating our tests * from our concrete activity implementations. */ public interface CommonAddons { void restoreRootAndDirectory(); + void refreshCurrentRootAndDirectory(@AnimationType int anim); + void onRootPicked(RootInfo root); + // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity. void onDocumentsPicked(List<DocumentInfo> docs); + void onDocumentPicked(DocumentInfo doc); + RootInfo getCurrentRoot(); + DocumentInfo getCurrentDirectory(); + UserId getSelectedUser(); + /** * Check whether current directory is root of recent. */ boolean isInRecents(); + void setRootsDrawerOpen(boolean open); /** diff --git a/src/com/android/documentsui/MultiRootDocumentsLoader.java b/src/com/android/documentsui/MultiRootDocumentsLoader.java index 7668b0693..f450814b9 100644 --- a/src/com/android/documentsui/MultiRootDocumentsLoader.java +++ b/src/com/android/documentsui/MultiRootDocumentsLoader.java @@ -196,11 +196,11 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory final FilteringCursorWrapper filteredCursor = new FilteringCursorWrapper(cursor) { - @Override - public void close() { - // Ignored, since we manage cursor lifecycle internally - } - }; + @Override + public void close() { + // Ignored, since we manage cursor lifecycle internally + } + }; filteredCursor.filterHiddenFiles(mState.showHiddenFiles); filteredCursor.filterMimes(mState.acceptMimes, getRejectMimes()); filteredCursor.filterLastModified(rejectBefore); diff --git a/src/com/android/documentsui/ProfileTabs.java b/src/com/android/documentsui/ProfileTabs.java index ded78e031..43c12e4cf 100644 --- a/src/com/android/documentsui/ProfileTabs.java +++ b/src/com/android/documentsui/ProfileTabs.java @@ -23,6 +23,7 @@ import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB; import android.app.admin.DevicePolicyManager; import android.os.Build; +import android.os.UserManager; import android.view.View; import android.view.ViewGroup; @@ -38,8 +39,10 @@ import com.android.modules.utils.build.SdkLevel; import com.google.android.material.tabs.TabLayout; import com.google.common.base.Objects; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; /** * A manager class to control UI on a {@link TabLayout} for cross-profile purpose. @@ -114,7 +117,7 @@ public class ProfileTabs implements ProfileTabsAddons { } /** - * Update the tab layout based on conditions. + * Update the tab layout based on status of availability of the hidden profile. */ public void updateView() { updateTabsIfNeeded(); @@ -124,8 +127,13 @@ public class ProfileTabs implements ProfileTabsAddons { // Update the layout according to the current root if necessary. // Make sure we do not invoke callback. Otherwise, it is likely to cause infinite loop. mTabs.removeOnTabSelectedListener(mOnTabSelectedListener); - mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(currentRoot.userId))); - mTabs.addOnTabSelectedListener(mOnTabSelectedListener); + if (!mUserIds.contains(currentRoot.userId)) { + mTabs.addOnTabSelectedListener(mOnTabSelectedListener); + mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(UserId.CURRENT_USER))); + } else { + mTabs.selectTab(mTabs.getTabAt(mUserIds.indexOf(currentRoot.userId))); + mTabs.addOnTabSelectedListener(mOnTabSelectedListener); + } } mTabsContainer.setVisibility(shouldShow() ? View.VISIBLE : View.GONE); @@ -172,16 +180,15 @@ public class ProfileTabs implements ProfileTabsAddons { // Given that mUserIds was initialized with only the current user, if getUserIds() // returns just the current user, we don't need to do anything on the tab layout. if (!userIds.equals(mUserIds)) { - mUserIds = userIds; + mUserIds = new ArrayList<>(); + mUserIds.addAll(userIds); mTabs.removeAllTabs(); if (mUserIds.size() > 1) { - // set setSelected to false otherwise it will trigger callback. - mTabs.addTab(createTab( - getEnterpriseString(PERSONAL_TAB, R.string.personal_tab), - mUserIdManager.getSystemUser()), /* setSelected= */false); - mTabs.addTab(createTab( - getEnterpriseString(WORK_TAB, R.string.work_tab), - mUserIdManager.getManagedUser()), /* setSelected= */false); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + addTabsPrivateSpaceEnabled(); + } else { + addTabsPrivateSpaceDisabled(); + } } } } @@ -195,6 +202,28 @@ public class ProfileTabs implements ProfileTabsAddons { return mUserIdManager.getUserIds(); } + private void addTabsPrivateSpaceEnabled() { + // set setSelected to false otherwise it will trigger callback. + assert mUserManagerState != null; + Map<UserId, String> userIdToLabelMap = mUserManagerState.getUserIdToLabelMap(); + UserManager userManager = mTabsContainer.getContext().getSystemService(UserManager.class); + assert userManager != null; + for (UserId userId : mUserIds) { + mTabs.addTab(createTab(userIdToLabelMap.get(userId), userId), /* setSelected= */false); + } + } + + private void addTabsPrivateSpaceDisabled() { + // set setSelected to false otherwise it will trigger callback. + assert mUserIdManager != null; + mTabs.addTab(createTab( + getEnterpriseString(PERSONAL_TAB, R.string.personal_tab), + mUserIdManager.getSystemUser()), /* setSelected= */false); + mTabs.addTab(createTab( + getEnterpriseString(WORK_TAB, R.string.work_tab), + mUserIdManager.getManagedUser()), /* setSelected= */false); + } + private String getEnterpriseString(String updatableStringId, int defaultStringId) { if (SdkLevel.isAtLeastT()) { return getUpdatableEnterpriseString(updatableStringId, defaultStringId); diff --git a/src/com/android/documentsui/UserManagerState.java b/src/com/android/documentsui/UserManagerState.java index c67cc702e..50858f389 100644 --- a/src/com/android/documentsui/UserManagerState.java +++ b/src/com/android/documentsui/UserManagerState.java @@ -242,10 +242,12 @@ public interface UserManagerState { if (userHandle.getIdentifier() == ActivityManager.getCurrentUser()) { result.add(UserId.of(userHandle)); } else { - final UserProperties userProperties = - mUserManager.getUserProperties(userHandle); - if (userProperties.getShowInSharingSurfaces() - == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { + // Out of all the profiles returned by user manager the profiles that are + // returned should satisfy both the following conditions: + // 1. It has user property SHOW_IN_SHARING_SURFACES_SEPARATE + // 2. Quite mode is not enabled, if it is enabled then the profile's user + // property is not SHOW_IN_QUIET_MODE_HIDDEN + if (isProfileAllowed(userHandle)) { result.add(UserId.of(userHandle)); } } @@ -255,6 +257,19 @@ public interface UserManagerState { } } + @SuppressLint("NewApi") + private boolean isProfileAllowed(UserHandle userHandle) { + final UserProperties userProperties = + mUserManager.getUserProperties(userHandle); + if (userProperties.getShowInSharingSurfaces() + == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { + return !UserId.of(userHandle).isQuietModeEnabled(mContext) + || userProperties.getShowInQuietMode() + != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + } + return false; + } + private void getUserIdsInternalPreV(List<UserHandle> userProfiles, List<UserId> result) { result.add(mCurrentUser); UserId systemUser = null; diff --git a/src/com/android/documentsui/base/UserId.java b/src/com/android/documentsui/base/UserId.java index 642ebe636..21842917a 100644 --- a/src/com/android/documentsui/base/UserId.java +++ b/src/com/android/documentsui/base/UserId.java @@ -149,8 +149,8 @@ public final class UserId { * Returns true if the this user is in quiet mode. */ public boolean isQuietModeEnabled(Context context) { - final UserManager userManager = - (UserManager) context.getSystemService(Context.USER_SERVICE); + final UserManager userManager = context.getSystemService(UserManager.class); + assert userManager != null; return userManager.isQuietModeEnabled(mUserHandle); } diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java index 7c09811c9..746a069a7 100644 --- a/src/com/android/documentsui/files/FilesActivity.java +++ b/src/com/android/documentsui/files/FilesActivity.java @@ -61,6 +61,7 @@ import com.android.documentsui.services.FileOperationService; import com.android.documentsui.sidebar.RootsFragment; import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.MessageBuilder; +import com.android.documentsui.util.FeatureFlagUtils; import java.util.ArrayList; import java.util.List; @@ -83,8 +84,15 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler } // make these methods visible in this package to work around compiler bug http://b/62218600 - @Override protected boolean focusSidebar() { return super.focusSidebar(); } - @Override protected boolean popDir() { return super.popDir(); } + @Override + protected boolean focusSidebar() { + return super.focusSidebar(); + } + + @Override + protected boolean popDir() { + return super.popDir(); + } @Override public void onCreate(Bundle icicle) { @@ -157,8 +165,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler mInjector.selectionMgr, mProfileTabsAddonsStub); - mAppsRowManager = new AppsRowManager(mInjector.actions, mState.supportsCrossProfile(), - mUserIdManager); + mAppsRowManager = getAppsRowManager(); mInjector.appsRowManager = mAppsRowManager; mActivityInputHandler = @@ -194,6 +201,14 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler presentFileErrors(icicle, intent); } + private AppsRowManager getAppsRowManager() { + return FeatureFlagUtils.isPrivateSpaceEnabled() + ? new AppsRowManager(mInjector.actions, mState.supportsCrossProfile(), + mUserManagerState) + : new AppsRowManager(mInjector.actions, mState.supportsCrossProfile(), + mUserIdManager); + } + // This is called in the intent contains label and icon resources. // When that is true, the launcher activity has supplied them so we // can adapt our presentation to how we were launched. @@ -207,11 +222,11 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler // called Downloads, which is also not the desired behavior. private void updateTaskDescription(final Intent intent) { int labelRes = intent.getIntExtra(LauncherActivity.TASK_LABEL_RES, -1); - assert(labelRes > -1); + assert (labelRes > -1); String label = getResources().getString(labelRes); int iconRes = intent.getIntExtra(LauncherActivity.TASK_ICON_RES, -1); - assert(iconRes > -1); + assert (iconRes > -1); setTaskDescription(new TaskDescription(label, iconRes)); } @@ -244,14 +259,14 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler final Intent intent = getIntent(); // This is a remnant of old logic where we used to initialize accept MIME types in - // BaseActivity. ProvidersAccess still rely on this being correctly initialized so we still have - // to initialize it in FilesActivity. + // BaseActivity. ProvidersAccess still rely on this being correctly initialized, so we + // still have to initialize it in FilesActivity. state.initAcceptMimes(intent, "*/*"); state.action = State.ACTION_BROWSE; state.allowMultiple = true; // Options specific to the DocumentsActivity. - assert(!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY)); + assert (!intent.hasExtra(Intent.EXTRA_LOCAL_ONLY)); } @Override @@ -338,7 +353,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler final RootInfo root = getCurrentRoot(); final DocumentInfo cwd = getCurrentDirectory(); - assert(!mSearchManager.isSearching()); + assert (!mSearchManager.isSearching()); if (mState.stack.isRecents()) { DirectoryFragment.showRecentsOpen(fm, anim); @@ -360,7 +375,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler @Override public void onDirectoryCreated(DocumentInfo doc) { - assert(doc.isDirectory()); + assert (doc.isDirectory()); mInjector.focusManager.focusDocument(doc.documentId); } diff --git a/src/com/android/documentsui/roots/ProvidersCache.java b/src/com/android/documentsui/roots/ProvidersCache.java index 15f346bf4..e822325b4 100644 --- a/src/com/android/documentsui/roots/ProvidersCache.java +++ b/src/com/android/documentsui/roots/ProvidersCache.java @@ -138,7 +138,10 @@ public class ProvidersCache implements ProvidersAccess, LookupApplicationName { mUserManagerState = userManagerState; } - private RootInfo generateRecentsRoot(UserId rootUserId) { + /** + * Generates recent root for the provided user id + */ + public RootInfo generateRecentsRoot(UserId rootUserId) { return new RootInfo() {{ // Special root for recents userId = rootUserId; diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java index f3f238e84..27ac4f55c 100644 --- a/src/com/android/documentsui/sidebar/RootsFragment.java +++ b/src/com/android/documentsui/sidebar/RootsFragment.java @@ -432,30 +432,53 @@ public class RootsFragment extends Fragment { final List<Item> rootList = new ArrayList<>(); final List<Item> rootListOtherUser = new ArrayList<>(); + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + for (int i = 0; i < userIds.size(); ++i) { + rootListAllUsers.add(new ArrayList<>()); + } + mApplicationItemList = new ArrayList<>(); if (handlerAppIntent != null) { includeHandlerApps(state, handlerAppIntent, excludePackage, rootList, rootListOtherUser, - otherProviders, userIds, maybeShowBadge); + rootListAllUsers, otherProviders, userIds, maybeShowBadge); } else { // Only add providers - Collections.sort(otherProviders, comp); + otherProviders.sort(comp); for (RootItem item : otherProviders) { - if (UserId.CURRENT_USER.equals(item.userId)) { - rootList.add(item); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + createRootListsPrivateSpaceEnabled(item, userIds, rootListAllUsers); } else { - rootListOtherUser.add(item); + createRootListsPrivateSpaceDisabled(item, rootList, rootListOtherUser); } mApplicationItemList.add(item); } } - List<Item> presentableList = new UserItemsCombiner( - context.getResources(), context.getSystemService(DevicePolicyManager.class), state) + List<Item> presentableList = + FeatureFlagUtils.isPrivateSpaceEnabled() ? getPresentableListPrivateSpaceEnabled( + context, state, rootListAllUsers, userIds) : + getPresentableListPrivateSpaceDisabled(context, state, rootList, + rootListOtherUser); + addListToResult(result, presentableList); + return result; + } + + private List<Item> getPresentableListPrivateSpaceEnabled(Context context, State state, + List<List<Item>> rootListAllUsers, List<UserId> userIds) { + return new UserItemsCombiner(context.getResources(), + context.getSystemService(DevicePolicyManager.class), state) + .setRootListForAllUsers(rootListAllUsers) + .createPresentableListForAllUsers(userIds, + DocumentsApplication.getUserManagerState(context).getUserIdToLabelMap()); + } + + private List<Item> getPresentableListPrivateSpaceDisabled(Context context, State state, + List<Item> rootList, List<Item> rootListOtherUser) { + return new UserItemsCombiner(context.getResources(), + context.getSystemService(DevicePolicyManager.class), state) .setRootListForCurrentUser(rootList) .setRootListForOtherUser(rootListOtherUser) .createPresentableList(); - addListToResult(result, presentableList); - return result; } private void addListToResult(List<Item> result, List<Item> rootList) { @@ -471,8 +494,8 @@ public class RootsFragment extends Fragment { */ private void includeHandlerApps(State state, Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList, - List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds, - boolean maybeShowBadge) { + List<Item> rootListOtherUser, List<List<Item>> rootListAllUsers, + List<RootItem> otherProviders, List<UserId> userIds, boolean maybeShowBadge) { if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent); Context context = getContext(); @@ -525,29 +548,49 @@ public class RootsFragment extends Fragment { item = rootItem; } - if (UserId.CURRENT_USER.equals(item.userId)) { - if (VERBOSE) Log.v(TAG, "Adding provider : " + item); - rootList.add(item); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + createRootListsPrivateSpaceEnabled(item, userIds, rootListAllUsers); } else { - if (VERBOSE) Log.v(TAG, "Adding provider to other users : " + item); - rootListOtherUser.add(item); + createRootListsPrivateSpaceDisabled(item, rootList, rootListOtherUser); } } for (Item item : appItems.values()) { - if (UserId.CURRENT_USER.equals(item.userId)) { - rootList.add(item); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + createRootListsPrivateSpaceEnabled(item, userIds, rootListAllUsers); } else { - rootListOtherUser.add(item); + createRootListsPrivateSpaceDisabled(item, rootList, rootListOtherUser); } } final String preferredRootPackage = getResources().getString( R.string.preferred_root_package, ""); final ItemComparator comp = new ItemComparator(preferredRootPackage); - Collections.sort(rootList, comp); - Collections.sort(rootListOtherUser, comp); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + addToApplicationItemListPrivateSpaceEnabled(userIds, rootListAllUsers, comp, state); + } else { + addToApplicationItemListPrivateSpaceDisabled(rootList, rootListOtherUser, comp, state); + } + + } + + private void addToApplicationItemListPrivateSpaceEnabled(List<UserId> userIds, + List<List<Item>> rootListAllUsers, ItemComparator comp, State state) { + for (int i = 0; i < userIds.size(); ++i) { + rootListAllUsers.get(i).sort(comp); + if (UserId.CURRENT_USER.equals(userIds.get(i))) { + mApplicationItemList.addAll(rootListAllUsers.get(i)); + } else if (state.supportsCrossProfile && state.canInteractWith(userIds.get(i))) { + mApplicationItemList.addAll(rootListAllUsers.get(i)); + } + } + } + + private void addToApplicationItemListPrivateSpaceDisabled(List<Item> rootList, + List<Item> rootListOtherUser, ItemComparator comp, State state) { + rootList.sort(comp); + rootListOtherUser.sort(comp); if (state.supportsCrossProfile() && state.canShareAcrossProfile) { mApplicationItemList.addAll(rootList); mApplicationItemList.addAll(rootListOtherUser); @@ -556,6 +599,25 @@ public class RootsFragment extends Fragment { } } + private void createRootListsPrivateSpaceEnabled(Item item, List<UserId> userIds, + List<List<Item>> rootListAllUsers) { + for (int i = 0; i < userIds.size(); ++i) { + if (userIds.get(i).equals(item.userId)) { + rootListAllUsers.get(i).add(item); + break; + } + } + } + + private void createRootListsPrivateSpaceDisabled(Item item, List<Item> rootList, + List<Item> rootListOtherUser) { + if (UserId.CURRENT_USER.equals(item.userId)) { + rootList.add(item); + } else { + rootListOtherUser.add(item); + } + } + @Override public void onResume() { super.onResume(); diff --git a/src/com/android/documentsui/sidebar/UserItemsCombiner.java b/src/com/android/documentsui/sidebar/UserItemsCombiner.java index 1a68ca778..564a51127 100644 --- a/src/com/android/documentsui/sidebar/UserItemsCombiner.java +++ b/src/com/android/documentsui/sidebar/UserItemsCombiner.java @@ -36,6 +36,7 @@ import com.android.modules.utils.build.SdkLevel; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Converts user-specific lists of items into a single merged list appropriate for displaying in the @@ -49,6 +50,7 @@ class UserItemsCombiner { private final State mState; private List<Item> mRootList; private List<Item> mRootListOtherUser; + private List<List<Item>> mRootListAllUsers; UserItemsCombiner(Resources resources, DevicePolicyManager dpm, State state) { mCurrentUser = UserId.CURRENT_USER; @@ -73,6 +75,11 @@ class UserItemsCombiner { return this; } + UserItemsCombiner setRootListForAllUsers(List<List<Item>> listOfRootLists) { + mRootListAllUsers = checkNotNull(listOfRootLists); + return this; + } + /** * Returns a combined lists from the provided lists. {@link HeaderItem}s indicating profile * will be added if the list of current user and the other user are not empty. @@ -109,6 +116,52 @@ class UserItemsCombiner { return result; } + public List<Item> createPresentableListForAllUsers(List<UserId> userIds, + Map<UserId, String> userIdToLabelMap) { + + checkArgument(mRootListAllUsers != null, "RootListForAllUsers is not set"); + + final List<Item> result = new ArrayList<>(); + if (mState.supportsCrossProfile()) { + // headerItemList will hold headers for userIds that are accessible, and + final List<Item> headerItemList = new ArrayList<>(); + int accessibleProfilesCount = 0; + for (int i = 0; i < userIds.size(); ++i) { + // The received user id list contains all users present on the device, + // the headerItemList will contain header item or null at the same index as + // the user id in the received list + if (mState.canInteractWith(userIds.get(i)) + && !mRootListAllUsers.get(i).isEmpty()) { + accessibleProfilesCount += 1; + headerItemList.add(new HeaderItem(userIdToLabelMap.get(userIds.get(i)))); + } else { + headerItemList.add(null); + } + } + // Do not add header item if: + // 1. only the current profile is accessible + // 2. only one profile has non-empty root item list + if (accessibleProfilesCount == 1) { + for (int i = 0; i < userIds.size(); ++i) { + if (headerItemList.get(i) == null) continue; + result.addAll(mRootListAllUsers.get(i)); + break; + } + } else { + for (int i = 0; i < userIds.size(); ++i) { + // Since the header item and the corresponding accessible user id share the same + // index we add the user id along with its non-null header to the result. + if (headerItemList.get(i) == null) continue; + result.add(headerItemList.get(i)); + result.addAll(mRootListAllUsers.get(i)); + } + } + } else { + result.addAll(mRootListAllUsers.get(userIds.indexOf(mCurrentUser))); + } + return result; + } + private String getEnterpriseString(String updatableStringId, int defaultStringId) { if (SdkLevel.isAtLeastT()) { return getUpdatableEnterpriseString(updatableStringId, defaultStringId); diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java index eb321fac0..5aa3faace 100644 --- a/tests/common/com/android/documentsui/TestActivity.java +++ b/tests/common/com/android/documentsui/TestActivity.java @@ -260,6 +260,14 @@ public abstract class TestActivity extends AbstractBase { } @Override + public final String getSystemServiceName(Class<?> serviceName) { + if (serviceName == UserManager.class) { + return Context.USER_SERVICE; + } + throw new IllegalArgumentException("Unknown service name " + serviceName); + } + + @Override public final void finish() { finishedHandler.accept(null); } diff --git a/tests/common/com/android/documentsui/testing/TestProvidersAccess.java b/tests/common/com/android/documentsui/testing/TestProvidersAccess.java index 4fae7a715..554c42c7d 100644 --- a/tests/common/com/android/documentsui/testing/TestProvidersAccess.java +++ b/tests/common/com/android/documentsui/testing/TestProvidersAccess.java @@ -229,6 +229,67 @@ public class TestProvidersAccess implements ProvidersAccess { } } + public static class AnotherUser { + public static final UserHandle USER_HANDLE = UserHandle.of( + TestProvidersAccess.USER_ID.getIdentifier() + 2); + public static final UserId USER_ID = UserId.of(AnotherUser.USER_HANDLE); + + public static final RootInfo DOWNLOADS; + public static final RootInfo HOME; + public static final RootInfo IMAGE; + public static final RootInfo PICKLES; + public static final RootInfo MTP_ROOT; + + static { + UserId userId = AnotherUser.USER_ID; + + DOWNLOADS = new RootInfo(); + DOWNLOADS.userId = userId; + DOWNLOADS.authority = Providers.AUTHORITY_DOWNLOADS; + DOWNLOADS.rootId = Providers.ROOT_ID_DOWNLOADS; + DOWNLOADS.title = "Downloads"; + DOWNLOADS.derivedType = RootInfo.TYPE_DOWNLOADS; + DOWNLOADS.flags = Root.FLAG_LOCAL_ONLY + | Root.FLAG_SUPPORTS_CREATE + | Root.FLAG_SUPPORTS_RECENTS; + + HOME = new RootInfo(); + HOME.userId = userId; + HOME.authority = Providers.AUTHORITY_STORAGE; + HOME.rootId = Providers.ROOT_ID_HOME; + HOME.title = "Home"; + HOME.derivedType = RootInfo.TYPE_LOCAL; + HOME.flags = Root.FLAG_LOCAL_ONLY + | Root.FLAG_SUPPORTS_CREATE + | Root.FLAG_SUPPORTS_IS_CHILD + | Root.FLAG_SUPPORTS_RECENTS; + + IMAGE = new RootInfo(); + IMAGE.userId = userId; + IMAGE.authority = Providers.AUTHORITY_MEDIA; + IMAGE.rootId = Providers.ROOT_ID_IMAGES; + IMAGE.title = "Images"; + IMAGE.derivedType = RootInfo.TYPE_IMAGES; + + PICKLES = new RootInfo(); + PICKLES.userId = userId; + PICKLES.authority = "yummies"; + PICKLES.rootId = "pickles"; + PICKLES.title = "Pickles"; + PICKLES.summary = "Yummy pickles"; + + MTP_ROOT = new RootInfo(); + MTP_ROOT.userId = userId; + MTP_ROOT.authority = Providers.AUTHORITY_MTP; + MTP_ROOT.rootId = Providers.ROOT_ID_DOCUMENTS; + MTP_ROOT.title = "MTP"; + MTP_ROOT.derivedType = RootInfo.TYPE_MTP; + MTP_ROOT.flags = Root.FLAG_SUPPORTS_CREATE + | Root.FLAG_LOCAL_ONLY + | Root.FLAG_SUPPORTS_IS_CHILD; + } + } + public final Map<String, Collection<RootInfo>> roots = new HashMap<>(); private @Nullable RootInfo nextRoot; diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java index 3eb8ab2fd..35acdffaa 100644 --- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java @@ -32,7 +32,6 @@ import android.provider.DocumentsContract.Path; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.test.filters.MediumTest; -import androidx.test.runner.AndroidJUnit4; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.EventListener; @@ -48,10 +47,16 @@ import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestEventHandler; import com.android.documentsui.testing.TestProvidersAccess; import com.android.documentsui.testing.UserManagers; +import com.android.documentsui.util.FeatureFlagUtils; + +import com.google.android.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.concurrent.CountDownLatch; @@ -60,7 +65,7 @@ import java.util.concurrent.TimeUnit; /** * A unit test *for* AbstractActionHandler, not an abstract test baseclass. */ -@RunWith(AndroidJUnit4.class) +@RunWith(Parameterized.class) @MediumTest public class AbstractActionHandlerTest { @@ -68,11 +73,26 @@ public class AbstractActionHandlerTest { private TestEnv mEnv; private AbstractActionHandler<TestActivity> mHandler; + @Parameter(0) + public boolean isPrivateSpaceEnabled; + + /** + * Parametrize values for {@code isPrivateSpaceEnabled} to run all the tests twice once with + * private space flag enabled and once with it disabled. + */ + @Parameters(name = "privateSpaceEnabled={0}") + public static Iterable<?> data() { + return Lists.newArrayList(true, false); + } + @Before public void setUp() { mEnv = TestEnv.create(); mActivity = TestActivity.create(mEnv); mActivity.userManager = UserManagers.create(); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.USER_ID, true); + } mHandler = new AbstractActionHandler<TestActivity>( mActivity, mEnv.state, @@ -281,7 +301,11 @@ public class AbstractActionHandlerTest { @Test public void testCrossProfileDocuments_success() throws Exception { mEnv.state.action = State.ACTION_GET_CONTENT; - mEnv.state.canShareAcrossProfile = true; + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, true); + } else { + mEnv.state.canShareAcrossProfile = true; + } mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); @@ -308,7 +332,11 @@ public class AbstractActionHandlerTest { @Test public void testLoadCrossProfileDoc_failsWithQuietModeException() throws Exception { mEnv.state.action = State.ACTION_GET_CONTENT; - mEnv.state.canShareAcrossProfile = true; + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, true); + } else { + mEnv.state.canShareAcrossProfile = true; + } mEnv.state.stack.changeRoot(TestProvidersAccess.OtherUser.HOME); mEnv.state.stack.push(TestEnv.OtherUser.FOLDER_0); // Turn off the other user. @@ -389,7 +417,11 @@ public class AbstractActionHandlerTest { .setNextChildDocumentsReturns(TestEnv.OtherUser.FILE_PNG); // Disallow sharing across profile - mEnv.state.canShareAcrossProfile = false; + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, false); + } else { + mEnv.state.canShareAcrossProfile = false; + } TestEventHandler<Model.Update> listener = new TestEventHandler<>(); mEnv.model.addUpdateListener(listener::accept); @@ -404,7 +436,11 @@ public class AbstractActionHandlerTest { .isInstanceOf(CrossProfileNoPermissionException.class); // Allow sharing across profile. - mEnv.state.canShareAcrossProfile = true; + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mEnv.state.canForwardToProfileIdMap.put(TestProvidersAccess.OtherUser.USER_ID, true); + } else { + mEnv.state.canShareAcrossProfile = true; + } CountDownLatch latch2 = new CountDownLatch(1); mEnv.model.addUpdateListener(update -> latch2.countDown()); diff --git a/tests/unit/com/android/documentsui/DocumentsAccessTest.java b/tests/unit/com/android/documentsui/DocumentsAccessTest.java index 8a08d431b..300a67890 100644 --- a/tests/unit/com/android/documentsui/DocumentsAccessTest.java +++ b/tests/unit/com/android/documentsui/DocumentsAccessTest.java @@ -18,10 +18,12 @@ package com.android.documentsui; import static junit.framework.Assert.fail; +import static org.mockito.Mockito.mock; + +import android.content.ContentProviderClient; import android.content.pm.PackageManager; import androidx.test.filters.MediumTest; -import androidx.test.runner.AndroidJUnit4; import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestProvidersAccess; @@ -31,14 +33,24 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; -@RunWith(AndroidJUnit4.class) +@RunWith(Parameterized.class) @MediumTest public class DocumentsAccessTest { private TestActivity mActivity; private DocumentsAccess mDocumentsAccess; private TestEnv mEnv; + private ContentProviderClient mMockContentProviderClient = mock(ContentProviderClient.class); + + @Parameterized.Parameter(0) + public boolean isPrivateSpaceEnabled; + + @Parameterized.Parameters(name = "privateSpaceEnabled={0}") + public static Iterable<?> data() { + return com.google.android.collect.Lists.newArrayList(true, false); + } @Before public void setUp() throws PackageManager.NameNotFoundException { diff --git a/tests/unit/com/android/documentsui/ProfileTabsTest.java b/tests/unit/com/android/documentsui/ProfileTabsTest.java index 373f4e5cd..48250ddbb 100644 --- a/tests/unit/com/android/documentsui/ProfileTabsTest.java +++ b/tests/unit/com/android/documentsui/ProfileTabsTest.java @@ -18,6 +18,8 @@ package com.android.documentsui; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.content.Context; import android.net.Uri; import android.os.UserHandle; @@ -32,20 +34,28 @@ import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestProvidersAccess; +import com.android.documentsui.util.FeatureFlagUtils; +import com.android.modules.utils.build.SdkLevel; -import com.google.android.collect.Lists; import com.google.android.material.tabs.TabLayout; +import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; -import java.util.Collections; import java.util.List; +import java.util.Map; +@RunWith(Parameterized.class) public class ProfileTabsTest { - private final UserId systemUser = UserId.of(UserHandle.SYSTEM); - private final UserId managedUser = UserId.of(100); + private final UserId mSystemUser = UserId.of(UserHandle.SYSTEM); + private final UserId mManagedUser = UserId.of(100); + private final UserId mPrivateUser = UserId.of(101); private ProfileTabs mProfileTabs; @@ -55,9 +65,22 @@ public class ProfileTabsTest { private TestEnvironment mTestEnv; private State mState; private TestUserIdManager mTestUserIdManager; + private TestUserManagerState mTestUserManagerState; private TestCommonAddons mTestCommonAddons; private boolean mIsListenerInvoked; + @Parameter(0) + public boolean isPrivateSpaceEnabled; + + /** + * Parametrize values for {@code isPrivateSpaceEnabled} to run all the tests twice once with + * private space flag enabled and once with it disabled. + */ + @Parameters(name = "privateSpaceEnabled={0}") + public static Iterable<?> data() { + return Lists.newArrayList(true, false); + } + @Before public void setUp() { TestEnv.create(); @@ -76,14 +99,19 @@ public class ProfileTabsTest { mTestEnv = new TestEnvironment(); mTestEnv.isSearchExpanded = false; - mTestUserIdManager = new TestUserIdManager(); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mTestUserManagerState = new TestUserManagerState(); + } else { + mTestUserIdManager = new TestUserIdManager(); + } + mTestCommonAddons = new TestCommonAddons(); mTestCommonAddons.mCurrentRoot = TestProvidersAccess.DOWNLOADS; } @Test public void testUpdateView_singleUser_shouldHide() { - initializeWithUsers(systemUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.GONE); assertThat(mTabLayout.getTabCount()).isEqualTo(0); @@ -91,23 +119,40 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_shouldShow() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mTabLayout.getTabCount()).isEqualTo(2); TabLayout.Tab tab1 = mTabLayout.getTabAt(0); - assertThat(tab1.getTag()).isEqualTo(systemUser); + assertThat(tab1.getTag()).isEqualTo(mSystemUser); assertThat(tab1.getText()).isEqualTo(mContext.getString(R.string.personal_tab)); TabLayout.Tab tab2 = mTabLayout.getTabAt(1); - assertThat(tab2.getTag()).isEqualTo(managedUser); + assertThat(tab2.getTag()).isEqualTo(mManagedUser); assertThat(tab2.getText()).isEqualTo(mContext.getString(R.string.work_tab)); } @Test + public void testUpdateView_multiUsers_shouldShow() { + assumeTrue(SdkLevel.isAtLeastV() && FeatureFlagUtils.isPrivateSpaceEnabled()); + initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + + assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mTabLayout.getTabCount()).isEqualTo(3); + + final Map<UserId, String> userIdToLabelMap = mTestUserManagerState.getUserIdToLabelMap(); + for (int i = 0; i < 3; ++i) { + TabLayout.Tab tab = mTabLayout.getTabAt(i); + assertThat(tab).isNotNull(); + UserId userId = (UserId) tab.getTag(); + assertThat(tab.getText()).isEqualTo(userIdToLabelMap.get(userId)); + } + } + + @Test public void testUpdateView_twoUsers_doesNotSupportCrossProfile_shouldHide() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); mState.supportsCrossProfile = false; mProfileTabs.updateView(); @@ -117,7 +162,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_subFolder_shouldHide() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); // Push 1 more folder. Now the stack has size of 2. mState.stack.push(TestEnv.FOLDER_1); @@ -129,7 +174,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_recents_subFolder_shouldHide() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); mState.stack.changeRoot(TestProvidersAccess.RECENTS); // This(stack of size 2 in Recents) may not happen in real world. @@ -142,7 +187,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_thirdParty_shouldHide() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); mState.stack.changeRoot(TestProvidersAccess.PICKLES); mState.stack.push((TestEnv.FOLDER_0)); @@ -155,7 +200,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_isSearching_shouldHide() { mTestEnv.isSearchExpanded = true; - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.GONE); assertThat(mTabLayout.getTabCount()).isEqualTo(2); @@ -163,78 +208,147 @@ public class ProfileTabsTest { @Test public void testUpdateView_getSelectedUser_afterUsersChanged() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); mProfileTabs.updateView(); mTabLayout.selectTab(mTabLayout.getTabAt(1)); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mManagedUser); - mTestUserIdManager.userIds = Collections.singletonList(systemUser); + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mTestUserManagerState.userIds = Lists.newArrayList(mSystemUser); + } else { + mTestUserIdManager.userIds = Lists.newArrayList(mSystemUser); + } mProfileTabs.updateView(); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.GONE); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); } @Test public void testUpdateView_afterCurrentRootChanged_shouldChangeSelectedUser() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); mProfileTabs.updateView(); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); RootInfo newRoot = RootInfo.copyRootInfo(mTestCommonAddons.mCurrentRoot); - newRoot.userId = managedUser; + newRoot.userId = mManagedUser; mTestCommonAddons.mCurrentRoot = newRoot; mProfileTabs.updateView(); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mManagedUser); // updating view should not trigger listener callback. assertThat(mIsListenerInvoked).isFalse(); } @Test - public void testgetSelectedUser_twoUsers() { - initializeWithUsers(systemUser, managedUser); + public void testUpdateView_afterCurrentRootChangedMultiUser_shouldChangeSelectedUser() { + assumeTrue(SdkLevel.isAtLeastV() && FeatureFlagUtils.isPrivateSpaceEnabled()); + initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + mProfileTabs.updateView(); + + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + + for (UserId userId : Lists.newArrayList(mManagedUser, mPrivateUser)) { + RootInfo newRoot = RootInfo.copyRootInfo(mTestCommonAddons.mCurrentRoot); + newRoot.userId = userId; + mTestCommonAddons.mCurrentRoot = newRoot; + mProfileTabs.updateView(); + + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(userId); + // updating view should not trigger listener callback. + assertThat(mIsListenerInvoked).isFalse(); + } + } + + @Test + public void testUpdateView_afterSelectedUserBecomesUnavailable_shouldSwitchToCurrentUser() { + // here current user refers to UserId.CURRENT_USER, which in this case will be mSystemUser + assumeTrue(SdkLevel.isAtLeastV() && FeatureFlagUtils.isPrivateSpaceEnabled()); + initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + + mTabLayout.selectTab(mTabLayout.getTabAt(2)); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrivateUser); + + mTestUserManagerState.userIds.remove(mPrivateUser); + mTestUserManagerState.userIdToLabelMap.remove(mPrivateUser); + mProfileTabs.updateView(); + + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + } + + @Test + public void testGetSelectedUser_twoUsers() { + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); mTabLayout.selectTab(mTabLayout.getTabAt(0)); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); mTabLayout.selectTab(mTabLayout.getTabAt(1)); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(managedUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mManagedUser); assertThat(mIsListenerInvoked).isTrue(); } @Test + public void testGetSelectedUser_multiUsers() { + assumeTrue(SdkLevel.isAtLeastV() && FeatureFlagUtils.isPrivateSpaceEnabled()); + initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + + List<UserId> expectedProfiles = Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser); + + for (int i = 0; i < 3; ++i) { + mTabLayout.selectTab(mTabLayout.getTabAt(i)); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(expectedProfiles.get(i)); + if (i == 0) continue; + assertThat(mIsListenerInvoked).isTrue(); + } + } + + @Test public void testReselectedUser_doesNotInvokeListener() { - initializeWithUsers(systemUser, managedUser); + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser, mManagedUser); assertThat(mTabLayout.getSelectedTabPosition()).isAtLeast(0); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); mTabLayout.selectTab(mTabLayout.getTabAt(0)); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); assertThat(mIsListenerInvoked).isFalse(); } @Test - public void testgetSelectedUser_singleUsers() { - initializeWithUsers(systemUser); + public void testGetSelectedUser_singleUsers() { + initializeWithUsers(FeatureFlagUtils.isPrivateSpaceEnabled(), mSystemUser); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(systemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); } - private void initializeWithUsers(UserId... userIds) { - mTestUserIdManager.userIds = Lists.newArrayList(userIds); - for (UserId userId : userIds) { - if (userId.isSystem()) { - mTestUserIdManager.systemUser = userId; - } else { - mTestUserIdManager.managedUser = userId; + private void initializeWithUsers(boolean isPrivateSpaceEnabled, UserId... userIds) { + if (FeatureFlagUtils.isPrivateSpaceEnabled()) { + mTestUserManagerState.userIds = Lists.newArrayList(userIds); + for (UserId userId : userIds) { + if (userId.isSystem()) { + mTestUserManagerState.userIdToLabelMap.put(userId, "Personal"); + } else if (userId.getIdentifier() == 100) { + mTestUserManagerState.userIdToLabelMap.put(userId, "Work"); + } else { + mTestUserManagerState.userIdToLabelMap.put(userId, "Private"); + } } + mProfileTabs = new ProfileTabs(mTabLayoutContainer, mState, mTestUserManagerState, + mTestEnv, mTestCommonAddons); + } else { + mTestUserIdManager.userIds = Lists.newArrayList(userIds); + for (UserId userId : userIds) { + if (userId.isSystem()) { + mTestUserIdManager.systemUser = userId; + } else { + mTestUserIdManager.managedUser = userId; + } + } + mProfileTabs = new ProfileTabs(mTabLayoutContainer, mState, mTestUserIdManager, + mTestEnv, mTestCommonAddons); } - - mProfileTabs = new ProfileTabs(mTabLayoutContainer, mState, mTestUserIdManager, mTestEnv, - mTestCommonAddons); mProfileTabs.updateView(); mProfileTabs.setListener(userId -> mIsListenerInvoked = true); } diff --git a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java index ea88cfb31..fcfd6200b 100644 --- a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java +++ b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java @@ -28,26 +28,31 @@ import android.content.pm.ResolveInfo; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.testing.TestProvidersAccess; import com.android.documentsui.testing.TestResolveInfo; +import com.android.documentsui.util.FeatureFlagUtils; + +import com.google.android.collect.Lists; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * An unit test for RootsFragment. + * A unit test for RootsFragment. */ -@RunWith(AndroidJUnit4.class) +@RunWith(Parameterized.class) @MediumTest public class RootsFragmentTest { @@ -69,6 +74,18 @@ public class RootsFragmentTest { TestProvidersAccess.INSPECTOR.title, TestProvidersAccess.PICKLES.title}; + @Parameter(0) + public boolean isPrivateSpaceEnabled; + + /** + * Parametrize values for {@code isPrivateSpaceEnabled} to run all the tests twice once with + * private space flag enabled and once with it disabled. + */ + @Parameters(name = "privateSpaceEnabled={0}") + public static Iterable<?> data() { + return Lists.newArrayList(true, false); + } + @Before public void setUp() { mContext = mock(Context.class); @@ -83,6 +100,7 @@ public class RootsFragmentTest { @Test public void testSortLoadResult_WithCorrectOrder() { + if (FeatureFlagUtils.isPrivateSpaceEnabled()) return; List<Item> items = mRootsFragment.sortLoadResult( mContext, new State(), diff --git a/tests/unit/com/android/documentsui/sidebar/UserItemsCombinerTest.java b/tests/unit/com/android/documentsui/sidebar/UserItemsCombinerTest.java index 2a8086791..5fcd22bd6 100644 --- a/tests/unit/com/android/documentsui/sidebar/UserItemsCombinerTest.java +++ b/tests/unit/com/android/documentsui/sidebar/UserItemsCombinerTest.java @@ -30,6 +30,8 @@ import com.android.documentsui.R; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.testing.TestProvidersAccess; +import com.android.documentsui.util.FeatureFlagUtils; +import com.android.modules.utils.build.SdkLevel; import com.google.common.collect.Lists; import com.google.common.truth.Correspondence; @@ -38,8 +40,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -50,6 +55,7 @@ import java.util.Objects; public class UserItemsCombinerTest { private static final UserId PERSONAL_USER = TestProvidersAccess.USER_ID; private static final UserId WORK_USER = TestProvidersAccess.OtherUser.USER_ID; + private static final UserId PRIVATE_USER = TestProvidersAccess.AnotherUser.USER_ID; private static final List<Item> PERSONAL_ITEMS = Lists.newArrayList( personalItem("personal 1"), @@ -61,10 +67,14 @@ public class UserItemsCombinerTest { workItem("work 1") ); + private static final List<Item> PRIVATE_ITEMS = Lists.newArrayList( + privateItem("private1") + ); + private static final Correspondence<Item, Item> ITEM_CORRESPONDENCE = Correspondence.from((Item actual, Item expected) -> { - return Objects.equals(actual.title, expected.title) - && Objects.equals(actual.userId, expected.userId); + return Objects.equals(actual.title, expected.title) + && Objects.equals(actual.userId, expected.userId); }, "has same title and userId as in"); private final State mState = new State(); @@ -73,12 +83,25 @@ public class UserItemsCombinerTest { private final DevicePolicyManager mDpm = InstrumentationRegistry.getInstrumentation().getTargetContext().getSystemService( DevicePolicyManager.class); + private final List<UserId> mUserIds = new ArrayList<>(); + private final Map<UserId, String> mUserIdToLabelMap = new HashMap<>(); private UserItemsCombiner mCombiner; @Before public void setUp() { mState.canShareAcrossProfile = true; mState.supportsCrossProfile = true; + mUserIds.add(PERSONAL_USER); + mUserIds.add(WORK_USER); + mUserIdToLabelMap.put(PERSONAL_USER, "Personal"); + mUserIdToLabelMap.put(WORK_USER, "Work"); + mState.canForwardToProfileIdMap.put(PERSONAL_USER, true); + mState.canForwardToProfileIdMap.put(WORK_USER, true); + if (SdkLevel.isAtLeastV()) { + mUserIds.add(PRIVATE_USER); + mUserIdToLabelMap.put(PRIVATE_USER, "Private"); + mState.canForwardToProfileIdMap.put(PRIVATE_USER, true); + } } @Test @@ -90,6 +113,20 @@ public class UserItemsCombinerTest { } @Test + public void testCreatePresentableListForAllUsers_empty() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Collections.emptyList()); + rootListAllUsers.add(Collections.emptyList()); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(Collections.emptyList()); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .setRootListForAllUsers(rootListAllUsers); + assertThat( + mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)).isEmpty(); + } + + @Test public void testCreatePresentableList_currentIsPersonal_personalItemsOnly() { mCombiner = new UserItemsCombiner(mResources, mDpm, mState) .setRootListForCurrentUser(PERSONAL_ITEMS) @@ -113,6 +150,55 @@ public class UserItemsCombinerTest { } @Test + public void testCreatePresentableListForAllUsers_currentIsPersonal_personalItemsOnly() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Lists.newArrayList(PERSONAL_ITEMS)); + rootListAllUsers.add(Collections.emptyList()); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(Collections.emptyList()); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .setRootListForAllUsers(rootListAllUsers); + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(PERSONAL_ITEMS) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsWork_personalItemsOnly() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Lists.newArrayList(PERSONAL_ITEMS)); + rootListAllUsers.add(Collections.emptyList()); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(Collections.emptyList()); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(WORK_USER) + .setRootListForAllUsers(rootListAllUsers); + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(PERSONAL_ITEMS) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsPrivate_personalItemsOnly() { + if (!SdkLevel.isAtLeastV()) return; + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Lists.newArrayList(PERSONAL_ITEMS)); + rootListAllUsers.add(Collections.emptyList()); + rootListAllUsers.add(Collections.emptyList()); + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(PRIVATE_USER) + .setRootListForAllUsers(rootListAllUsers); + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(PERSONAL_ITEMS) + .inOrder(); + } + + @Test public void testCreatePresentableList_currentIsPersonal_workItemsOnly() { mCombiner = new UserItemsCombiner(mResources, mDpm, mState) .setRootListForCurrentUser(Collections.emptyList()) @@ -136,6 +222,56 @@ public class UserItemsCombinerTest { } @Test + public void testCreatePresentableListForAllUsers_currentIsPersonal_workItemsOnly() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Collections.emptyList()); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(Collections.emptyList()); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(PERSONAL_USER) + .setRootListForAllUsers(rootListAllUsers); + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(WORK_ITEMS) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsWork_workItemsOnly() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Collections.emptyList()); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(Collections.emptyList()); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(WORK_USER) + .setRootListForAllUsers(rootListAllUsers); + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(WORK_ITEMS) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsPrivate_workItemsOnly() { + if (!SdkLevel.isAtLeastV()) return; + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(Collections.emptyList()); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + rootListAllUsers.add(Collections.emptyList()); + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(PRIVATE_USER) + .setRootListForAllUsers(rootListAllUsers); + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(WORK_ITEMS) + .inOrder(); + } + + @Test public void testCreatePresentableList_currentIsPersonal_personalAndWorkItems() { mCombiner = new UserItemsCombiner(mResources, mDpm, mState) .setRootListForCurrentUser(PERSONAL_ITEMS) @@ -173,6 +309,89 @@ public class UserItemsCombinerTest { } @Test + public void testCreatePresentableListForAllUsers_currentIsPersonal_allUsersItems() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(PERSONAL_ITEMS); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(PRIVATE_ITEMS); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .setRootListForAllUsers(rootListAllUsers); + + List<Item> expected = Lists.newArrayList(); + expected.add(new HeaderItem("Personal")); + expected.addAll(PERSONAL_ITEMS); + expected.add(new HeaderItem("Work")); + expected.addAll(WORK_ITEMS); + if (SdkLevel.isAtLeastV()) { + expected.add(new HeaderItem("Private")); + expected.addAll(PRIVATE_ITEMS); + } + + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(expected) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsWork_allUsersItems() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(PERSONAL_ITEMS); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(PRIVATE_ITEMS); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(WORK_USER) + .setRootListForAllUsers(rootListAllUsers); + + List<Item> expected = Lists.newArrayList(); + expected.add(new HeaderItem("Personal")); + expected.addAll(PERSONAL_ITEMS); + expected.add(new HeaderItem("Work")); + expected.addAll(WORK_ITEMS); + if (SdkLevel.isAtLeastV()) { + expected.add(new HeaderItem("Private")); + expected.addAll(PRIVATE_ITEMS); + } + + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(expected) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsPrivate_allUsersItems() { + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(PERSONAL_ITEMS); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(PRIVATE_ITEMS); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .overrideCurrentUserForTest(PRIVATE_USER) + .setRootListForAllUsers(rootListAllUsers); + + List<Item> expected = Lists.newArrayList(); + expected.add(new HeaderItem("Personal")); + expected.addAll(PERSONAL_ITEMS); + expected.add(new HeaderItem("Work")); + expected.addAll(WORK_ITEMS); + if (SdkLevel.isAtLeastV()) { + expected.add(new HeaderItem("Private")); + expected.addAll(PRIVATE_ITEMS); + } + + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(expected) + .inOrder(); + } + + @Test public void testCreatePresentableList_currentIsPersonal_personalAndWorkItems_cannotShare() { mState.canShareAcrossProfile = false; mCombiner = new UserItemsCombiner(mResources, mDpm, mState) @@ -196,6 +415,56 @@ public class UserItemsCombinerTest { assertThat(mCombiner.createPresentableList()).isEmpty(); } + @Test + public void testCreatePresentableListForAllUsers_currentIsPersonal_cannotShareToWork() { + if (!FeatureFlagUtils.isPrivateSpaceEnabled()) return; + mState.canForwardToProfileIdMap.put(WORK_USER, false); + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(PERSONAL_ITEMS); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(PRIVATE_ITEMS); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .setRootListForAllUsers(rootListAllUsers); + + List<Item> expected = Lists.newArrayList(); + expected.add(new HeaderItem("Personal")); + expected.addAll(PERSONAL_ITEMS); + if (SdkLevel.isAtLeastV()) { + expected.add(new HeaderItem("Private")); + expected.addAll(PRIVATE_ITEMS); + } + + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(expected) + .inOrder(); + } + + @Test + public void testCreatePresentableListForAllUsers_currentIsWork_AllItems_cannotSharePersonal() { + if (!FeatureFlagUtils.isPrivateSpaceEnabled()) return; + mState.canForwardToProfileIdMap.put(PERSONAL_USER, false); + // In the current implementation of cross profile content sharing strategy work profile will + // be able to share to all the child profiles of the parent/personal profile only if it is + // able to share with parent/personal profile + mState.canForwardToProfileIdMap.put(PRIVATE_USER, false); + final List<List<Item>> rootListAllUsers = new ArrayList<>(); + rootListAllUsers.add(PERSONAL_ITEMS); + rootListAllUsers.add(Lists.newArrayList(WORK_ITEMS)); + if (SdkLevel.isAtLeastV()) { + rootListAllUsers.add(PRIVATE_ITEMS); + } + mCombiner = new UserItemsCombiner(mResources, mDpm, mState) + .setRootListForAllUsers(rootListAllUsers); + + assertThat(mCombiner.createPresentableListForAllUsers(mUserIds, mUserIdToLabelMap)) + .comparingElementsUsing(ITEM_CORRESPONDENCE) + .containsExactlyElementsIn(WORK_ITEMS) + .inOrder(); + } + private static TestItem personalItem(String title) { return new TestItem(title, PERSONAL_USER); } @@ -204,6 +473,10 @@ public class UserItemsCombinerTest { return new TestItem(title, WORK_USER); } + private static TestItem privateItem(String title) { + return new TestItem(title, PRIVATE_USER); + } + private static class TestItem extends Item { TestItem(String title, UserId userId) { |