From 5a4fc52fc73c8a0f17634d7b5815d52d1bcba811 Mon Sep 17 00:00:00 2001 From: Saumya Pathak Date: Wed, 17 Mar 2021 11:27:01 +0000 Subject: [PATCH] Add app clone profile and APIs. This change adds a new hidden user type "android.os.usertype.profile.CLONE" and two new system APIs "isCloneProfile" and "hasSharedMedia". To support App cloning, we need to support sharing media between the owner user and the clone user. Adding this as a property in UserTypeDetails. Bug: 182396009 Test: atest android.multiuser.cts.UserManagerTest#testCloneUser, atest com.android.server.pm.UserManagerTest#testCloneUser and manually ran shell commands on the clone user. Change-Id: I49ddcbd499944ca9ec05ef4dc10d2e2b231d8b88 (cherry picked from commit db930667a21dac9f0de7cb2fe99175b3b4545bf4) --- core/api/system-current.txt | 3 + core/api/test-current.txt | 1 + core/java/android/content/pm/UserInfo.java | 4 ++ core/java/android/os/IUserManager.aidl | 2 + core/java/android/os/UserManager.java | 69 +++++++++++++++++++ .../android/server/pm/UserManagerService.java | 20 ++++++ .../android/server/pm/UserTypeDetails.java | 31 ++++++++- .../android/server/pm/UserTypeFactory.java | 17 +++++ .../android/server/pm/UserManagerTest.java | 35 ++++++++++ 9 files changed, 179 insertions(+), 3 deletions(-) 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 {