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));
+ }
+}