Add First version of Bedstead-nene Test API library.

Test: atest NeneTest
Bug: 167945074
Change-Id: I7b5ddc9a34560e9ca08a2d18430d2a0093f1a2e8
diff --git a/common/device-side/bedstead/OWNERS b/common/device-side/bedstead/OWNERS
new file mode 100644
index 0000000..ce3438f
--- /dev/null
+++ b/common/device-side/bedstead/OWNERS
@@ -0,0 +1,2 @@
+scottjonathan@google.com
+alexkershaw@google.com
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/Android.bp b/common/device-side/bedstead/nene/Android.bp
new file mode 100644
index 0000000..9a86c93
--- /dev/null
+++ b/common/device-side/bedstead/nene/Android.bp
@@ -0,0 +1,32 @@
+android_library {
+    name: "Nene",
+    sdk_version: "test_current",
+    srcs: [
+        "src/main/java/**/*.java"
+    ],
+    manifest: "src/main/AndroidManifest.xml",
+    static_libs: [
+        "compatibility-device-util-axt",
+    ],
+    min_sdk_version: "26"
+}
+
+android_test {
+    name: "NeneTest",
+    srcs: [
+        "src/test/java/**/*.java"
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    static_libs: [
+        "Nene",
+        "androidx.test.ext.junit",
+        "ctstestrunner-axt",
+        "compatibility-device-util-axt",
+        "truth-prebuilt",
+        "testng" // for assertThrows
+    ],
+    manifest: "src/test/AndroidManifest.xml",
+    min_sdk_version: "26"
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/AndroidTest.xml b/common/device-side/bedstead/nene/AndroidTest.xml
new file mode 100644
index 0000000..89005a5
--- /dev/null
+++ b/common/device-side/bedstead/nene/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+<configuration description="Config for Nene test cases">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="NeneTest.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bedstead.nene.test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/TEST_MAPPING b/common/device-side/bedstead/nene/TEST_MAPPING
new file mode 100644
index 0000000..8beeebe
--- /dev/null
+++ b/common/device-side/bedstead/nene/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "NeneTest"
+    }
+  ]
+}
diff --git a/common/device-side/bedstead/nene/src/main/AndroidManifest.xml b/common/device-side/bedstead/nene/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a8d91e7
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.nene">
+    <uses-sdk android:minSdkVersion="26" />
+    <application>
+    </application>
+</manifest>
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/TestApis.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/TestApis.java
new file mode 100644
index 0000000..18b38f0
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/TestApis.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene;
+
+import com.android.bedstead.nene.users.Users;
+
+/**
+ * Entry point to Nene Test APIs.
+ */
+public final class TestApis {
+
+    private final Users mUsers = new Users();
+
+    /** Access Test APIs related to Users. */
+    public Users users() {
+        return mUsers;
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbException.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbException.java
new file mode 100644
index 0000000..c424223
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbException.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.exceptions;
+
+/**
+ * An exception that gets thrown when interacting with Adb.
+ */
+public class AdbException extends Exception {
+
+    private final String command;
+    private final String output;
+
+    public AdbException(String message, String command, String output) {
+        super(message);
+        if (command == null || output == null) {
+            throw new NullPointerException();
+        }
+        this.command = command;
+        this.output = output;
+    }
+
+    public AdbException(String message, String command, String output, Throwable cause) {
+        super(message, cause);
+        if (command == null || output == null) {
+            throw new NullPointerException();
+        }
+        this.command = command;
+        this.output = output;
+    }
+
+    public String command() {
+        return command;
+    }
+
+    public String output() {
+        return output;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "[command=\"" + command + "\" output=\"" + output + "\"]";
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbParseException.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbParseException.java
new file mode 100644
index 0000000..5347408
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/exceptions/AdbParseException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.exceptions;
+
+/** An exception that gets thrown when an error occurred parsing Adb output. */
+public class AdbParseException extends Exception {
+
+    private final String adbOutput;
+
+    AdbParseException(String message, String adbOutput) {
+        super(message);
+        if (message == null || adbOutput == null) {
+            throw new NullPointerException();
+        }
+        this.adbOutput = adbOutput;
+    }
+
+    public AdbParseException(String message, String adbOutput, Throwable cause) {
+        super(message, cause);
+        if (message == null || adbOutput == null || cause == null) {
+            throw new NullPointerException();
+        }
+        this.adbOutput = adbOutput;
+    }
+
+    public String adbOutput() {
+        return adbOutput;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "[output=\"" + adbOutput + "\"]";
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java
new file mode 100644
index 0000000..3e6b02e1
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.Map;
+
+/**
+ * Parser for the output of "adb dumpsys user".
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public interface AdbUserParser {
+
+    static AdbUserParser get(int sdkVersion) {
+        if (sdkVersion >= 30) {
+            return new AdbUserParser30();
+        }
+        return new AdbUserParser26();
+    }
+
+    /**
+     * The result of parsing.
+     *
+     * <p>Values which are not used on the current version of Android will be {@code null}.
+     */
+    class ParseResult {
+        Map<Integer, User> mUsers;
+        @Nullable Map<String, UserType> mUserTypes;
+    }
+
+    default ParseResult parse(String dumpsysUsersOutput) throws AdbParseException {
+        throw new UnsupportedOperationException();
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser26.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser26.java
new file mode 100644
index 0000000..b8fedfc
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser26.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parser for "adb dumpsys user" on Android 26+
+ *
+ * <p>Example output:
+ * {@code
+ * Users:
+ *   UserInfo{0:null:13} serialNo=0
+ *     State: RUNNING_UNLOCKED
+ *     Created: <unknown>
+ *     Last logged in: +11m34s491ms ago
+ *     Last logged in fingerprint: generic/gce_x86_phone/gce_x86:8.0.0/OPR1.170623
+ *     .041/4833325:userdebug/test-keys
+ *     Has profile owner: false
+ *     Restrictions:
+ *       none
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       null
+ *     Effective restrictions:
+ *       none
+ *   UserInfo{10:managedprofileuser:20} serialNo=10
+ *     State: -1
+ *     Created: +1s901ms ago
+ *     Last logged in: <unknown>
+ *     Last logged in fingerprint: generic/gce_x86_phone/gce_x86:8.0.0/OPR1.170623
+ *     .041/4833325:userdebug/test-keys
+ *     Has profile owner: false
+ *     Restrictions:
+ *       none
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       null
+ *     Effective restrictions:
+ *       none
+ *
+ *   Device owner id:-10000
+ *
+ *   Guest restrictions:
+ *     no_sms
+ *     no_install_unknown_sources
+ *     no_config_wifi
+ *     no_outgoing_calls
+ *
+ *   Device managed: false
+ *   Started users state: {0=3}
+ *
+ *   Max users: 4
+ *   Supports switchable users: false
+ *   All guests ephemeral: false
+ * @}
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+public class AdbUserParser26 implements AdbUserParser {
+    static final int USER_LIST_BASE_INDENTATION = 2;
+
+    @Override
+    public ParseResult parse(String dumpsysUsersOutput) throws AdbParseException {
+        ParseResult parseResult = new ParseResult();
+        parseResult.mUsers = parseUsers(dumpsysUsersOutput);
+        return parseResult;
+    }
+
+    Map<Integer, User> parseUsers(String dumpsysUsersOutput) throws AdbParseException {
+        String usersList = extractUsersList(dumpsysUsersOutput);
+        Set<String> userStrings = extractUserStrings(usersList);
+        Map<Integer, User> users = new HashMap<>();
+        for (String userString : userStrings) {
+            User user = new User(parseUser(userString));
+            users.put(user.id(), user);
+        }
+        return users;
+    }
+
+    String extractUsersList(String dumpsysUsersOutput) throws AdbParseException {
+        try {
+            return dumpsysUsersOutput.split("Users:\n", 2)[1].split("\n\n", 2)[0];
+        } catch (RuntimeException e) {
+            throw new AdbParseException("Error extracting user list", dumpsysUsersOutput, e);
+        }
+    }
+
+    Set<String> extractUserStrings(String usersList) throws AdbParseException {
+        return extractIndentedSections(usersList, USER_LIST_BASE_INDENTATION);
+    }
+
+    Set<String> extractIndentedSections(String list, int baseIndentation) throws AdbParseException {
+        try {
+            Set<String> sections = new HashSet<>();
+            String[] lines = list.split("\n");
+            StringBuilder sectionBuilder = null;
+            for (String line : lines) {
+                int indentation = countIndentation(line);
+                if (indentation == baseIndentation) {
+                    // New item
+                    if (sectionBuilder != null) {
+                        sections.add(sectionBuilder.toString().trim());
+                    }
+                    sectionBuilder = new StringBuilder(line).append("\n");
+                } else {
+                    sectionBuilder.append(line).append("\n");
+                }
+            }
+            sections.add(sectionBuilder.toString().trim());
+            return sections;
+        } catch (RuntimeException e) {
+            throw new AdbParseException("Error extracting indented sections", list, e);
+        }
+    }
+
+    int countIndentation(String s) {
+        String trimmed = s.trim();
+        if (trimmed.isEmpty()) {
+            return s.length();
+        }
+        return s.indexOf(trimmed);
+    }
+
+    User.MutableUser parseUser(String userString) throws AdbParseException {
+        try {
+            String userInfo[] = userString.split("UserInfo\\{", 2)[1].split("\\}", 2)[0].split(":");
+            User.MutableUser user = new User.MutableUser();
+            user.mName = userInfo[1];
+            user.mId = Integer.parseInt(userInfo[0]);
+            user.mSerialNo = Integer.parseInt(
+                    userString.split("serialNo=", 2)[1].split("[ \n]", 2)[0]);
+            user.mHasProfileOwner =
+                    Boolean.parseBoolean(
+                            userString.split("Has profile owner: ", 2)[1].split("\n", 2)[0]);
+            return user;
+        } catch (RuntimeException e) {
+            throw new AdbParseException("Error parsing user", userString, e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser30.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser30.java
new file mode 100644
index 0000000..0fd9eeb
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/AdbUserParser30.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parser for "adb dumpsys user" on Android 30+
+ *
+ * <p>Example output:
+ * {@code
+ * Current user: 0
+ * Users:
+ *   UserInfo{0:null:c13} serialNo=0 isPrimary=true
+ *     Type: android.os.usertype.full.SYSTEM
+ *     Flags: 3091 (ADMIN|FULL|INITIALIZED|PRIMARY|SYSTEM)
+ *     State: RUNNING_UNLOCKED
+ *     Created: <unknown>
+ *     Last logged in: +10m10s675ms ago
+ *     Last logged in fingerprint: generic/cf_x86_phone/vsoc_x86:11/RP1A.201005.004
+ *     .A1/6934943:userdebug/dev-keys
+ *     Start time: +12m26s184ms ago
+ *     Unlock time: +12m7s388ms ago
+ *     Has profile owner: false
+ *     Restrictions:
+ *       none
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       none
+ *     Effective restrictions:
+ *       none
+ *   UserInfo{10:managedprofileuser:1020} serialNo=10 isPrimary=false
+ *     Type: android.os.usertype.profile.MANAGED
+ *     Flags: 4128 (MANAGED_PROFILE|PROFILE)
+ *     State: -1
+ *     Created: +2s690ms ago
+ *     Last logged in: <unknown>
+ *     Last logged in fingerprint: generic/cf_x86_phone/vsoc_x86:11/RP1A.201005.004
+ *     .A1/6934943:userdebug/dev-keys
+ *     Start time: <unknown>
+ *     Unlock time: <unknown>
+ *     Has profile owner: false
+ *     Restrictions:
+ *       null
+ *     Device policy global restrictions:
+ *       null
+ *     Device policy local restrictions:
+ *       none
+ *     Effective restrictions:
+ *       null
+ *
+ *   Device owner id:-10000
+ *
+ *   Guest restrictions:
+ *     no_sms
+ *     no_install_unknown_sources
+ *     no_config_wifi
+ *     no_outgoing_calls
+ *
+ *   Device managed: false
+ *   Started users state: {0=3}
+ *
+ *   Max users: 4 (limit reached: false)
+ *   Supports switchable users: false
+ *   All guests ephemeral: false
+ *   Force ephemeral users: false
+ *   Is split-system user: false
+ *   Is headless-system mode: false
+ *   User version: 9
+ *
+ *   User types (7 types):
+ *     android.os.usertype.full.GUEST:
+ *         mName: android.os.usertype.full.GUEST
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: 1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: GUEST
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             no_sms
+ *             no_install_unknown_sources
+ *             no_config_wifi
+ *             no_outgoing_calls
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.profile.MANAGED:
+ *         mName: android.os.usertype.profile.MANAGED
+ *         mBaseType: PROFILE
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: 1
+ *         mDefaultUserInfoFlags: MANAGED_PROFILE
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             null
+ *         mIconBadge: 17302387
+ *         mBadgePlain: 17302382
+ *         mBadgeNoBackground: 17302384
+ *         mBadgeLabels.length: 3
+ *         mBadgeColors.length: 3
+ *         mDarkThemeBadgeColors.length: 3
+ *     android.os.usertype.system.HEADLESS:
+ *         mName: android.os.usertype.system.HEADLESS
+ *         mBaseType: SYSTEM
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: 0
+ *         mLabel: 0
+ *         config_defaultFirstUserRestrictions:
+ *             none
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.SYSTEM:
+ *         mName: android.os.usertype.full.SYSTEM
+ *         mBaseType: FULL|SYSTEM
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: 0
+ *         mLabel: 0
+ *         config_defaultFirstUserRestrictions:
+ *             none
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.SECONDARY:
+ *         mName: android.os.usertype.full.SECONDARY
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: 0
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             no_sms
+ *             no_outgoing_calls
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.RESTRICTED:
+ *         mName: android.os.usertype.full.RESTRICTED
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: RESTRICTED
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             null
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *     android.os.usertype.full.DEMO:
+ *         mName: android.os.usertype.full.DEMO
+ *         mBaseType: FULL
+ *         mEnabled: true
+ *         mMaxAllowed: -1
+ *         mMaxAllowedPerParent: -1
+ *         mDefaultUserInfoFlags: DEMO
+ *         mLabel: 0
+ *         mDefaultRestrictions:
+ *             null
+ *         mIconBadge: 0
+ *         mBadgePlain: 0
+ *         mBadgeNoBackground: 0
+ *         mBadgeLabels.length: 0(null)
+ *         mBadgeColors.length: 0(null)
+ *         mDarkThemeBadgeColors.length: 0(null)
+ *
+ * Whitelisted packages per user type
+ *     Mode: 13 (enforced) (implicit)
+ *     Legend
+ *         0 -> android.os.usertype.full.DEMO
+ *         1 -> android.os.usertype.full.GUEST
+ *         2 -> android.os.usertype.full.RESTRICTED
+ *         3 -> android.os.usertype.full.SECONDARY
+ *         4 -> android.os.usertype.full.SYSTEM
+ *         5 -> android.os.usertype.profile.MANAGED
+ *         6 -> android.os.usertype.system.HEADLESS
+ *     20 packages:
+ *         com.android.internal.display.cutout.emulation.corner: 0 1 2 3 4
+ *         com.android.internal.display.cutout.emulation.double: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural_wide_back: 0 1 2 3 4
+ *         com.android.wallpapercropper: 0 1 2 3 4
+ *         com.android.internal.display.cutout.emulation.tall: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.threebutton: 0 1 2 3 4
+ *         android: 0 1 2 3 4 5 6
+ *         com.google.android.deskclock: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.twobutton: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural_extra_wide_back: 0 1 2 3 4
+ *         com.android.providers.settings: 0 1 2 3 4 5 6
+ *         com.google.android.calculator: 0 1 2 3 4
+ *         com.google.android.apps.wallpaper.nexus: 0 1 2 3 4
+ *         com.google.android.apps.nexuslauncher: 0 1 2 3 4
+ *         com.android.wallpaper.livepicker: 0 1 2 3 4
+ *         com.google.android.apps.wallpaper: 0 1 2 3 4
+ *         com.android.wallpaperbackup: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural: 0 1 2 3 4
+ *         com.android.pixellogger: 0 1 2 3 4
+ *         com.android.internal.systemui.navbar.gestural_narrow_back: 0 1 2 3 4
+ *     No errors
+ *     2 warnings
+ *         com.android.wallpapercropper is whitelisted but not present.
+ *         com.google.android.apps.wallpaper.nexus is whitelisted but not present.
+ * }
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public class AdbUserParser30 extends AdbUserParser26 {
+
+    static int USER_TYPES_LIST_BASE_INDENTATION = 4;
+
+    private Map<String, UserType> mUserTypes;
+
+    @Override
+    public ParseResult parse(String dumpsysUsersOutput) throws AdbParseException {
+        mUserTypes = parseUserTypes(dumpsysUsersOutput);
+
+        ParseResult parseResult = super.parse(dumpsysUsersOutput);
+        parseResult.mUserTypes = mUserTypes;
+
+        return parseResult;
+    }
+
+    @Override
+    User.MutableUser parseUser(String userString) throws AdbParseException {
+        // This will be called after parseUserTypes, so the user types are already accessible
+        User.MutableUser user = super.parseUser(userString);
+
+        try {
+            user.mIsPrimary = Boolean.parseBoolean(
+                    userString.split("isPrimary=", 2)[1].split("[ \n]", 2)[0]);
+            user.mType = mUserTypes.get(userString.split("Type: ", 2)[1].split("\n", 2)[0]);
+        } catch (RuntimeException e) {
+            throw new AdbParseException("Error parsing user", userString, e);
+        }
+
+        return user;
+    }
+
+    Map<String, UserType> parseUserTypes(String dumpsysUsersOutput) throws AdbParseException {
+        String userTypesList = extractUserTypesList(dumpsysUsersOutput);
+        Set<String> userTypeStrings = extractUserTypesStrings(userTypesList);
+
+        Map<String, UserType> userTypes = new HashMap<>();
+        for (String userTypeString : userTypeStrings) {
+            UserType userType = new UserType(parseUserType(userTypeString));
+            userTypes.put(userType.name(), userType);
+        }
+
+        return userTypes;
+    }
+
+    String extractUserTypesList(String dumpsysUsersOutput) throws AdbParseException {
+        try {
+            return dumpsysUsersOutput.split(
+                    "User types \\(\\d+ types\\):\n", 2)[1].split("\n\n", 2)[0];
+        } catch (RuntimeException e) {
+            throw new AdbParseException("Error extracting user types list", dumpsysUsersOutput, e);
+        }
+    }
+
+    Set<String> extractUserTypesStrings(String userTypesList) throws AdbParseException {
+        return extractIndentedSections(userTypesList, USER_TYPES_LIST_BASE_INDENTATION);
+    }
+
+    UserType.MutableUserType parseUserType(String userTypeString) throws AdbParseException {
+        try {
+            UserType.MutableUserType userType = new UserType.MutableUserType();
+
+            userType.mName = userTypeString.split("mName: ", 2)[1].split("\n")[0];
+            userType.mBaseType = new HashSet<>();
+            for (String baseType : userTypeString.split("mBaseType: ", 2)[1]
+                    .split("\n")[0].split("\\|")) {
+                if (!baseType.isEmpty()) {
+                    userType.mBaseType.add(UserType.BaseType.valueOf(baseType));
+                }
+            }
+
+            userType.mEnabled = Boolean.parseBoolean(
+                    userTypeString.split("mEnabled: ", 2)[1].split("\n")[0]);
+            userType.mMaxAllowed = Integer.parseInt(
+                    userTypeString.split("mMaxAllowed: ", 2)[1].split("\n")[0]);
+            userType.mMaxAllowedPerParent = Integer.parseInt(
+                    userTypeString.split("mMaxAllowedPerParent: ", 2)[1].split("\n")[0]);
+
+            return userType;
+        } catch (RuntimeException e) {
+            throw new AdbParseException("Error parsing userType", userTypeString, e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/User.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/User.java
new file mode 100644
index 0000000..856e3af
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/User.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import androidx.annotation.Nullable;
+import android.os.Build;
+import android.os.UserHandle;
+
+import androidx.annotation.RequiresApi;
+
+/** Representation of a user on an Android device. */
+public final class User {
+    static final class MutableUser {
+        @Nullable Integer mId;
+        @Nullable Integer mSerialNo;
+        @Nullable String mName;
+        @Nullable UserType mType;
+        @Nullable Boolean mHasProfileOwner;
+        @Nullable Boolean mIsPrimary;
+    }
+
+    private final MutableUser mMutableUser;
+
+    User(MutableUser mutableUser) {
+        mMutableUser = mutableUser;
+    }
+
+    /**
+     * Get a {@link UserHandle} for the {@link #id()}.
+     *
+     * <p>If the {@link #id()} has not been set then this will return {@code null}.
+     */
+    public UserHandle userHandle() {
+        if (mMutableUser.mId == null) {
+            return null;
+        }
+        return UserHandle.of(mMutableUser.mId);
+    }
+
+    public Integer id() {
+        return mMutableUser.mId;
+    }
+
+    public Integer serialNo() {
+        return mMutableUser.mSerialNo;
+    }
+
+    public String name() {
+        return mMutableUser.mName;
+    }
+
+    /**
+     * Get the user type.
+     *
+     * <p>On Android versions < 11, this will return {@code null}.
+     */
+    @RequiresApi(Build.VERSION_CODES.R)
+    public UserType type() {
+        return mMutableUser.mType;
+    }
+
+    public Boolean hasProfileOwner() {
+        return mMutableUser.mHasProfileOwner;
+    }
+
+    /**
+     * Return {@code true} if this is the primary user.
+     *
+     * <p>On Android versions < 11, this will return {@code null}.
+     */
+    @RequiresApi(Build.VERSION_CODES.R)
+    public Boolean isPrimary() {
+        return mMutableUser.mIsPrimary;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("User{");
+        stringBuilder.append("id=" + mMutableUser.mId);
+        stringBuilder.append(", serialNo=" + mMutableUser.mSerialNo);
+        stringBuilder.append(", name=" + mMutableUser.mName);
+        stringBuilder.append(", type=" + mMutableUser.mType);
+        stringBuilder.append(", hasProfileOwner" + mMutableUser.mHasProfileOwner);
+        stringBuilder.append(", isPrimary=" + mMutableUser.mIsPrimary);
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserType.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserType.java
new file mode 100644
index 0000000..86e6882
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserType.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import java.util.Set;
+
+/**
+ * Represents information about an Android User type.
+ *
+ * <p>Only supported on Android 11 and above.
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+public final class UserType {
+
+    public static final int UNLIMITED = -1;
+
+    public enum BaseType {
+        SYSTEM, PROFILE, FULL
+    }
+
+    static final class MutableUserType {
+        String mName;
+        Set<BaseType> mBaseType;
+        Boolean mEnabled;
+        Integer mMaxAllowed;
+        Integer mMaxAllowedPerParent;
+    }
+
+    private final MutableUserType mMutableUserType;
+
+    UserType(MutableUserType mutableUserType) {
+        mMutableUserType = mutableUserType;
+    }
+
+    public String name() {
+        return mMutableUserType.mName;
+    }
+
+    public Set<BaseType> baseType() {
+        return mMutableUserType.mBaseType;
+    }
+
+    public Boolean enabled() {
+        return mMutableUserType.mEnabled;
+    }
+
+    /**
+     * The maximum number of this user type allowed on the device.
+     *
+     * <p>This value will be {@link #UNLIMITED} if there is no limit.
+     */
+    public Integer maxAllowed() {
+        return mMutableUserType.mMaxAllowed;
+    }
+
+    /**
+     * The maximum number of this user type allowed for a single parent profile
+     *
+     * <p>This value will be {@link #UNLIMITED} if there is no limit.
+     */
+    public Integer maxAllowedPerParent() {
+        return mMutableUserType.mMaxAllowedPerParent;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("UserType{");
+        stringBuilder.append("name=" + mMutableUserType.mName);
+        stringBuilder.append(", baseType=" + mMutableUserType.mBaseType);
+        stringBuilder.append(", enabled=" + mMutableUserType.mEnabled);
+        stringBuilder.append(", maxAllowed=" + mMutableUserType.mMaxAllowed);
+        stringBuilder.append(", maxAllowedPerParent=" + mMutableUserType.mMaxAllowedPerParent);
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java
new file mode 100644
index 0000000..6f88a70
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/Users.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.util.Collection;
+import java.util.Map;
+
+public final class Users {
+
+    private Map<Integer, User> mCachedUsers = null;
+    private Map<String, UserType> mCachedUserTypes = null;
+    private final AdbUserParser parser = AdbUserParser.get(SDK_INT);
+
+    /** Get all users on the device. */
+    public Collection<User> users() {
+        fillCache();
+
+        return mCachedUsers.values();
+    }
+
+    /** Get all supported user types. */
+    @RequiresApi(Build.VERSION_CODES.R)
+    public Collection<UserType> supportedTypes() {
+        if (SDK_INT < Build.VERSION_CODES.R) {
+            return null;
+        }
+        if (mCachedUserTypes == null) {
+            // supportedTypes cannot change so we don't need to refill the cache
+            fillCache();
+        }
+        return mCachedUserTypes.values();
+    }
+
+    /** Get a {@link UserType} with the given {@code typeName}. */
+    @RequiresApi(Build.VERSION_CODES.R)
+    public UserType supportedType(String typeName) {
+        if (SDK_INT < Build.VERSION_CODES.R) {
+            return null;
+        }
+        if (mCachedUserTypes == null) {
+            // supportedTypes cannot change so we don't need to refill the cache
+            fillCache();
+        }
+        return mCachedUserTypes.get(typeName);
+    }
+
+    private void fillCache() {
+        try {
+            // TODO: Replace use of adb on supported versions of Android
+            String userDumpsysOutput = ShellCommandUtils.executeCommand("dumpsys user");
+            AdbUserParser.ParseResult result = parser.parse(userDumpsysOutput);
+
+            mCachedUsers = result.mUsers;
+            mCachedUserTypes = result.mUserTypes;
+        } catch (AdbException | AdbParseException e) {
+            throw new RuntimeException("Error filling cache", e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java
new file mode 100644
index 0000000..e95245b
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommandUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.utils;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.os.Build;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.compatibility.common.util.SystemUtil;
+
+/**
+ * Utilities for interacting with adb shell commands.
+ */
+public final class ShellCommandUtils {
+
+    private ShellCommandUtils() { }
+
+    /**
+     * Execute an adb shell command.
+     *
+     * <p>When running on S and above, any failures in executing the command will result in an
+     * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException}
+     * will be thrown when the command returns no output (indicating that there is an error on
+     * stderr which cannot be read by this method) but some failures will return seemingly correctly
+     * but with an error in the returned string.
+     *
+     * <p>Callers should be careful to check the command's output is valid.
+     */
+    public static String executeCommand(String command) throws AdbException {
+        if (SDK_INT < Build.VERSION_CODES.S) {
+            return executeCommandPreS(command);
+        }
+
+        // TODO: Add argument to force errors to stderr
+        try {
+            return SystemUtil.runShellCommandOrThrow(command);
+        } catch (AssertionError e) {
+            throw new AdbException("Error executing command", command, /* output= */ null, e);
+        }
+    }
+
+    private static String executeCommandPreS(String command) throws AdbException {
+        String result = SystemUtil.runShellCommand(command);
+
+        if (result.isEmpty()) {
+            throw new AdbException(
+                    "No output from command. There's likely an error on stderr", command, result);
+        }
+
+        return result;
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/AndroidManifest.xml b/common/device-side/bedstead/nene/src/test/AndroidManifest.xml
new file mode 100644
index 0000000..94ad2ad
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.nene.test">
+    <application
+        android:label="Nene Tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="26"/>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.bedstead.nene.test"
+                     android:label="Nene Tests" />
+</manifest>
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/TestApisTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/TestApisTest.java
new file mode 100644
index 0000000..db10699
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/TestApisTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TestApisTest {
+
+    private final TestApis mTestApis = new TestApis();
+
+    @Test
+    public void users_returnsInstance() {
+        Truth.assertThat(mTestApis.users()).isNotNull();
+    }
+
+    @Test
+    public void users_multipleCalls_returnsSameInstance() {
+        Truth.assertThat(mTestApis.users()).isEqualTo(mTestApis.users());
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTest.java
new file mode 100644
index 0000000..6a946f2
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class UserTest {
+
+    private static final int INT_VALUE = 1;
+    private static final String STRING_VALUE = "String";
+    private static final UserType USER_TYPE = new UserType(new UserType.MutableUserType());
+
+    @Test
+    public void id_returnsId() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mId = INT_VALUE;
+        User user = new User(mutableUser);
+
+        assertThat(user.id()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void id_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.id()).isNull();
+    }
+
+    @Test
+    public void serialNo_returnsSerialNo() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mSerialNo = INT_VALUE;
+        User user = new User(mutableUser);
+
+        assertThat(user.serialNo()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void serialNo_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.serialNo()).isNull();
+    }
+
+    @Test
+    public void name_returnsName() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mName = STRING_VALUE;
+        User user = new User(mutableUser);
+
+        assertThat(user.name()).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void name_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.name()).isNull();
+    }
+
+    @Test
+    public void type_returnsName() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mType = USER_TYPE;
+        User user = new User(mutableUser);
+
+        assertThat(user.type()).isEqualTo(USER_TYPE);
+    }
+
+    @Test
+    public void type_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.type()).isNull();
+    }
+
+    @Test
+    public void hasProfileOwner_returnsHasProfileOwner() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mHasProfileOwner = true;
+        User user = new User(mutableUser);
+
+        assertThat(user.hasProfileOwner()).isTrue();
+    }
+
+    @Test
+    public void hasProfileOwner_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.hasProfileOwner()).isNull();
+    }
+
+    @Test
+    public void isPrimary_returnsIsPrimary() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mIsPrimary = true;
+        User user = new User(mutableUser);
+
+        assertThat(user.isPrimary()).isTrue();
+    }
+
+    @Test
+    public void isPrimary_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.isPrimary()).isNull();
+    }
+
+    @Test
+    public void userHandle_returnsUserHandle() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        mutableUser.mId = INT_VALUE;
+        User user = new User(mutableUser);
+
+        assertThat(user.userHandle().getIdentifier()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void userHandle_notSet_returnsNull() {
+        User.MutableUser mutableUser = new User.MutableUser();
+        User user = new User(mutableUser);
+
+        assertThat(user.userHandle()).isNull();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java
new file mode 100644
index 0000000..d05e091
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserTypeTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class UserTypeTest {
+
+    private static final int INT_VALUE = 1;
+    private static final String STRING_VALUE = "String";
+
+    @Test
+    public void name_returnsName() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mName = STRING_VALUE;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.name()).isEqualTo(STRING_VALUE);
+    }
+
+    @Test
+    public void name_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.name()).isNull();
+    }
+
+    @Test
+    public void baseType_returnsBaseType() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mBaseType = Set.of(UserType.BaseType.FULL);
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.baseType()).containsExactly(UserType.BaseType.FULL);
+    }
+
+    @Test
+    public void baseType_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.baseType()).isNull();
+    }
+
+    @Test
+    public void enabled_returnsEnabled() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mEnabled = true;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.enabled()).isTrue();
+    }
+
+    @Test
+    public void enabled_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.enabled()).isNull();
+    }
+
+    @Test
+    public void maxAllowed_returnsMaxAllowed() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mMaxAllowed = INT_VALUE;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowed()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void maxAllowed_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowed()).isNull();
+    }
+
+    @Test
+    public void maxAllowedPerParent_returnsMaxAllowedPerParent() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        mutableUserType.mMaxAllowedPerParent = INT_VALUE;
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowedPerParent()).isEqualTo(INT_VALUE);
+    }
+
+    @Test
+    public void maxAllowedParParent_notSet_returnsNull() {
+        UserType.MutableUserType mutableUserType = new UserType.MutableUserType();
+        UserType userType = new UserType(mutableUserType);
+
+        assertThat(userType.maxAllowedPerParent()).isNull();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UsersTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UsersTest.java
new file mode 100644
index 0000000..6c35a8c
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UsersTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.users;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+
+import static org.junit.Assume.assumeTrue;
+
+import android.os.Build;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class UsersTest {
+
+    private static final String INVALID_TYPE = "invalidType";
+    private static final String SYSTEM_USER_TYPE = "android.os.usertype.full.SYSTEM";
+    private static final int MAX_SYSTEM_USERS = UserType.UNLIMITED;
+    private static final int MAX_SYSTEM_USERS_PER_PARENT = UserType.UNLIMITED;
+    private static final String MANAGED_PROFILE_TYPE = "android.os.usertype.profile.MANAGED";
+    private static final int MAX_MANAGED_PROFILES = UserType.UNLIMITED;
+    private static final int MAX_MANAGED_PROFILES_PER_PARENT = 1;
+    private final Users mUsers = new Users();
+    private static final long WAIT_FOR_REMOVE_USER_TIMEOUT_MS = 30000;
+
+    // We don't want to test the exact list of any specific device, so we check that it returns
+    // some known types which will exist on the emulators (used for presubmit tests).
+
+    @Test
+    public void supportedTypes_containsManagedProfile() {
+        assumeTrue(
+                "supportedTypes is only supported on Android 11+",
+                SDK_INT >= Build.VERSION_CODES.R);
+
+        UserType managedProfileUserType =
+                mUsers.supportedTypes().stream().filter(
+                        (ut) -> ut.name().equals(MANAGED_PROFILE_TYPE)).findFirst().get();
+
+        assertThat(managedProfileUserType.baseType()).containsExactly(UserType.BaseType.PROFILE);
+        assertThat(managedProfileUserType.enabled()).isTrue();
+        assertThat(managedProfileUserType.maxAllowed()).isEqualTo(MAX_MANAGED_PROFILES);
+        assertThat(managedProfileUserType.maxAllowedPerParent())
+                .isEqualTo(MAX_MANAGED_PROFILES_PER_PARENT);
+    }
+
+    @Test
+    public void supportedTypes_containsSystemUser() {
+        assumeTrue(
+                "supportedTypes is only supported on Android 11+",
+                SDK_INT >= Build.VERSION_CODES.R);
+
+        UserType systemUserType =
+                mUsers.supportedTypes().stream().filter(
+                        (ut) -> ut.name().equals(SYSTEM_USER_TYPE)).findFirst().get();
+
+        assertThat(systemUserType.baseType()).containsExactly(
+                UserType.BaseType.SYSTEM, UserType.BaseType.FULL);
+        assertThat(systemUserType.enabled()).isTrue();
+        assertThat(systemUserType.maxAllowed()).isEqualTo(MAX_SYSTEM_USERS);
+        assertThat(systemUserType.maxAllowedPerParent()).isEqualTo(MAX_SYSTEM_USERS_PER_PARENT);
+    }
+
+    @Test
+    public void supportedTypes_androidVersionLessThan11_returnsNull() {
+        assumeTrue("supportedTypes is supported on Android 11+", SDK_INT < Build.VERSION_CODES.R);
+
+        assertThat(mUsers.supportedTypes()).isNull();
+    }
+
+    @Test
+    public void supportedType_validType_returnsType() {
+        assumeTrue(
+                "supportedTypes is only supported on Android 11+",
+                SDK_INT >= Build.VERSION_CODES.R);
+
+        UserType managedProfileUserType = mUsers.supportedType(MANAGED_PROFILE_TYPE);
+
+        assertThat(managedProfileUserType.baseType()).containsExactly(UserType.BaseType.PROFILE);
+        assertThat(managedProfileUserType.enabled()).isTrue();
+        assertThat(managedProfileUserType.maxAllowed()).isEqualTo(MAX_MANAGED_PROFILES);
+        assertThat(managedProfileUserType.maxAllowedPerParent())
+                .isEqualTo(MAX_MANAGED_PROFILES_PER_PARENT);
+    }
+
+    @Test
+    public void supportedType_invalidType_androidVersionLessThan11_returnsNull() {
+        assumeTrue("supportedTypes is supported on Android 11+", SDK_INT < Build.VERSION_CODES.R);
+
+        assertThat(mUsers.supportedType(MANAGED_PROFILE_TYPE)).isNull();
+    }
+
+    @Test
+    public void supportedType_validType_androidVersionLessThan11_returnsNull() {
+        assumeTrue("supportedTypes is supported on Android 11+", SDK_INT < Build.VERSION_CODES.R);
+
+        assertThat(mUsers.supportedType(MANAGED_PROFILE_TYPE)).isNull();
+    }
+
+    @Test
+    public void users_containsCreatedUser() {
+        int userId = createUser();
+
+        try {
+            User foundUser = mUsers.users().stream().filter(
+                    u -> u.id().equals(userId)).findFirst().get();
+
+            assertThat(foundUser).isNotNull();
+        } finally {
+            removeUser(userId);
+        }
+    }
+
+    @Test
+    public void users_userAddedSinceLastCallToUsers_containsNewUser() {
+        int userId = createUser();
+        mUsers.users();
+        int userId2 = createUser();
+
+        try {
+            User foundUser = mUsers.users().stream().filter(
+                    u -> u.id().equals(userId)).findFirst().get();
+
+            assertThat(foundUser).isNotNull();
+        } finally {
+            removeUser(userId);
+            removeUser(userId2);
+        }
+    }
+
+    @Test
+    public void users_userRemovedSinceLastCallToUsers_doesNotContainRemovedUser() {
+        int userId = createUser();
+        mUsers.users();
+        removeUser(userId);
+
+        assertThat(mUsers.users().stream().anyMatch(u -> u.id().equals(userId))).isFalse();
+    }
+
+    private int createUser() {
+        try {
+            String createUserOutput = ShellCommandUtils.executeCommand("pm create-user testuser");
+            return Integer.parseInt(createUserOutput.split("id ")[1].trim());
+        } catch (AdbException e) {
+            throw new AssertionError("Error creating user", e);
+        }
+    }
+
+    private void removeUser(int userId) {
+        try {
+            ShellCommandUtils.executeCommand("pm remove-user " + userId);
+            PollingCheck.waitFor(WAIT_FOR_REMOVE_USER_TIMEOUT_MS,
+                    () -> {
+                        try {
+                            return !ShellCommandUtils.executeCommand("dumpsys user").contains(
+                                            "UserInfo{" + userId);
+                        } catch (AdbException e) {
+                            e.printStackTrace();
+                        }
+                        return false;
+                    });
+        } catch (AdbException e) {
+            throw new AssertionError("Error creating user", e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandUtilsTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandUtilsTest.java
new file mode 100644
index 0000000..d2d6ae0
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/utils/ShellCommandUtilsTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 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.bedstead.nene.utils;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.testng.Assert.assertThrows;
+
+import android.os.Build;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ShellCommandUtilsTest {
+
+    private static final String LIST_USERS_COMMAND = "pm list users";
+    private static final String LIST_USERS_EXPECTED_OUTPUT = "Users:";
+    private static final String INVALID_COMMAND_LEGACY_OUTPUT = "pm list-users";
+    private static final String INVALID_COMMAND_EXPECTED_LEGACY_OUTPUT = "Unknown command:";
+    private static final String INVALID_COMMAND_CORRECT_OUTPUT = "pm set-harmful-app-warning --no";
+
+    @Test
+    public void executeCommand_returnsOutput() throws Exception {
+        assertThat(ShellCommandUtils.executeCommand(LIST_USERS_COMMAND))
+                .contains(LIST_USERS_EXPECTED_OUTPUT);
+    }
+
+    @Test
+    @Ignore("This behaviour is not implemented yet")
+    public void executeCommand_invalidCommand_legacyOutput_throwsException() {
+        assumeTrue(
+                "New behaviour is only supported on Android 11+", SDK_INT >= Build.VERSION_CODES.R);
+        assertThrows(AdbException.class,
+                () -> ShellCommandUtils.executeCommand(INVALID_COMMAND_LEGACY_OUTPUT));
+    }
+
+    @Test
+    public void executeCommand_invalidCommand_legacyOutput_preAndroid11_throwsException()
+            throws Exception {
+        // This is currently still the default behaviour
+        //assumeTrue("Legacy behaviour is only supported before 11", SDK_INT < Build.VERSION_CODES.R);
+        assumeTrue("This command's behaviour changed in Android P", SDK_INT >= Build.VERSION_CODES.P);
+        assertThat(ShellCommandUtils.executeCommand(INVALID_COMMAND_LEGACY_OUTPUT))
+                .contains(INVALID_COMMAND_EXPECTED_LEGACY_OUTPUT);
+    }
+
+    @Test
+    public void executeCommand_invalidCommand_correctOutput_throwsException() {
+        assertThrows(AdbException.class,
+                () -> ShellCommandUtils.executeCommand(INVALID_COMMAND_CORRECT_OUTPUT));
+    }
+}