Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c22af8e2 authored by Jordan Jozwiak's avatar Jordan Jozwiak
Browse files

Add removeUserOrSetEphemeral

To support removing a user as soon as possible, we introduce an
API that will remove a non-current user immediately or mark the
current user as ephemeral so that it is removed on user switch
or reboot.

This API will be used by CarService, which runs in a different
process, so the API must be exposed on UserManager (instead of
just UserManagerInternal).

Bug: 155913815
Test: adb shell pm remove-user --set-ephemeral-if-in-use <uid>
Change-Id: I5975a70c439bd7892939bcee65be2f2dd85026ed
parent fc97abca
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -86,6 +86,7 @@ interface IUserManager {
    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);
+17 −0
Original line number Diff line number Diff line
@@ -3977,6 +3977,23 @@ public class UserManager {
        }
    }

    /**
     * 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.
     *
+47 −3
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATIO
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;
@@ -2685,9 +2686,18 @@ class PackageManagerShellCommand extends ShellCommand {
        }
    }

    // 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;
@@ -2695,6 +2705,15 @@ class PackageManagerShellCommand extends ShellCommand {
        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;
@@ -2704,6 +2723,27 @@ class PackageManagerShellCommand extends ShellCommand {
        }
    }

    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();
@@ -3768,9 +3808,13 @@ class PackageManagerShellCommand extends ShellCommand {
        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("");
+123 −9
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
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.internal.widget.LockPatternUtils;
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.IOException;
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 @@ public class UserManagerService extends IUserManager.Stub {
    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 @@ public class UserManagerService extends IUserManager.Stub {

    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 @@ public class UserManagerService extends IUserManager.Stub {
        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 @@ public class UserManagerService extends IUserManager.Stub {
        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) {
        long ident = Binder.clearCallingIdentity();
        try {
@@ -3974,6 +4030,64 @@ public class UserManagerService extends IUserManager.Stub {
        }
    }

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

+106 −20
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import android.test.suitebuilder.annotation.MediumTest;
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.Executors;
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 @@ public final class UserManagerTest {
    @SmallTest
    @Test
    public void testHasSystemUser() throws Exception {
        assertThat(findUser(UserHandle.USER_SYSTEM)).isTrue();
        assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue();
    }

    @MediumTest
@@ -164,9 +167,9 @@ public final class UserManagerTest {
        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 @@ public final class UserManagerTest {
        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 @@ public final class UserManagerTest {
            }
        }

        assertThat(findUser(userInfo.id)).isFalse();
        assertThat(hasUser(userInfo.id)).isFalse();
    }

    @MediumTest
@@ -208,6 +211,79 @@ public final class UserManagerTest {
        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 @@ public final class UserManagerTest {
                () -> 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,6 +999,12 @@ public final class UserManagerTest {
    private void removeUser(int userId) {
        synchronized (mUserRemoveLock) {
            mUserManager.removeUser(userId);
            waitForUserRemovalLocked(userId);
        }
    }

    @GuardedBy("mUserRemoveLock")
    private void waitForUserRemovalLocked(int userId) {
        long time = System.currentTimeMillis();
        while (mUserManager.getUserInfo(userId) != null) {
            try {
@@ -931,7 +1018,6 @@ public final class UserManagerTest {
            }
        }
    }
    }

    private UserInfo createUser(String name, int flags) {
        UserInfo user = mUserManager.createUser(name, flags);