summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Tyler Saunders <tylersaunders@google.com> 2025-03-13 12:51:25 -0700
committer Android (Google) Code Review <android-gerrit@google.com> 2025-03-13 12:51:25 -0700
commita76927b02bf259ea557d4cdf1cd3967f29f72851 (patch)
tree7b97f68f88f0d608b80b2ee6e86a0082aab1d2d5
parent69c71c18f7143fffc0559bd46491876d34245bd7 (diff)
parent001b7bd2fae2078b6a6cd490958d495508a6d881 (diff)
Merge changes Iabc4de0d,I147d67b2 into main
* changes: Handle x-profile delegation for java picker. Properly handle delegation in x-profile checks.
-rw-r--r--photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt14
-rw-r--r--photopicker/src/com/android/photopicker/core/user/UserMonitor.kt130
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt9
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt310
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt9
-rw-r--r--photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt32
-rw-r--r--src/com/android/providers/media/photopicker/data/UserManagerState.java330
-rw-r--r--tests/src/com/android/providers/media/photopicker/data/UserManagerStateTest.java245
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) {