diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a3bca4c8976ec..2a8c88ec05cdc 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -305,6 +305,7 @@ public class UserManagerService extends IUserManager.Stub { private PackageManagerInternal mPmInternal; private DevicePolicyManagerInternal mDevicePolicyManagerInternal; + private ActivityManagerInternal mAmInternal; /** Indicates that this is the 1st boot after the system user mode was changed by emulation. */ private boolean mUpdatingSystemUserMode; @@ -383,7 +384,7 @@ public class UserManagerService extends IUserManager.Stub { } @GuardedBy("mUsersLock") - private final SparseArray mUsers = new SparseArray<>(); + private final SparseArray mUsers; /** * Map of user type names to their corresponding {@link UserTypeDetails}. @@ -626,8 +627,6 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserStates") private final WatchedUserStates mUserStates = new WatchedUserStates(); - - /** * Set on on devices that support background users (key) running on secondary displays (value). */ @@ -707,10 +706,11 @@ public class UserManagerService extends IUserManager.Stub { } } - // TODO b/28848102 Add support for test dependencies injection + // TODO(b/28848102) Add support for test dependencies injection @VisibleForTesting UserManagerService(Context context) { - this(context, null, null, new Object(), context.getCacheDir()); + this(context, /* pm= */ null, /* userDataPreparer= */ null, + /* packagesLock= */ new Object(), context.getCacheDir(), /* users= */ null); } /** @@ -720,14 +720,18 @@ public class UserManagerService extends IUserManager.Stub { */ UserManagerService(Context context, PackageManagerService pm, UserDataPreparer userDataPreparer, Object packagesLock) { - this(context, pm, userDataPreparer, packagesLock, Environment.getDataDirectory()); + this(context, pm, userDataPreparer, packagesLock, Environment.getDataDirectory(), + /* users= */ null); } - private UserManagerService(Context context, PackageManagerService pm, - UserDataPreparer userDataPreparer, Object packagesLock, File dataDir) { + @VisibleForTesting + UserManagerService(Context context, PackageManagerService pm, + UserDataPreparer userDataPreparer, Object packagesLock, File dataDir, + SparseArray users) { mContext = context; mPm = pm; mPackagesLock = packagesLock; + mUsers = users != null ? users : new SparseArray<>(); mHandler = new MainHandler(); mUserDataPreparer = userDataPreparer; mUserTypes = UserTypeFactory.getUserTypes(); @@ -1088,9 +1092,20 @@ public class UserManagerService extends IUserManager.Stub { @Override public int getProfileParentId(@UserIdInt int userId) { checkManageUsersPermission("get the profile parent"); - return mLocalService.getProfileParentId(userId); + return getProfileParentIdUnchecked(userId); } + private @UserIdInt int getProfileParentIdUnchecked(@UserIdInt int userId) { + synchronized (mUsersLock) { + UserInfo profileParent = getProfileParentLU(userId); + if (profileParent == null) { + return userId; + } + return profileParent.id; + } + } + + @GuardedBy("mUsersLock") private UserInfo getProfileParentLU(@UserIdInt int userId) { UserInfo profile = getUserInfoLU(userId); @@ -1702,8 +1717,7 @@ public class UserManagerService extends IUserManager.Stub { + ") is running in the foreground"); } - int currentUser = Binder.withCleanCallingIdentity(() -> ActivityManager.getCurrentUser()); - return currentUser == userId; + return userId == getCurrentUserId(); } @Override @@ -1751,15 +1765,39 @@ public class UserManagerService extends IUserManager.Stub { } } - private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) { - int currentUserId = Binder.withCleanCallingIdentity(() -> ActivityManager.getCurrentUser()); + /** + * Gets the current user id, calling {@link ActivityManagerInternal} directly (and without + * performing any permission check). + * + * @return id of current foreground user, or {@link UserHandle#USER_NULL} if + * {@link ActivityManagerInternal} is not available yet. + */ + @VisibleForTesting + int getCurrentUserId() { + ActivityManagerInternal activityManagerInternal = getActivityManagerInternal(); + if (activityManagerInternal == null) { + Slog.w(LOG_TAG, "getCurrentUserId() called too early, ActivityManagerInternal" + + " is not set yet"); + return UserHandle.USER_NULL; + } + return activityManagerInternal.getCurrentUserId(); + } + + /** + * Gets whether the user is the current foreground user or a started profile of that user. + * + *

Doesn't perform any permission check. + */ + @VisibleForTesting + boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) { + int currentUserId = getCurrentUserId(); if (currentUserId == userId) { return true; } - if (isProfile(userId)) { - int parentId = Binder.withCleanCallingIdentity(() -> getProfileParentId(userId)); + if (isProfileUnchecked(userId)) { + int parentId = getProfileParentIdUnchecked(userId); if (parentId == currentUserId) { return isUserRunning(userId); } @@ -5080,7 +5118,7 @@ public class UserManagerService extends IUserManager.Stub { final long ident = Binder.clearCallingIdentity(); try { final UserData userData; - int currentUser = ActivityManager.getCurrentUser(); + int currentUser = getCurrentUserId(); if (currentUser == userId) { Slog.w(LOG_TAG, "Current user cannot be removed."); return false; @@ -5208,7 +5246,7 @@ public class UserManagerService extends IUserManager.Stub { } // Attempt to immediately remove a non-current user - final int currentUser = ActivityManager.getCurrentUser(); + final int currentUser = getCurrentUserId(); if (currentUser != userId) { // Attempt to remove the user. This will fail if the user is the current user if (removeUserUnchecked(userId)) { @@ -6009,11 +6047,10 @@ public class UserManagerService extends IUserManager.Stub { return; } - final ActivityManagerInternal amInternal = LocalServices - .getService(ActivityManagerInternal.class); + final int currentUserId = getCurrentUserId(); pw.print("Current user: "); - if (amInternal != null) { - pw.println(amInternal.getCurrentUserId()); + if (currentUserId != UserHandle.USER_NULL) { + pw.println(currentUserId); } else { pw.println("N/A"); } @@ -6140,13 +6177,13 @@ public class UserManagerService extends IUserManager.Stub { private void dumpUser(PrintWriter pw, @UserIdInt int userId, StringBuilder sb, long now, long nowRealtime) { if (userId == UserHandle.USER_CURRENT) { - final ActivityManagerInternal amInternal = LocalServices - .getService(ActivityManagerInternal.class); - if (amInternal == null) { + final int currentUserId = getCurrentUserId(); + pw.print("Current user: "); + if (currentUserId == UserHandle.USER_NULL) { pw.println("Cannot determine current user"); return; } - userId = amInternal.getCurrentUserId(); + userId = currentUserId; } synchronized (mUsersLock) { @@ -6392,7 +6429,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public void removeAllUsers() { - if (UserHandle.USER_SYSTEM == ActivityManager.getCurrentUser()) { + if (UserHandle.USER_SYSTEM == getCurrentUserId()) { // Remove the non-system users straight away. removeNonSystemUsers(); } else { @@ -6574,13 +6611,7 @@ public class UserManagerService extends IUserManager.Stub { @Override public int getProfileParentId(@UserIdInt int userId) { - synchronized (mUsersLock) { - UserInfo profileParent = getProfileParentLU(userId); - if (profileParent == null) { - return userId; - } - return profileParent.id; - } + return getProfileParentIdUnchecked(userId); } @Override @@ -6909,4 +6940,12 @@ public class UserManagerService extends IUserManager.Stub { } return mDevicePolicyManagerInternal; } + + /** Returns the internal activity manager interface. */ + private @Nullable ActivityManagerInternal getActivityManagerInternal() { + if (mAmInternal == null) { + mAmInternal = LocalServices.getService(ActivityManagerInternal.class); + } + return mAmInternal; + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java new file mode 100644 index 0000000000000..9aa28ce741aa6 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java @@ -0,0 +1,81 @@ +/* + * 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import android.util.Log; + +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.After; +import org.junit.Before; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +/** + * Base class to make it easier to write tests that uses {@code ExtendedMockito}. + * + */ +public abstract class ExtendedMockitoTestCase { + + private static final String TAG = ExtendedMockitoTestCase.class.getSimpleName(); + + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private MockitoSession mSession; + + @Before + public void startSession() { + if (DEBUG) { + Log.d(TAG, "startSession()"); + } + StaticMockitoSessionBuilder builder = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT); + initializeSession(builder); + mSession = builder.startMocking(); + } + + /** + * Initializes the mockito session. + * + *

Typically used to define which classes should have static methods mocked or spied. + */ + protected void initializeSession(StaticMockitoSessionBuilder builder) { + if (DEBUG) { + Log.d(TAG, "initializeSession()"); + } + } + + @After + public final void finishSession() { + if (mSession == null) { + Log.w(TAG, "finishSession(): no session"); + return; + } + try { + if (DEBUG) { + Log.d(TAG, "finishSession()"); + } + } finally { + // mSession.finishMocking() must ALWAYS be called (hence the over-protective try/finally + // statements), otherwise it would cause failures on future tests as mockito + // cannot start a session when a previous one is not finished + mSession.finishMocking(); + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java new file mode 100644 index 0000000000000..64931116bd01c --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -0,0 +1,221 @@ +/* + * 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.pm; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; +import android.util.SparseArray; + +import androidx.test.annotation.UiThreadTest; + +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; +import com.android.server.ExtendedMockitoTestCase; +import com.android.server.LocalServices; +import com.android.server.am.UserState; +import com.android.server.pm.UserManagerService.UserData; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +/** + * Run as {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserManagerServiceTest} + */ +public final class UserManagerServiceTest extends ExtendedMockitoTestCase { + + private static final String TAG = UserManagerServiceTest.class.getSimpleName(); + + private final Object mPackagesLock = new Object(); + private final Context mRealContext = androidx.test.InstrumentationRegistry.getInstrumentation() + .getTargetContext(); + private Context mSpiedContext; + + private @Mock PackageManagerService mMockPms; + private @Mock UserDataPreparer mMockUserDataPreparer; + private @Mock ActivityManagerInternal mActivityManagerInternal; + + private final SparseArray mUsers = new SparseArray<>(); + private UserManagerService mUms; + private UserManagerInternal mUmi; + + @Override + protected void initializeSession(StaticMockitoSessionBuilder builder) { + builder + .spyStatic(UserManager.class) + .spyStatic(LocalServices.class); + } + + @Before + @UiThreadTest // Needed to initialize main handler + public void setFixtures() { + mSpiedContext = spy(mRealContext); + + // Called when WatchedUserStates is constructed + doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache()); + + mUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer, + mPackagesLock, mRealContext.getDataDir(), mUsers); + mUmi = LocalServices.getService(UserManagerInternal.class); + + assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mUmi) + .isNotNull(); + } + + @After + public void resetLocalService() { + // LocalServices follows the "Highlander rule" - There can be only one! + LocalServices.removeServiceForTest(UserManagerInternal.class); + } + + @Test + public void testgetCurrentUserId_amInternalNotReady() { + mockGetLocalService(ActivityManagerInternal.class, null); + + assertWithMessage("getCurrentUserId()").that(mUms.getCurrentUserId()) + .isEqualTo(UserHandle.USER_NULL); + } + + @Test + public void testgetCurrentUserId() { + mockCurrentUser(42); + + assertWithMessage("getCurrentUserId()").that(mUms.getCurrentUserId()) + .isEqualTo(42); + } + + @Test + public void testIsCurrentUserOrRunningProfileOfCurrentUser_currentUser() { + int userId = 42; + mockCurrentUser(userId); + + assertWithMessage("isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId) + .that(mUms.isCurrentUserOrRunningProfileOfCurrentUser(userId)).isTrue(); + } + + @Test + public void testIsCurrentUserOrRunningProfileOfCurrentUser_notCurrentUser() { + int userId = 42; + mockCurrentUser(108); + + assertWithMessage("isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId) + .that(mUms.isCurrentUserOrRunningProfileOfCurrentUser(userId)).isFalse(); + } + + @Test + public void testIsCurrentUserOrRunningProfileOfCurrentUser_startedProfileOfCurrentUser() { + int parentId = 108; + int profileId = 42; + addUser(parentId); + addProfile(profileId, parentId); + mockCurrentUser(parentId); + setUserState(profileId, UserState.STATE_RUNNING_UNLOCKED); + + assertWithMessage("isCurrentUserOrRunningProfileOfCurrentUser(%s)", profileId) + .that(mUms.isCurrentUserOrRunningProfileOfCurrentUser(profileId)).isTrue(); + } + + @Test + public void testIsCurrentUserOrRunningProfileOfCurrentUser_stoppedProfileOfCurrentUser() { + int parentId = 108; + int profileId = 42; + addUser(parentId); + addProfile(profileId, parentId); + mockCurrentUser(parentId); + // TODO(b/244798930): should set it to STATE_STOPPING or STATE_SHUTDOWN instead + removeUserState(profileId); + + assertWithMessage("isCurrentUserOrRunningProfileOfCurrentUser(%s)", profileId) + .that(mUms.isCurrentUserOrRunningProfileOfCurrentUser(profileId)).isFalse(); + } + + @Test + public void testIsCurrentUserOrRunningProfileOfCurrentUser_profileOfNonCurrentUSer() { + int parentId = 108; + int profileId = 42; + int currentUserId = 666; + addUser(parentId); + addProfile(profileId, parentId); + mockCurrentUser(currentUserId); + + assertWithMessage("isCurrentUserOrRunningProfileOfCurrentUser(%s)", profileId) + .that(mUms.isCurrentUserOrRunningProfileOfCurrentUser(profileId)).isFalse(); + } + + private void mockCurrentUser(@UserIdInt int userId) { + mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal); + + when(mActivityManagerInternal.getCurrentUserId()).thenReturn(userId); + } + + private void mockGetLocalService(Class serviceClass, T service) { + doReturn(service).when(() -> LocalServices.getService(serviceClass)); + } + + private void addProfile(@UserIdInt int profileId, @UserIdInt int parentId) { + TestUserData profileData = new TestUserData(profileId); + profileData.info.flags = UserInfo.FLAG_PROFILE; + profileData.info.profileGroupId = parentId; + + addUserData(profileData); + } + + private void addUser(@UserIdInt int userId) { + TestUserData userData = new TestUserData(userId); + + addUserData(userData); + } + + private void addUserData(TestUserData userData) { + Log.d(TAG, "Adding " + userData); + mUsers.put(userData.info.id, userData); + } + + private void setUserState(@UserIdInt int userId, int userState) { + mUmi.setUserState(userId, userState); + } + + private void removeUserState(@UserIdInt int userId) { + mUmi.removeUserState(userId); + } + + private static final class TestUserData extends UserData { + + @SuppressWarnings("unused") + TestUserData(@UserIdInt int userId) { + info = new UserInfo(); + info.id = userId; + } + + @Override + public String toString() { + return "TestUserData[" + info.toFullString() + "]"; + } + } +}