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)
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -149,6 +149,13 @@ public final class UserTypeDetails {
|
||||
*/
|
||||
private final @Nullable int[] mDarkThemeBadgeColors;
|
||||
|
||||
/**
|
||||
* Denotes if the user shares media with its parent user.
|
||||
*
|
||||
* <p> 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<DefaultCrossProfileIntentFilter> defaultCrossProfileIntentFilters) {
|
||||
@Nullable List<DefaultCrossProfileIntentFilter> 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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<UserInfo> list = mUserManager.getUsers();
|
||||
List<UserInfo> 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 {
|
||||
|
||||
Reference in New Issue
Block a user