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

Commit 857e47c8 authored by Felipe Leme's avatar Felipe Leme Committed by Android (Google) Code Review
Browse files

Merge "UserVisibilityMediator refactoring, step 1."

parents 5802e1f2 a2c65f11
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -1650,7 +1650,12 @@ class UserController implements Handler.Callback {
                return false;
            }

            mInjector.getUserManagerInternal().assignUserToDisplay(userId, displayId);
            if (!userInfo.preCreated) {
                // TODO(b/244644281): UMI should return whether the user is visible. And if fails,
                // the user should not be in the mediator's started users structure
                mInjector.getUserManagerInternal().assignUserToDisplay(userId,
                        userInfo.profileGroupId, foreground, displayId);
            }

            // TODO(b/239982558): might need something similar for bg users on secondary display
            if (foreground && isUserSwitchUiEnabled()) {
@@ -1716,6 +1721,9 @@ class UserController implements Handler.Callback {
                    userSwitchUiEnabled = mUserSwitchUiEnabled;
                }
                mInjector.updateUserConfiguration();
                // TODO(b/244644281): updateProfileRelatedCaches() is called on both if and else
                // parts, ideally it should be moved outside, but for now it's not as there are many
                // calls to external components here afterwards
                updateProfileRelatedCaches();
                mInjector.getWindowManager().setCurrentUser(userId);
                mInjector.reportCurWakefulnessUsageEvent();
+8 −3
Original line number Diff line number Diff line
@@ -348,13 +348,18 @@ public abstract class UserManagerInternal {
     * <p>On most devices this call will be a no-op, but it will be used on devices that support
     * multiple users on multiple displays (like automotives with passenger displays).
     *
     * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user
     * is started)
     *
     * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to
     * check it. In fact, one of the intended clients for this method is
     * {@code DisplayManagerService}, which will call it when a virtual display is created (another
     * client is {@code UserController}, which will call it when a user is started).
     *
     */
    public abstract void assignUserToDisplay(@UserIdInt int userId, int displayId);
    // TODO(b/244644281): rename to assignUserToDisplayOnStart() and make sure it's called on boot
    // as well
    public abstract void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId,
            boolean foreground, int displayId);

    /**
     * Unassigns a user from its current display.
@@ -363,7 +368,7 @@ public abstract class UserManagerInternal {
     * multiple users on multiple displays (like automotives with passenger displays).
     *
     * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user
     * is stopped) and {@code DisplayManagerService} (when a virtual display is destroyed).
     * is stopped).
     */
    public abstract void unassignUserFromDisplay(@UserIdInt int userId);

+8 −6
Original line number Diff line number Diff line
@@ -633,7 +633,7 @@ public class UserManagerService extends IUserManager.Stub {
    @GuardedBy("mUserStates")
    private final WatchedUserStates mUserStates = new WatchedUserStates();

    private final UserVisibilityMediator mUserVisibilityMediator;
    private final UserVisibilityMediator mUserVisibilityMediator = new UserVisibilityMediator();

    private static UserManagerService sInstance;

@@ -756,7 +756,6 @@ public class UserManagerService extends IUserManager.Stub {
        mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING);
        mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null;
        emulateSystemUserModeIfNeeded();
        mUserVisibilityMediator = new UserVisibilityMediator(this);
    }

    void systemReady() {
@@ -6154,7 +6153,7 @@ public class UserManagerService extends IUserManager.Stub {
                    dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime);
                    return;
                case "--visibility-mediator":
                    mUserVisibilityMediator.dump(pw);
                    mUserVisibilityMediator.dump(pw, args);
                    return;
            }
        }
@@ -6220,7 +6219,7 @@ public class UserManagerService extends IUserManager.Stub {
        } // synchronized (mPackagesLock)

        pw.println();
        mUserVisibilityMediator.dump(pw);
        mUserVisibilityMediator.dump(pw, args);
        pw.println();

        // Dump some capabilities
@@ -6799,13 +6798,16 @@ public class UserManagerService extends IUserManager.Stub {
        }

        @Override
        public void assignUserToDisplay(@UserIdInt int userId, int displayId) {
            mUserVisibilityMediator.assignUserToDisplay(userId, displayId);
        public void assignUserToDisplay(@UserIdInt int userId, @UserIdInt int profileGroupId,
                boolean foreground, int displayId) {
            mUserVisibilityMediator.startUser(userId, profileGroupId, foreground, displayId);
            mUserVisibilityMediator.assignUserToDisplay(userId, profileGroupId, displayId);
        }

        @Override
        public void unassignUserFromDisplay(@UserIdInt int userId) {
            mUserVisibilityMediator.unassignUserFromDisplay(userId);
            mUserVisibilityMediator.stopUser(userId);
        }

        @Override
+224 −74
Original line number Diff line number Diff line
@@ -15,10 +15,18 @@
 */
package com.android.server.pm;

import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID;
import static android.os.UserHandle.USER_CURRENT;
import static android.os.UserHandle.USER_NULL;
import static android.view.Display.DEFAULT_DISPLAY;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.DebugUtils;
import android.util.Dumpable;
import android.util.IndentingPrintWriter;
import android.util.SparseIntArray;
import android.view.Display;
@@ -29,6 +37,8 @@ import com.android.internal.util.Preconditions;
import com.android.server.utils.Slogf;

import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Class responsible for deciding whether a user is visible (or visible for a given display).
@@ -36,73 +46,147 @@ import java.io.PrintWriter;
 * <p>This class is thread safe.
 */
// TODO(b/244644281): improve javadoc (for example, explain all cases / modes)
public final class UserVisibilityMediator {
public final class UserVisibilityMediator implements Dumpable {

    private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE

    private static final String TAG = UserVisibilityMediator.class.getSimpleName();

    private final Object mLock = new Object();
    private static final String PREFIX_START_USER_RESULT = "START_USER_";

    // NOTE: it's set as USER_CURRENT instead of USER_NULL because NO_PROFILE_GROUP_ID has the same
    // falue of USER_NULL, which would complicate some checks (especially on unit tests)
    @VisibleForTesting
    static final int INITIAL_CURRENT_USER_ID = USER_CURRENT;

    public static final int START_USER_RESULT_SUCCESS_VISIBLE = 1;
    public static final int START_USER_RESULT_SUCCESS_INVISIBLE = 2;
    public static final int START_USER_RESULT_FAILURE = -1;

    // TODO(b/244644281): should not depend on service, but keep its own internal state (like
    // current user and profile groups), but it is initially as the code was just moved from UMS
    // "as is". Similarly, it shouldn't need to pass the SparseIntArray on constructor (which was
    // added to UMS for testing purposes)
    private final UserManagerService mService;
    @IntDef(flag = false, prefix = {PREFIX_START_USER_RESULT}, value = {
            START_USER_RESULT_SUCCESS_VISIBLE,
            START_USER_RESULT_SUCCESS_INVISIBLE,
            START_USER_RESULT_FAILURE
    })
    public @interface StartUserResult {}

    private final Object mLock = new Object();

    private final boolean mUsersOnSecondaryDisplaysEnabled;

    @UserIdInt
    @GuardedBy("mLock")
    private int mCurrentUserId = INITIAL_CURRENT_USER_ID;

    @Nullable
    @GuardedBy("mLock")
    private final SparseIntArray mUsersOnSecondaryDisplays;
    private final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray();

    UserVisibilityMediator(UserManagerService service) {
        this(service, UserManager.isUsersOnSecondaryDisplaysEnabled(),
                /* usersOnSecondaryDisplays= */ null);
    /**
     * Mapping from each started user to its profile group.
     */
    @GuardedBy("mLock")
    private final SparseIntArray mStartedProfileGroupIds = new SparseIntArray();

    UserVisibilityMediator() {
        this(UserManager.isUsersOnSecondaryDisplaysEnabled());
    }

    @VisibleForTesting
    UserVisibilityMediator(UserManagerService service, boolean usersOnSecondaryDisplaysEnabled,
            @Nullable SparseIntArray usersOnSecondaryDisplays) {
        mService = service;
    UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled) {
        mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
        if (mUsersOnSecondaryDisplaysEnabled) {
            mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null
                    ? new SparseIntArray() // default behavior
                    : usersOnSecondaryDisplays; // passed by unit test
    }

    /**
     * TODO(b/244644281): merge with assignUserToDisplay() or add javadoc.
     */
    public @StartUserResult int startUser(@UserIdInt int userId, @UserIdInt int profileGroupId,
            boolean foreground, int displayId) {
        int actualProfileGroupId = profileGroupId == NO_PROFILE_GROUP_ID
                ? userId
                : profileGroupId;
        if (DBG) {
            Slogf.d(TAG, "startUser(%d, %d, %b, %d): actualProfileGroupId=%d",
                    userId, profileGroupId, foreground, displayId, actualProfileGroupId);
        }
        if (foreground && displayId != DEFAULT_DISPLAY) {
            Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start foreground user on "
                    + "secondary display", userId, actualProfileGroupId, foreground, displayId);
            return START_USER_RESULT_FAILURE;
        }

        int visibility;
        synchronized (mLock) {
            if (isProfile(userId, actualProfileGroupId)) {
                if (displayId != DEFAULT_DISPLAY) {
                    Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user on "
                            + "secondary display", userId, actualProfileGroupId, foreground,
                            displayId);
                    return START_USER_RESULT_FAILURE;
                }
                if (foreground) {
                    Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user in "
                            + "foreground");
                    return START_USER_RESULT_FAILURE;
                } else {
                    boolean isParentRunning = mStartedProfileGroupIds
                            .get(actualProfileGroupId) == actualProfileGroupId;
                    if (DBG) {
                        Slogf.d(TAG, "profile parent running: %b", isParentRunning);
                    }
                    visibility = isParentRunning
                            ? START_USER_RESULT_SUCCESS_VISIBLE
                            : START_USER_RESULT_SUCCESS_INVISIBLE;
                }
            } else if (foreground) {
                mCurrentUserId = userId;
                visibility = START_USER_RESULT_SUCCESS_VISIBLE;
            } else {
            mUsersOnSecondaryDisplays = null;
                visibility = START_USER_RESULT_SUCCESS_INVISIBLE;
            }
            if (DBG) {
                Slogf.d(TAG, "adding user / profile mapping (%d -> %d) and returning %s",
                        userId, actualProfileGroupId, startUserResultToString(visibility));
            }
            mStartedProfileGroupIds.put(userId, actualProfileGroupId);
        }
        return visibility;
    }

    /**
     * See {@link UserManagerInternal#assignUserToDisplay(int, int)}.
     * TODO(b/244644281): merge with unassignUserFromDisplay() or add javadoc (and unit tests)
     */
    public void assignUserToDisplay(int userId, int displayId) {
    public void stopUser(@UserIdInt int userId) {
        if (DBG) {
            Slogf.d(TAG, "assignUserToDisplay(%d, %d)", userId, displayId);
            Slogf.d(TAG, "stopUser(%d)", userId);
        }
        synchronized (mLock) {
            mStartedProfileGroupIds.delete(userId);
        }
    }

        // NOTE: Using Boolean instead of boolean as it will be re-used below
        Boolean isProfile = null;
        if (displayId == Display.DEFAULT_DISPLAY) {
            if (mUsersOnSecondaryDisplaysEnabled) {
                // Profiles are only supported in the default display, but it cannot return yet
                // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY
                // (this is done indirectly below when it checks that the profile parent is the
                // current user, as the current user is always assigned to the DEFAULT_DISPLAY).
                isProfile = isProfileUnchecked(userId);
    /**
     * See {@link UserManagerInternal#assignUserToDisplay(int, int)}.
     */
    public void assignUserToDisplay(int userId, int profileGroupId, int displayId) {
        if (DBG) {
            Slogf.d(TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplaysEnabled=%b",
                    userId, displayId, mUsersOnSecondaryDisplaysEnabled);
        }
            if (isProfile == null || !isProfile) {

        if (displayId == DEFAULT_DISPLAY
                && (!mUsersOnSecondaryDisplaysEnabled || !isProfile(userId, profileGroupId))) {
            // Don't need to do anything because methods (such as isUserVisible()) already
                // know that the current user (and their profiles) is assigned to the default
                // display.
            // know that the current user (and their profiles) is assigned to the default display.
            // But on MUMD devices, it profiles are only supported in the default display, so it
            // cannot return yet as it needs to check if the parent is also assigned to the
            // DEFAULT_DISPLAY (this is done indirectly below when it checks that the profile parent
            // is the current user, as the current user is always assigned to the DEFAULT_DISPLAY).
            if (DBG) {
                Slogf.d(TAG, "ignoring on default display");
            }
            return;
        }
        }

        if (!mUsersOnSecondaryDisplaysEnabled) {
            throw new UnsupportedOperationException("assignUserToDisplay(" + userId + ", "
@@ -119,16 +203,12 @@ public final class UserVisibilityMediator {
        Preconditions.checkArgument(userId != currentUserId,
                "Cannot assign current user (%d) to other displays", currentUserId);

        if (isProfile == null) {
            isProfile = isProfileUnchecked(userId);
        }
        synchronized (mLock) {
            if (isProfile) {
        if (isProfile(userId, profileGroupId)) {
            // Profile can only start in the same display as parent. And for simplicity,
            // that display must be the DEFAULT_DISPLAY.
            Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
                    "Profile user can only be started in the default display");
                int parentUserId = getProfileParentId(userId);
            int parentUserId = getStartedProfileGroupId(userId);
            Preconditions.checkArgument(parentUserId == currentUserId,
                    "Only profile of current user can be assigned to a display");
            if (DBG) {
@@ -137,6 +217,7 @@ public final class UserVisibilityMediator {
            return;
        }

        synchronized (mLock) {
            // Check if display is available
            for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
                int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i);
@@ -289,7 +370,7 @@ public final class UserVisibilityMediator {
                    continue;
                }
                int userId = mUsersOnSecondaryDisplays.keyAt(i);
                if (!isProfileUnchecked(userId)) {
                if (!isStartedProfile(userId)) {
                    return userId;
                } else if (DBG) {
                    Slogf.d(TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's "
@@ -307,8 +388,25 @@ public final class UserVisibilityMediator {
    }

    private void dump(IndentingPrintWriter ipw) {
        ipw.println("UserVisibilityManager");
        ipw.println("UserVisibilityMediator");
        ipw.increaseIndent();

        synchronized (mLock) {
            ipw.print("Current user id: ");
            ipw.println(mCurrentUserId);

            ipw.print("Number of started user / profile group mappings: ");
            ipw.println(mStartedProfileGroupIds.size());
            if (mStartedProfileGroupIds.size() > 0) {
                ipw.increaseIndent();
                for (int i = 0; i < mStartedProfileGroupIds.size(); i++) {
                    ipw.print("User #");
                    ipw.print(mStartedProfileGroupIds.keyAt(i));
                    ipw.print(" -> profile #");
                    ipw.println(mStartedProfileGroupIds.valueAt(i));
                }
                ipw.decreaseIndent();
            }

            ipw.print("Supports users on secondary displays: ");
            ipw.println(mUsersOnSecondaryDisplaysEnabled);
@@ -319,11 +417,13 @@ public final class UserVisibilityMediator {
                    ipw.println(mUsersOnSecondaryDisplays);
                }
            }
        }

        ipw.decreaseIndent();
    }

    void dump(PrintWriter pw) {
    @Override
    public void dump(PrintWriter pw, String[] args) {
        if (pw instanceof IndentingPrintWriter) {
            dump((IndentingPrintWriter) pw);
            return;
@@ -331,20 +431,70 @@ public final class UserVisibilityMediator {
        dump(new IndentingPrintWriter(pw));
    }

    // TODO(b/244644281): remove methods below once this class caches that state
    private @UserIdInt int getCurrentUserId() {
        return mService.getCurrentUserId();
    @VisibleForTesting
    Map<Integer, Integer> getUsersOnSecondaryDisplays() {
        Map<Integer, Integer> map;
        synchronized (mLock) {
            int size = mUsersOnSecondaryDisplays.size();
            map = new LinkedHashMap<>(size);
            for (int i = 0; i < size; i++) {
                map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i));
            }
        }
        Slogf.v(TAG, "getUsersOnSecondaryDisplays(): returning %s", map);
        return map;
    }

    /**
     * Gets the user-friendly representation of the {@code result}.
     */
    public static String startUserResultToString(@StartUserResult int result) {
        return DebugUtils.constantToString(UserVisibilityMediator.class, PREFIX_START_USER_RESULT,
                result);
    }

    // TODO(b/244644281): methods below are needed because some APIs use the current users (full and
    // profiles) state to decide whether a user is visible or not. If we decide to always store that
    // info into intermediate maps, we should remove them.

    @VisibleForTesting
    @UserIdInt int getCurrentUserId() {
        synchronized (mLock) {
            return mCurrentUserId;
        }
    }

    @VisibleForTesting
    boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
        synchronized (mLock) {
            // Special case as NO_PROFILE_GROUP_ID == USER_NULL
            if (userId == USER_NULL || mCurrentUserId == USER_NULL) {
                return false;
            }
            if (mCurrentUserId == userId) {
                return true;
            }
            return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID) == mCurrentUserId;
        }
    }

    private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
        return mService.isCurrentUserOrRunningProfileOfCurrentUser(userId);
    private static boolean isProfile(@UserIdInt int userId, @UserIdInt int profileGroupId) {
        return profileGroupId != NO_PROFILE_GROUP_ID && profileGroupId != userId;
    }

    private boolean isProfileUnchecked(@UserIdInt int userId) {
        return mService.isProfileUnchecked(userId);
    @VisibleForTesting
    boolean isStartedProfile(@UserIdInt int userId) {
        int profileGroupId;
        synchronized (mLock) {
            profileGroupId = mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
        }
        return isProfile(userId, profileGroupId);
    }

    private @UserIdInt int getProfileParentId(@UserIdInt int userId) {
        return mService.getProfileParentId(userId);
    @VisibleForTesting
    @UserIdInt int getStartedProfileGroupId(@UserIdInt int userId) {
        synchronized (mLock) {
            return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
        }
    }
}
+91 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server;

import android.util.Dumpable;
import android.util.Log;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * {@code JUnit} rule that logs (using tag {@value #TAG} the contents of
 * {@link Dumpable dumpables} in case of failure.
 */
public final class DumpableDumperRule implements TestRule {

    private static final String TAG = DumpableDumperRule.class.getSimpleName();

    private static final String[] NO_ARGS = {};

    private final List<Dumpable> mDumpables = new ArrayList<>();

    /**
     * Adds a {@link Dumpable} to be logged if the test case fails.
     */
    public void addDumpable(Dumpable dumpable) {
        mDumpables.add(dumpable);
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                try {
                    base.evaluate();
                } catch (Throwable t) {
                    dumpOnFailure(description.getMethodName());
                    throw t;
                }
            }
        };
    }

    private void dumpOnFailure(String testName) throws IOException {
        if (mDumpables.isEmpty()) {
            return;
        }
        Log.w(TAG, "Dumping " + mDumpables.size() + " dumpables on failure of " + testName);
        mDumpables.forEach(d -> logDumpable(d));
    }

    private void logDumpable(Dumpable dumpable) {
        try {
            try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
                dumpable.dump(pw, NO_ARGS);
                String[] dump = sw.toString().split(System.lineSeparator());
                Log.w(TAG, "Dumping " + dumpable.getDumpableName() + " (" + dump.length
                        + " lines):");
                for (String line : dump) {
                    Log.w(TAG, line);
                }

            } catch (RuntimeException e) {
                Log.e(TAG, "RuntimeException dumping " + dumpable.getDumpableName(), e);
            }
        } catch (IOException e) {
            Log.e(TAG, "IOException dumping " + dumpable.getDumpableName(), e);
        }
    }
}
Loading