diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 7bbabb6632695..a3b4a5b5b9dc3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -8495,6 +8495,7 @@ package android.os { method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean hasRestrictedProfiles(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean hasUserRestrictionForUser(@NonNull String, @NonNull android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isAdminUser(); + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isCloneProfile(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isGuestUser(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isManagedProfile(int); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isPrimaryUser(); @@ -8508,6 +8509,7 @@ package android.os { method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean removeUser(@NonNull android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setUserIcon(@NonNull android.graphics.Bitmap) throws android.os.UserManager.UserOperationException; method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setUserName(@Nullable String); + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean sharesMediaWithParent(); field public static final String ACTION_USER_RESTRICTIONS_CHANGED = "android.os.action.USER_RESTRICTIONS_CHANGED"; field @Deprecated public static final String DISALLOW_OEM_UNLOCK = "no_oem_unlock"; field public static final String DISALLOW_RUN_IN_BACKGROUND = "no_run_in_background"; @@ -8521,6 +8523,7 @@ package android.os { field public static final int SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED = 2; // 0x2 field public static final String USER_TYPE_FULL_SECONDARY = "android.os.usertype.full.SECONDARY"; field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM"; + field public static final String USER_TYPE_PROFILE_CLONE = "android.os.usertype.profile.CLONE"; field public static final String USER_TYPE_PROFILE_MANAGED = "android.os.usertype.profile.MANAGED"; field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS"; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index ae1cbf77dd8a7..09e0088cf9169 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -817,6 +817,7 @@ package android.content.pm { method public int describeContents(); method public android.os.UserHandle getUserHandle(); method public boolean isAdmin(); + method public boolean isCloneProfile(); method public boolean isDemo(); method public boolean isEnabled(); method public boolean isEphemeral(); diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index cfb6e1b572aaa..5a89708b58831 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -321,6 +321,10 @@ public class UserInfo implements Parcelable { return UserManager.isUserTypeManagedProfile(userType); } + public boolean isCloneProfile() { + return UserManager.isUserTypeCloneProfile(userType); + } + @UnsupportedAppUsage public boolean isEnabled() { return (flags & FLAG_DISABLED) != FLAG_DISABLED; diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 087568defb270..34f2c103f8660 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -99,6 +99,8 @@ interface IUserManager { boolean someUserHasSeedAccount(in String accountName, in String accountType); boolean isProfile(int userId); boolean isManagedProfile(int userId); + boolean isCloneProfile(int userId); + boolean sharesMediaWithParent(int userId); boolean isDemoUser(int userId); boolean isPreCreated(int userId); UserInfo createProfileForUserEvenWhenDisallowedWithThrow(in String name, in String userType, int flags, diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 5069e03191197..d4de4fa655265 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.StringDef; +import android.annotation.SuppressAutoDoc; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; @@ -135,6 +136,16 @@ public class UserManager { @SystemApi public static final String USER_TYPE_PROFILE_MANAGED = "android.os.usertype.profile.MANAGED"; + /** + * User type representing a clone profile. Clone profile is a user profile type used to run + * second instance of an otherwise single user App (eg, messengers). Only the primary user + * is allowed to have a clone profile. + * + * @hide + */ + @SystemApi + public static final String USER_TYPE_PROFILE_CLONE = "android.os.usertype.profile.CLONE"; + /** * User type representing a generic profile for testing purposes. Only on debuggable builds. * @hide @@ -1983,6 +1994,14 @@ public class UserManager { return USER_TYPE_FULL_DEMO.equals(userType); } + /** + * Returns whether the user type is a {@link UserManager#USER_TYPE_PROFILE_CLONE clone user}. + * @hide + */ + public static boolean isUserTypeCloneProfile(String userType) { + return USER_TYPE_PROFILE_CLONE.equals(userType); + } + /** * Returns the enum defined in the statsd UserLifecycleJourneyReported atom corresponding to the * user type. @@ -2232,6 +2251,31 @@ public class UserManager { } } + /** + * Checks if the context user is a clone profile. + * + *

Requires {@link android.Manifest.permission#MANAGE_USERS} or + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS} permission, otherwise the caller + * must be in the same profile group of the user. + * + * @return whether the context user is a clone profile. + * + * @see android.os.UserManager#USER_TYPE_PROFILE_CLONE + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, + Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) + @UserHandleAware + @SuppressAutoDoc + public boolean isCloneProfile() { + try { + return mService.isCloneProfile(mUserId); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** * Checks if the calling app is running as an ephemeral user. * @@ -4063,6 +4107,31 @@ public class UserManager { } } + /** + * If the user is a {@link UserManager#isProfile profile}, checks if the user + * shares media with its parent user (the user that created this profile). + * Returns false for any other type of user. + * + *

Requires {@link android.Manifest.permission#MANAGE_USERS} or + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS} permission, otherwise the + * caller must be in the same profile group as the user. + * + * @return true if the user shares media with its parent user, false otherwise. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS, + Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true) + @UserHandleAware + @SuppressAutoDoc + public boolean sharesMediaWithParent() { + try { + return mService.sharesMediaWithParent(mUserId); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** * Removes a user and all associated data. * @param userId the integer handle of the user. diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index fe19956ce8ce0..2a0257dd2a209 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1506,6 +1506,26 @@ public class UserManagerService extends IUserManager.Stub { } } + @Override + public boolean isCloneProfile(@UserIdInt int userId) { + checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isCloneProfile"); + synchronized (mUsersLock) { + UserInfo userInfo = getUserInfoLU(userId); + return userInfo != null && userInfo.isCloneProfile(); + } + } + + @Override + public boolean sharesMediaWithParent(@UserIdInt int userId) { + checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, + "sharesMediaWithParent"); + synchronized (mUsersLock) { + UserTypeDetails userTypeDetails = getUserTypeDetailsNoChecks(userId); + return userTypeDetails != null ? userTypeDetails.isProfile() + && userTypeDetails.sharesMediaWithParent() : false; + } + } + @Override public boolean isUserUnlockingOrUnlocked(@UserIdInt int userId) { checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java index 17ce386e37705..6824f7d8fa558 100644 --- a/services/core/java/com/android/server/pm/UserTypeDetails.java +++ b/services/core/java/com/android/server/pm/UserTypeDetails.java @@ -149,6 +149,13 @@ public final class UserTypeDetails { */ private final @Nullable int[] mDarkThemeBadgeColors; + /** + * Denotes if the user shares media with its parent user. + * + *

Default value is false + */ + private final boolean mSharesMediaWithParent; + private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed, @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label, int maxAllowedPerParent, @@ -158,7 +165,8 @@ public final class UserTypeDetails { @Nullable Bundle defaultRestrictions, @Nullable Bundle defaultSystemSettings, @Nullable Bundle defaultSecureSettings, - @Nullable List defaultCrossProfileIntentFilters) { + @Nullable List defaultCrossProfileIntentFilters, + boolean sharesMediaWithParent) { this.mName = name; this.mEnabled = enabled; this.mMaxAllowed = maxAllowed; @@ -177,6 +185,7 @@ public final class UserTypeDetails { this.mBadgeLabels = badgeLabels; this.mBadgeColors = badgeColors; this.mDarkThemeBadgeColors = darkThemeBadgeColors; + this.mSharesMediaWithParent = sharesMediaWithParent; } /** @@ -291,6 +300,13 @@ public final class UserTypeDetails { return (mBaseType & UserInfo.FLAG_SYSTEM) != 0; } + /** + * Returns true if the user has shared media with parent user or false otherwise. + */ + public boolean sharesMediaWithParent() { + return mSharesMediaWithParent; + } + /** Returns a {@link Bundle} representing the default user restrictions. */ @NonNull Bundle getDefaultRestrictions() { return BundleUtils.clone(mDefaultRestrictions); @@ -318,7 +334,6 @@ public final class UserTypeDetails { : Collections.emptyList(); } - /** Dumps details of the UserTypeDetails. Do not parse this. */ public void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("mName: "); pw.println(mName); @@ -383,6 +398,7 @@ public final class UserTypeDetails { private @DrawableRes int mIconBadge = Resources.ID_NULL; private @DrawableRes int mBadgePlain = Resources.ID_NULL; private @DrawableRes int mBadgeNoBackground = Resources.ID_NULL; + private boolean mSharesMediaWithParent = false; public Builder setName(String name) { mName = name; @@ -473,6 +489,15 @@ public final class UserTypeDetails { return this; } + /** + * Sets shared media property for the user. + * @param sharesMediaWithParent the value to be set, true or false + */ + public Builder setSharesMediaWithParent(boolean sharesMediaWithParent) { + mSharesMediaWithParent = sharesMediaWithParent; + return this; + } + @UserInfoFlag int getBaseType() { return mBaseType; } @@ -502,7 +527,7 @@ public final class UserTypeDetails { mIconBadge, mBadgePlain, mBadgeNoBackground, mBadgeLabels, mBadgeColors, mDarkThemeBadgeColors == null ? mBadgeColors : mDarkThemeBadgeColors, mDefaultRestrictions, mDefaultSystemSettings, mDefaultSecureSettings, - mDefaultCrossProfileIntentFilters); + mDefaultCrossProfileIntentFilters, mSharesMediaWithParent); } private boolean hasBadge() { diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java index 6aac0b2f3d0fe..e8421a5d966d9 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -29,6 +29,7 @@ import static android.os.UserManager.USER_TYPE_FULL_GUEST; import static android.os.UserManager.USER_TYPE_FULL_RESTRICTED; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_FULL_SYSTEM; +import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_TEST; import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS; @@ -100,6 +101,7 @@ public final class UserTypeFactory { builders.put(USER_TYPE_FULL_DEMO, getDefaultTypeFullDemo()); builders.put(USER_TYPE_FULL_RESTRICTED, getDefaultTypeFullRestricted()); builders.put(USER_TYPE_SYSTEM_HEADLESS, getDefaultTypeSystemHeadless()); + builders.put(USER_TYPE_PROFILE_CLONE, getDefaultTypeProfileClone()); if (Build.IS_DEBUGGABLE) { builders.put(USER_TYPE_PROFILE_TEST, getDefaultTypeProfileTest()); } @@ -107,6 +109,21 @@ public final class UserTypeFactory { return builders; } + /** + * Returns the Builder for the default {@link UserManager#USER_TYPE_PROFILE_CLONE} + * configuration. + */ + // TODO(b/182396009): Add default restrictions, if needed for clone user type. + private static UserTypeDetails.Builder getDefaultTypeProfileClone() { + return new UserTypeDetails.Builder() + .setName(USER_TYPE_PROFILE_CLONE) + .setBaseType(FLAG_PROFILE) + .setMaxAllowedPerParent(1) + .setLabel(0) + .setDefaultRestrictions(null) + .setSharesMediaWithParent(true); + } + /** * Returns the Builder for the default {@link UserManager#USER_TYPE_PROFILE_MANAGED} * configuration. diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 395b643e37770..fc2661103491d 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -58,6 +58,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import javax.annotation.concurrent.GuardedBy; @@ -158,6 +159,40 @@ public final class UserManagerTest { fail("Didn't find a guest: " + list); } + @Test + public void testCloneUser() throws Exception { + // Test that only one clone user can be created + final int primaryUserId = mUserManager.getPrimaryUser().id; + UserInfo userInfo = createProfileForUser("Clone user1", + UserManager.USER_TYPE_PROFILE_CLONE, + primaryUserId); + assertThat(userInfo).isNotNull(); + UserInfo userInfo2 = createProfileForUser("Clone user2", + UserManager.USER_TYPE_PROFILE_CLONE, + primaryUserId); + assertThat(userInfo2).isNull(); + + final Context userContext = mContext.createPackageContextAsUser("system", 0, + UserHandle.of(userInfo.id)); + assertThat(userContext.getSystemService( + UserManager.class).sharesMediaWithParent()).isTrue(); + + List list = mUserManager.getUsers(); + List cloneUsers = list.stream().filter( + user -> (user.id == userInfo.id && user.name.equals("Clone user1") + && user.isCloneProfile())) + .collect(Collectors.toList()); + assertThat(cloneUsers.size()).isEqualTo(1); + + // Verify clone user parent + assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id); + assertThat(parentProfileInfo).isNotNull(); + assertThat(primaryUserId).isEqualTo(parentProfileInfo.id); + removeUser(userInfo.id); + assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + } + @MediumTest @Test public void testAdd2Users() throws Exception {