Loading services/core/java/com/android/server/am/UserController.java +9 −1 Original line number Diff line number Diff line Loading @@ -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()) { Loading Loading @@ -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(); Loading services/core/java/com/android/server/pm/UserManagerInternal.java +8 −3 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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); Loading services/core/java/com/android/server/pm/UserManagerService.java +8 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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() { Loading Loading @@ -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; } } Loading Loading @@ -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 Loading Loading @@ -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 Loading services/core/java/com/android/server/pm/UserVisibilityMediator.java +224 −74 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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). Loading @@ -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 + ", " Loading @@ -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) { Loading @@ -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); Loading Loading @@ -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 " Loading @@ -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); Loading @@ -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; Loading @@ -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); } } } services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java 0 → 100644 +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
services/core/java/com/android/server/am/UserController.java +9 −1 Original line number Diff line number Diff line Loading @@ -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()) { Loading Loading @@ -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(); Loading
services/core/java/com/android/server/pm/UserManagerInternal.java +8 −3 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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); Loading
services/core/java/com/android/server/pm/UserManagerService.java +8 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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() { Loading Loading @@ -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; } } Loading Loading @@ -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 Loading Loading @@ -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 Loading
services/core/java/com/android/server/pm/UserVisibilityMediator.java +224 −74 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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). Loading @@ -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 + ", " Loading @@ -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) { Loading @@ -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); Loading Loading @@ -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 " Loading @@ -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); Loading @@ -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; Loading @@ -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); } } }
services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java 0 → 100644 +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); } } }