Merge "Add UserChecker to System Status Checkers." into pie-cts-dev
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/src/com/android/tradefed/device/ITestDevice.java
index 597b7a3..01312d1 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/src/com/android/tradefed/device/ITestDevice.java
@@ -399,6 +399,15 @@
     public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException;
 
     /**
+     * Create a user with a given name and default flags 0.
+     *
+     * @param name of the user to create on the device
+     * @return the integer for the user id created or -1 for error.
+     * @throws DeviceNotAvailableException
+     */
+    public int createUserNoThrow(String name) throws DeviceNotAvailableException;
+
+    /**
      * Create a user with a given name and the provided flags
      *
      * @param name of the user to create on the device
@@ -503,6 +512,14 @@
     public int getUserFlags(int userId) throws DeviceNotAvailableException;
 
     /**
+     * Return whether the specified user is a secondary user according to it's flags.
+     *
+     * @return true if the user is secondary, false otherwise.
+     * @throws DeviceNotAvailableException
+     */
+    public boolean isUserSecondary(int userId) throws DeviceNotAvailableException;
+
+    /**
      * Return the serial number associated to the userId if found, -10000 in any other cases.
      *
      * @throws DeviceNotAvailableException
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 2a20d12..cfbf3a1 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -3573,6 +3573,12 @@
         throw new UnsupportedOperationException("No support for user's feature.");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public int createUserNoThrow(String name) throws DeviceNotAvailableException {
+        throw new UnsupportedOperationException("No support for user's feature.");
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -3655,6 +3661,13 @@
         throw new UnsupportedOperationException("No support for user's feature.");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
+        throw new UnsupportedOperationException("No support for user's feature.");
+    }
+
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 386db34..bc40082 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -29,6 +29,7 @@
 import com.android.tradefed.util.KeyguardControllerState;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.UserUtil;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
@@ -853,6 +854,17 @@
         return createUser(name, false, false);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public int createUserNoThrow(String name) throws DeviceNotAvailableException {
+        try {
+            return createUser(name);
+        } catch (IllegalStateException e) {
+            CLog.e("Error creating user: " + e.toString());
+            return -1;
+        }
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1000,6 +1012,19 @@
         return INVALID_USER_ID;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
+        if (userId == UserUtil.USER_SYSTEM) {
+            return false;
+        }
+        int flags = getUserFlags(userId);
+        if (flags == INVALID_USER_ID) {
+            return false;
+        }
+        return (flags & UserUtil.FLAGS_NOT_SECONDARY) == 0;
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/suite/checker/UserChecker.java b/src/com/android/tradefed/suite/checker/UserChecker.java
new file mode 100644
index 0000000..d23b88c
--- /dev/null
+++ b/src/com/android/tradefed/suite/checker/UserChecker.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2019 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.tradefed.suite.checker;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+import com.android.tradefed.util.UserUtil;
+import com.android.tradefed.util.UserUtil.UserSwitchFailedException;
+
+/**
+ * Checks if users have changed during the test.
+ *
+ * <p>Optionally can also setup the current user.
+ */
+@OptionClass(alias = "user-system-checker")
+public class UserChecker implements ISystemStatusChecker {
+
+    @Option(
+        name = "user-type",
+        description = "The type of user to switch to before each module run."
+    )
+    private UserUtil.UserType mUserToSwitchTo = UserUtil.UserType.CURRENT;
+
+    public static final String DEFAULT_NAME = "TFauto";
+
+    private DeviceUserState mPreExecutionUserState;
+
+    /** {@inheritDoc} */
+    @Override
+    public StatusCheckerResult preExecutionCheck(ITestDevice device)
+            throws DeviceNotAvailableException {
+
+        String userSwitchErrorMsg = null;
+        try {
+            switchToExistingOrCreateUserType(device);
+        } catch (UserSwitchFailedException err) {
+            userSwitchErrorMsg = err.toString();
+        }
+
+        mPreExecutionUserState = new DeviceUserState(device);
+        CLog.d("preExecutionUsers=" + mPreExecutionUserState);
+
+        if (userSwitchErrorMsg == null) {
+            return new StatusCheckerResult(CheckStatus.SUCCESS);
+        } else {
+            StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
+            result.setErrorMessage(userSwitchErrorMsg);
+            return result;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public StatusCheckerResult postExecutionCheck(ITestDevice device)
+            throws DeviceNotAvailableException {
+        DeviceUserState postDeviceUserState = new DeviceUserState(device);
+        CLog.d("postExecutionUsers=" + postDeviceUserState);
+
+        ArrayList<String> errors = new ArrayList<>();
+
+        for (Integer removedUser : mPreExecutionUserState.findRemovedUsers(postDeviceUserState)) {
+            errors.add(String.format("User %d no longer exists after test", removedUser));
+        }
+
+        for (Integer addedUser : mPreExecutionUserState.findAddedUsers(postDeviceUserState)) {
+            errors.add(
+                    String.format(
+                            "User %d was created during the test and not deleted", addedUser));
+        }
+
+        if (mPreExecutionUserState.currentUserChanged(postDeviceUserState)) {
+            errors.add(
+                    String.format(
+                            "User %d was the currentUser before, has changed to %d",
+                            mPreExecutionUserState.getCurrentUser(),
+                            postDeviceUserState.getCurrentUser()));
+        }
+
+        for (int userId : mPreExecutionUserState.findStoppedUsers(postDeviceUserState)) {
+            CLog.w("User %d was running but is now stopped.", userId);
+        }
+
+        for (int userId : mPreExecutionUserState.findStartedUsers(postDeviceUserState)) {
+            CLog.w("User %d was stopped but is now running.", userId);
+        }
+
+        if (errors.size() > 0) {
+            StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
+            result.setErrorMessage(String.join("\n", errors));
+            return result;
+        } else {
+            return new StatusCheckerResult(CheckStatus.SUCCESS);
+        }
+    }
+
+    /**
+     * Switches to the mUserType, creating if necessary.
+     *
+     * <p>Returns null if success, the error string if there is an error.
+     */
+    private void switchToExistingOrCreateUserType(ITestDevice device)
+            throws DeviceNotAvailableException, UserSwitchFailedException {
+        try {
+            UserUtil.switchToUserType(device, mUserToSwitchTo);
+        } catch (UserUtil.SecondaryUserNotFoundException attemptCreate) {
+            CLog.d("No secondary users exist, creating one.");
+            int secondary = device.createUserNoThrow(DEFAULT_NAME);
+            if (secondary <= 0) {
+                throw new UserSwitchFailedException("Failed to create secondary user");
+            }
+            UserUtil.switchToUserType(device, mUserToSwitchTo);
+        }
+    }
+
+    /** Class for monitoring changes to the user state between pre/post check. */
+    static class DeviceUserState {
+        private final int mCurrentUser;
+        private final ArrayList<Integer> mUsers;
+        private final HashMap<Integer, Boolean> mUserRunningStates;
+
+        DeviceUserState(ITestDevice device) throws DeviceNotAvailableException {
+            mCurrentUser = device.getCurrentUser();
+            mUsers = device.listUsers();
+            mUserRunningStates = new HashMap(mUsers.size());
+            for (Integer userId : mUsers) {
+                mUserRunningStates.put(userId, device.isUserRunning(userId));
+            }
+        }
+
+        public int getCurrentUser() {
+            return mCurrentUser;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append(String.format("currentUser=%d;", getCurrentUser()));
+            for (Integer userId : mUsers) {
+                String running = mUserRunningStates.get(userId) ? "running" : "stopped";
+                builder.append(String.format(" %d:%s", userId, running));
+            }
+            return builder.toString();
+        }
+
+        List<Integer> findRemovedUsers(DeviceUserState otherState) {
+            ArrayList<Integer> removedUsers = new ArrayList<>();
+            for (Integer userId : mUsers) {
+                if (!otherState.containsUser(userId)) {
+                    removedUsers.add(userId);
+                }
+            }
+            return removedUsers;
+        }
+
+        List<Integer> findAddedUsers(DeviceUserState otherState) {
+            ArrayList<Integer> addedUsers = new ArrayList<>();
+            for (Integer userId : otherState.mUsers) {
+                if (!this.containsUser(userId)) {
+                    addedUsers.add(userId);
+                }
+            }
+            return addedUsers;
+        }
+
+        boolean currentUserChanged(DeviceUserState otherState) {
+            return this.getCurrentUser() != otherState.getCurrentUser();
+        }
+
+        List<Integer> findStartedUsers(DeviceUserState otherState) {
+            ArrayList<Integer> startedUsers = new ArrayList<>();
+            for (Integer userId : mUsers) {
+                if (!this.isUserRunning(userId) && otherState.isUserRunning(userId)) {
+                    startedUsers.add(userId);
+                }
+            }
+            return startedUsers;
+        }
+
+        List<Integer> findStoppedUsers(DeviceUserState otherState) {
+            ArrayList<Integer> stoppedUsers = new ArrayList<>();
+            for (Integer userId : mUsers) {
+                if (this.isUserRunning(userId) && !otherState.isUserRunning(userId)) {
+                    stoppedUsers.add(userId);
+                }
+            }
+            return stoppedUsers;
+        }
+
+        private boolean containsUser(int userId) {
+            return mUserRunningStates.containsKey(userId);
+        }
+
+        private boolean isUserRunning(int userId) {
+            return mUserRunningStates.getOrDefault(userId, /* default= */ false);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java b/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
index 9b4fe49..891e651 100644
--- a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
@@ -22,6 +22,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.UserUtil;
 
 /**
  * A {@link ITargetPreparer} that switches to the specified user kind in setUp. By default it
@@ -31,25 +32,11 @@
  */
 @OptionClass(alias = "switch-user-target-preparer")
 public class SwitchUserTargetPreparer extends BaseTargetPreparer implements ITargetCleaner {
-    private static final int USER_SYSTEM = 0; // From the UserHandle class.
-
-    /** Parameters that specify which user to run the test module as. */
-    public enum UserType {
-        // TODO:(b/123077733) Add support for guest and secondary.
-
-        /** current foreground user of the device */
-        CURRENT,
-        /** user flagged as primary on the device; most often primary = system user = user 0 */
-        PRIMARY,
-        /** system user = user 0 */
-        SYSTEM
-    }
-
     @Option(
         name = "user-type",
         description = "The type of user to switch to before the module run."
     )
-    private UserType mUserToSwitchTo = UserType.CURRENT;
+    private UserUtil.UserType mUserToSwitchTo = UserUtil.UserType.CURRENT;
 
     private int mPreExecutionCurrentUser;
 
@@ -59,39 +46,25 @@
 
         mPreExecutionCurrentUser = device.getCurrentUser();
 
-        switch (mUserToSwitchTo) {
-            case SYSTEM:
-                switchToUser(USER_SYSTEM, device);
-                break;
-            case PRIMARY:
-                switchToUser(device.getPrimaryUserId(), device);
-                break;
-        }
-    }
-
-    private static void switchToUser(int userId, ITestDevice device)
-            throws TargetSetupError, DeviceNotAvailableException {
-        if (device.getCurrentUser() == userId) {
-            return;
-        }
-
-        // Otherwise, switch to user with userId.
-        if (device.switchUser(userId)) {
-            // Successful switch.
-            CLog.i("Switched to user %d.", userId);
-        } else {
-            // Couldn't switch, throw.
+        try {
+            UserUtil.switchToUserType(device, mUserToSwitchTo);
+        } catch (UserUtil.UserSwitchFailedException err) {
             throw new TargetSetupError(
-                    String.format("Failed switch to user %d.", userId),
+                    String.format("Failed switch to user type %s", mUserToSwitchTo),
+                    err,
                     device.getDeviceDescriptor());
         }
+
+        CLog.d("Successfully switched to user type %s", mUserToSwitchTo);
     }
 
     @Override
     public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
             throws DeviceNotAvailableException {
         // Restore the previous user as the foreground.
-        if (!device.switchUser(mPreExecutionCurrentUser)) {
+        if (device.switchUser(mPreExecutionCurrentUser)) {
+            CLog.d("Successfully switched back to user id: %d", mPreExecutionCurrentUser);
+        } else {
             CLog.w("Could not switch back to the user id: %d", mPreExecutionCurrentUser);
         }
     }
diff --git a/src/com/android/tradefed/util/UserUtil.java b/src/com/android/tradefed/util/UserUtil.java
new file mode 100644
index 0000000..608b84c
--- /dev/null
+++ b/src/com/android/tradefed/util/UserUtil.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 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.tradefed.util;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+public class UserUtil {
+    // From the UserInfo class.
+    public static final int FLAG_PRIMARY = 0x00000001;
+    public static final int FLAG_GUEST = 0x00000004;
+    public static final int FLAG_RESTRICTED = 0x00000008;
+    public static final int FLAG_MANAGED_PROFILE = 0x00000020;
+    public static final int USER_SYSTEM = 0;
+
+    public static final int FLAGS_NOT_SECONDARY =
+            FLAG_PRIMARY | FLAG_MANAGED_PROFILE | FLAG_GUEST | FLAG_RESTRICTED;
+
+    /** Thrown if a user switch could not happen. */
+    public static class UserSwitchFailedException extends Exception {
+        public UserSwitchFailedException(String message) {
+            super(message);
+        }
+    }
+
+    /** Thrown if a user switch could not happen because the secondary user could not be found. */
+    public static class SecondaryUserNotFoundException extends UserSwitchFailedException {
+        public SecondaryUserNotFoundException() {
+            super("Secondary User Not Found");
+        }
+    }
+
+    /** Parameters that specify which user to run the test module as. */
+    public enum UserType {
+        // TODO:(b/123077733) Add support for guest
+
+        /** current foreground user of the device */
+        CURRENT,
+        /** user flagged as primary on the device; most often primary = system user = user 0 */
+        PRIMARY,
+        /** system user = user 0 */
+        SYSTEM,
+        /** secondary user, i.e. non-primary and non-system. */
+        SECONDARY,
+    }
+
+    /**
+     * Attempt to switch to a user type.
+     *
+     * @returns true if successful, false if not.
+     */
+    public static void switchToUserType(ITestDevice device, UserType userType)
+            throws DeviceNotAvailableException, UserSwitchFailedException {
+        switch (userType) {
+            case CURRENT:
+                return; // do nothing
+            case SYSTEM:
+                switchUser(device, USER_SYSTEM);
+                return;
+            case PRIMARY:
+                switchUser(device, device.getPrimaryUserId());
+                return;
+            case SECONDARY:
+                switchToSecondaryUser(device);
+                return;
+        }
+        throw new RuntimeException("userType case not covered: " + userType);
+    }
+
+    /**
+     * Attempt to switch to a secondary user, creating one if necessary.
+     *
+     * @returns true if successful, false if not.
+     */
+    private static void switchToSecondaryUser(ITestDevice device)
+            throws DeviceNotAvailableException, UserSwitchFailedException {
+        int currentUser = device.getCurrentUser();
+        if (device.isUserSecondary(currentUser)) {
+            CLog.d("currentUser is already secondary, no action.");
+            return;
+        }
+
+        int secondary = findExistingSecondary(device);
+        if (secondary <= 0) {
+            throw new SecondaryUserNotFoundException();
+        }
+
+        switchUser(device, secondary);
+    }
+
+    private static void switchUser(ITestDevice device, int userId)
+            throws DeviceNotAvailableException, UserSwitchFailedException {
+        if (!device.switchUser(userId)) {
+            throw new UserSwitchFailedException("Failed to switch to user " + userId);
+        }
+    }
+
+    /**
+     * Finds an arbitrary secondary user and returns the userId.
+     *
+     * <p>TODO: evaluate if a more comprehensive API is needed for this or not.
+     *
+     * @return id of the secondary user or -1 if one could not be found.
+     * @throws DeviceNotAvailableException
+     */
+    private static int findExistingSecondary(ITestDevice device)
+            throws DeviceNotAvailableException {
+        for (int userId : device.listUsers()) {
+            if (device.isUserSecondary(userId)) {
+                return userId;
+            }
+        }
+        // Returns a negative id if we couldn't find a proper existing secondary user.
+        return -1;
+    }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 2cd4e0b..83b3641 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -135,6 +135,7 @@
 import com.android.tradefed.suite.checker.SystemServerFileDescriptorCheckerTest;
 import com.android.tradefed.suite.checker.SystemServerStatusCheckerTest;
 import com.android.tradefed.suite.checker.TimeStatusCheckerTest;
+import com.android.tradefed.suite.checker.UserCheckerTest;
 import com.android.tradefed.targetprep.AllTestAppsInstallSetupTest;
 import com.android.tradefed.targetprep.AppSetupTest;
 import com.android.tradefed.targetprep.BuildInfoAttributePreparerTest;
@@ -272,6 +273,7 @@
 import com.android.tradefed.util.TestMappingTest;
 import com.android.tradefed.util.TimeUtilTest;
 import com.android.tradefed.util.TimeValTest;
+import com.android.tradefed.util.UserUtilTest;
 import com.android.tradefed.util.ZipUtil2Test;
 import com.android.tradefed.util.ZipUtilTest;
 import com.android.tradefed.util.hostmetric.AbstractHostMonitorTest;
@@ -488,6 +490,7 @@
     SystemServerFileDescriptorCheckerTest.class,
     SystemServerStatusCheckerTest.class,
     TimeStatusCheckerTest.class,
+    UserCheckerTest.class,
 
     // testtype
     AndroidJUnitTestTest.class,
@@ -609,6 +612,7 @@
     TestMappingTest.class,
     TimeUtilTest.class,
     TimeValTest.class,
+    UserUtilTest.class,
     ZipUtilTest.class,
     ZipUtil2Test.class,
 
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index e570966..9ca9df7 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -43,6 +43,7 @@
 import com.android.tradefed.util.KeyguardControllerState;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.UserUtil;
 import com.android.tradefed.util.ZipUtil2;
 
 import com.google.common.util.concurrent.SettableFuture;
@@ -2165,6 +2166,31 @@
     }
 
     /**
+     * Test that successful user creation is handled by {@link
+     * TestDevice#createUserNoThrow(String)}.
+     */
+    public void testCreateUserNoThrow() throws Exception {
+        final String createUserCommand = "pm create-user foo";
+        injectShellResponse(createUserCommand, "Success: created user id 10");
+        replayMocks();
+        assertEquals(10, mTestDevice.createUserNoThrow("foo"));
+    }
+
+    /** Test that {@link TestDevice#createUserNoThrow(String)} fails when bad output */
+    public void testCreateUserNoThrow_wrongOutput() throws Exception {
+        mTestDevice =
+                new TestableTestDevice() {
+                    @Override
+                    public String executeShellCommand(String command)
+                            throws DeviceNotAvailableException {
+                        return "Success: created user id WRONG";
+                    }
+                };
+
+        assertEquals(-1, mTestDevice.createUserNoThrow("TEST"));
+    }
+
+    /**
      * Test that successful user removal is handled by {@link TestDevice#removeUser(int)}.
      */
     public void testRemoveUser() throws Exception {
@@ -2443,6 +2469,40 @@
         assertEquals(21, flags);
     }
 
+    /** Unit test for {@link TestDevice#isUserSecondary(int)} */
+    public void testIsUserSecondary() throws Exception {
+        mTestDevice =
+                new TestableTestDevice() {
+                    @Override
+                    public String executeShellCommand(String command)
+                            throws DeviceNotAvailableException {
+                        return String.format(
+                                "Users:\n\tUserInfo{0:Owner:0}\n\t"
+                                        + "UserInfo{10:Primary:%x} Running\n\t"
+                                        + "UserInfo{11:Guest:%x}\n\t"
+                                        + "UserInfo{12:Secondary:0}\n\t"
+                                        + "UserInfo{13:Managed:%x}\n\t"
+                                        + "UserInfo{100:Restricted:%x}\n\t",
+                                UserUtil.FLAG_PRIMARY,
+                                UserUtil.FLAG_GUEST,
+                                UserUtil.FLAG_MANAGED_PROFILE,
+                                UserUtil.FLAG_RESTRICTED);
+                    }
+
+                    @Override
+                    public int getApiLevel() throws DeviceNotAvailableException {
+                        return 22;
+                    }
+                };
+        assertEquals(false, mTestDevice.isUserSecondary(0));
+        assertEquals(false, mTestDevice.isUserSecondary(-1));
+        assertEquals(false, mTestDevice.isUserSecondary(10));
+        assertEquals(false, mTestDevice.isUserSecondary(11));
+        assertEquals(true, mTestDevice.isUserSecondary(12));
+        assertEquals(false, mTestDevice.isUserSecondary(13));
+        assertEquals(false, mTestDevice.isUserSecondary(100));
+    }
+
     /**
      * Unit test for {@link TestDevice#getUserSerialNumber(int)}
      */
diff --git a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
new file mode 100644
index 0000000..6c000ce
--- /dev/null
+++ b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2019 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.tradefed.suite.checker;
+
+import com.android.tradefed.suite.checker.UserChecker.DeviceUserState;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashSet;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/** Unit tests for {@link UserChecker} */
+@RunWith(JUnit4.class)
+public class UserCheckerTest {
+    @Test
+    public void testNoWarningsIsSuccess() throws Exception {
+        UserChecker checker = new UserChecker();
+
+        ITestDevice preDevice =
+                mockDeviceUserState(
+                        /* users=        */ new Integer[] {0},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+        assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+
+        ITestDevice postDevice =
+                mockDeviceUserState(
+                        /* users=        */ new Integer[] {0},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+        assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+    }
+
+    @Test
+    /** Returns FAILED in the precessense of errors */
+    public void testAllErrorsIsFailed() throws Exception {
+        UserChecker checker = new UserChecker();
+
+        ITestDevice preDevice =
+                mockDeviceUserState(
+                        /* users=        */ new Integer[] {0, 10, 11},
+                        /* runningUsers= */ new Integer[] {0, 10},
+                        /* currentUser=  */ 10);
+        assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+
+        // User12 created, User11 deleted, User10 stopped, currentUser changed
+        ITestDevice postDevice =
+                mockDeviceUserState(
+                        /* users=        */ new Integer[] {0, 10, 12},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+        assertEquals(CheckStatus.FAILED, checker.postExecutionCheck(postDevice).getStatus());
+    }
+
+    @Test
+    public void testSwitchToExistingOrCreateUserType() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "secondary");
+        ITestDevice device =
+                mockDeviceUserState(
+                        /* users=        */ new Integer[] {0},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+
+        when(device.getCurrentUser()).thenReturn(0);
+        mockListUsers(device, new Integer[] {0});
+        when(device.createUserNoThrow(UserChecker.DEFAULT_NAME)).thenReturn(10);
+        when(device.getCurrentUser()).thenReturn(0);
+        mockListUsers(device, new Integer[] {0, 10});
+        when(device.isUserSecondary(10)).thenReturn(true);
+        when(device.switchUser(10)).thenReturn(true);
+
+        StatusCheckerResult result = checker.preExecutionCheck(device);
+        assertEquals(CheckStatus.SUCCESS, result.getStatus());
+        verify(device, times(1)).switchUser(10);
+    }
+
+    @Test
+    public void testSwitchToSecondaryUserCreateNewFail() throws Exception {
+        UserChecker checker = new UserChecker();
+        OptionSetter mOptionSetter = new OptionSetter(checker);
+        mOptionSetter.setOptionValue("user-type", "secondary");
+        ITestDevice device =
+                mockDeviceUserState(
+                        /* users=        */ new Integer[] {0},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+
+        when(device.getCurrentUser()).thenReturn(0);
+        mockListUsers(device, new Integer[] {0});
+        when(device.createUserNoThrow(UserChecker.DEFAULT_NAME)).thenReturn(-1);
+
+        StatusCheckerResult result = checker.preExecutionCheck(device);
+        assertEquals(CheckStatus.FAILED, result.getStatus());
+        verify(device, times(1)).createUserNoThrow(UserChecker.DEFAULT_NAME);
+    }
+
+    @Test
+    public void testFindRemovedUsers() throws Exception {
+        DeviceUserState preState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0, 10},
+                        /* currentUser=  */ 0);
+        DeviceUserState postState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+
+        assertArrayEquals(new Integer[] {10}, preState.findRemovedUsers(postState).toArray());
+    }
+
+    @Test
+    public void testFindAddedUsers() throws Exception {
+        DeviceUserState preState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+        DeviceUserState postState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+
+        assertArrayEquals(new Integer[] {10}, preState.findAddedUsers(postState).toArray());
+    }
+
+    @Test
+    public void testCurrentUserChanged() throws Exception {
+        DeviceUserState preState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0, 10},
+                        /* currentUser=  */ 10);
+        DeviceUserState postState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0, 10},
+                        /* currentUser=  */ 0);
+
+        assertEquals(true, preState.currentUserChanged(postState));
+    }
+
+    @Test
+    public void testfindStartedUsers() throws Exception {
+        DeviceUserState preState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+        DeviceUserState postState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0, 10},
+                        /* currentUser=  */ 0);
+
+        assertArrayEquals(new Integer[] {10}, preState.findStartedUsers(postState).toArray());
+        assertArrayEquals(new Integer[] {}, preState.findStoppedUsers(postState).toArray());
+    }
+
+    @Test
+    public void testFindStopedUsers() throws Exception {
+        DeviceUserState preState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0, 10},
+                        /* currentUser=  */ 0);
+        DeviceUserState postState =
+                getMockedUserState(
+                        /* users=        */ new Integer[] {0, 10},
+                        /* runningUsers= */ new Integer[] {0},
+                        /* currentUser=  */ 0);
+
+        assertArrayEquals(new Integer[] {}, preState.findStartedUsers(postState).toArray());
+        assertArrayEquals(new Integer[] {10}, preState.findStoppedUsers(postState).toArray());
+    }
+
+    // TEST HELPERS
+
+    /** Return an instantiated DeviceUserState which was mocked. */
+    private DeviceUserState getMockedUserState(
+            Integer[] userIds, Integer[] runningUsers, int currentUser) throws Exception {
+        ITestDevice device = mockDeviceUserState(userIds, runningUsers, currentUser);
+        return new UserChecker.DeviceUserState(device);
+    }
+
+    /** Return a device with the user state calls mocked. */
+    private ITestDevice mockDeviceUserState(
+            Integer[] userIds, Integer[] runningUsers, int currentUser) throws Exception {
+        HashSet<Integer> runningUsersSet = new HashSet<Integer>(Arrays.asList(runningUsers));
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.getCurrentUser()).thenReturn(currentUser);
+        mockListUsers(device, userIds);
+        for (int userId : userIds) {
+            when(device.isUserRunning(userId)).thenReturn(runningUsersSet.contains(userId));
+        }
+
+        return device;
+    }
+
+    private void mockListUsers(ITestDevice device, Integer[] userIds) throws Exception {
+        when(device.listUsers()).thenReturn(new ArrayList<Integer>(Arrays.asList(userIds)));
+    }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
index 6b55093..70187a2 100644
--- a/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
@@ -53,7 +53,7 @@
     }
 
     @Test
-    public void testSetUpRunAsPrimary_ifAlreadyInPrimary_noUserSwitch()
+    public void testSetUpRunAsPrimary_ifAlreadyInPrimary_switchToPrimary()
             throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
         // setup
         mockUsers(/* primaryUserId= */ 11, /* currentUserId= */ 11);
@@ -63,11 +63,11 @@
         mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
 
         // assert
-        verify(mMockDevice, never()).switchUser(anyInt());
+        verify(mMockDevice, times(1)).switchUser(11);
     }
 
     @Test
-    public void testSetUpRunAsSystem_ifAlreadyInSystem_noUserSwitch()
+    public void testSetUpRunAsSystem_ifAlreadyInSystem_switchToSystem()
             throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
         // setup
         mockUsers(/* primaryUserId= */ 11, /* currentUserId= */ USER_SYSTEM);
@@ -77,7 +77,7 @@
         mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
 
         // assert
-        verify(mMockDevice, never()).switchUser(anyInt());
+        verify(mMockDevice, times(1)).switchUser(0);
     }
 
     @Test
diff --git a/tests/src/com/android/tradefed/util/UserUtilTest.java b/tests/src/com/android/tradefed/util/UserUtilTest.java
new file mode 100644
index 0000000..ee18375
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/UserUtilTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 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.tradefed.util;
+
+import com.android.tradefed.util.UserUtil.UserType;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.device.ITestDevice;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+/** Unit tests for {@link UserChecker} */
+@RunWith(JUnit4.class)
+public class UserUtilTest {
+    @Test
+    public void testSwitchToUserSystemSuccess() throws Exception {
+        int currentUser = 12;
+
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.switchUser(UserUtil.USER_SYSTEM)).thenReturn(true);
+
+        UserUtil.switchToUserType(device, UserType.SYSTEM);
+        verify(device, times(1)).switchUser(UserUtil.USER_SYSTEM);
+    }
+
+    @Test
+    public void testSwitchToUserSystemFail() throws Exception {
+        int currentUser = 12;
+
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.switchUser(UserUtil.USER_SYSTEM)).thenReturn(false);
+
+        try {
+            UserUtil.switchToUserType(device, UserType.SYSTEM);
+            fail();
+        } catch (UserUtil.UserSwitchFailedException _expected) {
+        }
+        verify(device, times(1)).switchUser(UserUtil.USER_SYSTEM);
+    }
+
+    @Test
+    public void testSwitchToSecondaryUserCurrent() throws Exception {
+        int currentUser = 10;
+
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.getCurrentUser()).thenReturn(currentUser);
+        when(device.isUserSecondary(currentUser)).thenReturn(true);
+
+        UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
+        verify(device, never()).switchUser(currentUser);
+    }
+
+    @Test
+    public void testSwitchToSecondaryUserExists() throws Exception {
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.getCurrentUser()).thenReturn(0);
+        mockListUsers(device, new Integer[] {0, 10});
+        when(device.isUserSecondary(10)).thenReturn(true);
+        when(device.switchUser(10)).thenReturn(true);
+
+        UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
+        verify(device, times(1)).switchUser(10);
+    }
+
+    @Test
+    /** Validate that invalid user types will be skipped as secondaries. */
+    public void testSwitchToSecondaryUserWithInvalid() throws Exception {
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.getCurrentUser()).thenReturn(0);
+        mockListUsers(device, new Integer[] {0, 10, 11, 12});
+        when(device.isUserSecondary(10)).thenReturn(false);
+        when(device.isUserSecondary(11)).thenReturn(false);
+        when(device.isUserSecondary(12)).thenReturn(true);
+        when(device.switchUser(12)).thenReturn(true);
+
+        UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
+        verify(device, times(1)).switchUser(12);
+    }
+
+    @Test
+    public void testSwitchToPrimaryUserNonSystem() throws Exception {
+        ITestDevice device = mock(ITestDevice.class);
+        when(device.getCurrentUser()).thenReturn(0);
+        when(device.getPrimaryUserId()).thenReturn(10);
+        when(device.switchUser(10)).thenReturn(true);
+
+        UserUtil.switchToUserType(device, UserUtil.UserType.PRIMARY);
+        verify(device, times(1)).switchUser(10);
+    }
+
+    // Helpers
+
+    private void mockListUsers(ITestDevice device, Integer[] userIds) throws Exception {
+        when(device.listUsers()).thenReturn(new ArrayList<Integer>(Arrays.asList(userIds)));
+    }
+}