Merge "Add removeUserOrSetEphemeral"
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 07363ed..6fe5777 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -86,6 +86,7 @@
Bundle getApplicationRestrictionsForUser(in String packageName, int userId);
void setDefaultGuestRestrictions(in Bundle restrictions);
Bundle getDefaultGuestRestrictions();
+ int removeUserOrSetEphemeral(int userId);
boolean markGuestForDeletion(int userId);
UserInfo findCurrentGuestUser();
boolean isQuietModeEnabled(int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index ddc21ab..42ae930 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -3978,6 +3978,23 @@
}
/**
+ * Immediately removes the user or, if the user cannot be removed, such as when the user is
+ * the current user, then set the user as ephemeral so that it will be removed when it is
+ * stopped.
+ *
+ * @return the {@link com.android.server.pm.UserManagerService.RemoveResult} code
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+ public int removeUserOrSetEphemeral(@UserIdInt int userId) {
+ try {
+ return mService.removeUserOrSetEphemeral(userId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Updates the user's name.
*
* @param userId the user's integer id
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index c356fc7..c46a7ef 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -24,6 +24,7 @@
import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
import android.accounts.IAccountManager;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.role.IRoleManager;
@@ -2686,9 +2687,18 @@
}
}
+ // pm remove-user [--set-ephemeral-if-in-use] USER_ID
public int runRemoveUser() throws RemoteException {
int userId;
- String arg = getNextArg();
+ String arg;
+ boolean setEphemeralIfInUse = false;
+ while ((arg = getNextOption()) != null) {
+ if (arg.equals("--set-ephemeral-if-in-use")) {
+ setEphemeralIfInUse = true;
+ }
+ }
+
+ arg = getNextArg();
if (arg == null) {
getErrPrintWriter().println("Error: no user id specified.");
return 1;
@@ -2696,6 +2706,15 @@
userId = UserHandle.parseUserArg(arg);
IUserManager um = IUserManager.Stub.asInterface(
ServiceManager.getService(Context.USER_SERVICE));
+ if (setEphemeralIfInUse) {
+ return removeUserOrSetEphemeral(um, userId);
+ } else {
+ return removeUser(um, userId);
+ }
+ }
+
+ private int removeUser(IUserManager um, @UserIdInt int userId) throws RemoteException {
+ Slog.i(TAG, "Removing user " + userId);
if (um.removeUser(userId)) {
getOutPrintWriter().println("Success: removed user");
return 0;
@@ -2705,6 +2724,27 @@
}
}
+ private int removeUserOrSetEphemeral(IUserManager um, @UserIdInt int userId)
+ throws RemoteException {
+ Slog.i(TAG, "Removing " + userId + " or set as ephemeral if in use.");
+ int result = um.removeUserOrSetEphemeral(userId);
+ switch (result) {
+ case UserManagerService.REMOVE_RESULT_REMOVED:
+ getOutPrintWriter().printf("Success: user %d removed\n", userId);
+ return 0;
+ case UserManagerService.REMOVE_RESULT_SET_EPHEMERAL:
+ getOutPrintWriter().printf("Success: user %d set as ephemeral\n", userId);
+ return 0;
+ case UserManagerService.REMOVE_RESULT_ALREADY_BEING_REMOVED:
+ getOutPrintWriter().printf("Success: user %d is already being removed\n", userId);
+ return 0;
+ default:
+ getErrPrintWriter().printf("Error: couldn't remove or mark ephemeral user id %d\n",
+ userId);
+ return 1;
+ }
+ }
+
public int runSetUserRestriction() throws RemoteException {
int userId = UserHandle.USER_SYSTEM;
String opt = getNextOption();
@@ -3769,9 +3809,13 @@
pw.println(" --restricted is shorthand for '--user-type android.os.usertype.full.RESTRICTED'.");
pw.println(" --guest is shorthand for '--user-type android.os.usertype.full.GUEST'.");
pw.println("");
- pw.println(" remove-user USER_ID");
+ pw.println(" remove-user [--set-ephemeral-if-in-use] USER_ID");
pw.println(" Remove the user with the given USER_IDENTIFIER, deleting all data");
- pw.println(" associated with that user");
+ pw.println(" associated with that user.");
+ pw.println(" --set-ephemeral-if-in-use: If the user is currently running and");
+ pw.println(" therefore cannot be removed immediately, mark the user as ephemeral");
+ pw.println(" so that it will be automatically removed when possible (after user");
+ pw.println(" switch or reboot)");
pw.println("");
pw.println(" set-user-restriction [--user USER_ID] RESTRICTION VALUE");
pw.println("");
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3ec156f..766b30f 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -22,6 +22,7 @@
import android.Manifest;
import android.annotation.ColorRes;
import android.annotation.DrawableRes;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
@@ -110,7 +111,6 @@
import com.android.server.LocalServices;
import com.android.server.LockGuard;
import com.android.server.SystemService;
-import com.android.server.SystemService.TargetUser;
import com.android.server.am.UserState;
import com.android.server.storage.DeviceStorageMonitorInternal;
import com.android.server.utils.TimingsTraceAndSlog;
@@ -132,6 +132,8 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@@ -246,6 +248,43 @@
static final int WRITE_USER_MSG = 1;
static final int WRITE_USER_DELAY = 2*1000; // 2 seconds
+ /**
+ * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that the specified
+ * user has been successfully removed.
+ */
+ public static final int REMOVE_RESULT_REMOVED = 0;
+
+ /**
+ * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that the specified
+ * user has had its {@link UserInfo#FLAG_EPHEMERAL} flag set to {@code true}, so that it will be
+ * removed when the user is stopped or on boot.
+ */
+ public static final int REMOVE_RESULT_SET_EPHEMERAL = 1;
+
+ /**
+ * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that the specified
+ * user is already in the process of being removed.
+ */
+ public static final int REMOVE_RESULT_ALREADY_BEING_REMOVED = 2;
+
+ /**
+ * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that an error occurred
+ * that prevented the user from being removed or set as ephemeral.
+ */
+ public static final int REMOVE_RESULT_ERROR = 3;
+
+ /**
+ * Possible response codes from {@link #removeUserOrSetEphemeral(int)}.
+ */
+ @IntDef(prefix = { "REMOVE_RESULT_" }, value = {
+ REMOVE_RESULT_REMOVED,
+ REMOVE_RESULT_SET_EPHEMERAL,
+ REMOVE_RESULT_ALREADY_BEING_REMOVED,
+ REMOVE_RESULT_ERROR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RemoveResult {}
+
// Tron counters
private static final String TRON_GUEST_CREATED = "users_guest_created";
private static final String TRON_USER_CREATED = "users_user_created";
@@ -253,9 +292,17 @@
private final Context mContext;
private final PackageManagerService mPm;
+
+ /**
+ * Lock for packages. If using with {@link #mUsersLock}, {@link #mPackagesLock} should be
+ * acquired first.
+ */
private final Object mPackagesLock;
private final UserDataPreparer mUserDataPreparer;
- // Short-term lock for internal state, when interaction/sync with PM is not required
+ /**
+ * Short-term lock for internal state, when interaction/sync with PM is not required. If using
+ * with {@link #mPackagesLock}, {@link #mPackagesLock} should be acquired first.
+ */
private final Object mUsersLock = LockGuard.installNewLock(LockGuard.INDEX_USER);
private final Object mRestrictionsLock = new Object();
// Used for serializing access to app restriction files
@@ -3862,13 +3909,7 @@
Slog.i(LOG_TAG, "removeUser u" + userId);
checkManageOrCreateUsersPermission("Only the system can remove users");
- final boolean isManagedProfile;
- synchronized (mUsersLock) {
- UserInfo userInfo = getUserInfoLU(userId);
- isManagedProfile = userInfo != null && userInfo.isManagedProfile();
- }
- String restriction = isManagedProfile
- ? UserManager.DISALLOW_REMOVE_MANAGED_PROFILE : UserManager.DISALLOW_REMOVE_USER;
+ final String restriction = getUserRemovalRestriction(userId);
if (getUserRestrictions(UserHandle.getCallingUserId()).getBoolean(restriction, false)) {
Slog.w(LOG_TAG, "Cannot remove user. " + restriction + " is enabled.");
return false;
@@ -3882,6 +3923,21 @@
return removeUserUnchecked(userId);
}
+ /**
+ * Returns the string name of the restriction to check for user removal. The restriction name
+ * varies depending on whether the user is a managed profile.
+ */
+ private String getUserRemovalRestriction(@UserIdInt int userId) {
+ final boolean isManagedProfile;
+ final UserInfo userInfo;
+ synchronized (mUsersLock) {
+ userInfo = getUserInfoLU(userId);
+ }
+ isManagedProfile = userInfo != null && userInfo.isManagedProfile();
+ return isManagedProfile
+ ? UserManager.DISALLOW_REMOVE_MANAGED_PROFILE : UserManager.DISALLOW_REMOVE_USER;
+ }
+
private boolean removeUserUnchecked(@UserIdInt int userId) {
final long ident = Binder.clearCallingIdentity();
try {
@@ -3974,6 +4030,64 @@
}
}
+ @Override
+ public @RemoveResult int removeUserOrSetEphemeral(@UserIdInt int userId) {
+ Slog.i(LOG_TAG, "removeUserOrSetEphemeral u" + userId);
+ checkManageUsersPermission("Only the system can remove users");
+ final String restriction = getUserRemovalRestriction(userId);
+ if (getUserRestrictions(UserHandle.getCallingUserId()).getBoolean(restriction, false)) {
+ Slog.w(LOG_TAG, "Cannot remove user. " + restriction + " is enabled.");
+ return REMOVE_RESULT_ERROR;
+ }
+ if (userId == UserHandle.USER_SYSTEM) {
+ Slog.e(LOG_TAG, "System user cannot be removed.");
+ return REMOVE_RESULT_ERROR;
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final UserData userData;
+ synchronized (mPackagesLock) {
+ synchronized (mUsersLock) {
+ userData = mUsers.get(userId);
+ if (userData == null) {
+ Slog.e(LOG_TAG,
+ "Cannot remove user " + userId + ", invalid user id provided.");
+ return REMOVE_RESULT_ERROR;
+ }
+
+ if (mRemovingUserIds.get(userId)) {
+ Slog.e(LOG_TAG, "User " + userId + " is already scheduled for removal.");
+ return REMOVE_RESULT_ALREADY_BEING_REMOVED;
+ }
+ }
+
+ // Attempt to immediately remove a non-current user
+ final int currentUser = ActivityManager.getCurrentUser();
+ if (currentUser != userId) {
+ // Attempt to remove the user. This will fail if the user is the current user
+ if (removeUser(userId)) {
+ return REMOVE_RESULT_REMOVED;
+ }
+
+ Slog.w(LOG_TAG, "Unable to immediately remove non-current user: " + userId
+ + ". User is still set as ephemeral and will be removed on user "
+ + "switch or reboot.");
+ }
+
+ // If the user was not immediately removed, make sure it is marked as ephemeral.
+ // Don't mark as disabled since, per UserInfo.FLAG_DISABLED documentation, an
+ // ephemeral user should only be marked as disabled when its removal is in progress.
+ userData.info.flags |= UserInfo.FLAG_EPHEMERAL;
+ writeUserLP(userData);
+
+ return REMOVE_RESULT_SET_EPHEMERAL;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
void finishRemoveUser(final @UserIdInt int userId) {
if (DBG) Slog.i(LOG_TAG, "finishRemoveUser " + userId);
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 22b0715..7579956 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -41,6 +41,7 @@
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Slog;
+import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -58,6 +59,8 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.GuardedBy;
+
/** Test {@link UserManager} functionality. */
@RunWith(AndroidJUnit4.class)
public final class UserManagerTest {
@@ -134,7 +137,7 @@
@SmallTest
@Test
public void testHasSystemUser() throws Exception {
- assertThat(findUser(UserHandle.USER_SYSTEM)).isTrue();
+ assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue();
}
@MediumTest
@@ -164,9 +167,9 @@
assertThat(user1).isNotNull();
assertThat(user2).isNotNull();
- assertThat(findUser(UserHandle.USER_SYSTEM)).isTrue();
- assertThat(findUser(user1.id)).isTrue();
- assertThat(findUser(user2.id)).isTrue();
+ assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue();
+ assertThat(hasUser(user1.id)).isTrue();
+ assertThat(hasUser(user2.id)).isTrue();
}
@MediumTest
@@ -175,7 +178,7 @@
UserInfo userInfo = createUser("Guest 1", UserInfo.FLAG_GUEST);
removeUser(userInfo.id);
- assertThat(findUser(userInfo.id)).isFalse();
+ assertThat(hasUser(userInfo.id)).isFalse();
}
@MediumTest
@@ -199,7 +202,7 @@
}
}
- assertThat(findUser(userInfo.id)).isFalse();
+ assertThat(hasUser(userInfo.id)).isFalse();
}
@MediumTest
@@ -208,6 +211,79 @@
assertThrows(IllegalArgumentException.class, () -> mUserManager.removeUser(null));
}
+ @MediumTest
+ @Test
+ public void testRemoveUserOrSetEphemeral_restrictedReturnsError() throws Exception {
+ final int currentUser = ActivityManager.getCurrentUser();
+ final UserInfo user1 = createUser("User 1", /* flags= */ 0);
+ mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_USER, /* value= */ true,
+ asHandle(currentUser));
+ try {
+ assertThat(mUserManager.removeUserOrSetEphemeral(user1.id)).isEqualTo(
+ UserManagerService.REMOVE_RESULT_ERROR);
+ } finally {
+ mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_USER, /* value= */ false,
+ asHandle(currentUser));
+ }
+
+ assertThat(hasUser(user1.id)).isTrue();
+ assertThat(getUser(user1.id).isEphemeral()).isFalse();
+ }
+
+ @MediumTest
+ @Test
+ public void testRemoveUserOrSetEphemeral_systemUserReturnsError() throws Exception {
+ assertThat(mUserManager.removeUserOrSetEphemeral(UserHandle.USER_SYSTEM)).isEqualTo(
+ UserManagerService.REMOVE_RESULT_ERROR);
+
+ assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue();
+ }
+
+ @MediumTest
+ @Test
+ public void testRemoveUserOrSetEphemeral_invalidUserReturnsError() throws Exception {
+ assertThat(hasUser(Integer.MAX_VALUE)).isFalse();
+ assertThat(mUserManager.removeUserOrSetEphemeral(Integer.MAX_VALUE)).isEqualTo(
+ UserManagerService.REMOVE_RESULT_ERROR);
+ }
+
+ @MediumTest
+ @Test
+ public void testRemoveUserOrSetEphemeral_currentUserSetEphemeral() throws Exception {
+ final int startUser = ActivityManager.getCurrentUser();
+ final UserInfo user1 = createUser("User 1", /* flags= */ 0);
+ // Switch to the user just created.
+ switchUser(user1.id, null, /* ignoreHandle= */ true);
+
+ assertThat(mUserManager.removeUserOrSetEphemeral(user1.id)).isEqualTo(
+ UserManagerService.REMOVE_RESULT_SET_EPHEMERAL);
+
+ assertThat(hasUser(user1.id)).isTrue();
+ assertThat(getUser(user1.id).isEphemeral()).isTrue();
+
+ // Switch back to the starting user.
+ switchUser(startUser, null, /* ignoreHandle= */ true);
+
+ // User is removed once switch is complete
+ synchronized (mUserRemoveLock) {
+ waitForUserRemovalLocked(user1.id);
+ }
+ assertThat(hasUser(user1.id)).isFalse();
+ }
+
+ @MediumTest
+ @Test
+ public void testRemoveUserOrSetEphemeral_nonCurrentUserRemoved() throws Exception {
+ final UserInfo user1 = createUser("User 1", /* flags= */ 0);
+ synchronized (mUserRemoveLock) {
+ assertThat(mUserManager.removeUserOrSetEphemeral(user1.id)).isEqualTo(
+ UserManagerService.REMOVE_RESULT_REMOVED);
+ waitForUserRemovalLocked(user1.id);
+ }
+
+ assertThat(hasUser(user1.id)).isFalse();
+ }
+
/** Tests creating a FULL user via specifying userType. */
@MediumTest
@Test
@@ -608,15 +684,20 @@
() -> mUserManager.getUserCreationTime(asHandle(user.id)));
}
- private boolean findUser(int id) {
+ @Nullable
+ private UserInfo getUser(int id) {
List<UserInfo> list = mUserManager.getUsers();
for (UserInfo user : list) {
if (user.id == id) {
- return true;
+ return user;
}
}
- return false;
+ return null;
+ }
+
+ private boolean hasUser(int id) {
+ return getUser(id) != null;
}
@MediumTest
@@ -918,17 +999,22 @@
private void removeUser(int userId) {
synchronized (mUserRemoveLock) {
mUserManager.removeUser(userId);
- long time = System.currentTimeMillis();
- while (mUserManager.getUserInfo(userId) != null) {
- try {
- mUserRemoveLock.wait(REMOVE_CHECK_INTERVAL_MILLIS);
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- return;
- }
- if (System.currentTimeMillis() - time > REMOVE_TIMEOUT_MILLIS) {
- fail("Timeout waiting for removeUser. userId = " + userId);
- }
+ waitForUserRemovalLocked(userId);
+ }
+ }
+
+ @GuardedBy("mUserRemoveLock")
+ private void waitForUserRemovalLocked(int userId) {
+ long time = System.currentTimeMillis();
+ while (mUserManager.getUserInfo(userId) != null) {
+ try {
+ mUserRemoveLock.wait(REMOVE_CHECK_INTERVAL_MILLIS);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ if (System.currentTimeMillis() - time > REMOVE_TIMEOUT_MILLIS) {
+ fail("Timeout waiting for removeUser. userId = " + userId);
}
}
}