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);
             }
         }
     }