diff options
author | 2025-03-13 12:51:25 -0700 | |
---|---|---|
committer | 2025-03-13 12:51:25 -0700 | |
commit | a76927b02bf259ea557d4cdf1cd3967f29f72851 (patch) | |
tree | 7b97f68f88f0d608b80b2ee6e86a0082aab1d2d5 | |
parent | 69c71c18f7143fffc0559bd46491876d34245bd7 (diff) | |
parent | 001b7bd2fae2078b6a6cd490958d495508a6d881 (diff) |
Merge changes Iabc4de0d,I147d67b2 into main
* changes:
Handle x-profile delegation for java picker.
Properly handle delegation in x-profile checks.
8 files changed, 757 insertions, 322 deletions
diff --git a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt index 74bde6db9..4cd6037b8 100644 --- a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt +++ b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt @@ -95,13 +95,14 @@ data class PhotopickerConfiguration( * the intent to check for CrossProfileIntentForwarder's. Rather than exposing intent as a * public field, this method can be called to do the check, if an Intent exists. * - * @param packageManager the PM of the process owner - * @param handle the [UserHandle] of the target user + * @param packageManager the PM of the "from" user + * @param targetUserHandle the [UserHandle] of the target user * @return Whether the current Intent Photopicker may be running under has a matching * CrossProfileIntentForwarderActivity */ fun doesCrossProfileIntentForwarderExists( packageManager: PackageManager, + fromUserHandle: UserHandle, targetUserHandle: UserHandle, ): Boolean { @@ -124,7 +125,11 @@ data class PhotopickerConfiguration( it.setPackage(null) for (info: ResolveInfo? in - packageManager.queryIntentActivities(it, PackageManager.MATCH_DEFAULT_ONLY)) { + packageManager.queryIntentActivitiesAsUser( + it, + PackageManager.MATCH_DEFAULT_ONLY, + fromUserHandle, + )) { info?.let { if (it.isCrossProfileIntentForwarderActivity()) { @@ -147,7 +152,7 @@ data class PhotopickerConfiguration( it::class.java.getDeclaredField("targetUserId").apply { isAccessible = true } - property?.get(it) as? Int + property.get(it) as? Int } catch (e: Exception) { when (e) { is NoSuchFieldException, @@ -192,6 +197,7 @@ data class PhotopickerConfiguration( "No intent available for checking cross-profile access.", ) + // Nothing left to check return false } diff --git a/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt b/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt index 15c1b403c..3c6402697 100644 --- a/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt +++ b/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt @@ -100,7 +100,17 @@ class UserMonitor( properties.getShowInSharingSurfaces() == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE } else { - true + when { + processOwnerUserHandle.identifier == it.identifier -> true + // For SDK < V, accept all managed profiles, and the parent + // of the current process owner. Ignore all others. + userManager.isManagedProfile(it.identifier) -> true + it.identifier == + userManager + .getProfileParent(processOwnerUserHandle) + ?.identifier -> true + else -> false + } } } .map { getUserProfileFromHandle(it, context) }, @@ -255,8 +265,7 @@ class UserMonitor( ) { Log.i( TAG, - "The active profile is no longer enabled, transitioning back to the process" + - " owner's profile.", + "The active profile is no longer enabled, transitioning back to the process owner's profile.", ) // The current profile is disabled, we need to transition back to the process @@ -281,8 +290,7 @@ class UserMonitor( ?: run { Log.w( TAG, - "Could not find the process owner's profile to switch to when the" + - " active profile was disabled.", + "Could not find the process owner's profile to switch to when the active profile was disabled.", ) // Still attempt to update the list of profiles. @@ -307,42 +315,103 @@ class UserMonitor( /** * Determines if the current handle supports CrossProfile content sharing. * + * This method accepts a pair of user handles (from/to) and determines if CrossProfile access is + * permitted between those two profiles. + * + * There are differences is on how the access is determined based on the platform SDK: + * - For Platform SDK < V: + * + * A check for CrossProfileIntentForwarders in the origin (from) profile that target the + * destination (to) profile. If such a forwarder exists, then access is allowed, and denied + * otherwise. + * - For Platform SDK >= V: + * + * The method now takes into account access delegation, which was first added in Android V. + * + * For profiles that set the [CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT] property in + * its [UserProperties], its parent profile will be substituted in for its side of the check. + * + * ex. For access checks between a Managed (from) and Private (to) profile, where: + * - Managed does not delegate to its parent + * - Private delegates to its parent + * + * The following logic is performed: Managed -> parent(Private) + * + * The same check in the other direction would yield: parent(Private) -> Managed + * + * Note how the private profile is never actually used for either side of the check, since it + * is delegating its access check to the parent. And thus, if Managed can access the parent, + * it can also access the private. + * + * @param context Current context object, for switching user contexts. + * @param fromUser The Origin profile, where the user is coming from + * @param toUser The destination profile, where the user is attempting to go to. * @return Whether CrossProfile content sharing is supported in this handle. */ - private fun getIsCrossProfileAllowedForHandle(handle: UserHandle): Boolean { + private fun getIsCrossProfileAllowedForHandle( + context: Context, + fromUser: UserHandle, + toUser: UserHandle, + ): Boolean { + + /** + * Determine if the provided [UserHandle] delegates its cross profile content sharing (both + * to / from this profile) to its parent's access. + * + * @return True if the profile delegates to its parent, false otherwise. + */ + fun profileDelegatesToParent(handle: UserHandle): Boolean { + + // Early exit, this check only exists on V+ + if (!SdkLevel.isAtLeastV()) { + return false + } - // Early exit conditions - if (handle == processOwnerUserHandle) { - return true + val props = userManager.getUserProperties(handle) + return props.getCrossProfileContentSharingStrategy() == + UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT } - // First, check if cross profile is delegated to parent profile - if (SdkLevel.isAtLeastV()) { - val properties: UserProperties = userManager.getUserProperties(handle) - if ( - /* - * All user profiles with user property - * [UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT] - * can access each other including its parent. - */ - properties.getCrossProfileContentSharingStrategy() == - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT - ) { - val parent = userManager.getProfileParent(handle) + // Early exit conditions, accessing self. + // NOTE: It is also possible to reach this state if this method is recursively checking + // from: parent(A) to:parent(B) where A and B are both children of the same parent. + if (fromUser.identifier == toUser.identifier) { + return true + } - parent?.let { - return getIsCrossProfileAllowedForHandle(it) - } + // Decide if we should use actual from or parent(from) + val currentFromUser: UserHandle = + if (profileDelegatesToParent(fromUser)) { + userManager.getProfileParent(fromUser) ?: fromUser + } else { + fromUser + } - // Couldn't resolve parent, fail closed. - return false + // Decide if we should use actual to or parent(to) + val currentToUser: UserHandle = + if (profileDelegatesToParent(toUser)) { + userManager.getProfileParent(toUser) ?: toUser + } else { + toUser } + + // When the from/to has changed from the original parameters, recursively restart the checks + // with the new from/to handles. + if ( + fromUser.identifier != currentFromUser.identifier || + toUser.identifier != currentToUser.identifier + ) { + return getIsCrossProfileAllowedForHandle(context, currentFromUser, currentToUser) } // As a last resort, no applicable cross profile information found, so inspect the current // configuration and if there is an intent set, try to see // if there is a matching CrossProfileIntentForwarder - return configuration.value.doesCrossProfileIntentForwarderExists(packageManager, handle) + return configuration.value.doesCrossProfileIntentForwarderExists( + packageManager, + fromUser, + toUser, + ) } /** @@ -355,7 +424,8 @@ class UserMonitor( val isParentProfile = userManager.getProfileParent(handle) == null val isManaged = userManager.isManagedProfile(handle.getIdentifier()) val isQuietModeEnabled = userManager.isQuietModeEnabled(handle) - var isCrossProfileSupported = getIsCrossProfileAllowedForHandle(handle) + var isCrossProfileSupported = + getIsCrossProfileAllowedForHandle(context, processOwnerUserHandle, handle) val (icon, label) = try { @@ -410,8 +480,6 @@ class UserMonitor( processOwnerUserHandle -> emptySet() else -> buildSet { - if (isParentProfile) - return@buildSet // Parent profile can always be accessed by children if (isQuietModeEnabled) { add(UserProfile.DisabledReason.QUIET_MODE) diff --git a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt index 5b40fa827..fb47d8a1a 100644 --- a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt @@ -65,6 +65,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString +import org.mockito.Mockito.eq import org.mockito.Mockito.isNull import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -161,7 +162,13 @@ class BannerManagerImplTest { whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY } val mockResolveInfo = ReflectedResolveInfo(USER_ID_MANAGED) - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + eq(USER_HANDLE_PRIMARY), + ) + ) { listOf(mockResolveInfo) } diff --git a/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt b/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt index adca148b4..aca667018 100644 --- a/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt +++ b/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt @@ -55,6 +55,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.mock @@ -88,6 +89,10 @@ class UserMonitorTest { private val USER_ID_MANAGED: Int = 10 private val MANAGED_PROFILE_BASE: UserProfile + private val USER_HANDLE_PRIVATE: UserHandle + private val USER_ID_PRIVATE: Int = 11 + private val PRIVATE_PROFILE_BASE: UserProfile + private val initialExpectedStatus: UserStatus private val mockContentResolver: ContentResolver = mock(ContentResolver::class.java) @@ -128,6 +133,19 @@ class UserMonitorTest { label = PLATFORM_PROVIDED_PROFILE_LABEL, ) + val parcel3 = Parcel.obtain() + parcel2.writeInt(USER_ID_PRIVATE) + parcel2.setDataPosition(0) + USER_HANDLE_PRIVATE = UserHandle(parcel3) + parcel3.recycle() + + PRIVATE_PROFILE_BASE = + UserProfile( + handle = USER_HANDLE_PRIVATE, + profileType = UserProfile.ProfileType.UNKNOWN, + label = PLATFORM_PROVIDED_PROFILE_LABEL, + ) + initialExpectedStatus = UserStatus( activeUserProfile = PRIMARY_PROFILE_BASE, @@ -161,7 +179,13 @@ class UserMonitorTest { // Fake for a CrossProfileIntentForwarderActivity for the managed profile val resolveInfoForManagedUser = ReflectedResolveInfo(USER_HANDLE_MANAGED.getIdentifier()) - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + eq(USER_HANDLE_PRIMARY), + ) + ) { listOf(resolveInfoForManagedUser) } @@ -214,6 +238,242 @@ class UserMonitorTest { } } + @Test + fun testProfilesForCrossProfileNoDelegationVPlus() { + assumeTrue(SdkLevel.isAtLeastV()) + + // Add a third profile (private) to the list of profiles + whenever(mockUserManager.userProfiles) { + listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, USER_HANDLE_PRIVATE) + } + whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIVATE)) { false } + whenever(mockUserManager.isManagedProfile(USER_ID_PRIVATE)) { false } + whenever(mockUserManager.getProfileParent(USER_HANDLE_PRIVATE)) { USER_HANDLE_PRIMARY } + + // The private profile should delegate its access to the parent + whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIVATE)) @JvmSerializableLambda { + UserProperties.Builder() + .setCrossProfileContentSharingStrategy( + UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION + ) + .build() + } + + runTest { // this: TestScope + + // When the primary profile is the process owner + userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = + TestPhotopickerConfiguration.build { + action(MediaStore.ACTION_PICK_IMAGES) + intent(Intent(MediaStore.ACTION_PICK_IMAGES)) + }, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY, + ) + + var reportedStatus = userMonitor.userStatus.first() + var expectedStatus = + UserStatus( + activeUserProfile = PRIMARY_PROFILE_BASE, + allProfiles = + listOf( + PRIMARY_PROFILE_BASE, + MANAGED_PROFILE_BASE, + PRIVATE_PROFILE_BASE.copy( + disabledReasons = + setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED) + ), + ), + activeContentResolver = mockContentResolver, + ) + assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus) + + // Reset user monitor, private user is now the process owner + userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = + TestPhotopickerConfiguration.build { + action(MediaStore.ACTION_PICK_IMAGES) + intent(Intent(MediaStore.ACTION_PICK_IMAGES)) + }, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIVATE, + ) + + reportedStatus = userMonitor.userStatus.first() + expectedStatus = + UserStatus( + activeUserProfile = PRIVATE_PROFILE_BASE, + allProfiles = + listOf( + PRIMARY_PROFILE_BASE.copy( + disabledReasons = + setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED) + ), + MANAGED_PROFILE_BASE.copy( + disabledReasons = + setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED) + ), + PRIVATE_PROFILE_BASE, + ), + activeContentResolver = mockContentResolver, + ) + assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus) + // + // Reset user monitor, managed user is now the process owner + userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = + TestPhotopickerConfiguration.build { + action(MediaStore.ACTION_PICK_IMAGES) + intent(Intent(MediaStore.ACTION_PICK_IMAGES)) + }, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_MANAGED, + ) + + reportedStatus = userMonitor.userStatus.first() + expectedStatus = + UserStatus( + activeUserProfile = MANAGED_PROFILE_BASE, + allProfiles = + listOf( + PRIMARY_PROFILE_BASE, + MANAGED_PROFILE_BASE, + PRIVATE_PROFILE_BASE.copy( + disabledReasons = + setOf(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED) + ), + ), + activeContentResolver = mockContentResolver, + ) + assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus) + } + } + + @Test + fun testProfilesForCrossProfileDelegationVPlus() { + assumeTrue(SdkLevel.isAtLeastV()) + + // Add a third profile (private) to the list of profiles + whenever(mockUserManager.userProfiles) { + listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, USER_HANDLE_PRIVATE) + } + whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIVATE)) { false } + whenever(mockUserManager.isManagedProfile(USER_ID_PRIVATE)) { false } + whenever(mockUserManager.getProfileParent(USER_HANDLE_PRIVATE)) { USER_HANDLE_PRIMARY } + + // The private profile should delegate its access to the parent + whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIVATE)) @JvmSerializableLambda { + UserProperties.Builder() + .setCrossProfileContentSharingStrategy( + UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT + ) + .build() + } + + runTest { // this: TestScope + + // When the primary profile is the process owner + userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = + TestPhotopickerConfiguration.build { + action(MediaStore.ACTION_PICK_IMAGES) + intent(Intent(MediaStore.ACTION_PICK_IMAGES)) + }, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIMARY, + ) + + var reportedStatus = userMonitor.userStatus.first() + var expectedStatus = + UserStatus( + activeUserProfile = PRIMARY_PROFILE_BASE, + allProfiles = + listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE, PRIVATE_PROFILE_BASE), + activeContentResolver = mockContentResolver, + ) + assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus) + + // Reset user monitor, private user is now the process owner + userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = + TestPhotopickerConfiguration.build { + action(MediaStore.ACTION_PICK_IMAGES) + intent(Intent(MediaStore.ACTION_PICK_IMAGES)) + }, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_PRIVATE, + ) + + reportedStatus = userMonitor.userStatus.first() + expectedStatus = + UserStatus( + activeUserProfile = PRIVATE_PROFILE_BASE, + allProfiles = + listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE, PRIVATE_PROFILE_BASE), + activeContentResolver = mockContentResolver, + ) + assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus) + // + // Reset user monitor, managed user is now the process owner + userMonitor = + UserMonitor( + mockContext, + provideTestConfigurationFlow( + scope = this.backgroundScope, + defaultConfiguration = + TestPhotopickerConfiguration.build { + action(MediaStore.ACTION_PICK_IMAGES) + intent(Intent(MediaStore.ACTION_PICK_IMAGES)) + }, + ), + this.backgroundScope, + StandardTestDispatcher(this.testScheduler), + USER_HANDLE_MANAGED, + ) + + reportedStatus = userMonitor.userStatus.first() + expectedStatus = + UserStatus( + activeUserProfile = MANAGED_PROFILE_BASE, + allProfiles = + listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE, PRIVATE_PROFILE_BASE), + activeContentResolver = mockContentResolver, + ) + assertUserStatusIsEqualIgnoringFields(reportedStatus, expectedStatus) + } + } + /** Ensures profiles with a cross profile forwarding intent are active */ @Test fun testProfilesForCrossProfileIntentForwardingVPlus() { @@ -302,7 +562,13 @@ class UserMonitorTest { ) .build() } - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + any(UserHandle::class.java), + ) + ) { emptyList<ResolveInfo>() } @@ -350,7 +616,13 @@ class UserMonitorTest { assumeFalse(SdkLevel.isAtLeastV()) - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + any(UserHandle::class.java), + ) + ) { emptyList<ResolveInfo>() } @@ -441,7 +713,13 @@ class UserMonitorTest { .build() } - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + eq(USER_HANDLE_PRIMARY), + ) + ) { listOf(ReflectedResolveInfo(USER_HANDLE_MANAGED.getIdentifier())) } @@ -517,7 +795,13 @@ class UserMonitorTest { whenever(mockUserManager.getProfileParent(userHandleUnknownManaged)) { USER_HANDLE_PRIMARY } whenever(mockUserManager.isQuietModeEnabled(userHandleUnknownManaged)) { false } - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + eq(USER_HANDLE_PRIMARY), + ) + ) { listOf(ReflectedResolveInfo(USER_HANDLE_MANAGED.getIdentifier())) } @@ -613,7 +897,13 @@ class UserMonitorTest { .build() } - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + any(UserHandle::class.java), + ) + ) { emptyList<ResolveInfo>() } @@ -696,7 +986,13 @@ class UserMonitorTest { whenever(mockUserManager.getProfileParent(userHandleUnknownManaged)) { USER_HANDLE_PRIMARY } whenever(mockUserManager.isQuietModeEnabled(userHandleUnknownManaged)) { false } - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + any(UserHandle::class.java), + ) + ) { emptyList<ResolveInfo>() } diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt index 7d776d813..5f52fe3d6 100644 --- a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt @@ -62,6 +62,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq import org.mockito.MockitoAnnotations @SmallTest @@ -170,7 +171,13 @@ class ProfileSelectorViewModelTest { whenever(mockUserManager.getProfileLabel()) { "label" } } val mockResolveInfo = ReflectedResolveInfo(USER_ID_MANAGED) - whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) { + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + eq(USER_HANDLE_PRIMARY), + ) + ) { listOf(mockResolveInfo) } } diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt index e69173012..b30286412 100644 --- a/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt +++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt @@ -18,9 +18,12 @@ package com.android.photopicker.features.profileselector import android.content.ContentResolver import android.content.Context +import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.os.UserHandle import android.os.UserManager +import android.provider.MediaStore import android.test.mock.MockContentResolver import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert @@ -73,6 +76,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoAnnotations @@ -87,6 +93,17 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) class SwitchProfileBannerTest : PhotopickerFeatureBaseTest() { + /** + * Class that exposes the @hide api [targetUserId] in order to supply proper values for + * reflection based code that is inspecting this field. + * + * @property targetUserId + */ + private class ReflectedResolveInfo(@JvmField val targetUserId: Int) : ResolveInfo() { + + override fun isCrossProfileIntentForwarderActivity(): Boolean = true + } + companion object { val USER_ID_PRIMARY: Int = 0 val USER_HANDLE_PRIMARY: UserHandle = UserHandle.of(USER_ID_PRIMARY) @@ -147,6 +164,18 @@ class SwitchProfileBannerTest : PhotopickerFeatureBaseTest() { whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY } whenever(mockUserManager.getProfileParent(USER_HANDLE_PRIMARY)) { null } + // Fake for a CrossProfileIntentForwarderActivity for the managed profile + val resolveInfoForPrimaryUser = ReflectedResolveInfo(USER_HANDLE_PRIMARY.getIdentifier()) + whenever( + mockPackageManager.queryIntentActivitiesAsUser( + any(Intent::class.java), + anyInt(), + eq(USER_HANDLE_MANAGED), + ) + ) { + listOf(resolveInfoForPrimaryUser) + } + val resources = getTestableContext().getResources() if (SdkLevel.isAtLeastV()) { whenever(mockUserManager.getProfileLabel()) @@ -159,6 +188,9 @@ class SwitchProfileBannerTest : PhotopickerFeatureBaseTest() { resources.getString(R.string.photopicker_profile_managed_label), ) } + + // Ensure an intent is set for cross profile checking + configurationManager.get().setIntent(Intent(MediaStore.ACTION_PICK_IMAGES)) } @Test diff --git a/src/com/android/providers/media/photopicker/data/UserManagerState.java b/src/com/android/providers/media/photopicker/data/UserManagerState.java index 74c06e872..8df364c3c 100644 --- a/src/com/android/providers/media/photopicker/data/UserManagerState.java +++ b/src/com/android/providers/media/photopicker/data/UserManagerState.java @@ -21,8 +21,6 @@ import static androidx.core.util.Preconditions.checkNotNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresApi; -import android.annotation.SuppressLint; -import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -242,48 +240,40 @@ public interface UserManagerState { setUserIds(); } - private UserId getSystemUser() { - return UserId.of(UserHandle.of(ActivityManager.getCurrentUser())); - } - private void setUserIds() { - setUserIdsInternal(); - mIsMultiUserProfiles.postValue(isMultiUserProfiles()); - } - - private void setUserIdsInternal() { mUserProfileIds.clear(); - mUserProfileIds.add(getSystemUser()); - if (mUserManager == null) { - Log.e(TAG, "Cannot obtain user manager"); - return; - } - // Here there could be other profiles too , that we do not want to show anywhere - // in photo picker at all. - final List<UserHandle> userProfiles = mUserManager.getUserProfiles(); - if (SdkLevel.isAtLeastV()) { - for (UserHandle userHandle : userProfiles) { - UserProperties userProperties = mUserManager.getUserProperties(userHandle); - UserId userId = UserId.of(userHandle); - - // Check if we want to show this profile data in PhotoPicker or if it is - // an owner profile itself. - if (getSystemUser().getIdentifier() != userHandle.getIdentifier() - && userProperties.getShowInSharingSurfaces() - == userProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { - mUserProfileIds.add(userId); + mUserProfileIds.add(mCurrentUser); + boolean currentUserIsManaged = + mUserManager.isManagedProfile(mCurrentUser.getIdentifier()); + + for (UserHandle handle : mUserManager.getUserProfiles()) { + + // For >= Android V, check if the profile wants to be shown + if (SdkLevel.isAtLeastV()) { + + UserProperties properties = mUserManager.getUserProperties(handle); + if (properties.getShowInSharingSurfaces() + != UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { + continue; } - } - } else { - // if sdk version is less than V, then maximum two profiles with separate tab - // could only be available - for (UserHandle userHandle : userProfiles) { - if (mUserManager.isManagedProfile(userHandle.getIdentifier())) { - mUserProfileIds.add(UserId.of(userHandle)); + } else { + // Only allow managed profiles + the parent user on lower than V. + if (currentUserIsManaged + && mUserManager.getProfileParent(mCurrentUser.getUserHandle()) + == handle) { + // Intentionally empty so that this profile gets added. + } else if (!mUserManager.isManagedProfile(handle.getIdentifier())) { + continue; } } + + // Ensure the system user doesn't get added twice. + if (mUserProfileIds.contains(UserId.of(handle))) continue; + mUserProfileIds.add(UserId.of(handle)); } + + mIsMultiUserProfiles.postValue(isMultiUserProfiles()); } @Override @@ -312,12 +302,162 @@ public interface UserManagerState { return crossProfileAllowedStatusForAll; } + + /** + * External method that allows quick checking from the current user to a target user. + * + * Takes into account the On/Off state of the profile, as well as cross profile content + * sharing policies. + * + * @param targetUser the target of the access. Current User is the "from" user. + * @return If the target user currently is eligible for cross profile content sharing. + */ @Override - public boolean isCrossProfileAllowedToUser(UserId otherUser) { + public boolean isCrossProfileAllowedToUser(UserId targetUser) { assertMainThread(); - return !isProfileOff(otherUser) && !isBlockedByAdmin(otherUser); + return !isProfileOff(targetUser) && !isBlockedByAdmin(targetUser); } + /** + * Determines if the provided UserIds support CrossProfile content sharing. + * + * <p>This method accepts a pair of user handles (from/to) and determines if CrossProfile + * access is permitted between those two profiles. + * + * <p>There are differences is on how the access is determined based on the platform SDK: + * + * <p>For Platform SDK < V: + * + * <p>A check for CrossProfileIntentForwarders in the origin (from) profile that target the + * destination (to) profile. If such a forwarder exists, then access is allowed, and denied + * otherwise. + * + * <p>For Platform SDK >= V: + * + * <p>The method now takes into account access delegation, which was first added in Android + * V. + * + * <p>For profiles that set the [CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT] + * property in its [UserProperties], its parent profile will be substituted in for its side + * of the check. + * + * <p>ex. For access checks between a Managed (from) and Private (to) profile, where: - + * Managed does not delegate to its parent - Private delegates to its parent + * + * <p>The following logic is performed: Managed -> parent(Private) + * + * <p>The same check in the other direction would yield: parent(Private) -> Managed + * + * <p>Note how the private profile is never actually used for either side of the check, + * since it is delegating its access check to the parent. And thus, if Managed can access + * the parent, it can also access the private. + * + * @param context Current context object, for switching user contexts. + * @param intent The current intent the Photopicker is running under. + * @param fromUser The Origin profile, where the user is coming from + * @param toUser The destination profile, where the user is attempting to go to. + * @return Whether CrossProfile content sharing is supported in this handle. + */ + private boolean isCrossProfileAllowedToUser( + Context context, Intent intent, UserId fromUser, UserId toUser) { + + // Early exit conditions, accessing self. + // NOTE: It is also possible to reach this state if this method is recursively checking + // from: parent(A) to:parent(B) where A and B are both children of the same parent. + if (fromUser.getIdentifier() == toUser.getIdentifier()) { + return true; + } + + // Decide if we should use actual from or parent(from) + UserHandle currentFromUser = + getProfileToCheckCrossProfileAccess(fromUser.getUserHandle()); + + // Decide if we should use actual to or parent(to) + UserHandle currentToUser = getProfileToCheckCrossProfileAccess(toUser.getUserHandle()); + + // When the from/to has changed from the original parameters, recursively restart the + // checks with the new from/to handles. + if (fromUser.getIdentifier() != currentFromUser.getIdentifier() + || toUser.getIdentifier() != currentToUser.getIdentifier()) { + return isCrossProfileAllowedToUser( + context, intent, UserId.of(currentFromUser), UserId.of(currentToUser)); + } + + return doesCrossProfileIntentForwarderExist( + intent, + mContext.getPackageManager(), + fromUser.getUserHandle(), + toUser.getUserHandle()); + } + + /** + * Checks the Intent to see if it can be resolved as a CrossProfileIntentForwarderActivity + * for the target user. + * + * @param intent The current intent the photopicker is running under. + * @param pm the PM which will be used for querying. + * @param fromUser the [UserHandle] of the origin user + * @param targetUserHandle the [UserHandle] of the target user + * @return Whether the current Intent Photopicker may be running under has a matching + * CrossProfileIntentForwarderActivity + */ + private boolean doesCrossProfileIntentForwarderExist( + Intent intent, + PackageManager pm, + UserHandle fromUser, + UserHandle targetUserHandle) { + + // Clear out the component & package before attempting to match + Intent intentToCheck = (Intent) intent.clone(); + intentToCheck.setComponent(null); + intentToCheck.setPackage(null); + + for (ResolveInfo resolveInfo : + pm.queryIntentActivitiesAsUser( + intentToCheck, PackageManager.MATCH_DEFAULT_ONLY, fromUser)) { + + // If the activity is a CrossProfileIntentForwardingActivity, inspect its + // targetUserId to see if it targets the user we are currently checking for. + if (resolveInfo.isCrossProfileIntentForwarderActivity()) { + + /* + * IMPORTANT: This is a reflection based hack to ensure the profile is + * actually the installer of the CrossProfileIntentForwardingActivity. + * + * ResolveInfo.targetUserId exists, but is a hidden API not available to + * mainline modules, and no such API exists, so it is accessed via + * reflection below. All exceptions are caught to protect against + * reflection related issues such as: + * NoSuchFieldException / IllegalAccessException / SecurityException. + * + * In the event of an exception, the code fails "closed" for the current + * profile to avoid showing content that should not be visible. + */ + try { + Field targetUserIdField = + resolveInfo.getClass().getDeclaredField("targetUserId"); + targetUserIdField.setAccessible(true); + int targetUserId = (int) targetUserIdField.get(resolveInfo); + + if (targetUserId == targetUserHandle.getIdentifier()) { + // Don't need to look further, exit the loop. + return true; + } + + } catch (NoSuchFieldException | IllegalAccessException | SecurityException ex) { + // Couldn't check the targetUserId via reflection, so fail without + // further iterations. + Log.e(TAG, "Could not access targetUserId via reflection.", ex); + return false; + } catch (Exception ex) { + Log.e(TAG, "Exception occurred during cross profile checks", ex); + } + } + } + return false; + } + + @Override public MutableLiveData<Boolean> getIsMultiUserProfiles() { return mIsMultiUserProfiles; @@ -331,8 +471,7 @@ public interface UserManagerState { @Override public void resetUserIdsAndSetCrossProfileValues(Intent intent) { - assertMainThread(); - setUserIdsInternal(); + resetUserIds(); setCrossProfileValues(intent); mIsMultiUserProfiles.postValue(isMultiUserProfiles()); } @@ -510,6 +649,12 @@ public interface UserManagerState { || !CrossProfileUtils.isMediaProviderAvailable(userId, mContext); } + /** + * Determines if the target UserHandle delegates its content sharing to its parent. + * + * @param userHandle The target handle to check delegation for. + * @return TRUE if V+ and the handle delegates to parent. False otherwise. + */ private boolean isCrossProfileStrategyDelegatedToParent(UserHandle userHandle) { if (SdkLevel.isAtLeastV()) { if (mUserManager == null) { @@ -525,6 +670,15 @@ public interface UserManagerState { return false; } + /** + * Acquires the correct {@link UserHandle} which should be used for CrossProfile access + * checks. + * + * @param userHandle the origin handle. + * @return The UserHandle that should be used for cross profile access checks. In the event + * the origin handle delegates its access, this may not be the same handle as the origin + * handle. + */ private UserHandle getProfileToCheckCrossProfileAccess(UserHandle userHandle) { if (mUserManager == null) { Log.e(TAG, "Cannot obtain user manager"); @@ -551,7 +705,6 @@ public interface UserManagerState { * @param intent The intent Photopicker is currently running under, for * CrossProfileForwardActivity checking. */ - @SuppressLint("DiscouragedPrivateApi") private void setBlockedByAdminValue(Intent intent) { if (intent == null) { Log.e( @@ -561,95 +714,6 @@ public interface UserManagerState { return; } - Map<UserId, Boolean> profileIsAccessibleToProcessOwner = new HashMap<>(); - List<UserId> delegatedFromParent = new ArrayList<>(); - - final PackageManager pm = mContext.getPackageManager(); - - // Resolve CrossProfile activities for all user profiles that Photopicker is - // aware of. - for (UserId userId : mUserProfileIds) { - - // If the UserId is the system user, exit early. - if (userId.getIdentifier() == mCurrentUser.getIdentifier()) { - profileIsAccessibleToProcessOwner.put(userId, true); - continue; - } - - // This UserId delegates its strategy to the parent profile - if (isCrossProfileStrategyDelegatedToParent(userId.getUserHandle())) { - delegatedFromParent.add(userId); - continue; - } - - // Clear out the component & package before attempting to match - Intent intentToCheck = (Intent) intent.clone(); - intentToCheck.setComponent(null); - intentToCheck.setPackage(null); - - for (ResolveInfo resolveInfo : - pm.queryIntentActivities( - intentToCheck, PackageManager.MATCH_DEFAULT_ONLY)) { - - // If the activity is a CrossProfileIntentForwardingActivity, inspect its - // targetUserId to - // see if it targets the user we are currently checking for. - if (resolveInfo.isCrossProfileIntentForwarderActivity()) { - - /* - * IMPORTANT: This is a reflection based hack to ensure the profile is - * actually the installer of the CrossProfileIntentForwardingActivity. - * - * ResolveInfo.targetUserId exists, but is a hidden API not available to - * mainline modules, and no such API exists, so it is accessed via - * reflection below. All exceptions are caught to protect against - * reflection related issues such as: - * NoSuchFieldException / IllegalAccessException / SecurityException. - * - * In the event of an exception, the code fails "closed" for the current - * profile to avoid showing content that should not be visible. - */ - try { - Field targetUserIdField = - resolveInfo.getClass().getDeclaredField("targetUserId"); - targetUserIdField.setAccessible(true); - int targetUserId = (int) targetUserIdField.get(resolveInfo); - - if (targetUserId == userId.getIdentifier()) { - profileIsAccessibleToProcessOwner.put(userId, true); - - // Don't need to look further, exit the loop. - break; - } - - } catch (NoSuchFieldException - | IllegalAccessException - | SecurityException ex) { - // Couldn't check the targetUserId via reflection, so fail without - // further - // iterations. - Log.e(TAG, "Could not access targetUserId via reflection.", ex); - break; - } catch (Exception ex) { - Log.e(TAG, "Exception occurred during cross profile checks", ex); - } - } - } - // Fail case, was unable to find a suitable Activity for this user. - profileIsAccessibleToProcessOwner.putIfAbsent(userId, false); - } - - // For profiles that delegate their access to the parent, set the access for - // those profiles equal to the same as their parent. - for (UserId userId : delegatedFromParent) { - UserHandle parent = - mUserManager.getProfileParent(UserHandle.of(userId.getIdentifier())); - profileIsAccessibleToProcessOwner.put( - userId, - profileIsAccessibleToProcessOwner.getOrDefault( - UserId.of(parent), /* default= */ false)); - } - mIsProfileBlockedByAdminMap.clear(); for (UserId userId : mUserProfileIds) { mIsProfileBlockedByAdminMap.put( @@ -657,8 +721,8 @@ public interface UserManagerState { // calculated, (which are blocked, rather than which are accessible) so the // boolean needs to be inverted. userId, - !profileIsAccessibleToProcessOwner.getOrDefault( - userId, /* default= */ false)); + !isCrossProfileAllowedToUser(mContext, intent, UserId.CURRENT_USER, userId) + ); } } diff --git a/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java b/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java index dda66c673..f2fc38d9d 100644 --- a/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java +++ b/tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java @@ -23,6 +23,7 @@ import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -111,6 +112,7 @@ public class UserManagerStateTest { // set Managed Profile identification when(mMockUserManager.isManagedProfile(mManagedUser.getIdentifier())).thenReturn(true); + when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true); when(mMockUserManager.isManagedProfile(mPersonalUser.getIdentifier())).thenReturn(false); when(mMockUserManager.isManagedProfile(mOtherUser1.getIdentifier())).thenReturn(false); when(mMockUserManager.isManagedProfile(mOtherUser2.getIdentifier())).thenReturn(false); @@ -171,6 +173,62 @@ public class UserManagerStateTest { } @Test + public void testCrossProfileAccessWithDelegationVPlus() { + assumeTrue(SdkLevel.isAtLeastV()); + + // Return a ResolveInfo for the correct managed profile. + when(mMockPackageManager.queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), any(UserHandle.class))) + .thenReturn(List.of()); + + initializeUserManagerState( + UserId.of(mPersonalUser), + Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1)); + + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + mUserManagerState.setIntentAndCheckRestrictions(new Intent()); + assertThat( + mUserManagerState.isCrossProfileAllowedToUser( + UserId.of(mManagedUser))) + .isFalse(); + assertThat( + mUserManagerState.isCrossProfileAllowedToUser( + UserId.of(mOtherUser1))) + .isTrue(); + }); + } + + @Test + public void testCrossProfileAccessWithDelegationManagedToPrivateVPlus() { + assumeTrue(SdkLevel.isAtLeastV()); + + // Return a ResolveInfo for the personal profile only. + when(mMockPackageManager.queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), eq(mManagedUser))) + .thenReturn(List.of(new ReflectedResolveInfo(mPersonalUser.getIdentifier()))); + + initializeUserManagerState( + UserId.of(mManagedUser), + Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1)); + + InstrumentationRegistry.getInstrumentation() + .runOnMainSync( + () -> { + mUserManagerState.setIntentAndCheckRestrictions(new Intent()); + assertThat( + mUserManagerState.isCrossProfileAllowedToUser( + UserId.of(mPersonalUser))) + .isTrue(); + assertThat( + mUserManagerState.isCrossProfileAllowedToUser( + UserId.of(mOtherUser1))) + .isTrue(); + }); + } + + @Test public void testCrossProfileAccessWithMultipleManagedProfilesIsAllowedVPlus() { assumeTrue(SdkLevel.isAtLeastV()); @@ -186,7 +244,8 @@ public class UserManagerStateTest { when(mMockUserManager.getUserProperties(mManagedUser2)).thenReturn(mManagedUser2Properties); // Return a ResolveInfo for the correct managed profile. - when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + when(mMockPackageManager.queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), eq(mPersonalUser))) .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser2.getIdentifier()))); initializeUserManagerState( @@ -224,7 +283,8 @@ public class UserManagerStateTest { when(mMockUserManager.getUserProperties(mManagedUser2)).thenReturn(mManagedUser2Properties); // Return a ResolveInfo for the OTHER managed profile. - when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + when(mMockPackageManager.queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), eq(mPersonalUser))) .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser.getIdentifier()))); initializeUserManagerState( @@ -254,7 +314,8 @@ public class UserManagerStateTest { when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true); // Return a ResolveInfo for the correct managed profile. - when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + when(mMockPackageManager.queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), eq(mPersonalUser))) .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser2.getIdentifier()))); initializeUserManagerState( @@ -280,7 +341,8 @@ public class UserManagerStateTest { when(mMockUserManager.isManagedProfile(mManagedUser2.getIdentifier())).thenReturn(true); // Return a ResolveInfo for the OTHER managed profile. - when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + when(mMockPackageManager.queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), eq(mPersonalUser))) .thenReturn(List.of(new ReflectedResolveInfo(mManagedUser.getIdentifier()))); initializeUserManagerState( @@ -349,19 +411,14 @@ public class UserManagerStateTest { public void testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsPersonalUser() { initializeUserManagerState( UserId.of(mPersonalUser), - Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2)); + Arrays.asList(mPersonalUser, mManagedUser)); InstrumentationRegistry.getInstrumentation() .runOnMainSync( () -> { List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser), - UserId.of(mOtherUser1)) - : Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser)); + Arrays.asList( + UserId.of(mPersonalUser), + UserId.of(mManagedUser)); assertThat(mUserManagerState.getAllUserProfileIds()) .containsExactlyElementsIn(userIdList); @@ -372,19 +429,14 @@ public class UserManagerStateTest { public void testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsManagedUser() { initializeUserManagerState( UserId.of(mManagedUser), - Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2)); + Arrays.asList(mPersonalUser, mManagedUser)); InstrumentationRegistry.getInstrumentation() .runOnMainSync( () -> { List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser), - UserId.of(mOtherUser1)) - : Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser)); + Arrays.asList( + UserId.of(mPersonalUser), + UserId.of(mManagedUser)); assertThat(mUserManagerState.getAllUserProfileIds()) .containsExactlyElementsIn(userIdList); @@ -392,22 +444,21 @@ public class UserManagerStateTest { } @Test - public void testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsOtherUser1() { + public void + testGetAllUserProfileIdsThatNeedToShowInPhotoPicker_currentUserIsOtherUser1VPlus() { + assumeTrue(SdkLevel.isAtLeastV()); initializeUserManagerState( UserId.of(mOtherUser1), - Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2)); + Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1)); InstrumentationRegistry.getInstrumentation() .runOnMainSync( () -> { List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser), - UserId.of(mOtherUser1)) - : Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser)); + Arrays.asList( + UserId.of(mPersonalUser), + UserId.of(mManagedUser), + UserId.of(mOtherUser1)); + assertThat(mUserManagerState.getAllUserProfileIds()) .containsExactlyElementsIn(userIdList); }); @@ -433,6 +484,7 @@ public class UserManagerStateTest { @Test public void testUserIds_multiUserProfilesAvailable_currentUserIsPersonalUser() { + assumeFalse(SdkLevel.isAtLeastV()); UserId currentUser = UserId.of(mPersonalUser); // if available user profiles are {personal , managed, otherUser1 } @@ -446,13 +498,7 @@ public class UserManagerStateTest { assertThat(mUserManagerState.getCurrentUserProfileId()) .isEqualTo(UserId.of(mPersonalUser)); - List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser), - UserId.of(mOtherUser1)) - : Arrays.asList( + List<UserId> userIdList = Arrays.asList( UserId.of(mPersonalUser), UserId.of(mManagedUser)); assertThat(mUserManagerState.getAllUserProfileIds()) @@ -463,77 +509,36 @@ public class UserManagerStateTest { mUserManagerState.getCurrentUserProfileId())) .isFalse(); }); - - // if available user profiles are {personal , otherUser1 } - initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser, mOtherUser1)); - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - if (SdkLevel.isAtLeastV()) { - assertThat(mUserManagerState.isMultiUserProfiles()).isTrue(); - } else { - assertThat(mUserManagerState.isMultiUserProfiles()).isFalse(); - } - - List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mOtherUser1)) - : Arrays.asList(UserId.of(mPersonalUser)); - assertThat(mUserManagerState.getAllUserProfileIds()) - .containsExactlyElementsIn(userIdList); - }); - - // if available user profiles are {personal , otherUser2 } - initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser, mOtherUser2)); - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - assertThat(mUserManagerState.isMultiUserProfiles()).isFalse(); - - assertThat(mUserManagerState.getCurrentUserProfileId()) - .isEqualTo(UserId.of(mPersonalUser)); - assertThat(mUserManagerState.getAllUserProfileIds()) - .containsExactlyElementsIn( - Arrays.asList(UserId.of(mPersonalUser))); - }); } @Test - public void testUserIds_multiUserProfilesAvailable_currentUserIsOtherUser2() { - UserId currentUser = UserId.of(mOtherUser2); + public void testUserIds_multiUserProfilesAvailable_currentUserIsPersonalUserVPlus() { + assumeTrue(SdkLevel.isAtLeastV()); + UserId currentUser = UserId.of(mPersonalUser); + // if available user profiles are {personal , managed, otherUser1 } initializeUserManagerState( - currentUser, Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1, mOtherUser2)); + currentUser, Arrays.asList(mPersonalUser, mManagedUser, mOtherUser1)); InstrumentationRegistry.getInstrumentation() .runOnMainSync( () -> { assertThat(mUserManagerState.isMultiUserProfiles()).isTrue(); + assertThat(mUserManagerState.getCurrentUserProfileId()) - .isEqualTo(UserId.of(mOtherUser2)); + .isEqualTo(UserId.of(mPersonalUser)); List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser), - UserId.of(mOtherUser1)) - : Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser)); + Arrays.asList( + UserId.of(mPersonalUser), + UserId.of(mManagedUser), + UserId.of(mOtherUser1)); assertThat(mUserManagerState.getAllUserProfileIds()) .containsExactlyElementsIn(userIdList); - }); - initializeUserManagerState(currentUser, Arrays.asList(mPersonalUser, mOtherUser2)); - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - assertThat(mUserManagerState.isMultiUserProfiles()).isFalse(); - assertThat(mUserManagerState.getAllUserProfileIds()) - .containsExactlyElementsIn( - Arrays.asList(UserId.of(mPersonalUser))); + assertThat( + mUserManagerState.isManagedUserProfile( + mUserManagerState.getCurrentUserProfileId())) + .isFalse(); }); } @@ -556,56 +561,6 @@ public class UserManagerStateTest { mUserManagerState.getCurrentUserProfileId())) .isTrue(); }); - - // set current user as otherUser2 - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser2)); - assertThat(mUserManagerState.getCurrentUserProfileId()) - .isEqualTo(UserId.of(mManagedUser)); - }); - - // set current user as otherUser1 - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser1)); - UserHandle currentUserProfile = - SdkLevel.isAtLeastV() ? mOtherUser1 : mManagedUser; - assertThat(mUserManagerState.getCurrentUserProfileId()) - .isEqualTo(UserId.of(currentUserProfile)); - }); - - // set current user as personalUser - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mPersonalUser)); - assertThat(mUserManagerState.getCurrentUserProfileId()) - .isEqualTo(UserId.of(mPersonalUser)); - }); - - // set current user otherUser3 - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - mUserManagerState.setUserAsCurrentUserProfile(UserId.of(mOtherUser3)); - assertThat(mUserManagerState.getCurrentUserProfileId()) - .isEqualTo(UserId.of(mPersonalUser)); - - List<UserId> userIdList = - SdkLevel.isAtLeastV() - ? Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser), - UserId.of(mOtherUser1)) - : Arrays.asList( - UserId.of(mPersonalUser), - UserId.of(mManagedUser)); - assertThat(mUserManagerState.getAllUserProfileIds()) - .containsExactlyElementsIn(userIdList); - }); } private void initializeUserManagerState(UserId current, List<UserHandle> usersOnDevice) { |