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

Commit 097d283c authored by Adam Bookatz's avatar Adam Bookatz
Browse files

stopExcessUsers avoids stopping audio/alarm users

If we have too many running users, we try to stop some excess ones. We
take into account how recently the user was started, and whether it (or
its parent) is currently running, but not much else.
In particular, if the user (or its profile) is currently playing audio
or has an upcoming alarm, we may still stop it, which isn't ideal. So we
now take this into account. In such cases, we try to stop other users
instead, and if we cannot, we will schedule stopping the user for later.

Note that this requires making the scheduled-background-user-stop
infrastructure also work with profiles. This is pretty straightforward;
we just need to make sure not to stop the current user's profiles, nor
schedule stopping the current user's profiles. This can be done via
!stopProfileRegardlessOfParent and !isCurrentProfile, respectively.
Just to make the logic easier to think about, some extra checks related
to this have been added (even if, strictly speaking, they cannot happen
since they've already been checked by the caller.)

Bug: 401340391
Bug: 330351042
Bug: 378575468
Test: UserControllerTest
Flag: android.multiuser.schedule_stop_of_background_user
Change-Id: I9e63fb27b6bb25e3b3c1654d6b11adefd39856d8
parent 16ff8bfa
Loading
Loading
Loading
Loading
+73 −21
Original line number Diff line number Diff line
@@ -335,6 +335,7 @@ class UserController implements Handler.Callback {
    /**
     * Mapping from each known user ID to the profile group ID it is associated with.
     * <p>Users not present in this array have a profile group of NO_PROFILE_GROUP_ID.
     * <p>Users present in this array do not have a profile group of NO_PROFILE_GROUP_ID.
     *
     * <p>For better or worse, this class sometimes assumes that the profileGroupId of a parent user
     * is always identical with its userId. If that ever becomes false, this class needs updating.
@@ -605,31 +606,52 @@ class UserController implements Handler.Callback {

    private void stopExcessRunningUsers() {
        final ArraySet<Integer> exemptedUsers = new ArraySet<>();
        final ArraySet<Integer> avoidUsers = new ArraySet<>();

        final List<UserInfo> users = mInjector.getUserManager().getUsers(true);
        for (int i = 0; i < users.size(); i++) {
            final int userId = users.get(i).id;
            if (isAlwaysVisibleUser(userId)) {
                exemptedUsers.add(userId);
            } else if (avoidStoppingUserRightNow(userId)) {
                avoidUsers.add(userId);
            }
        }

        synchronized (mLock) {
            stopExcessRunningUsersLU(mMaxRunningUsers, exemptedUsers);
            stopExcessRunningUsersLU(mMaxRunningUsers, exemptedUsers, avoidUsers);
        }
    }

    /**
     * Attempts to stop users (based on {@link #getRunningUsersLU() when they were last started})
     * until the total running users is within maxRunningUsers.
     *
     * @param exemptedUsers users that must not be stopped by this method
     * @param avoidUsers users that we should avoid stopping if possible, but can be scheduled for
     *                   stopping later if necessary.
     */
    @GuardedBy("mLock")
    private void stopExcessRunningUsersLU(int maxRunningUsers, ArraySet<Integer> exemptedUsers) {
    private void stopExcessRunningUsersLU(int maxRunningUsers,
            ArraySet<Integer> exemptedUsers, ArraySet<Integer> avoidUsers) {

        List<Integer> candidatesForScheduledStopping = new ArrayList<>(avoidUsers.size());

        List<Integer> currentlyRunningLru = getRunningUsersLU();
        Iterator<Integer> iterator = currentlyRunningLru.iterator();
        while (currentlyRunningLru.size() > maxRunningUsers && iterator.hasNext()) {
            Integer userId = iterator.next();
            final Integer userId = iterator.next();
            if (userId == UserHandle.USER_SYSTEM
                    || userId == mCurrentUserId
                    || exemptedUsers.contains(userId)) {
                // System and current users can't be stopped, and an exempt user shouldn't be
                continue;
            }
            if (avoidUsers.contains(userId)) {
                // Try not to stop these users. Keep track (in order) in case second pass is needed.
                candidatesForScheduledStopping.add(userId);
                continue;
            }
            // allowDelayedLocking set here as stopping user is done without any explicit request
            // from outside.
            Slogf.i(TAG, "Too many running users (%d). Attempting to stop user %d",
@@ -645,6 +667,17 @@ class UserController implements Handler.Callback {
                iterator.remove();
            }
        }

        // If necessary, do a second pass, on candidatesForScheduledStopping.
        int excessUsers = currentlyRunningLru.size() - maxRunningUsers;
        for (int i = 0; excessUsers > 0 && i < candidatesForScheduledStopping.size(); i++) {
            final Integer userId = candidatesForScheduledStopping.get(i);
            Slogf.i(TAG, "Still %d too many running users. Scheduling to later stop user %d",
                    excessUsers, userId);
            if (rescheduleStopOfBackgroundUser(userId)) {
                excessUsers--;
            }
        }
    }

    /**
@@ -2243,7 +2276,7 @@ class UserController implements Handler.Callback {
        }

        if (android.multiuser.Flags.scheduleStopOfBackgroundUser()) {
            if (userStartMode == USER_START_MODE_BACKGROUND && userInfo.isFull() &&
            if (userStartMode == USER_START_MODE_BACKGROUND && !isCurrentProfile(userId) &&
                    (autoStopUserInSecs > 0 || mBackgroundUserScheduledStopTimeSecs > 0)) {
                if (!needStart
                        && !mHandler.hasEqualMessages(SCHEDULE_STOP_BACKGROUND_USER_MSG,
@@ -2661,37 +2694,50 @@ class UserController implements Handler.Callback {
     * Possibly schedules the user to be stopped at a future point, even though we had originally
     * wanted to stop it now. For example, perhaps we have to postpone a scheduled user stop for
     * some reason, and therefore are rescheduling it until a later point.
     * This is only intended for full users that are currently in the background.
     *
     * This is only intended for background users (including profiles).
     */
    private void rescheduleStopOfBackgroundUser(@UserIdInt int userId) {
        scheduleStopOfBackgroundUser(userId, POSTPONEMENT_TIME_FOR_BACKGROUND_USER_STOP_SECS);
    private boolean rescheduleStopOfBackgroundUser(@UserIdInt Integer userIdInteger) {
        // Clear any previous schedule (since we've already evaluated that we need to postpone) if
        // necessary, and schedule stopping the user soon.
        mHandler.removeEqualMessages(SCHEDULE_STOP_BACKGROUND_USER_MSG, userIdInteger);
        return scheduleStopOfBackgroundUser(userIdInteger,
                POSTPONEMENT_TIME_FOR_BACKGROUND_USER_STOP_SECS);
    }

    /**
     * Possibly schedules the user to be stopped after the given number of seconds.
     * This is only intended for full users that are currently in the background.
     *
     * This is only intended for background users (including profiles).
     */
    private void scheduleStopOfBackgroundUser(@UserIdInt int userId, int delayUptimeSecs) {
    private boolean scheduleStopOfBackgroundUser(@UserIdInt int userId, int delayUptimeSecs) {
        if (!android.multiuser.Flags.scheduleStopOfBackgroundUser()) {
            return;
            return false;
        }
        if (delayUptimeSecs <= 0) {
            return;
            return false;
        }
        if (UserManager.isVisibleBackgroundUsersEnabled()) {
            // Feature is not enabled on this device.
            return;
            return false;
        }
        if (userId == UserHandle.USER_SYSTEM) {
            // Never stop system user
            return;
            return false;
        }
        if (isAlwaysVisibleUser(userId)) {
            return false;
        }
        synchronized(mLock) {
            if (isCurrentProfile(userId)) {
                // Surprisingly, the user, or its parent, is the current user. Refuse to schedule.
                return false;
            }
            final UserState uss = mStartedUsers.get(userId);
            if (uss == null || uss.state == UserState.STATE_STOPPING
                    || uss.state == UserState.STATE_SHUTDOWN) {
                // We've stopped (or are stopping) the user anyway, so don't bother scheduling.
                return;
                return false;
            }
        }
        Slogf.d(TAG, "Scheduling to stop user %d in %d seconds", userId, delayUptimeSecs);
@@ -2700,16 +2746,17 @@ class UserController implements Handler.Callback {
        mHandler.sendMessageDelayed(
                mHandler.obtainMessage(SCHEDULE_STOP_BACKGROUND_USER_MSG, msgObj),
                delayUptimeMs);
        return true;
    }

    /**
     * Possibly stops the given full user due to it having been in the background for a long time.
     * Possibly stops the given user due to it having been in the background for a long time.
     * There is no guarantee of stopping the user; it is done discretionarily.
     *
     * This should never be called for background visible users; devices that support this should
     * not use {@link #scheduleStopOfBackgroundUser(int, int)}.
     *
     * @param userIdInteger a full user to be stopped if it is still in the background
     * @param userIdInteger a user to be stopped if it is still in the background
     */
    @VisibleForTesting
    void processScheduledStopOfBackgroundUser(Integer userIdInteger) {
@@ -2723,16 +2770,17 @@ class UserController implements Handler.Callback {
        if (avoidStoppingUserRightNow(userId)) {
            Slogf.d(TAG, "Rescheduling bg stopping of user %d because it (or its profile) should "
                    + "not be stopped right now ", userId);
            rescheduleStopOfBackgroundUser(userId);
            rescheduleStopOfBackgroundUser(userIdInteger);
            return;
        }

        synchronized (mLock) {
            if (getCurrentOrTargetUserIdLU() == userId) {
            final Integer userOrParent = mUserProfileGroupIds.get(userId, userIdInteger);
            if (getCurrentOrTargetUserIdLU() == userOrParent) {
                // User is (somehow) already in the foreground, or we're currently switching to it.
                return;
            }
            if (mPendingTargetUserIds.contains(userIdInteger)) {
            if (mPendingTargetUserIds.contains(userOrParent)) {
                // We'll soon want to switch to this user, so don't kill it now.
                return;
            }
@@ -2741,11 +2789,12 @@ class UserController implements Handler.Callback {
                // Don't kill any background users for the sake of a Guest. Just reschedule instead.
                Slogf.d(TAG, "Current user %d is a Guest, so reschedule bg stopping of user %d",
                        currentOrTargetUser.id, userId);
                rescheduleStopOfBackgroundUser(userId);
                rescheduleStopOfBackgroundUser(userIdInteger);
                return;
            }
            Slogf.i(TAG, "Stopping background user %d due to inactivity", userId);
            stopUsersLU(userId, /* allowDelayedLocking= */ true, null, null);
            stopUsersLU(userId, /* stopProfileRegardlessOfParent= */ false,
                    /* allowDelayedLocking= */ true, null, null);
        }
    }

@@ -2757,6 +2806,9 @@ class UserController implements Handler.Callback {
     * Makes requests of other services, so don't call while holding a lock.
     */
    private boolean avoidStoppingUserRightNow(@UserIdInt int userId) {
        if (!android.multiuser.Flags.scheduleStopOfBackgroundUser()) {
            return false;
        }
        final int[] usersThatWouldStop;
        synchronized (mLock) {
            usersThatWouldStop = getUsersToStopLU(userId);