diff options
author | Keun young Park <keunyoung@google.com> | 2019-03-13 14:06:42 -0700 |
---|---|---|
committer | Keun young Park <keunyoung@google.com> | 2019-04-02 15:29:54 -0700 |
commit | df54b66b2f281b901b25cacb27c3514eaad42e4f (patch) | |
tree | 67737bd499e33ca3ab6c35f0fb211b6322f0674d | |
parent | 29e5ea6741605fcc7699163a1cf36a3b9a042dd5 (diff) |
Add multiuserDelayUserDataLocking configuration
- In this mode, when user switching happens, old user (=background user) is
always stopped but the user's data is kept unlocked.
- Locking of user data is postponed until mMaxRunningUsers is reached.
So this change does not leave more users unlocked while all background users
, other than user 0, are stopped.
- For users in stopped state with data unlocked, user can be re-started
in background for usage like running JobScheduler jobs.
- Exceptions to this configuration of stopping / locking background users are:
User 0 is never stopped / locked.
Ephemeral user is always stopped / locked.
User with DISALLOW_RUN_IN_BACKGROUND flag will be always stopped / locked.
- Added tests for this mode vs normal mode.
Bug: 128038498
Test: Do user switch and check user locking / stopping through log.
Change-Id: I554d0744adad172f361e9f00f609e2f91100b9e4
5 files changed, 251 insertions, 17 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 726dccb9ad78..ec2a6ae572c0 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2442,6 +2442,16 @@ <!-- Maximum number of users we allow to be running at a time --> <integer name="config_multiuserMaxRunningUsers">3</integer> + <!-- Whether to delay user data locking for background user. + If false, user switched-out from user switching will still be in running state until + config_multiuserMaxRunningUsers is reached. Once config_multiuserMaxRunningUsers is + reached, user will be stopped and user data is locked. + If true, user switched out from user switching will always be stopped but its user data + is not locked. Total number of unlocked users will be limited by + config_multiuserMaxRunningUsers. Once that limit is reached, least recently stopped user + will be locked. --> + <bool name="config_multiuserDelayUserDataLocking">false</bool> + <!-- Whether UI for multi user should be shown --> <bool name="config_enableMultiUserUI">false</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a29a4f8845f5..3e23640ed872 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -498,6 +498,7 @@ <java-symbol type="integer" name="config_lockSoundVolumeDb" /> <java-symbol type="integer" name="config_multiuserMaximumUsers" /> <java-symbol type="integer" name="config_multiuserMaxRunningUsers" /> + <java-symbol type="bool" name="config_multiuserDelayUserDataLocking" /> <java-symbol type="integer" name="config_safe_media_volume_index" /> <java-symbol type="integer" name="config_safe_media_volume_usb_mB" /> <java-symbol type="integer" name="config_mobile_mtu" /> diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 3eb7c0374a01..e0e4d61c9309 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -8772,6 +8772,8 @@ public class ActivityManagerService extends IActivityManager.Stub com.android.internal.R.bool.config_customUserSwitchUi); mUserController.mMaxRunningUsers = res.getInteger( com.android.internal.R.integer.config_multiuserMaxRunningUsers); + mUserController.mDelayUserDataLocking = res.getBoolean( + com.android.internal.R.bool.config_multiuserDelayUserDataLocking); mWaitForNetworkTimeoutMs = waitForNetworkTimeoutMs; } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index cc4116ebb789..2bd91985eafb 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -241,6 +241,20 @@ class UserController implements Handler.Callback { volatile boolean mBootCompleted; + /** + * In this mode, user is always stopped when switched out but locking of user data is + * postponed until total number of unlocked users in the system reaches mMaxRunningUsers. + * Once total number of unlocked users reach mMaxRunningUsers, least recentely used user + * will be locked. + */ + boolean mDelayUserDataLocking; + /** + * Keep track of last active users for mDelayUserDataLocking. + * The latest stopped user is placed in front while the least recently stopped user in back. + */ + @GuardedBy("mLock") + private final ArrayList<Integer> mLastActiveUsers = new ArrayList<>(); + UserController(ActivityManagerService service) { this(new Injector(service)); } @@ -738,7 +752,9 @@ class UserController implements Handler.Callback { void finishUserStopped(UserState uss) { final int userId = uss.mHandle.getIdentifier(); final boolean stopped; + boolean lockUser = true; ArrayList<IStopUserCallback> callbacks; + int userIdToLock = userId; synchronized (mLock) { callbacks = new ArrayList<>(uss.mStopCallbacks); if (mStartedUsers.get(userId) != uss || uss.state != UserState.STATE_SHUTDOWN) { @@ -749,9 +765,12 @@ class UserController implements Handler.Callback { mStartedUsers.remove(userId); mUserLru.remove(Integer.valueOf(userId)); updateStartedUserArrayLU(); + userIdToLock = updateUserToLockLU(userId); + if (userIdToLock == UserHandle.USER_NULL) { + lockUser = false; + } } } - if (stopped) { mInjector.getUserManagerInternal().removeUserState(userId); mInjector.activityManagerOnUserStopped(userId); @@ -776,18 +795,22 @@ class UserController implements Handler.Callback { mInjector.getUserManager().removeUserEvenWhenDisallowed(userId); } + if (!lockUser) { + return; + } + final int userIdToLockF = userIdToLock; // Evict the user's credential encryption key. Performed on FgThread to make it // serialized with call to UserManagerService.onBeforeUnlockUser in finishUserUnlocking // to prevent data corruption. FgThread.getHandler().post(() -> { synchronized (mLock) { - if (mStartedUsers.get(userId) != null) { + if (mStartedUsers.get(userIdToLockF) != null) { Slog.w(TAG, "User was restarted, skipping key eviction"); return; } } try { - getStorageManager().lockUserKey(userId); + mInjector.getStorageManager().lockUserKey(userIdToLockF); } catch (RemoteException re) { throw re.rethrowAsRuntimeException(); } @@ -796,6 +819,39 @@ class UserController implements Handler.Callback { } /** + * For mDelayUserDataLocking mode, storage once unlocked is kept unlocked. + * Total number of unlocked user storage is limited by mMaxRunningUsers. + * If there are more unlocked users, evict and lock the least recently stopped user and + * lock that user's data. Regardless of the mode, ephemeral user is always locked + * immediately. + * + * @return user id to lock. UserHandler.USER_NULL will be returned if no user should be locked. + */ + @GuardedBy("mLock") + private int updateUserToLockLU(int userId) { + int userIdToLock = userId; + if (mDelayUserDataLocking && !getUserInfo(userId).isEphemeral() + && !hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) { + mLastActiveUsers.remove((Integer) userId); // arg should be object, not index + mLastActiveUsers.add(0, userId); + int totalUnlockedUsers = mStartedUsers.size() + mLastActiveUsers.size(); + if (totalUnlockedUsers > mMaxRunningUsers) { // should lock a user + userIdToLock = mLastActiveUsers.get(mLastActiveUsers.size() - 1); + mLastActiveUsers.remove(mLastActiveUsers.size() - 1); + Slog.i(TAG, "finishUserStopped, stopping user:" + userId + + " lock user:" + userIdToLock); + } else { + Slog.i(TAG, "finishUserStopped, user:" + userId + + ",skip locking"); + // do not lock + userIdToLock = UserHandle.USER_NULL; + + } + } + return userIdToLock; + } + + /** * Determines the list of users that should be stopped together with the specified * {@code userId}. The returned list includes {@code userId}. */ @@ -896,9 +952,6 @@ class UserController implements Handler.Callback { } } - private IStorageManager getStorageManager() { - return IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); - } boolean startUser(final int userId, final boolean foreground) { return startUser(userId, foreground, null); } @@ -1199,7 +1252,7 @@ class UserController implements Handler.Callback { UserState uss; if (!StorageManager.isUserKeyUnlocked(userId)) { final UserInfo userInfo = getUserInfo(userId); - final IStorageManager storageManager = getStorageManager(); + final IStorageManager storageManager = mInjector.getStorageManager(); try { // We always want to unlock user storage, even user is not started yet storageManager.unlockUserKey(userId, userInfo.serialNumber, token, secret); @@ -1334,9 +1387,9 @@ class UserController implements Handler.Callback { if (oldUserId == UserHandle.USER_SYSTEM) { return; } - // For now, only check for user restriction. Additional checks can be added here + // If running in background is disabled or mDelayUserDataLocking mode, stop the user. boolean disallowRunInBg = hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, - oldUserId); + oldUserId) || mDelayUserDataLocking; if (!disallowRunInBg) { return; } @@ -2033,6 +2086,8 @@ class UserController implements Handler.Callback { pw.println(mUserProfileGroupIds.valueAt(i)); } } + pw.println(" mCurrentUserId:" + mCurrentUserId); + pw.println(" mLastActiveUsers:" + mLastActiveUsers); } } @@ -2326,5 +2381,9 @@ class UserController implements Handler.Callback { protected boolean isCallerRecents(int callingUid) { return mService.mAtmInternal.isCallerRecents(callingUid); } + + protected IStorageManager getStorageManager() { + return IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); + } } } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index f39c71682c61..46d076184eef 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -68,12 +68,15 @@ import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManagerInternal; +import android.os.storage.IStorageManager; import android.platform.test.annotations.Presubmit; import android.util.Log; + import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; +import com.android.server.FgThread; import com.android.server.pm.UserManagerService; import com.android.server.wm.WindowManagerService; @@ -82,7 +85,9 @@ import org.junit.Before; import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -95,12 +100,20 @@ import java.util.Set; */ @SmallTest @Presubmit + public class UserControllerTest { - private static final int TEST_USER_ID = 10; + // Use big enough user id to avoid picking up already active user id. + private static final int TEST_USER_ID = 100; + private static final int TEST_USER_ID1 = 101; + private static final int TEST_USER_ID2 = 102; private static final int NONEXIST_USER_ID = 2; private static final String TAG = UserControllerTest.class.getSimpleName(); + + private static final long HANDLER_WAIT_TIME_MS = 100; + private UserController mUserController; private TestInjector mInjector; + private final HashMap<Integer, UserState> mUserStates = new HashMap<>(); private static final List<String> START_FOREGROUND_USER_ACTIONS = newArrayList( Intent.ACTION_USER_STARTED, @@ -130,6 +143,11 @@ public class UserControllerTest { doNothing().when(mInjector).startHomeActivity(anyInt(), anyString()); doReturn(false).when(mInjector).stackSupervisorSwitchUser(anyInt(), any()); doNothing().when(mInjector).stackSupervisorResumeFocusedStackTopActivity(); + doNothing().when(mInjector).systemServiceManagerCleanupUser(anyInt()); + doNothing().when(mInjector).activityManagerForceStopPackage(anyInt(), anyString()); + doNothing().when(mInjector).activityManagerOnUserStopped(anyInt()); + doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt()); + doNothing().when(mInjector).stackSupervisorRemoveUser(anyInt()); mUserController = new UserController(mInjector); setUpUser(TEST_USER_ID, 0); }); @@ -261,7 +279,7 @@ public class UserControllerTest { } @Test - public void testContinueUserSwitch() { + public void testContinueUserSwitch() throws RemoteException { // Start user -- this will update state of mUserController mUserController.startUser(TEST_USER_ID, true); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); @@ -273,11 +291,11 @@ public class UserControllerTest { // Verify that continueUserSwitch worked as expected mUserController.continueUserSwitch(userState, oldUserId, newUserId); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); - continueUserSwitchAssertions(); + continueUserSwitchAssertions(TEST_USER_ID, false); } @Test - public void testContinueUserSwitchUIDisabled() { + public void testContinueUserSwitchUIDisabled() throws RemoteException { mUserController.mUserSwitchUiEnabled = false; // Start user -- this will update state of mUserController mUserController.startUser(TEST_USER_ID, true); @@ -290,16 +308,21 @@ public class UserControllerTest { // Verify that continueUserSwitch worked as expected mUserController.continueUserSwitch(userState, oldUserId, newUserId); verify(mInjector.getWindowManager(), never()).stopFreezingScreen(); - continueUserSwitchAssertions(); + continueUserSwitchAssertions(TEST_USER_ID, false); } - private void continueUserSwitchAssertions() { - Set<Integer> expectedCodes = Collections.singleton(REPORT_USER_SWITCH_COMPLETE_MSG); + private void continueUserSwitchAssertions(int expectedUserId, boolean backgroundUserStopping) + throws RemoteException { + Set<Integer> expectedCodes = new LinkedHashSet<>(); + expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG); + if (backgroundUserStopping) { + expectedCodes.add(0); // this is for directly posting in stopping. + } Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes(); assertEquals("Unexpected message sent", expectedCodes, actualCodes); Message msg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_COMPLETE_MSG); assertNotNull(msg); - assertEquals("Unexpected userId", TEST_USER_ID, msg.arg1); + assertEquals("Unexpected userId", expectedUserId, msg.arg1); } @Test @@ -332,6 +355,120 @@ public class UserControllerTest { assertTrue(mUserController.isSystemUserStarted()); } + /** + * Test stopping of user from max running users limit. + */ + @Test + public void testUserStoppingForMultipleUsersNormalMode() + throws InterruptedException, RemoteException { + setUpUser(TEST_USER_ID1, 0); + setUpUser(TEST_USER_ID2, 0); + mUserController.mMaxRunningUsers = 3; + int numerOfUserSwitches = 1; + addForegroundUserAndContinueUserSwitch(TEST_USER_ID, UserHandle.USER_SYSTEM, + numerOfUserSwitches, false); + // running: user 0, USER_ID + assertTrue(mUserController.canStartMoreUsers()); + assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID}), + mUserController.getRunningUsersLU()); + + numerOfUserSwitches++; + addForegroundUserAndContinueUserSwitch(TEST_USER_ID1, TEST_USER_ID, + numerOfUserSwitches, false); + // running: user 0, USER_ID, USER_ID1 + assertFalse(mUserController.canStartMoreUsers()); + assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID, TEST_USER_ID1}), + mUserController.getRunningUsersLU()); + + numerOfUserSwitches++; + addForegroundUserAndContinueUserSwitch(TEST_USER_ID2, TEST_USER_ID1, + numerOfUserSwitches, false); + UserState ussUser2 = mUserStates.get(TEST_USER_ID2); + // skip middle step and call this directly. + mUserController.finishUserSwitch(ussUser2); + waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS); + // running: user 0, USER_ID1, USER_ID2 + // USER_ID should be stopped as it is least recently used non user0. + assertFalse(mUserController.canStartMoreUsers()); + assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1, TEST_USER_ID2}), + mUserController.getRunningUsersLU()); + } + + /** + * This test tests delayed locking mode using 4 users. As core logic of delayed locking is + * happening in finishUserStopped call, the test also calls finishUserStopped while skipping + * all middle steps which takes too much work to mock. + */ + @Test + public void testUserStoppingForMultipleUsersDelayedLockingMode() + throws InterruptedException, RemoteException { + setUpUser(TEST_USER_ID1, 0); + setUpUser(TEST_USER_ID2, 0); + mUserController.mMaxRunningUsers = 3; + mUserController.mDelayUserDataLocking = true; + int numerOfUserSwitches = 1; + addForegroundUserAndContinueUserSwitch(TEST_USER_ID, UserHandle.USER_SYSTEM, + numerOfUserSwitches, false); + // running: user 0, USER_ID + assertTrue(mUserController.canStartMoreUsers()); + assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID}), + mUserController.getRunningUsersLU()); + numerOfUserSwitches++; + + addForegroundUserAndContinueUserSwitch(TEST_USER_ID1, TEST_USER_ID, + numerOfUserSwitches, true); + // running: user 0, USER_ID1 + // stopped + unlocked: USER_ID + numerOfUserSwitches++; + assertTrue(mUserController.canStartMoreUsers()); + assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1}), + mUserController.getRunningUsersLU()); + // Skip all other steps and test unlock delaying only + UserState uss = mUserStates.get(TEST_USER_ID); + uss.setState(UserState.STATE_SHUTDOWN); // necessary state change from skipped part + mUserController.finishUserStopped(uss); + // Cannot mock FgThread handler, so confirm that there is no posted message left before + // checking. + waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS); + verify(mInjector.mStorageManagerMock, times(0)) + .lockUserKey(anyInt()); + + addForegroundUserAndContinueUserSwitch(TEST_USER_ID2, TEST_USER_ID1, + numerOfUserSwitches, true); + // running: user 0, USER_ID2 + // stopped + unlocked: USER_ID1 + // stopped + locked: USER_ID + assertTrue(mUserController.canStartMoreUsers()); + assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID2}), + mUserController.getRunningUsersLU()); + UserState ussUser1 = mUserStates.get(TEST_USER_ID1); + ussUser1.setState(UserState.STATE_SHUTDOWN); + mUserController.finishUserStopped(ussUser1); + waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS); + verify(mInjector.mStorageManagerMock, times(1)) + .lockUserKey(TEST_USER_ID); + } + + private void addForegroundUserAndContinueUserSwitch(int newUserId, int expectedOldUserId, + int expectedNumberOfCalls, boolean expectOldUserStopping) + throws RemoteException { + // Start user -- this will update state of mUserController + mUserController.startUser(newUserId, true); + Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); + assertNotNull(reportMsg); + UserState userState = (UserState) reportMsg.obj; + int oldUserId = reportMsg.arg1; + assertEquals(expectedOldUserId, oldUserId); + assertEquals(newUserId, reportMsg.arg2); + mUserStates.put(newUserId, userState); + mInjector.mHandler.clearAllRecordedMessages(); + // Verify that continueUserSwitch worked as expected + mUserController.continueUserSwitch(userState, oldUserId, newUserId); + verify(mInjector.getWindowManager(), times(expectedNumberOfCalls)) + .stopFreezingScreen(); + continueUserSwitchAssertions(newUserId, expectOldUserStopping); + } + private void setUpUser(int userId, int flags) { UserInfo userInfo = new UserInfo(userId, "User" + userId, flags); when(mInjector.mUserManagerMock.getUserInfo(eq(userId))).thenReturn(userInfo); @@ -345,6 +482,22 @@ public class UserControllerTest { return result; } + private void waitForHandlerToComplete(Handler handler, long waitTimeMs) + throws InterruptedException { + if (!handler.hasMessagesOrCallbacks()) { // if nothing queued, do not wait. + return; + } + final Object lock = new Object(); + synchronized (lock) { + handler.post(() -> { + synchronized (lock) { + lock.notify(); + } + }); + lock.wait(waitTimeMs); + } + } + // Should be public to allow mocking private static class TestInjector extends UserController.Injector { public final TestHandler mHandler; @@ -353,8 +506,11 @@ public class UserControllerTest { public final List<Intent> mSentIntents = new ArrayList<>(); private final TestHandler mUiHandler; + + private final IStorageManager mStorageManagerMock; private final UserManagerInternal mUserManagerInternalMock; private final WindowManagerService mWindowManagerMock; + private final Context mCtx; TestInjector(Context ctx) { @@ -367,6 +523,7 @@ public class UserControllerTest { mUserManagerMock = mock(UserManagerService.class); mUserManagerInternalMock = mock(UserManagerInternal.class); mWindowManagerMock = mock(WindowManagerService.class); + mStorageManagerMock = mock(IStorageManager.class); } @Override @@ -434,6 +591,11 @@ public class UserControllerTest { // to pass all metrics related calls return true; } + + @Override + protected IStorageManager getStorageManager() { + return mStorageManagerMock; + } } private static class TestHandler extends Handler { |