summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKeun young Park <keunyoung@google.com>2019-03-13 14:06:42 -0700
committerKeun young Park <keunyoung@google.com>2019-04-02 15:29:54 -0700
commitdf54b66b2f281b901b25cacb27c3514eaad42e4f (patch)
tree67737bd499e33ca3ab6c35f0fb211b6322f0674d
parent29e5ea6741605fcc7699163a1cf36a3b9a042dd5 (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
-rw-r--r--core/res/res/values/config.xml10
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java2
-rw-r--r--services/core/java/com/android/server/am/UserController.java77
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java178
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 {